fix(daemon): event-driven worktree reconcile check so cleanup self-heals - #129
fix(daemon): event-driven worktree reconcile check so cleanup self-heals#129tbrownio wants to merge 3 commits into
Conversation
…f-heals The Issue-completed webhook was never subscribed on the live app, so the cleanup pipeline never ran and 32 worktrees (18 GB) accumulated. The new WorktreeReconciler sweeps at startup, on turn completion, and on Issue-completed events (no standing timer), confirms each worktree's issue state with Linear, and closes out resolved ones through the existing CleanupWorker path — preserving dirty or PR-less state to the agents/<id> branch (or a git bundle) before any force removal. Unlinked worktrees get a persisted 14-day grace clock (WORKTREE_UNLINKED_GRACE_DAYS); lookup errors always skip; foreign directories are never touched. Closes #128 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…inst live sessions Review fixes for PR #129: a recreated worktree now always gets a fresh unlinked grace period (durable directory identity resets the clock), and preserveAndRemove revalidates worktree identity/generation inside the WorktreeManager mutation lock, skipping removal when ensureWorktree interleaved — a session's live worktree can no longer be deleted from under it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review pass 2 follow-up for PR #129: CleanupWorker now captures a fresh worktree identity/generation snapshot after finalization and passes it to every removal; a generation change from an interleaving ensureWorktree preserves the live worktree and re-pends the job instead of deleting it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QA evidence — command-shaped pass (post-review, final tree @ 3dd4b02)Independent verifier run from Gates
Per-AC evidence (quoted test names)
Split
|
type: wrap-up-report
|
type: postmortem
|
| Step / dispatch | Start–End (min) | Dur | Tokens | Note |
|---|---|---|---|---|
| Step 0 preflight + load | 0–3 | 3m | overseer | bundle pull, hygiene scan (89 PRs, 0 Fixes lines) |
| code-researcher | 3–13.5 | 10.5m | 104,767 | overseer read sources in parallel |
| Plan draft | 13.5–17 | 3.5m | overseer | |
| plan-review p1→p3 (+fixes) | 17–25.5 | 8.5m | 156,756 | 3 passes: 4+2+1 MF, all folded |
| implementer r1 | 25–34 | 9m | 228,862 | full slice, suite green |
| backend-verifier v1 | 34.5–36.3 | 2m | 79,717 | fail: AC1 e2e gap + idempotency blocker |
| implementer r2 | 36.3–38.5 | 2m | Δ38,864 | two new tests |
| backend-verifier v2 | 38.6–42.5 | 4m | 49,202 | pass |
| Step 4 gate/commit/PR | 42.5–49.5 | 7m | overseer | PR #129 open at ~min 50 |
| code-review p1 | 43.2–47 | 4m | 80,479 | overlapped Step 4; 2 MF (races) |
| PR diagram (author+fix+host) | 44–56.5 | ~9m | overseer | fully overlapped with review/fix lanes |
| implementer r3 | 48–50.5 | 2.5m | Δ63,556 | MF-1 fixed; MF-2 partially |
| code-review p2 | 52–57 | 5m | 74,865 | MF-1 verified; MF-2 persists (session path) |
| implementer r4 | 54–57.5 | 3.5m | Δ74,742 | snapshot guard in CleanupWorker |
| code-review p3 | 57.7–59.2 | 1.5m | 48,552 | Approve, 0 findings |
| QA backend-verifier | 59.6–62 | 2.5m | 63,670 | final-tree re-proof, all ACs |
| Step 6 wrap-up | 62–63.5 | 1.5m | overseer | label, notify, bundle |
Aggregates: plan phase ≈ 40% of wall-clock (research-heavy by design at
zone 1 full lane), implement+verify ≈ 27%, post-PR review+QA ≈ 30%, wrap-up
≈ 3%. Summed overseer turnaround between dispatches ≈ 4 min (report pickup +
next dispatch authoring). Overlap discipline was good: the PR diagram and
Step 4 ran inside review/fix lanes' shadow.
Stalls: none. Blocker inventory: no AskUserQuestion gates, no rate
limits; all waits were legitimate detached-dispatch waits (the two long
foreground polls — 10 min researcher, 15 min implementer window — were
tracked waiters, not idle). Two minor operational blemishes: (1) one Codex
dispatch launch failed silently-ish because the overseer's cwd had drifted
to the excalidraw skill directory and the relative .codex-dispatches/...
paths didn't resolve — caught immediately, ~1 min lost; (2) the Step 3
verifier round-trip (fail→fix→re-verify) cost ~8 min for two test-coverage
gaps a sharper implementer test contract could have prevented.
Single change with the biggest payoff: pin dispatch artifact paths to the
repo root so cwd drift can't break a launch (proposal below).
What we asked for
Fix the never-fired worktree cleanup (Issues webhook never subscribed; 32
worktrees / 18 GB / disk 93%): enable-webhook as a human task plus an
event-driven reconcile check with preserve-then-remove and a 14-day grace
for unlinked worktrees, per brief D1/D2 and AC1–AC8.
Outcome vs intended
On-target — no outcome gap known at wrap-up (all 8 ACs proven; review ended
Approve). The outcome half defers to the human's PR review.
What to change so it doesn't recur
-
claude/skills/codex/SKILL.md— the Execute section's launch
instructions assume the harness cwd is the repo root, but other skills
(e.g. the excalidraw renderer) legitimatelycdelsewhere; one dispatch
this run wrote its prompt/launcher to a nonexistent relative path and
died at launch. Proposed edit — in "### 2. Execute", after "Launch every
dispatch fully detached from the harness, from the repo root.", add:Resolve the owner directory to an absolute path once
(dir="$(git rev-parse --show-toplevel)/.codex-dispatches/$own") and
use it in the prompt/launcher/marker paths — a previous tool may have
moved the shell's cwd, and a relative.codex-dispatches/...then
fails the launch silently. -
references/agents/implementer/instructions.md— both Step 3
verifier failures were the same shape: the implementer's tests proved a
stage's enqueue but not the end state, and proved concurrent
coalescing but not sequential idempotency. Proposed addition to its
testing guidance:When a pipeline hands off between workers, at least one test drives the
complete chain to its observable end state (not the handoff), and
any at-least-once worker gets a sequential double-invoke test (a
second run over settled state is a no-op) — concurrency coalescing
tests don't cover it.
Neither change is applied here; both await the human via /postmortem-loop.
Dial record & right-sizing
zone: 1
lanes: single-codex
passes: {plan: 3/3, post_pr: 3/3}
findings: {plan: {pass1: {codex: 5, claude: 0}, later: {codex: 4, claude: 0}},
post_pr: {pass1: {codex: 2, claude: 0}, later: {codex: 1, claude: 0}}}
verifiers: {frontend: disabled_by_item, qa_pass: ran}
qa_findings: 0
wall_clock: 1:03
deviations: none
pr_size: {files_changed: 19, additions: 1401, deletions: 42}
tokens:
codex: {total: 1064032, by_role: {code_researcher: 104767, plan_reviewer: 156756,
implementer: 406024, backend_verifier: 192589, code_reviewer: 203896}}
claude_subagents: 0
overseer: 20062993 # includes cache reads; output tokens 114855
total: 21127025
spend_ratio: 14641.0
agents:
- {role: code-researcher, model: gpt-5.6-sol, effort: low, dispatches: 1, wall_clock: "10:30", tokens: 104767}
- {role: plan-reviewer, model: gpt-5.6-sol, effort: low, dispatches: 3, wall_clock: "8:30", tokens: 156756}
- {role: implementer, model: gpt-5.6-sol, effort: medium, dispatches: 4, wall_clock: "17:00", tokens: 406024}
- {role: backend-verifier, model: gpt-5.6-sol, effort: low, dispatches: 3, wall_clock: "8:30", tokens: 192589}
- {role: code-reviewer, model: gpt-5.6-sol, effort: low, dispatches: 3, wall_clock: "10:30", tokens: 203896}Right-sizing judgment: review effort right-sized — every pass in
both loops yielded findings (plan: 5/3/1; post-PR: 2/1/0 with the final pass
the required convergence proof), so no pass was pure spend; the dial that
would have changed the outcome most is none — zone 1 single-lane was exactly
enough for a change whose two real bugs (a grace-clock reuse and a
deletion race) were both caught by the Codex lane alone.
System changes
Posted as comments on: PR #129 and issue #128 (URLs recorded after posting).

Summary — What / Why / How
What. Adds an event-driven worktree reconcile check (
WorktreeReconciler) to the Linear-agent daemon, plus preserve-then-remove semantics for every worktree close-out, so cleanup no longer depends on a single manually configured webhook subscription.Why. The cleanup pipeline has never fired once: the
Issue completedtrigger (daemon/src/eventlog.tsappend path) requires Issues data-change webhooks, which were never enabled on the livebloom-implementerapp — the events table holds 193AgentSessionEventrows and zeroIssuerows,cleanup_jobsis empty since provisioning, and 32 worktrees (18 GB) have accumulated on a 48 GB root volume now at 93%. A full disk halts every agent pipeline on the host (#128, artifact bundle linked from the issue).How. A new worker sweeps at daemon startup, on turn completion, and on Issue-completed events — no standing timer. It enumerates on-disk worktrees plus retained cleanup jobs, confirms each issue's workflow state via a new
LinearGatewayquery, and:CleanupWorkerfinalize-then-remove path;WORKTREE_UNLINKED_GRACE_DAYS), removal only after expiry;agents/<id>branch, git-bundle fallback (WORKTREE_BUNDLES_DIR) — force removal is always reversible. Terminal retention now happens only when preservation itself fails.SessionWorker,CleanupWorker, and the reconciler now share oneWorktreeManagerinstance, so creation and removal serialize on a single mutation lock.Done means: a resolved issue's worktree and
agents/<id>branch are removed on the next trigger even with the Issues webhook unsubscribed; nothing is deleted without positive confirmation or grace expiry; nothing dirty or unpushed is deleted without a preserved copy; the webhook fast path is unchanged; all proven inpnpm test.Visual overview
Before → after lifecycle of the cleanup path (the
.excalidrawsource lives in the run's artifact bundle):User journeys
Operator/lifecycle change — no end-user UI. The journeys are daemon flows:
Coverage gaps: none — every branch above has an automated test; J7's live half is deploy-time.
Verification
Independently re-proven by a fresh verifier pass over the final tree (
pnpm typecheck,pnpm build,pnpm test: 27 files passed, 392 tests passed / 1 skipped — the skip is the pre-existingproxy-integrationtest, unrelated):worktree-reconciler.test.ts› "closes a resolved session-backed worktree end-to-end without an Issue webhook": job reachesdone, worktree path absent,refs/heads/agents/ENG-1absent.server.test.ts(18 tests) incl. signed Issue-completed POST → persisted event + callback; non-completed events don't fire it.config.test.ts(20 tests) coversWORKTREE_UNLINKED_GRACE_DAYS.startedstate).pnpm test) — ✅ full suite green including the 11-case reconciler matrix.pnpm typecheck && pnpm build && pnpm test+bash -nover the 7 ops scripts, all exit 0.Manual tests
Must (breaks data if wrong):
agents/<id>branch removed on startup sweep — left to human: live-host deploy stepgit log origin/agents/<id>or the bundle file) and the notification names it — left to human: live-host deploy stepworktree_reconcilelog shows skips — left to human: live-host deploy stepImportant:
df -h /improved; remaining worktrees are only unresolved issues + within-grace unlinked dirs — left to human: live-host deploy stepsudo daemonctl statushealthy after deploy; no error spam from the reconciler in service logs — left to human: live-host deploy stepAreas not affected: webhook signature verification, session scheduling/turn execution, artifact store, OTLP relay, ack worker, Linear MCP monitor.
QA results
Command-shaped QA executed post-review over the final tree (evidence in the QA proof comment): 0 of 7 Manual-test items are machine-runnable (all are live-host deploy steps — left to the human); the full automated proof re-ran green —
pnpm typecheck,pnpm build,pnpm test(394 passed / 1 pre-existing skip), all 8 ACs re-quoted including both review-fix regressions (fresh grace clock on recreated worktrees; live session worktree survives an interleaved cleanup). Review loop: 3 passes, 3 Must Fix found → fixed → verified, final verdict Approve with zero findings. No bugs surfaced by QA.Deploy notes
Ordered; nothing here blocks verification (all evidence above ran against local fixtures).
linear-agenthost (gcloud compute ssh linear-agent --project=bloom-agents --zone=us-central1-a): fetch/review/fast-forward/opt/orchestra-sourceto the merged commit, thensudo daemonctl reload(runbook procedure). The restart fires the first reconcile sweep, which reclaims the resolved subset of the 32-worktree backlog and starts grace clocks for unlinked directories. Expect a burst ofworktree_reconciledlog lines and preservation pushes toagents/<id>branches.bloom-implementerapp → webhooks → check Issues under Data change events (docs/linear-agent-daemon-setup.md §Data change events; this restores the fast path — the reconciler is the safety net either way).worktree_unlinked_observationsinevents.db, created byCREATE TABLE IF NOT EXISTSat daemon startup. Additive; no external migration, no consumer-facing schema.WORKTREE_UNLINKED_GRACE_DAYS(default 14) andWORKTREE_BUNDLES_DIR(default<db-dir>/worktree-bundles) — documented indaemon/ops/runbook.md.agents/<id>branches on the target repo's origin; prune them at leisure once inspected (they exist so every deletion is reversible).Residual risks
issue(id: <identifier>)error shape for "entity not found" vs transient failures is classified defensively (anything non-definitive skips); if Linear's wording shifts, the failure mode is "worktree retained longer", never "worktree deleted".agents/<id>branches in one burst; a credential problem on the host's checkout degrades to bundle files beside the DB (disk-cheap, logged), not to data loss.feat/frontend-verifier-ios-simulator) predates this item — the branch had no open PR and its previous PR is merged; kept per pipeline rules (no new branches). Content is solely this fix.Closes #128
🤖 Generated with Claude Code