17 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/system-page/storage-maintenance.md | 2026-07-14 | done |
Implementation Plan: Storage Maintenance
Overview
Build one install-wide storage service under internal/system/storage, backed by durable settings,
run history, quarantine records, and task cleanup intents. Land persistence and activity admission
first, then implement ownership-specific providers, expose the System API, and finish with a
responsive Storage settings page and desktop/mobile/container E2E coverage. The existing
office/infra sweep is retired only after the replacement workspace reconciler is wired.
The implementation preserves PR #1687's unarchive contract: archived filesystem payload may be reclaimed, historical worktree rows and branch metadata remain, and pending archive cleanup is cancelled when a task becomes active again.
The 2026-07-22 follow-up removes per-instance temporary-directory injection. Host-local agents
inherit the Kandev service's TMPDIR, TMP, and TEMP unchanged so temp-derived Node/Playwright
caches can be reused. The superseded marker, snapshot, Storage-provider, UI, and E2E work is removed;
managed Go-cache defaults and injection remain unchanged. A separate test-harness task addresses
the observed accumulation of kandev-e2e-* roots.
Backend
Durable task lifecycle cleanup
- Add
task_resource_cleanup_jobscreation and replay-safe indexes inapps/backend/internal/task/repository/sqlite/base_schema.goandbase_migrations.go, plus repository operations in a newresource_cleanup.go. - Define the serialized cleanup snapshot and job state in
apps/backend/internal/task/models/resource_cleanup.go. - Replace
Service.runAsyncTaskCleanupinapps/backend/internal/task/service/service_tasks.gowith enqueue/claim/retry logic in newresource_cleanup_jobs.goandresource_cleanup_worker.gofiles. Capture cleanup handles before archive/delete/cascade mutations and keep the worker independent of the optional maintenance scheduler. - Check archived state before each archive-job destructive step. Wire unarchive in
handoff_cascade.goto cancel non-terminal archive jobs before publishing the restored task. - Preserve
task_session_worktreeshistory for archived tasks. Delete snapshots carry paths and runtime handles that would otherwise disappear through foreign-key cascades.
Persistent storage contracts
- Create
apps/backend/internal/system/storage/withtypes.go,settings.go,store.go, andstore_migrations.go. - Store
StorageMaintenanceSettingsas JSON under the existingsettingskeystorage_maintenance, followinginternal/system/metrics/store.go. - Create
storage_maintenance_runsandstorage_quarantine_entrieswith replay-safe SQLite and Postgres DDL. Normalize settings with the ranges and cross-field dedicated-Docker validation in the spec. - Persist every analysis/cleanup run and retain the newest 20 terminal rows plus all non-terminal rows.
Resource activity coordinator
- Add a neutral package at
apps/backend/internal/agent/runtime/activity/with a process-wideCoordinator, cancellable maintenance lease, active-resource snapshot, and quiet-period clock. - Instrument lifecycle launch/prepare/stop and agent turn execution in
internal/agent/runtime/lifecycle/; instrument task shell/process requests in the agentctl client or API boundary; instrument worktree setup/cleanup scripts and Docker image builds. - A task-resource lease cancels a maintenance lease and waits for provider cancellation before admission. Maintenance acquisition rechecks active leases after locking to close the idle-check race.
- Scheduled acquisition enforces the configured quiet period. Manual Run now skips that elapsed-idle check while retaining the same current-activity and mutual-exclusion checks.
- Expose the same coordinator to the storage scheduler through backend composition.
Workspace inventory and quarantine
- Implement
internal/system/storage/workspaces/with an authoritativeInventoryinterface, analyzer, candidate classifier, quarantine mover, restore operation, permanent deletion, and startup reconciliation. - Extend task/worktree repository queries to return protected paths from active worktrees,
TaskEnvironment.WorkspacePath, active execution metadata, and repository-less task paths. Normalize paths and protect every ancestor beneath<KANDEV_HOME_DIR>/tasks. - Update task-root creation in lifecycle/worktree preparation to write an atomic ownership marker.
Support both semantic multi-repo roots and
<workspace-id>/<task-id>scratch roots. - Move candidates using same-filesystem rename into
<KANDEV_HOME_DIR>/trash/tasks, persist the quarantine record, and reconcile marker/DB disagreement after restart. Never follow symlinks or operate outside the configured roots. - Add a
WorkspaceRecoveryhook to the merged PR #1687 unarchive path intask_http_handlers.go: restore a task-matched quarantine entry beforeRecoverTaskBranches, reportrestored|not_found|failed, and leave branch recovery unchanged. - Remove
internal/office/infra/gc.gostartup ownership after parity tests prove the System provider protects all live layouts. Do not leave both periodic sweepers enabled.
Managed Go build cache
- Implement
internal/system/storage/gocache/analysis and cleanup for<KANDEV_HOME_DIR>/cache/go-build, including ownership marker, containment validation, quarantine rotation, recreation, and optional confirmed adoption of an external absolute path. - Inject the managed
GOCACHEinto host-local executor, setup/cleanup script, shell, and agent process environments whengo_cache.enabled=true. Centralize this in lifecycle environment construction so profile values cannot create inconsistent paths within one execution. - Treat the configured byte maximum as an idle-time cleanup trigger, not a quota. Never discover
and delete
$HOME/.cache/go-buildimplicitly. - Keep disabled-cache cleanup out of scheduled and global manual runs. Dispatch the provider's
explicit cleanup path only for a non-empty manual resource selection containing
go_cache.
Docker storage provider
- Extend
internal/agent/docker/client.gowith typed usage and prune methods backed by the Docker SDK: Kandev-labeled container inventory/removal, build-cache usage/prune filters, and unused-image usage/removal filters. - Implement
internal/system/storage/dockerstore/as an optional provider. Kandev container cleanup requires label and positive task/runtime inventory evidence; it never removes running containers. - Include exactly labeled managed-container count and writable-layer bytes in read-only analysis, independently degrading Docker usage errors.
- Revalidate
dedicated_daemon_acknowledgedimmediately before build-cache or image deletion. Do not expose daemon-wide volume/network prune operations. - Report unavailable Docker capability without failing other storage providers.
Scheduler, service, and API
- Add
service.go,scheduler.go,runner.go, andhandler.gounderinternal/system/storage/. The runner executes selected providers independently, aggregates bytes/counts/warnings, and mirrors persistent run state into the existing System jobs tracker. - Start the scheduler from
internal/system/system.go:StartBackgroundonly when persisted settings enable it. The first run waits a full interval; busy intervals createskipped_busyhistory. - Register the spec routes under
/api/v1/system/storage, including confirmed Go-cache adoption, settings, analyze, manual run, run history, quarantine restore, and confirmed permanent delete. - Wire settings store, task cleanup worker, lifecycle activity coordinator, worktree/task inventory,
Docker client, and unarchive recovery in
internal/backendappcomposition. - Add health issues for invalid persisted settings, retry-exhausted cleanup jobs, failed quarantine entries, and unavailable explicitly-enabled providers.
Frontend
Route and navigation
- Add
apps/web/app/settings/system/storage/page.tsxand a Storage leaf tocomponents/app-sidebar/sections/settings/system-group.tsx. - Reuse
SystemPageShell; hydrate initial storage state through the existing Go-served SPA/API conventions rather than fetching directly from components.
API, types, state, and hooks
- Add storage contracts to
apps/web/lib/types/system.tsand API calls toapps/web/lib/api/domains/system-api.ts. - Extend the system Zustand slice and WS job handling for persisted storage runs, capabilities, settings, summary, and quarantine entries.
- Add
hooks/domains/system/use-storage-maintenance.tsto own loading, save, analyze, run, restore, delete, polling fallback, and feedback state.
Responsive Storage page
- Add components beneath
apps/web/components/settings/system/storage/:storage-summary-card.tsx,maintenance-settings-card.tsx,storage-resource-card.tsx,maintenance-history-card.tsx,quarantine-card.tsx, and confirmation dialogs. - Desktop uses a two-column summary/settings grid followed by full-width resource, quarantine, and history cards. Mobile stacks every card, wraps action bars, renders resource/history rows as vertical key/value blocks, and keeps all primary actions at least 44 px high.
- The dedicated-Docker acknowledgment and external Go-cache adoption use explicit warning dialogs.
Permanent quarantine deletion requires typing
DELETE; restore remains a normal confirm action. - No required action is hover-only. Long paths wrap, byte counts remain aligned, and the page does not introduce horizontal document scrolling at Pixel 5 width.
- Show quarantine count/bytes and managed-container count/writable bytes in the analysis accordion, and expose a responsive, threshold/ownership-gated Clean Go cache action on the Go-cache row.
Tests
- What: task archive/delete persists cleanup before mutation, retries across restart, captures
handles before FK cascades, and cancels on unarchive.
File:
apps/backend/internal/task/service/resource_cleanup_worker_test.goandinternal/task/repository/sqlite/resource_cleanup_test.go. How: table-driven service tests plus real SQLite replay tests; include a backend-restart worker reconstruction test. - What: settings defaults, range validation, dedicated-Docker cross-field validation, run
persistence/retention, and quarantine state transitions.
File:
apps/backend/internal/system/storage/settings_test.goandstore_test.go. How: table-driven normalization plus SQLite and env-gated Postgres replay tests. - What: activity admission closes the idle race, task work cancels maintenance, scheduled runs
enforce the quiet period, and manual runs gate only on current activity or another maintenance run.
File:
apps/backend/internal/agent/runtime/activity/coordinator_test.go. How:testing/synctesttests with explicit lease channels; no sleeps. - What: live semantic roots, live scratch roots, multi-repo ancestors, inventory errors,
symlinks, grace periods, quarantine reconciliation, restore conflicts, and permanent deletion.
File:
apps/backend/internal/system/storage/workspaces/provider_test.go. How: table-driven filesystem tests undert.TempDir()with fake complete/error inventories. - What: ownership-marker creation for every local task layout. File: lifecycle and worktree preparation tests beside the existing layout tests. How: real temporary worktree and repository-less launch fixtures.
- What: unarchive cancels pending cleanup, restores task-matched quarantine first, preserves
historical worktree records, and reports restore failures without blocking unarchive.
File:
apps/backend/internal/task/service/handoff_unarchive_test.go,task/handlers/task_http_handlers_test.go, and storage workspace integration tests. How: service integration tests aligned with PR #1687 branch-recovery cases. - What: managed Go path injection is consistent across processes; cleanup only rotates marked
owned paths above threshold; unadopted
/root/.cache/go-buildis untouched. File:apps/backend/internal/system/storage/gocache/provider_test.goand lifecycle env tests. How: temporary filesystem tests and environment-construction unit tests. - What: Docker provider scopes Kandev containers by label, keeps running/unrelated containers,
rejects global cleanup without acknowledgment, and applies age/storage filters.
File:
apps/backend/internal/system/storage/dockerstore/provider_test.goandinternal/agent/docker/client_test.go. How: mocked SDK unit tests plus an env-gated real-Docker integration test. - What: handler-to-service-to-store contracts for every Storage endpoint, including
409busy,400validation, async job IDs, andDELETEconfirmation. File:apps/backend/internal/system/storage/handler_test.go. How: Gin integration tests using a real SQLite store and fake providers. - What: frontend API shapes, settings state, capability warnings, action feedback, responsive row
rendering, and typed confirmation state.
File: colocated
*.test.ts(x)files underlib/api/domains,lib/state/slices/system, andcomponents/settings/system/storage. How: Vitest API mocks and Testing Library interaction tests.
Verification for backend tasks starts with focused package tests, then runs:
make -C apps/backend fmt
make -C apps/backend test
make -C apps/backend lint
Frontend verification runs:
cd apps/web && pnpm run typecheck
cd apps && pnpm --filter @kandev/web lint
cd apps && pnpm --filter @kandev/web test
E2E Tests
- Scenario: scheduling defaults off; analyze reports an orphan task directory with
node_modules; manual run quarantines it; restore returns it to the original path. File:apps/web/e2e/tests/system/storage-maintenance.spec.ts. What to verify: settings persist after reload, job progress/result renders, quarantine row appears, byte counts change, and restore succeeds through the UI. - Scenario: a busy task blocks manual maintenance and scheduled maintenance records
skipped_busywithout moving its workspace. File:apps/web/e2e/tests/system/storage-maintenance.spec.ts. What to verify: visible busy feedback and unchanged task files. - Scenario: an archived task with a quarantined workspace is unarchived.
File:
apps/web/e2e/tests/task/unarchive-storage-recovery.spec.ts. What to verify: workspace recovery is reported before branch recovery and the task can launch from the restored path. - Scenario: the complete Storage workflow works through the mobile settings sheet.
File:
apps/web/e2e/tests/system/mobile-storage-maintenance.spec.ts. What to verify: navigate from the mobile sheet, analyze, expand resource details, save safe settings, and restore a quarantine entry without horizontal page scrolling. - Scenario: real Docker analysis and Kandev-labeled stopped-container cleanup do not remove an
unrelated container; build-cache cleanup remains gated by acknowledgment.
File:
apps/web/e2e/tests/docker/storage-maintenance.spec.tsusingdocker-test-base. What to verify: labeled orphan removed, unrelated/running containers retained, global action disabled until acknowledgment.
Run focused UI tests through the managed production-build runner:
cd apps/web && pnpm e2e:run tests/system/storage-maintenance.spec.ts
cd apps/web && pnpm e2e:run tests/system/mobile-storage-maintenance.spec.ts -- --project=mobile-chrome
cd apps/web && KANDEV_E2E_CONTAINERS=1 pnpm e2e --project=containers tests/docker/storage-maintenance.spec.ts
Implementation Waves
Wave 1 (parallel foundations):
Wave 2 (parallel providers after Task 02; activity also depends on Task 01):
- Task 03 — Activity gate and scheduler core
- Task 04 — Workspace quarantine and unarchive
- Task 05 — Managed Go build cache
- Task 06 — Docker storage provider
Wave 3:
Wave 4:
Wave 5:
Wave 6:
Wave 7:
Wave 8:
Wave 9: