Skip to content

fix: reclaim stranded worktrees and stop repeating issue status comments - #180

Merged
jeremymcs merged 1 commit into
mainfrom
fix/worktree-janitor-and-comment-spam
Aug 20, 2026
Merged

fix: reclaim stranded worktrees and stop repeating issue status comments#180
jeremymcs merged 1 commit into
mainfrom
fix/worktree-janitor-and-comment-spam

Conversation

@jeremymcs

Copy link
Copy Markdown
Owner

Summary

Reported from the field: ❌ Issue work failed — Reason: Refusing to reclone repo cache while 22 registered worktree(s) still exist, repeated dozens of times on the same issues. That is two separate bugs compounding.

Worktrees were never reclaimed after a crash

removePrWorktree is called from finally blocks in babysitter.ts, issueWorkAgent.ts, and ciHealingAgent.ts, which covers the success and throw paths — but not the process dying. A crash, kill, or machine restart mid-run stranded the worktree, and nothing swept it: worktreeRootDir appeared only in paths.ts (definition) and repoWorkspace.ts (path construction).

They only accumulated. Once the cache needed a reclone, assertRepoCacheCanBeRecloned refused and that repo's automation was dead until a human intervened. git worktree prune could not save it — prune only clears registrations whose directory is already gone, and these directories still existed, so the count never dropped.

This is the exact failure class #174 set out to eliminate, and the retry policy made it visible: the error classifies as retryable, so it retried, parked, and the recovery sweep revived it hourly forever.

Orphans are now reclaimed in three places:

  • On startup with includeActive, because instanceLock guarantees no worktree can legitimately be in use.
  • On the watcher tick, for anything this process is not actively holding — live worktrees are tracked in activeWorktreePaths and skipped.
  • Before refusing a reclone, so a cache that is already stuck heals itself without a redeploy.

A registration whose gitdir pointer cannot be resolved still refuses rather than guessing, and the existing safety tests for that and for active workspaces still pass unchanged.

Issue status comments repeated instead of updating

postIssueWorkStatusComment called createComment unconditionally — no marker, no dedupe. Every recovery cycle added a fresh "started" and a fresh "failed" notice, so a permanently failing issue collected identical comments indefinitely.

The PR path already solved this with an anchored reply it edits in place (postStatusReplyForFeedbackItem / updateStatusReply); issues never got the same treatment. They now carry a hidden <!-- patchdeck:issue-work-status:owner/repo#N --> marker and update the existing comment. Because in-place editing would otherwise hide that anything retried, the comment carries an attempt count.

Test isolation

The new sweep does real git and filesystem work, which made the watcher tests flaky by touching the real PatchDeck home. It is injectable as reclaimOrphanedWorktreesFn and stubbed across the runtime tests.

Verification

  • npm run check — clean
  • npm run test:all — 809 tests, 0 failures (ran three times to confirm the flakiness is gone)
  • npx eslint . — clean
  • npm run build — succeeds

New coverage: reclaiming a stranded worktree so a blocked reclone proceeds; the sweep reporting what it cleared and being idempotent; an in-flight worktree surviving the sweep; and three consecutive failing issue-work runs producing exactly one comment instead of three.

Related Issue

  • None

Two faults combined to strand a repo permanently and then announce it dozens of
times on the issue.

Worktree cleanup only ran in a `finally` inside each run, which covers the
success and throw paths but not the process dying. A crash, a kill, or a machine
restart mid-run left the worktree registered forever, and nothing swept it —
`worktreeRootDir` was referenced only where paths are built. Once enough leaked,
`assertRepoCacheCanBeRecloned` refused to heal the cache and that repo's
automation stopped for good. `git worktree prune` could not help, because it
only clears registrations whose directory is already gone.

Orphans are now reclaimed: on startup, where the single-instance lock proves
none can legitimately be in use; on the watcher tick, for any worktree this
process is not actively holding; and before refusing a reclone, so an already
stuck cache heals itself. Worktrees belonging to live runs are tracked and never
touched, and an unresolvable registration still refuses rather than guessing.

Issue status comments were posted with `createComment` every time, with no
marker and no dedupe. Each recovery cycle added a fresh "started" and "failed"
notice, so a permanently failing issue accumulated identical comments — the PR
side already solved this with an anchored reply it edits in place. Issue
comments now carry the same kind of hidden marker and update the existing
comment, with an attempt count so a reader can still tell it retried.

The runtime's worktree sweep is injectable and stubbed in the runtime tests, so
the suite never touches the real PatchDeck home.

Verified: npm run check, npm run test:all (809 tests), npx eslint ., npm run build.
@github-actions

Copy link
Copy Markdown
Contributor

📄 Documentation Review Reminder

This PR changes source files but does not update any public documentation.

Changed source files:

server/appRuntime.test.ts
server/appRuntime.ts
server/backgroundJobHandlers.test.ts
server/backgroundJobHandlers.ts
server/issueFormatter.ts
server/repoWorkspace.test.ts
server/repoWorkspace.ts

Please check if documentation in docs/public/ needs to be updated:

  • No documentation update needed
  • Documentation updated in this PR
  • Follow-up documentation issue created

Public docs are Markdown files in docs/public/. They are automatically
rendered to HTML and deployed to GitHub Pages when merged to main.

Available docs: getting-started.md, pr-babysitter.md, agent-dispatch.md, configuration.md, pr-questions.md

@jeremymcs
jeremymcs merged commit 2b75dd1 into main Aug 20, 2026
8 checks passed
@jeremymcs jeremymcs mentioned this pull request Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant