Bug
WorkspaceWorktreeCleanupEngine::worktree_cleanup_recent_activity_protection() protects worktrees from retention removal when metadata last_seen_at ?? observed_at is younger than RETENTION_RECENT_ACTIVITY_SECONDS (86400s).
But last_seen_at / observed_at are observation heartbeats written by the cleanup system's own scan passes (plan inventory, active-no-signal report, reconcile-metadata, drains). Every scan re-stamps them.
Consequence: self-deadlock
On any workspace scanned at least once per day, retention apply can never remove anything:
- Nightly active-no-signal run stamps
last_seen_at = T and marks rows cleanup_eligible with clean upstream-equivalence evidence.
- Operator runs
workspace cleanup plan --mode=retention → 7 reviewed worktree_removal rows.
workspace cleanup apply <run-id> revalidation blocks all 7 with recent_activity ("worktree metadata was seen N second(s) ago; retention cleanup requires at least 86400 second(s) without activity").
- Re-running the drain re-observes the rows, re-bumping the heartbeat — guaranteeing the next apply is blocked too.
Observed 2026-07-12 on a ~1100-worktree workspace: three full drain passes (retention plan/apply ×3, cleanup-eligible-drain, active-no-signal-drain, abandoned safe mode) removed 0 worktrees; 836 rows sit in active_no_signal and every finalized cleanup_eligible row is perpetually recent_activity-protected.
Fix shape
"Recent activity" must measure worktree activity, not observer activity. Options (either resolves the deadlock):
- Base the protection on actual worktree signals: HEAD reflog timestamp, index/worktree mtime, or lifecycle transitions (
finalized_at, cleanup_eligible_at) — explicitly excluding observed_at/last_seen_at.
- Or split the heartbeat: keep
observed_at/last_seen_at as scanner bookkeeping and introduce a distinct last_activity_at that only mutating operations (add, refresh-context, finalize, git operations through the workspace surface) update; point the protection at that field.
A row that is cleanup_eligible with clean upstream-equivalence evidence and no dirty/unpushed changes should not be protected merely because the scanner looked at it this morning.
AI assistance
- AI assistance: Yes
- Tool(s): Claude (Opus 4.5) via Kimaki/OpenCode
- Used for: Root-cause investigation and issue drafting from live drain evidence
Bug
WorkspaceWorktreeCleanupEngine::worktree_cleanup_recent_activity_protection()protects worktrees from retention removal when metadatalast_seen_at ?? observed_atis younger thanRETENTION_RECENT_ACTIVITY_SECONDS(86400s).But
last_seen_at/observed_atare observation heartbeats written by the cleanup system's own scan passes (plan inventory, active-no-signal report, reconcile-metadata, drains). Every scan re-stamps them.Consequence: self-deadlock
On any workspace scanned at least once per day, retention apply can never remove anything:
last_seen_at = Tand marks rowscleanup_eligiblewith clean upstream-equivalence evidence.workspace cleanup plan --mode=retention→ 7 reviewedworktree_removalrows.workspace cleanup apply <run-id>revalidation blocks all 7 withrecent_activity("worktree metadata was seen N second(s) ago; retention cleanup requires at least 86400 second(s) without activity").Observed 2026-07-12 on a ~1100-worktree workspace: three full drain passes (retention plan/apply ×3, cleanup-eligible-drain, active-no-signal-drain, abandoned safe mode) removed 0 worktrees; 836 rows sit in
active_no_signaland every finalizedcleanup_eligiblerow is perpetuallyrecent_activity-protected.Fix shape
"Recent activity" must measure worktree activity, not observer activity. Options (either resolves the deadlock):
finalized_at,cleanup_eligible_at) — explicitly excludingobserved_at/last_seen_at.observed_at/last_seen_atas scanner bookkeeping and introduce a distinctlast_activity_atthat only mutating operations (add, refresh-context, finalize, git operations through the workspace surface) update; point the protection at that field.A row that is
cleanup_eligiblewith clean upstream-equivalence evidence and no dirty/unpushed changes should not be protected merely because the scanner looked at it this morning.AI assistance