Skip to content

Fix pause-abort worktree-slot leak + retry storm; add board auto-recovery - #1687

Merged
gsxdsm merged 5 commits into
mainfrom
fix/pause-abort-worktree-leak-storm
Jun 20, 2026
Merged

Fix pause-abort worktree-slot leak + retry storm; add board auto-recovery#1687
gsxdsm merged 5 commits into
mainfrom
fix/pause-abort-worktree-leak-storm

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

What & why

A manual global pause → resume could stall the entire board. We hit it live: ~93 task:failed vs 1 task:merged in an hour, zero real throughput, the queue concurrency-starved behind stuck holders.

Root cause (full chain)

  1. The global-pause handler (executor.ts:2814-2880) marks sessions pausedAborted and disposes them but releases no worktree/semaphore/lease slot.
  2. When the aborted graph run unwinds, handleGraphFailure parks the task status:"failed" ("operator action required") and the early-return path never deletes this.activeWorktrees — so a task that's been re-queued to todo keeps pinning its worktree slot (the "FN-6756 in todo yet still maxWorktrees=3/3 holder" symptom). That concurrency-blocks the whole queue.
  3. On re-dispatch, execute() doesn't clear pausedAborted, so genuinePauseAbort re-fires and re-parks instantly, no backoff → the 75×/hour retry storm.

The fix

  • Leak + storm (FN-6782): in handleGraphFailure, a pause-abort that left the task in todo is now treated as benign — it is not parked failed, it clearPausedAborted(task.id) so the next dispatch is clean, and it releases the leaked worktree slot (activeWorktrees.delete). The operator-action failed park is preserved for genuinely stranded non-todo columns (FN-6478 intact). Placed in the terminal handleGraphFailure branch — deliberately not the execute() finally, which the graph seam re-enters mid-run.
  • Auto-recovery (recoverPausedAbortFailures) — new self-healing batch-2 sweep that clears any pause-abort park still on the board (status:"failed" + "operator action required") and requeues it for normal scheduling, so the board self-heals without operator intervention. Respects the existing globalPause/enginePaused skip guard.

Correction worth noting

The initial theory was that recovered tasks parked with status:null were unschedulable. Verified from code (scheduler.ts:1288) that the dispatch set is column==="todo" && !pausedstatus:null is runnable; status:"queued" is the blocked marker. So no requeue-path change was needed; the queue stall was 100% the leaked worktree slots, not status. (R3 dropped after this finding.)

Deferred (follow-up)

  • Leaked-slot reaper (A2): a maintenance routine that reclaims slots whose holder is no longer in-progress was scoped but not shipped — doing it safely needs a new executor listWorktreeHolders() introspection API (today self-healing can only see getExecutingTaskIds(), already cleared by the time a slot leaks). The leak is now closed at its source (this PR) and parks auto-recover, so the incident is covered; the reaper is defense-in-depth for any future leak path. Tracked as a follow-up.

Tests / verification (independently re-run)

  • pnpm --filter @fusion/engine typecheckclean.
  • New: executor-paused-abort-todo-benign.test.ts, self-healing-paused-abort-recovery.test.ts.
  • New + regression (executor-pause, self-healing-in-progress-limbo) → 95 passed, 0 failed (re-run by the reviewer, not just the author).

Operational note for the live board

The fix only takes effect on the running engine after a restart. Until this merges and the engine restarts, keep the three poisoned tasks (FN-6749/6750/6706) paused — unpausing them on an unfixed engine re-triggers the storm.

🤖 Generated with Claude Code


Open in Stage

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed a pause/resume abort edge case that could leave tasks marked with “operator action required,” triggering retry storms and queue slowdowns.
    • Improved handling when pause-abort failures are requeued to todo, including clearing paused-abort markers and reclaiming leaked in-memory worktree/concurrency slots.
  • New Features

    • Added self-healing sweeps to recover paused-abort parks and reap leaked concurrency slots, with enhanced audit event visibility.
  • Tests

    • Added regression and self-healing coverage for benign requeue, recovery behavior, leak reaping, and global pause protections.

A global pause/resume cycle parked tasks that had re-queued to todo as
status:"failed" ("operator action required") and leaked their in-memory
worktree slot. The scheduler kept re-dispatching the todo task, the
genuine-pause-abort branch re-fired on the still-set pausedAborted marker,
and it re-parked instantly with no backoff — a retry storm (75x/hr) that
pinned maxWorktrees=3/3 and concurrency-starved the whole queue.

- R1+R2 (executor.ts handleGraphFailure): treat a pause-abort that left a
  task in `todo` as benign (FN-6782) — don't park failed, clear the
  pausedAborted marker so the next dispatch is clean, and release the
  leaked activeWorktrees slot. Operator-action failure preserved for
  genuinely stranded non-todo columns (FN-6478).
- A1 (self-healing.ts recoverPausedAbortFailures): new maintenance sweep
  that auto-recovers any pause-abort park still on the board and requeues
  it (status:null = schedulable) so the board self-heals.
- run-audit.ts: new mutation types for the recovery telemetry.

Corrected the spec's null-vs-queued assumption: the scheduler dispatch set
is column==="todo" && !paused (scheduler.ts:1288); status:"queued" is the
*blocked* marker, status:null is runnable — so recovered tasks are left null.

Deferred (documented): A2 leaked-slot reaper needs a new executor
listWorktreeHolders introspection API to reap in-memory worktree slots
safely; R1 closes the observed leak at its source.

Tests: self-healing-paused-abort-recovery.test.ts (3),
executor-paused-abort-todo-benign.test.ts (2). Engine typecheck clean;
106 existing pause/graph-failure/limbo tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Fixes a pause-abort failure mode in TaskExecutor.handleGraphFailure where tasks re-queued to todo were incorrectly parked as status:"failed", leaked in-memory worktree slots, and triggered retry storms. Adds two new DatabaseMutationType literals, a benign early-return path in the executor that clears leaked worktree slots, exports listWorktreeHolders() for self-healing cross-checks, and introduces two new SelfHealingManager sweeps: recoverPausedAbortFailures to reheal lingering operator-action parks, and reapLeakedConcurrencySlots to reclaim leaked in-memory slots. Tests cover all new behaviors.

Changes

Pause-abort benign handling and self-healing recovery

Layer / File(s) Summary
Contract and type extensions
packages/engine/src/run-audit.ts, packages/engine/src/self-healing.ts
DatabaseMutationType gains "task:auto-recover-paused-abort-park" and "task:reap-leaked-concurrency-slot"; SelfHealingOptions adds listWorktreeHolders() callback and updates clearPhantomExecutorBinding return type to `boolean
Executor benign early-return for todo pause-abort
packages/engine/src/executor.ts, packages/engine/src/__tests__/executor-paused-abort-todo-benign.test.ts, packages/engine/src/__tests__/executor-recovery.test.ts
handleGraphFailure adds a branch that clears pausedAborted state, deletes activeWorktrees[taskId] to release the slot, persists token usage, and returns early without status:"failed" when the task has re-queued to todo. Exports listWorktreeHolders() for self-healing worktree-holder inspection. Tests assert benign vs. operator-action outcomes for todo and in-review columns; updates paused graph-exit assertions in executor-recovery tests.
recoverPausedAbortFailures sweep
packages/engine/src/self-healing.ts, packages/engine/src/__tests__/self-healing-paused-abort-recovery.test.ts
New method scans for pause-abort-signature failed parks, skips executing/in-review/paused tasks, clears status/error for todo parks, moves in-progress parks to todo with preserveProgress/recoveryRehome, releases executor binding, and emits audit events. Wired into batch 2. Tests cover todo clearance, in-progress rehome, skip conditions, and global-pause self-guard.
reapLeakedConcurrencySlots sweep
packages/engine/src/self-healing.ts, packages/engine/src/__tests__/self-healing-leaked-slot-reaper.test.ts
New method iterates in-memory worktree holders, skips executing ones, reclaims slots for missing/todo/triage tasks beyond grace window when clearPhantomExecutorBinding() succeeds, and logs reclaimed count. Wired into batch 2. Tests verify reclamation for aged holders, skip conditions, orphan-holder handling, race-condition safety, and global-pause self-guard.
Runtime integration
packages/engine/src/runtimes/in-process-runtime.ts
Wires listWorktreeHolders callback into SelfHealingManager options, delegating to executor or empty array.
Changeset entry
.changeset/fix-pause-abort-leak-storm.md
Documents the behavioral fix and new self-healing sweeps for @runfusion/fusion patch release.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A task stuck in a storm-loop, oh dear!
Re-queued to todo but failing year after year.
Now the bunny clears the slot just right,
Self-healing sweeps banish the blight—
Leaked concurrency freed and parks restored,
Tasks hop forward as recovery's reward! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly summarizes the main changes: fixing a pause-abort worktree-slot leak and retry storm, and adding board auto-recovery. It aligns with the PR's primary objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pause-abort-worktree-leak-storm

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ghost

ghost commented Jun 20, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 5 individual chapters for you:

Title
1 Define shared constants and audit types
2 Fix pause-abort leak and retry storm
3 Implement pause-abort auto-recovery sweep
4 Add leaked concurrency slot reaper
5 Document changes with changeset
Open in Stage

Chapters generated by Stage for commit 7f0ad62 on Jun 20, 2026 3:54am UTC.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
packages/engine/src/__tests__/executor-paused-abort-todo-benign.test.ts (1)

72-85: ⚡ Quick win

Add an assertion for benign-path token-usage persistence.

The test covers status/log/worktree/marker behavior, but not the token-usage persistence contract described for this fix.

Suggested assertion
     await invokeGraphFailure(executor, task);
@@
     expect((executor as any).activeWorktrees.has(task.id)).toBe(false);
+    expect(store.recordTokenUsage).toHaveBeenCalled();

Based on PR objectives, the benign early-return path should also persist token usage; asserting it here hardens regression coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/src/__tests__/executor-paused-abort-todo-benign.test.ts`
around lines 72 - 85, Add an assertion to verify token usage persistence in the
benign early-return path test. After the existing expect statements for
parkedFailed, logText, pausedAborted, and activeWorktrees, add a new assertion
that checks the store.updateTask mock calls to confirm token usage was persisted
when invokeGraphFailure completes via the benign path. The assertion should
follow the same pattern as parkedFailed and verify that the token usage data was
properly passed to store.updateTask during this benign scenario.
packages/engine/src/self-healing.ts (1)

7878-7892: ⚡ Quick win

Apply FNXC_LOG formatting to the new recovery docblock.

This new comment block should use the required FNXC:Area-of-product yyyy-MM-dd-hh:mm format so it remains grep-friendly and policy-compliant.

As per coding guidelines, "Add FNXC_LOG comments describing the date (yyyy-MM-dd-hh:mm format)... Write FNXC:Area-of-product in front of all comments so they can be grepped."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/src/self-healing.ts` around lines 7878 - 7892, The docblock
comment describing the pause-abort auto-recovery logic in the failed-task
recovery section is missing the required FNXC_LOG format. Add the
FNXC:Area-of-product timestamp prefix in the format FNXC:Area-of-product
yyyy-MM-dd-hh:mm at the beginning of the docblock comment (after the /**
opening) to make it grep-friendly and policy-compliant with the coding
guidelines.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/engine/src/__tests__/executor-paused-abort-todo-benign.test.ts`:
- Around line 74-84: The inline comments in the test section do not follow the
required repository policy for comment formatting. Update all four inline
comments in the test block (the ones preceding the parkedFailed variable
declaration, the logText expect call, the pausedAborted.has call, and the
activeWorktrees.has call) by prefixing each comment with the appropriate
FNXC:Area-of-product tag according to the coding guidelines specified for
packages/**/*.{ts,tsx,js} files.

In `@packages/engine/src/run-audit.ts`:
- Around line 462-465: The comments for the new audit mutation entries
"task:auto-recover-paused-abort-park" and "task:reap-leaked-concurrency-slot" do
not follow the required FNXC-formatted comment style for files in the packages
directory. Update both comments to include the FNXC:Area-of-product prefix
followed by the appropriate FNXC_LOG date and requirement context, following the
established coding guidelines for packages/**/*.ts files. Replace the current
informal comment format with comments that start with FNXC: and include proper
dated requirement tracking.

In `@packages/engine/src/self-healing.ts`:
- Around line 7944-7954: The recordRunAuditEvent call in the self-healing
recovery block can throw an error that gets caught by the outer per-task catch
block, incorrectly marking the recovery as failed and preventing the recovered
counter from being incremented. Wrap the recordRunAuditEvent call in its own
try/catch block to make audit emission best-effort and isolated from the core
recovery logic. Keep the recovered++ increment outside and after this isolated
audit try/catch so that successful state mutations are always counted even if
audit fails.
- Around line 7922-7934: The guard condition at line 7924 validates the recovery
predicate using a potentially stale executingIds snapshot, but the mutations
(updateTask and moveTask calls on lines 7930-7936) are applied without
re-validating after the await operations. If executingIds changes between the
guard check and the mutation calls, an ineligible task could still be moved.
Re-validate the full recovery predicate on the fresh task object immediately
before calling updateTask, ensuring the task is still not in executingIds and
meets all other eligibility criteria at the moment of mutation.

---

Nitpick comments:
In `@packages/engine/src/__tests__/executor-paused-abort-todo-benign.test.ts`:
- Around line 72-85: Add an assertion to verify token usage persistence in the
benign early-return path test. After the existing expect statements for
parkedFailed, logText, pausedAborted, and activeWorktrees, add a new assertion
that checks the store.updateTask mock calls to confirm token usage was persisted
when invokeGraphFailure completes via the benign path. The assertion should
follow the same pattern as parkedFailed and verify that the token usage data was
properly passed to store.updateTask during this benign scenario.

In `@packages/engine/src/self-healing.ts`:
- Around line 7878-7892: The docblock comment describing the pause-abort
auto-recovery logic in the failed-task recovery section is missing the required
FNXC_LOG format. Add the FNXC:Area-of-product timestamp prefix in the format
FNXC:Area-of-product yyyy-MM-dd-hh:mm at the beginning of the docblock comment
(after the /** opening) to make it grep-friendly and policy-compliant with the
coding guidelines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e15634d-6fb7-4bb7-a52a-60ed2a9c9fc0

📥 Commits

Reviewing files that changed from the base of the PR and between 711bf3e and 9643563.

📒 Files selected for processing (6)
  • .changeset/fix-pause-abort-leak-storm.md
  • packages/engine/src/__tests__/executor-paused-abort-todo-benign.test.ts
  • packages/engine/src/__tests__/self-healing-paused-abort-recovery.test.ts
  • packages/engine/src/executor.ts
  • packages/engine/src/run-audit.ts
  • packages/engine/src/self-healing.ts

Comment thread packages/engine/src/__tests__/executor-paused-abort-todo-benign.test.ts Outdated
Comment thread packages/engine/src/run-audit.ts Outdated
Comment thread packages/engine/src/self-healing.ts Outdated
Comment thread packages/engine/src/self-healing.ts Outdated
@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the global-pause → resume stall that starved the live board: handleGraphFailure was parking re-queued todo tasks as status:"failed" and leaking their activeWorktrees slot, producing an instant retry storm and pinning maxWorktrees=3/3 until restart. Two new self-healing sweeps add auto-recovery and defense-in-depth for any future leak path.

  • Core fix (FN-6782): handleGraphFailure now classifies a pause-abort whose task landed back in todo as benign — clears pausedAborted, deletes the activeWorktrees slot, and returns without parking failed. The operator-action park is preserved for genuinely stranded non-todo columns per FN-6478.
  • recoverPausedAbortFailures: batch-2 sweep that identifies status:\"failed\" pause-abort parks, clears status/error to null, and rehomes in-progress stragglers back to todo; guarded by globalPause/enginePaused at method entry, fresh re-read before mutation, and audit emission.
  • reapLeakedConcurrencySlots: defense-in-depth sweep that cross-checks every activeWorktrees holder against its task column and reclaims slots whose holder is no longer legitimately in-progress, gated by clearPhantomExecutorBinding's live-session refusal.

Confidence Score: 5/5

Safe to merge — the benign-todo early-return in handleGraphFailure is tightly scoped, the double-check re-read guards in recoverPausedAbortFailures are thorough, and the leaked-slot reaper's conservative column/grace/refusal gating makes it safe to run continuously.

The three-part fix correctly closes the leak at its source, the auto-recovery sweep has the globalPause guard and fresh-re-read race protection that were flagged in earlier review rounds, and both new methods are covered by focused tests including a mid-sweep TOCTOU case. Minor observability gaps in reapLeakedConcurrencySlots do not affect correctness.

packages/engine/src/self-healing.ts — the reapLeakedConcurrencySlots method has two small observability gaps: the task:reap-leaked-concurrency-slot audit type is never emitted, and a refused clearPhantomExecutorBinding call goes unlogged.

Important Files Changed

Filename Overview
packages/engine/src/executor.ts Core fix: handleGraphFailure now treats a pause-abort that leaves a task in todo as benign — clears the pausedAborted marker, deletes the leaked activeWorktrees slot, and returns early without parking failed. New listWorktreeHolders() exposes a read-only snapshot for the reaper. Logic is sound.
packages/engine/src/self-healing.ts Two new sweep methods: recoverPausedAbortFailures (complete with outer try/catch, globalPause guard, double-check re-read, audit emission) and reapLeakedConcurrencySlots (task:reap-leaked-concurrency-slot audit type defined but never emitted; clearPhantomExecutorBinding return value discarded without logging). PAUSE_ABORT_PARK_* constants shared with executor correctly.
packages/engine/src/run-audit.ts Adds task:auto-recover-paused-abort-park (emitted by recoverPausedAbortFailures) and task:reap-leaked-concurrency-slot (defined but never emitted from reapLeakedConcurrencySlots — dead type).
packages/engine/src/runtimes/in-process-runtime.ts Wires listWorktreeHolders callback from executor to SelfHealingManager options with null-safe fallback. Minimal and correct change.
packages/engine/src/tests/executor-paused-abort-todo-benign.test.ts New test covering the benign todo requeue path: verifies no failed park, pausedAborted cleared, activeWorktrees slot released, and log message present. Also covers the still-parked in-review case.
packages/engine/src/tests/executor-recovery.test.ts Existing parameterized test correctly split into separate cases matching the new branching behaviour — todo is now benign, done still surfaces via log only.
packages/engine/src/tests/self-healing-paused-abort-recovery.test.ts Comprehensive tests for recoverPausedAbortFailures covering todo/in-progress recovery, skip predicates, and self-guard under globalPause.
packages/engine/src/tests/self-healing-leaked-slot-reaper.test.ts Good coverage including mid-sweep TOCTOU guard, grace window, global-pause skip, and orphaned-task reap.
.changeset/fix-pause-abort-leak-storm.md Correct patch bump for @runfusion/fusion per AGENTS.md. Accurately describes the three components of the fix.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Global Pause triggered] --> B[pausedAborted marker set on session]
    B --> C[handleGraphFailure called]
    C --> D{live.column?}

    D -->|todo| E[FN-6782 benign path]
    E --> F[clearPausedAborted]
    F --> G[activeWorktrees.delete]
    G --> H[Log benign + persistTokenUsage]
    H --> I[Return — no failed park]

    D -->|non-todo| J[Build park message with shared constants]
    J --> K{done/archived?}
    K -->|No| L[updateTask status:failed]
    K -->|Yes| M[Log only — terminal column]

    subgraph batch2[Batch-2 self-healing sweeps]
        N[recoverPausedAbortFailures] --> O{globalPause?}
        O -->|Yes| P[return 0]
        O -->|No| Q[listTasks — find park markers]
        Q --> R[re-read fresh + re-validate]
        R --> S[updateTask status:null error:null]
        S --> T{column todo?}
        T -->|No| U[moveTask to todo]
        T -->|Yes| V[clearPhantomExecutorBinding]
        U --> V
        V --> W[recordRunAuditEvent]

        X[reapLeakedConcurrencySlots] --> Y{globalPause?}
        Y -->|Yes| Z[return 0]
        Y -->|No| AA[listWorktreeHolders]
        AA --> AB[check column + grace + fresh executingIds]
        AB --> AC[clearPhantomExecutorBinding]
        AC --> AD{returned true?}
        AD -->|No| AE[skip]
        AD -->|Yes| AF[reaped++ logEntry]
    end

    L --> batch2
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Global Pause triggered] --> B[pausedAborted marker set on session]
    B --> C[handleGraphFailure called]
    C --> D{live.column?}

    D -->|todo| E[FN-6782 benign path]
    E --> F[clearPausedAborted]
    F --> G[activeWorktrees.delete]
    G --> H[Log benign + persistTokenUsage]
    H --> I[Return — no failed park]

    D -->|non-todo| J[Build park message with shared constants]
    J --> K{done/archived?}
    K -->|No| L[updateTask status:failed]
    K -->|Yes| M[Log only — terminal column]

    subgraph batch2[Batch-2 self-healing sweeps]
        N[recoverPausedAbortFailures] --> O{globalPause?}
        O -->|Yes| P[return 0]
        O -->|No| Q[listTasks — find park markers]
        Q --> R[re-read fresh + re-validate]
        R --> S[updateTask status:null error:null]
        S --> T{column todo?}
        T -->|No| U[moveTask to todo]
        T -->|Yes| V[clearPhantomExecutorBinding]
        U --> V
        V --> W[recordRunAuditEvent]

        X[reapLeakedConcurrencySlots] --> Y{globalPause?}
        Y -->|Yes| Z[return 0]
        Y -->|No| AA[listWorktreeHolders]
        AA --> AB[check column + grace + fresh executingIds]
        AB --> AC[clearPhantomExecutorBinding]
        AC --> AD{returned true?}
        AD -->|No| AE[skip]
        AD -->|Yes| AF[reaped++ logEntry]
    end

    L --> batch2
Loading

Reviews (4): Last reviewed commit: "Update executor-recovery test for benign..." | Re-trigger Greptile

Comment thread packages/engine/src/self-healing.ts
Comment thread packages/engine/src/self-healing.ts
Comment thread packages/engine/src/executor.ts
Comment thread packages/engine/src/self-healing.ts
gsxdsm and others added 2 commits June 19, 2026 20:03
reapLeakedConcurrencySlots() reclaims in-memory worktree slots whose
holder is no longer in-progress (the FN-6756 "in todo yet still a
maxWorktrees holder" leak) without an engine restart — defense-in-depth
behind the source fix.

- executor: new listWorktreeHolders() read-only introspection over
  activeWorktrees; wired through in-process-runtime to SelfHealingManager.
- reaper releases ONLY when every guard agrees: not executing, task
  missing or in todo/triage, past a 60s grace, and clearPhantomExecutor
  Binding itself refuses (returns false) if a live session surface is
  registered — so it can never pull a worktree from a running agent.
- registered in maintenance batch 2 (respects globalPause/enginePaused
  skip + FN-4962 ordering).
- widened the clearPhantomExecutorBinding option type to surface its
  boolean refusal signal.

Engine typecheck clean; 19 tests pass (new reaper 7 cases + regression).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Substantive (A1 recoverPausedAbortFailures):
- Self-guard on globalPause/enginePaused at method entry (greptile P1) — the
  public method must not requeue tasks an operator intentionally froze.
- Re-validate the FULL predicate with a FRESH executing set on the re-read
  before the backward move (coderabbit Major + greptile): add fresh.userPaused
  and column re-check so a task that became ineligible across awaits is skipped.
- Isolate audit emission in its own try/catch (coderabbit) so an audit throw
  after a successful mutation can't log a false "recovery failed".
- Decouple the recovery predicate from the literal error text via shared
  PAUSE_ABORT_PARK_ERROR_MARKER/OPERATOR_MARKER constants (greptile) — the
  executor builds the parked message from the same constants.
- Use the wired clearPhantomExecutorBinding (live-session-guarded) instead of
  the declared-but-never-wired releaseExecutorWorktreeOwnership, which no-op'd.

Nits:
- FNXC-prefix new comments in executor.ts, run-audit.ts, and the benign test
  per repo comment policy.
- Fix a test-only type error on the clearPhantomExecutorBinding mock.

Added a test asserting the globalPause self-guard. Engine typecheck clean;
pause-abort/reaper/benign + regression suites pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gsxdsm

gsxdsm commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

Review feedback addressed (commit 7eaf513)

All 8 review threads resolved. Summary of what changed in recoverPausedAbortFailures and around it:

Substantive (correctness):

  • globalPause/enginePaused self-guard at method entry (greptile P1) — the method is public/test-exercised, so it now returns early when the operator has frozen the board, not relying solely on the batch-2 runner guard. Added a test for it.
  • Full re-validation on the refreshed row with a fresh executing set (CodeRabbit Major + greptile) — the re-read now re-checks the entire predicate including fresh.userPaused and column eligibility with a freshly-fetched getExecutingTaskIds(), so a task that became ineligible across awaits never gets a backward move.
  • Best-effort audit emission (CodeRabbit) — recordRunAuditEvent is now wrapped in its own try/catch so an audit throw after a successful mutation can't log a false "recovery failed" or skip recovered++.
  • Decoupled the predicate from the literal error string (greptile) — extracted PAUSE_ABORT_PARK_ERROR_MARKER / PAUSE_ABORT_PARK_OPERATOR_MARKER; the executor builds the parked-failure message from the same constants, so the recovery predicate can't silently drift.
  • Bonus fix: A1 was calling releaseExecutorWorktreeOwnership, which is a declared-but-never-wired option (silent no-op). Switched to the wired, live-session-guarded clearPhantomExecutorBinding.

Nits: FNXC-prefixed the new comments in executor.ts, run-audit.ts, and the benign test per repo comment policy; fixed a test-only mock type.

Engine typecheck clean; pause-abort / reaper / benign + regression suites pass (re-run locally).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/engine/src/self-healing.ts (1)

7980-7993: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor clearPhantomExecutorBinding refusal before making the task runnable.

clearPhantomExecutorBinding can return false when a live session surface still exists, but the task is already cleared/requeued before that signal is ignored. That can make a todo task schedulable while the old binding/session is still live. Also, if moveTask fails after Line 7980, the failed marker is gone and the next sweep cannot retry this recovery. Gate on explicit refusal first, then move, then clear the failure state.

Proposed fix
-          await this.store.updateTask(task.id, { status: null, error: null });
+          const clearResult = this.options.clearPhantomExecutorBinding?.(fresh.id);
+          if (clearResult === false) {
+            log.log(`Skipped pause-abort park recovery for ${fresh.id}: live executor binding refused phantom clear`);
+            continue;
+          }
+
           if (fresh.column !== "todo") {
-            await this.store.moveTask(task.id, "todo", {
+            await this.store.moveTask(fresh.id, "todo", {
               preserveProgress: true,
               moveSource: "engine",
               recoveryRehome: true,
             });
           }
+          await this.store.updateTask(fresh.id, { status: null, error: null });
           // Release any in-memory worktree ownership the leaked park may still
           // pin, so the requeued task does not re-block the concurrency gate.
           // FNXC:WorkflowLifecycle 2026-06-20-00:00: use clearPhantomExecutorBinding
           // (wired + live-session-refusal guarded), NOT releaseExecutorWorktreeOwnership
           // which is a declared-but-never-wired option — it would silently no-op.
-          this.options.clearPhantomExecutorBinding?.(task.id);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/src/self-healing.ts` around lines 7980 - 7993, In the
self-healing.ts file, the order of operations in the task recovery block needs
to be restructured to honor clearPhantomExecutorBinding refusals. Currently, the
code updates the task status and moves it to todo before checking if
clearPhantomExecutorBinding succeeds, which can leave the task in a schedulable
state even when the old binding/session is still live. Refactor the code to call
clearPhantomExecutorBinding first and check if it returns false indicating
refusal, then only proceed with updateTask and moveTask if the binding was
successfully cleared (or the operation is not applicable). This ensures the task
state remains unchanged if a live session still holds the binding, and protects
against lost error markers if moveTask fails during recovery.
🧹 Nitpick comments (2)
packages/engine/src/self-healing.ts (2)

245-257: ⚡ Quick win

Use the required FNXC_LOG comment marker on the new requirement comments.

Several new FNXC requirement comments include area/date context but omit the grepable FNXC_LOG token required by the repo convention.

As per coding guidelines, “Add FNXC_LOG comments describing the date (yyyy-MM-dd-hh:mm format) and requirements/changes that made you implement functionality. Write FNXC:Area-of-product in front of all comments so they can be grepped.”

Also applies to: 371-376, 415-424, 2166-2169, 7926-7930, 7961-7966, 7990-8002, 8071-8090

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/src/self-healing.ts` around lines 245 - 257, Add the FNXC_LOG
comment marker to the requirement comments for the new functionality. Update the
comments describing listWorktreeHolders and clearPhantomExecutorBinding to
include the FNXC:Area-of-product prefix along with the date in yyyy-MM-dd-hh:mm
format and the requirement/change details, making them grepable according to
repo conventions. Apply the same fix to all other locations mentioned (371-376,
415-424, 2166-2169, 7926-7930, 7961-7966, 7990-8002, 8071-8090) where new
requirement comments lack the FNXC_LOG token.

Source: Coding guidelines


8121-8126: ⚡ Quick win

Emit the leaked-slot audit event when a slot is reaped.

task:reap-leaked-concurrency-slot was added to the audit mutation contract, but this sweep only writes a task log. Add a best-effort run-audit event here so reclaimed concurrency slots are queryable after the fact; avoid relying on logEntry for missing-task holders.

Proposed fix
-    for (const { taskId } of holders) {
+    for (const { taskId, worktreePath } of holders) {
@@
         reaped++;
-        await this.store.logEntry(
-          taskId,
-          "Auto-recovered: released leaked worktree/concurrency slot (holder no longer in-progress)",
-        );
+        if (task) {
+          await this.store.logEntry(
+            taskId,
+            "Auto-recovered: released leaked worktree/concurrency slot (holder no longer in-progress)",
+          );
+        }
+        try {
+          await createRunAuditor(this.store, {
+            runId: generateSyntheticRunId("self-healing-leaked-slot-reaper", taskId),
+            agentId: "self-healing",
+            taskId,
+            taskLineageId: task?.lineageId,
+            phase: "reap-leaked-concurrency-slots",
+          }).database({
+            type: "task:reap-leaked-concurrency-slot",
+            target: taskId,
+            metadata: {
+              taskId,
+              column: task?.column ?? "missing",
+              worktreePath,
+            },
+          });
+        } catch (auditErr: unknown) {
+          log.warn(`Leaked-slot reaper audit emission failed for ${taskId}: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`);
+        }
         log.warn(`Reaped leaked worktree slot held by ${taskId} (column=${task?.column ?? "missing"})`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/src/self-healing.ts` around lines 8121 - 8126, In the leaked
worktree slot reaping block where reaped is incremented and logEntry is called,
add a best-effort audit event emission for the task:reap-leaked-concurrency-slot
event alongside the existing logEntry call. The audit event should include the
taskId and relevant context (such as the column information) to make reclaimed
concurrency slots queryable for audit purposes. Since some holders may not have
corresponding tasks, use error handling (try-catch or optional chaining) to
ensure the audit event emission does not block the reaping operation if the task
or audit system is unavailable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/engine/src/self-healing.ts`:
- Around line 8100-8117: The executingIds set is captured once at the beginning
of the loop before iterating through holders, but a task can start executing
while the loop is processing and awaiting getTask. Before calling
clearPhantomExecutorBinding with the taskId, re-check the current executing set
by calling getExecutingTaskIds again and verify that the taskId is not in the
updated set. This ensures the reaper does not clear a phantom executor binding
for a holder that became active during the sweep, preventing race conditions
where a task starts executing between the initial check and the final release
operation.

---

Outside diff comments:
In `@packages/engine/src/self-healing.ts`:
- Around line 7980-7993: In the self-healing.ts file, the order of operations in
the task recovery block needs to be restructured to honor
clearPhantomExecutorBinding refusals. Currently, the code updates the task
status and moves it to todo before checking if clearPhantomExecutorBinding
succeeds, which can leave the task in a schedulable state even when the old
binding/session is still live. Refactor the code to call
clearPhantomExecutorBinding first and check if it returns false indicating
refusal, then only proceed with updateTask and moveTask if the binding was
successfully cleared (or the operation is not applicable). This ensures the task
state remains unchanged if a live session still holds the binding, and protects
against lost error markers if moveTask fails during recovery.

---

Nitpick comments:
In `@packages/engine/src/self-healing.ts`:
- Around line 245-257: Add the FNXC_LOG comment marker to the requirement
comments for the new functionality. Update the comments describing
listWorktreeHolders and clearPhantomExecutorBinding to include the
FNXC:Area-of-product prefix along with the date in yyyy-MM-dd-hh:mm format and
the requirement/change details, making them grepable according to repo
conventions. Apply the same fix to all other locations mentioned (371-376,
415-424, 2166-2169, 7926-7930, 7961-7966, 7990-8002, 8071-8090) where new
requirement comments lack the FNXC_LOG token.
- Around line 8121-8126: In the leaked worktree slot reaping block where reaped
is incremented and logEntry is called, add a best-effort audit event emission
for the task:reap-leaked-concurrency-slot event alongside the existing logEntry
call. The audit event should include the taskId and relevant context (such as
the column information) to make reclaimed concurrency slots queryable for audit
purposes. Since some holders may not have corresponding tasks, use error
handling (try-catch or optional chaining) to ensure the audit event emission
does not block the reaping operation if the task or audit system is unavailable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ca1a394a-ab2e-43e5-9a24-c5127d5ac677

📥 Commits

Reviewing files that changed from the base of the PR and between 9643563 and 7eaf513.

📒 Files selected for processing (8)
  • .changeset/fix-pause-abort-leak-storm.md
  • packages/engine/src/__tests__/executor-paused-abort-todo-benign.test.ts
  • packages/engine/src/__tests__/self-healing-leaked-slot-reaper.test.ts
  • packages/engine/src/__tests__/self-healing-paused-abort-recovery.test.ts
  • packages/engine/src/executor.ts
  • packages/engine/src/run-audit.ts
  • packages/engine/src/runtimes/in-process-runtime.ts
  • packages/engine/src/self-healing.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/fix-pause-abort-leak-storm.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/engine/src/run-audit.ts
  • packages/engine/src/tests/executor-paused-abort-todo-benign.test.ts

Comment thread packages/engine/src/self-healing.ts
coderabbit Major: reapLeakedConcurrencySlots captured executingIds once
before the loop, but each holder awaits getTask — a task could start
executing mid-sweep and have its worktree slot pulled. Refresh the
executing set immediately before clearPhantomExecutorBinding and skip if
the holder is now executing (same race the A1 recovery fix closed).
clearPhantomExecutorBinding's live-session refusal remains the last line
of defense; this avoids racing it. Added a mid-sweep race test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gate test asserted the OLD behavior — a paused graph exit in the `todo`
column parked `status:"failed"` with "operator action required". FN-6782
made the todo case benign (no failed park; benign log + cleared marker), so
split the parameterized test: `todo` now asserts the benign path (never
parked failed), `done` keeps the operator-action surfacing (log only, no
park). Full engine-core gate suite passes (644/644).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/engine/src/__tests__/executor-recovery.test.ts`:
- Around line 1897-1901: The inline comment starting with "FN-6782" does not
follow the repository's mandated FNXC comment format. Update the comment header
to use the required FNXC:Area-of-product format with proper dating for
grep-ability, replacing the current "FN-6782" prefix with the appropriate
FNXC:Area-of-product identifier. Apply this same format fix to all affected
inline comments in the test file (including the one at line 1931).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e01d3f4-7625-47be-85b5-8ff9ee11fab6

📥 Commits

Reviewing files that changed from the base of the PR and between 8c93e2a and 7f0ad62.

📒 Files selected for processing (1)
  • packages/engine/src/__tests__/executor-recovery.test.ts

Comment on lines +1897 to +1901
// FN-6782: a paused graph exit that already landed back in `todo` is BENIGN —
// it must NOT be parked `failed` (that re-fail loop was the retry storm). It
// logs a benign line, clears the pause-abort marker, and leaves the task in
// todo for normal scheduling. (Previously this was surfaced as an
// operator-action failure; see the `done` case below for the still-surfaced path.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use required FNXC-prefixed comment format for new inline comments.

The new comments don’t follow the repo’s mandated FNXC:Area-of-product + dated FNXC log style. Please update these comment headers to match policy.

Suggested edit
-  // FN-6782: a paused graph exit that already landed back in `todo` is BENIGN —
+  // FNXC:WorkflowLifecycle 2026-06-20-00:00:
+  // FN-6782: a paused graph exit that already landed back in `todo` is BENIGN —
   // it must NOT be parked `failed` (that re-fail loop was the retry storm). It
   // logs a benign line, clears the pause-abort marker, and leaves the task in
   // todo for normal scheduling. (Previously this was surfaced as an
   // operator-action failure; see the `done` case below for the still-surfaced path.)
@@
-    // done/archived are terminal — surfaced via log only, never parked failed.
+    // FNXC:WorkflowLifecycle 2026-06-20-00:00:
+    // done/archived are terminal — surfaced via log only, never parked failed.

As per coding guidelines, "packages/**/*.{ts,tsx,js,jsx}: Add FNXC_LOG comments ... Write FNXC:Area-of-product in front of all comments for grep-ability."

Also applies to: 1931-1931

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/src/__tests__/executor-recovery.test.ts` around lines 1897 -
1901, The inline comment starting with "FN-6782" does not follow the
repository's mandated FNXC comment format. Update the comment header to use the
required FNXC:Area-of-product format with proper dating for grep-ability,
replacing the current "FN-6782" prefix with the appropriate FNXC:Area-of-product
identifier. Apply this same format fix to all affected inline comments in the
test file (including the one at line 1931).

Source: Coding guidelines

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