13 KiB
spec, decision, created, status
| spec | decision | created | status |
|---|---|---|---|
| docs/specs/tasks/requirements/session-delete-resource-cleanup.md | docs/decisions/2026-08-08-task-owned-worktree-lifetime.md | 2026-08-08 | implemented |
Implementation Plan: Preserve Task-Owned Worktrees Across Session Deletion
Overview
Redesign the closed PR #2421 around the actual ownership boundary: a task owns its materialized worktrees, while sessions only reference them. Deleting a session must remove its conversation and associations without invoking any filesystem, Git worktree, or branch cleanup. Archive, delete, cascade, workspace delete, quick-chat expiry, and explicit environment reset remain the only physical cleanup owners and continue through the durable cleanup worker.
The persistence repair finishes the existing task-environment model instead of
adding another table. task_environment_repos becomes the sole physical
worktree record, task_sessions.task_environment_id is the only session link,
and task_session_worktrees plus deprecated flat environment worktree columns
are removed after a validated SQLite/PostgreSQL backfill. A durable task-lifecycle
preparation barrier closes the race between inventory capture and new
session/worktree creation without holding database locks while Git or filesystem
locks are held.
Implementation starts from main; do not carry forward the PR's
session_delete cleanup trigger/job, resource_cleanup_session_delete.go, or
ReclaimSessionWorktree physical cleanup path.
Persistence and Worktree Store
Likely files:
apps/backend/internal/task/repository/sqlite/base_schema.goapps/backend/internal/task/repository/sqlite/base_migrations.goapps/backend/internal/task/repository/sqlite/postgres_schema_test.goapps/backend/internal/task/repository/sqlite/schema_replay_test.goapps/backend/internal/worktree/store.goapps/backend/internal/worktree/manager.goapps/backend/internal/worktree/manager_lifecycle.goapps/backend/internal/backendapp/branch_materializer.goapps/backend/internal/task/models/models.goapps/web/lib/api/domains/task-environment-api.ts- task-environment API consumers that still read deprecated flat fields
Extend task_environment_repos with the lifecycle fields needed by worktree
cleanup, then backfill it from existing environment-repository rows, deprecated
flat task_environments metadata, and task_session_worktrees. Sessions with a
valid environment retain it; otherwise normalize each connected legacy
worktree/session group under exactly one task-owned environment. Fail closed on
incompatible task, environment, repository, identity, path, or branch data.
In the same upgrade, validate the final ownership graph, rebuild/drop
task_session_worktrees, and rebuild/drop the deprecated
task_environments.repository_id, worktree_id, worktree_path, and
worktree_branch columns. SQLite and PostgreSQL must roll back the entire
normalization if validation fails. Fresh schema contains only the final tables.
There is no runtime dual-read/dual-write window.
Implement this as a dedicated migration that returns every error; do not use the
best-effort MigrateLogger.Apply path. Acquire the SQLite writer lock or a
PostgreSQL migration advisory lock, populate shadow tables, and compare the
complete old/new worktree identity, path, and branch sets before any schema swap.
Validate session-to-environment resolution, canonical owner uniqueness, row
counts, constraints, foreign keys, and the exact final column/index set inside
the transaction. Injected failures at every copy/validation/drop/rename boundary
must prove rollback restores the complete pre-upgrade schema and data.
SQLite's existing pre-upgrade VACUUM INTO snapshot is mandatory and remains
fatal on failure. PostgreSQL relies on transactional DDL plus the repository's
existing operator-managed backup policy. Its transaction takes a migration
advisory lock plus exclusive locks on the affected ownership tables, and aborts
on lock timeout. Deployment documentation must require mixed-version PostgreSQL
writers to stop before cutover and explain that downgrade restores the backup;
the clean final schema is not readable by an older binary. The migration never
touches a workspace directory or invokes Git, and repository initialization must
complete before the cleanup worker starts.
Defensively remove any preview-build cleanup jobs with trigger
session_delete before the worker starts, then remove that trigger identity and
all session-specific cleanup implementation. Preserve every task-lifecycle job.
Persist the environment, repository worktrees, and session environment link in
the owning transaction after physical materialization. If persistence is
rejected, remove only the just-created physical worktree as compensation. Move
task/session inventory, storage inventory, branch updates, API projections, and
live-path queries to the normalized environment-repository graph. Remove the
TaskSessionWorktree model/CRUD surface and the worktree store's duplicate schema
initializer.
Task Lifecycle Barrier and Session Deletion
Likely files:
apps/backend/internal/task/service/resource_cleanup_jobs.goapps/backend/internal/task/repository/sqlite/resource_cleanup.goapps/backend/internal/task/repository/sqlite/session.goapps/backend/internal/orchestrator/task_operations.goapps/backend/internal/orchestrator/executor/task_environment.goapps/backend/internal/orchestrator/executor/executor_environment_reuse.go
Reserve a prepared task_resource_cleanup_jobs row before task resource
inventory is captured. Session creation and canonical environment-repository
persistence must serialize against the task row and reject creation while that
lifecycle barrier is active. PostgreSQL uses row locking; SQLite uses its
serialized writer transaction. Commit the barrier before acquiring target-path,
repository, or Git locks so the existing lock order is not inverted.
Keep DeleteSession limited to runtime quiescence, session-row deletion,
in-memory state cleanup, and primary-session promotion. Add a regression seam or
spy that proves the method cannot enqueue resource cleanup or call a physical
worktree cleaner, including when it deletes the final session.
Durable Task Cleanup
Likely files:
apps/backend/internal/task/service/service_tasks.goapps/backend/internal/task/service/handoff_cascade.goapps/backend/internal/task/service/resource_cleanup_jobs.goapps/backend/internal/task/service/service_cleanup_inventory_test.goapps/backend/internal/task/service/resource_cleanup_jobs_test.goapps/backend/internal/worktree/manager_cleanup.goapps/backend/internal/worktree/store.goapps/backend/internal/backendapp/storage_inventory.goapps/backend/internal/automation/run_retention.goapps/backend/internal/office/infra/gc.go
Change gatherWorktreesForDelete and related inventory to read
task_environment_repos through task_environments.task_id, so cleanup remains
possible after every session row is gone and after a backend restart. Snapshot
those handles into the durable job before archive/delete/cascade mutation.
Preserve the existing shared-environment and active-session checks, including
ownership transfer when another task borrows the environment. A transfer changes
the environment owner once; its repository worktrees follow automatically.
Continue to use CleanupWorktrees/removeWorktree only from task lifecycle
cleanup. Filesystem or Git failures remain retryable through the existing worker;
restart must reclaim pending jobs from their snapshots.
Frontend, Mobile, and Documentation
Likely files:
apps/web/components/task/session-tab-menu.tsxapps/web/components/task/session-tab-menu.test.tsxapps/web/components/task/mobile/mobile-sessions-section.tsxapps/web/components/task/mobile/mobile-sessions-section.test.tsxapps/web/src/locales/{en,pseudo,pt-pt,zh-cn}/task.jsonapps/web/e2e/tests/session/multi-session-ux.spec.tsapps/web/e2e/tests/session/mobile-session-deletion.spec.tsdocs/public/sessions-and-review.mddocs/public/operations.mddocs/public/k8s.md
Keep the existing desktop AlertDialog and native mobile session-actions dialog. Their destructive action deletes conversation history, so the confirmation copy must explicitly state that the task workspace and files are retained. Do not add uncommitted/unpushed-work fetches or warnings to either surface; those warnings remain on task archive/delete cleanup dialogs.
Desktop and mobile retain the same action availability and confirmation semantics. This is a copy/contract change, not a compressed desktop layout: mobile keeps its existing actions-sheet entry point, viewport-safe AlertDialog, and touch targets. Rendered component tests cover both surfaces and the existing mobile Playwright flow verifies the native entry point.
Update public session documentation to distinguish ordinary session deletion from task archive/delete. Do not change the separate Quick Chat documentation: closing Quick Chat deletes its backing task and is therefore a task-lifecycle cleanup operation. Update the operations and Kubernetes upgrade/rollback guides with the clean-cutover backup, stopped-writer, single-initializer, and downgrade restore requirements.
Implementation Waves
Wave 1:
Wave 2:
Wave 3:
Wave 4:
Verification
Backend focused tests:
cd apps/backend && go test ./internal/task/repository/... ./internal/worktree/...
cd apps/backend && go test ./internal/task/service/... ./internal/orchestrator/...
cd apps/backend && go test ./internal/backendapp/... ./internal/automation/... ./internal/office/infra/...
Migration safety tests must cover fresh, replay, legacy single-repository, multi-repository, shared-environment, zero-session, preview-PR, ambiguous-data, and injected-failure fixtures on both SQLite and PostgreSQL. PostgreSQL coverage must run with a real isolated database; a skipped run is not release evidence.
PostgreSQL migration tests must run with the repository's configured PostgreSQL test DSN in addition to SQLite. A skipped PostgreSQL run is not final evidence.
Frontend bootstrap and focused tests:
cd apps && pnpm install --frozen-lockfile
cd apps && pnpm --filter @kandev/web test -- components/task/session-tab-menu.test.tsx components/task/mobile/mobile-sessions-section.test.tsx
cd apps/web && pnpm run typecheck
cd apps && pnpm run i18n:ratchet && pnpm run i18n:check
Managed desktop and mobile E2E:
cd apps/web && pnpm e2e:run tests/session/multi-session-ux.spec.ts
cd apps/web && pnpm e2e:run --project mobile-chrome tests/session/mobile-session-deletion.spec.ts
Public documentation validation:
node --test scripts/validate-public-docs.test.mjs
node scripts/validate-public-docs.mjs
Risks
- Backfill ambiguity must stop the migration with diagnostic detail; silently choosing a path can leak or delete the wrong worktree later.
- The normalization must not use a helper that swallows unexpected migration errors. Every failure must abort repository initialization and roll back.
- PostgreSQL multi-instance startup must serialize through a database advisory and affected-table locks; process-local locking is insufficient.
- Canonical ownership must be persisted at every materialization path, including initial creation, resume/recreate, multi-repository additions, and mid-session branch materialization.
- The lifecycle barrier must cover session and worktree creation in PostgreSQL, not merely an in-process mutex or SQLite's single writer.
- Cleanup must not delete a worktree transferred to or actively borrowed by another task between preparation and execution.
- Physical compensation after a rejected environment-repository transaction must target only the just-created worktree and preserve pre-existing directories/registrations.
- Every legacy reader and writer must be removed in the normalization task; storage inventory, automation retention, branch updates, API projections, and orphan GC cannot ship with a fallback to the deleted table or columns.
- CI must assert that legacy table/type/trigger names occur only in the one-way migration and its fixtures, never in production runtime packages.
- Because obsolete schema is removed, rollback means restoring the pre-upgrade database backup; an old binary must never be started against the normalized database.
Documentation Impact
The session-delete preservation spec, runtime-cleanup spec, and ownership ADR define the durable contract. Public session documentation and localized confirmation copy must state that deleting a session retains the task workspace, while existing archive/delete warnings continue to describe physical cleanup.