Files
2026-08-24 11:09:03 +00:00

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.go
  • apps/backend/internal/task/repository/sqlite/base_migrations.go
  • apps/backend/internal/task/repository/sqlite/postgres_schema_test.go
  • apps/backend/internal/task/repository/sqlite/schema_replay_test.go
  • apps/backend/internal/worktree/store.go
  • apps/backend/internal/worktree/manager.go
  • apps/backend/internal/worktree/manager_lifecycle.go
  • apps/backend/internal/backendapp/branch_materializer.go
  • apps/backend/internal/task/models/models.go
  • apps/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.go
  • apps/backend/internal/task/repository/sqlite/resource_cleanup.go
  • apps/backend/internal/task/repository/sqlite/session.go
  • apps/backend/internal/orchestrator/task_operations.go
  • apps/backend/internal/orchestrator/executor/task_environment.go
  • apps/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.go
  • apps/backend/internal/task/service/handoff_cascade.go
  • apps/backend/internal/task/service/resource_cleanup_jobs.go
  • apps/backend/internal/task/service/service_cleanup_inventory_test.go
  • apps/backend/internal/task/service/resource_cleanup_jobs_test.go
  • apps/backend/internal/worktree/manager_cleanup.go
  • apps/backend/internal/worktree/store.go
  • apps/backend/internal/backendapp/storage_inventory.go
  • apps/backend/internal/automation/run_retention.go
  • apps/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.tsx
  • apps/web/components/task/session-tab-menu.test.tsx
  • apps/web/components/task/mobile/mobile-sessions-section.tsx
  • apps/web/components/task/mobile/mobile-sessions-section.test.tsx
  • apps/web/src/locales/{en,pseudo,pt-pt,zh-cn}/task.json
  • apps/web/e2e/tests/session/multi-session-ux.spec.ts
  • apps/web/e2e/tests/session/mobile-session-deletion.spec.ts
  • docs/public/sessions-and-review.md
  • docs/public/operations.md
  • docs/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.