Skip to content

Retention recent_activity protection is fed by the scanner's own heartbeat, deadlocking cleanup #886

Description

@chubes4

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:

  1. Nightly active-no-signal run stamps last_seen_at = T and marks rows cleanup_eligible with clean upstream-equivalence evidence.
  2. Operator runs workspace cleanup plan --mode=retention → 7 reviewed worktree_removal rows.
  3. 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").
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions