fix: report storage usage and reclaim archived workspaces (#1813)
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# ADR-2026-07-19-workspace-symlink-entries: Treat Nested Workspace Symlinks as Entries
|
||||
|
||||
**Status:** accepted
|
||||
**Date:** 2026-07-19
|
||||
**Area:** backend, infra
|
||||
|
||||
## Context
|
||||
|
||||
Kandev workspaces routinely contain repository-owned symlinks, including checked-in links such as
|
||||
`CLAUDE.md -> AGENTS.md` and package-manager links beneath `node_modules`. Storage analysis and
|
||||
orphan cleanup rejected any nested symlink, which made normal task roots unmeasurable and
|
||||
permanently ineligible for quarantine even when authoritative inventory classified them as orphaned.
|
||||
|
||||
## Decision
|
||||
|
||||
Workspace storage operations treat a nested symlink as an opaque directory entry and never follow
|
||||
its target. `internal/system/storage/workspaces` measures roots with `filepath.WalkDir`, skips
|
||||
symlink entries, quarantines the complete root with an atomic rename, and permanently deletes the
|
||||
quarantined tree with `os.RemoveAll`; these operations do not traverse nested symlink targets.
|
||||
|
||||
The fail-closed boundary remains unchanged for owned control paths. A symlinked task root, tasks or
|
||||
trash ancestor, quarantine path, ownership marker, or quarantine manifest is rejected before any
|
||||
mutation. Authoritative liveness inventory, containment validation, grace periods, and quarantine
|
||||
retention remain mandatory.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Normal Git and package-manager workspaces can be measured, quarantined, and deleted.
|
||||
- Files reached only through a nested symlink target are not counted and are never changed.
|
||||
- Reviewers must preserve no-follow filesystem primitives throughout workspace maintenance; a
|
||||
future traversal API that follows symlinks must supersede this decision or restore rejection.
|
||||
- Size totals describe bytes stored inside the workspace tree, excluding target data referenced by
|
||||
symlinks.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Reject every nested symlink.** Rejected because ordinary Kandev repositories then cannot be
|
||||
analyzed or reclaimed.
|
||||
- **Resolve each target and allow only links contained by the task root.** Rejected because cleanup
|
||||
does not need target contents, relative and temporarily broken links are legitimate repository
|
||||
state, and resolution adds race-prone complexity.
|
||||
- **Follow symlinks while measuring or deleting.** Rejected because it can count or destroy data
|
||||
outside Kandev-owned task roots.
|
||||
@@ -56,3 +56,4 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
|
||||
| 0046 | [Settings route save coordinator](0046-settings-route-save-coordinator.md) | accepted | frontend | 2026-07-14 |
|
||||
| 2026-07-18-turn-configuration-snapshots | [Attribute runtime configuration to turns](2026-07-18-turn-configuration-snapshots.md) | accepted | backend, frontend | 2026-07-18 |
|
||||
| 2026-07-19-reject-mcp-actions-on-raw-websocket | [Reject MCP Actions on the Raw WebSocket](2026-07-19-reject-mcp-actions-on-raw-websocket.md) | accepted | backend, protocol | 2026-07-19 |
|
||||
| 2026-07-19-workspace-symlink-entries | [Treat Nested Workspace Symlinks as Entries](2026-07-19-workspace-symlink-entries.md) | accepted | backend, infra | 2026-07-19 |
|
||||
|
||||
@@ -29,10 +29,11 @@ reclaim that space without maintaining cron or systemd configuration outside Kan
|
||||
pointer-accessible help that explains what it can change, when it runs, and which safety checks
|
||||
apply. Threshold and path fields are disabled while their parent cleanup option is disabled;
|
||||
quarantine retention remains independently editable because it also governs existing entries.
|
||||
- Read-only analysis is available even when scheduled maintenance is disabled. It reports
|
||||
active task workspace bytes, orphan-candidate bytes, active quarantined count and bytes, the
|
||||
managed Go cache, Kandev-managed container count and writable-layer bytes, Docker build cache,
|
||||
and unused Docker images.
|
||||
- Read-only analysis is available even when scheduled maintenance is disabled. It reports total
|
||||
task workspace bytes alongside active and orphan-candidate bytes, active quarantined count and
|
||||
bytes, the managed Go cache, the service user's default Go cache when it is a distinct path,
|
||||
Kandev-managed container count and writable-layer bytes, Docker image-layer bytes, Docker build
|
||||
cache, and unused Docker images.
|
||||
- Scheduled maintenance is install-wide, persists in Kandev's database, and is disabled by
|
||||
default. Enabling it does not require editing the VM, a systemd unit, or environment
|
||||
variables.
|
||||
@@ -66,6 +67,8 @@ reclaim that space without maintaining cron or systemd configuration outside Kan
|
||||
|
||||
### Task cleanup and orphan workspaces
|
||||
|
||||
Decision: [ADR-2026-07-19-workspace-symlink-entries](../../decisions/2026-07-19-workspace-symlink-entries.md)
|
||||
|
||||
- Archive, delete, cascade, workspace-delete, and quick-chat expiration persist a task resource
|
||||
cleanup intent before mutating or removing the task row. Cleanup inventory needed after a
|
||||
task deletion is captured in that intent and is not dependent on foreign-keyed rows surviving.
|
||||
@@ -81,6 +84,10 @@ reclaim that space without maintaining cron or systemd configuration outside Kan
|
||||
configured orphan grace period.
|
||||
- The authoritative inventory covers both task layouts:
|
||||
`tasks/<semantic-task-dir>/<repo>` and `tasks/<workspace-id>/<task-id>`.
|
||||
- Ready environment rows and active worktree rows protect files while their owning task exists and
|
||||
is not archived. A ready environment owned by an archived or deleted task remains protected while
|
||||
a live session of an unarchived task borrows it. Other rows retained for archived-task branch
|
||||
recovery are historical metadata, not live workspace references.
|
||||
- New task roots contain a Kandev ownership marker with the task ID, workspace ID, task directory
|
||||
name, layout version, and creation time. Legacy unmarked directories remain eligible only when
|
||||
the authoritative inventory and grace-period checks positively classify them as unreferenced.
|
||||
@@ -121,10 +128,12 @@ reclaim that space without maintaining cron or systemd configuration outside Kan
|
||||
- Kandev creates an ownership marker beside the managed cache. It never deletes the default user
|
||||
cache such as `/root/.cache/go-build` unless that exact path was explicitly adopted through the
|
||||
Storage page with a destructive confirmation.
|
||||
- Analysis reports the managed cache's current bytes. Cleanup rotates the owned cache into
|
||||
Kandev trash and recreates an empty cache when its size is greater than the configured maximum.
|
||||
The limit is a cleanup trigger, not a hard quota; the cache can temporarily grow beyond it while
|
||||
tasks are active.
|
||||
- Analysis reports the managed cache's current bytes and read-only usage for the service user's
|
||||
distinct default Go cache (`$GOCACHE` when absolute, otherwise the platform user-cache path).
|
||||
Reporting the default cache does not adopt it or grant cleanup ownership. Cleanup rotates the
|
||||
owned cache into Kandev trash and recreates an empty cache when its size is greater than the
|
||||
configured maximum. The limit is a cleanup trigger, not a hard quota; the cache can temporarily
|
||||
grow beyond it while tasks are active.
|
||||
- Disabling managed Go cache stops injecting `GOCACHE` into new executions. It does not delete the
|
||||
previously managed cache. Scheduled cleanup and a global manual run with no resource selection
|
||||
leave it untouched; only a manual run whose non-empty selection includes `go_cache` may rotate it.
|
||||
@@ -134,8 +143,9 @@ reclaim that space without maintaining cron or systemd configuration outside Kan
|
||||
- Kandev-owned container cleanup lists only containers labeled `kandev.managed=true`. It removes
|
||||
a stopped container only after the task/runtime inventory positively shows it is orphaned or no
|
||||
longer needed. Running containers are never removed by storage maintenance.
|
||||
- Analysis reports the count and writable-layer bytes of exactly labeled Kandev-managed containers;
|
||||
an unavailable usage API degrades Docker analysis without failing the other resource summaries.
|
||||
- Analysis reports the daemon's image-layer bytes and the count and writable-layer bytes of exactly
|
||||
labeled Kandev-managed containers; an unavailable usage API degrades Docker analysis without
|
||||
failing the other resource summaries.
|
||||
- Docker build-cache and unused-image analysis may inspect the configured Docker daemon without
|
||||
changing it.
|
||||
- Docker build-cache cleanup uses the Docker API's age/storage filters and does not invoke
|
||||
@@ -349,8 +359,9 @@ enabling host-global Docker cleanup require explicit UI confirmation and server-
|
||||
|
||||
- Any authoritative workspace inventory query failure aborts workspace classification and performs
|
||||
no workspace move or deletion.
|
||||
- Any uncertainty about path ownership, containment, active descendants, symlink traversal, or
|
||||
task activity keeps the directory and records a warning.
|
||||
- Any uncertainty about path ownership, containment, active descendants, owned control-path
|
||||
symlinks, or task activity keeps the directory and records a warning. Nested workspace symlinks
|
||||
are opaque entries: analysis, quarantine, and deletion never follow their targets.
|
||||
- A quarantine rename failure leaves the original directory untouched and records a failed entry
|
||||
only when the failure can be associated with a durable candidate ID.
|
||||
- A backend crash after rename but before the database update is reconciled at startup by scanning
|
||||
@@ -395,6 +406,13 @@ enabling host-global Docker cleanup require explicit UI confirmation and server-
|
||||
- **GIVEN** an unreferenced task directory older than the orphan grace period contains
|
||||
`node_modules`, **WHEN** workspace cleanup runs with a successful authoritative inventory,
|
||||
**THEN** the whole task root moves to quarantine and its measured bytes appear in the run result.
|
||||
- **GIVEN** task roots include active, recent orphan, and grace-eligible orphan directories,
|
||||
**WHEN** storage analysis runs, **THEN** total workspace bytes include every classified task root
|
||||
while active and reclaimable bytes remain separate subsets.
|
||||
- **GIVEN** archived or deleted tasks retain ready environment or active worktree rows for recovery,
|
||||
**WHEN** storage analysis or cleanup classifies their old directories, **THEN** those historical
|
||||
rows do not protect the directories from normal orphan grace and quarantine rules unless a live
|
||||
session of an unarchived task still borrows the environment.
|
||||
- **GIVEN** the worktree inventory query fails, **WHEN** workspace cleanup runs, **THEN** no task
|
||||
directory moves and the run reports the inventory error.
|
||||
- **GIVEN** a multi-repository task has one active descendant worktree, **WHEN** workspace cleanup
|
||||
@@ -416,6 +434,11 @@ enabling host-global Docker cleanup require explicit UI confirmation and server-
|
||||
reports the reclaimed bytes.
|
||||
- **GIVEN** `/root/.cache/go-build` was not explicitly adopted, **WHEN** storage cleanup runs,
|
||||
**THEN** Kandev does not modify it.
|
||||
- **GIVEN** `/root/.cache/go-build` is the service user's default Go cache and is not adopted,
|
||||
**WHEN** storage analysis runs, **THEN** its path and bytes are reported read-only while cleanup
|
||||
remains unavailable for that path.
|
||||
- **GIVEN** the Docker daemon reports image-layer usage, **WHEN** storage analysis runs, **THEN**
|
||||
image-layer bytes are shown separately from build-cache and managed-container writable bytes.
|
||||
- **GIVEN** an exited container has `kandev.managed=true` and its task is positively absent,
|
||||
**WHEN** container cleanup runs, **THEN** the container and its attached Kandev volumes are removed.
|
||||
- **GIVEN** an unrelated exited container exists, **WHEN** Kandev container cleanup runs, **THEN**
|
||||
|
||||
Reference in New Issue
Block a user