Skip to content

fix: abort dispatch after critical delivery failure#97

Closed
miyaontherelay wants to merge 2 commits into
mainfrom
codex/issue-96-critical-delivery-abort
Closed

fix: abort dispatch after critical delivery failure#97
miyaontherelay wants to merge 2 commits into
mainfrom
codex/issue-96-critical-delivery-abort

Conversation

@miyaontherelay

Copy link
Copy Markdown
Contributor

Summary

  • fail closed when initial critical briefing injection or confirmed reinjection exhausts
  • persist an exact-run abort intent before owner-fenced team cleanup and provider rollback
  • preserve takeover, retry, stale-event, terminal-race, and capacity safety across GitHub and Linear

Validation

  • npm run build
  • npx tsc -p tsconfig.build.json --noEmit
  • npm test (838 passed)
  • focused issue matrix (17 passed)
  • node bin/factory.mjs --help
  • manifest integrity (238 features / 19 categories)
  • npm pack --dry-run --json
  • git diff --check

Fixes #96

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cd85ce5-f0e2-48c1-9b40-5426afa647af

📥 Commits

Reviewing files that changed from the base of the PR and between e62b7b9 and ec8a958.

📒 Files selected for processing (6)
  • .agentworkforce/features/critical-paths.md
  • .agentworkforce/features/manifest.yaml
  • .agentworkforce/features/verify/procedures.md
  • src/orchestrator/factory.test.ts
  • src/orchestrator/factory.ts
  • src/ports/writeback.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/ports/writeback.ts
  • .agentworkforce/features/manifest.yaml
  • src/orchestrator/factory.test.ts
  • src/orchestrator/factory.ts

📝 Walkthrough

Walkthrough

Critical delivery failures now trigger run-fenced, durable abort recovery. The flow persists abort intent, releases affected agents, retries incomplete cleanup, publishes provider-visible notices, clears lifecycle status, and restores retry eligibility.

Changes

Critical delivery abort recovery

Layer / File(s) Summary
Durable abort contracts and state
src/ports/state.ts, src/orchestrator/batch-tracker.ts, src/state/*
Dispatch records retain runId; state stores support the aborting phase, exact-run abort intent, protected lifecycle updates, and critical-message cleanup.
Run-fenced abort orchestration
src/orchestrator/factory.ts
Factory dispatch validates run ownership, aborts failed critical injections or reinjections, fences agent exits, retries cleanup, persists recovery state, and publishes deduplicated abort notices.
Provider status cleanup
src/ports/writeback.ts, src/writeback/*
GitHub writeback can remove lifecycle labels after an aborted dispatch, with updated CLI behavior tests.
Abort recovery validation and criteria
src/orchestrator/factory.test.ts, .agentworkforce/features/*
Tests cover delivery failures, restart takeover, lease loss, stale runs, idempotency, release races, and provider cleanup; feature documentation records the fail-closed criteria.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Dispatch
  participant StateStore
  participant Fleet
  participant Provider
  Dispatch->>StateStore: Persist exact-run abort intent
  StateStore-->>Dispatch: Return aborting lifecycle
  Dispatch->>Fleet: Release and terminate affected agents
  Fleet-->>Dispatch: Report cleanup result
  Dispatch->>Provider: Publish correlated failure and clear status
  Provider-->>Dispatch: Confirm writeback cleanup
Loading

Possibly related PRs

  • AgentWorkforce/factory#49: Both modify GitHub writeback status cleanup and factory lifecycle integration for dispatch-failure aborts.

Poem

I’m a rabbit guarding the briefing gate,
No missing words shall seal a fate.
Run IDs fence each hopping trail,
Cleanup retries when first steps fail.
Labels fade and notices shine—
Safe little burrows, state in line.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: aborting dispatch after a critical delivery failure.
Description check ✅ Passed The description is directly about the same dispatch-abort and cleanup behavior described in the diff.
Linked Issues check ✅ Passed The changes implement fatal critical-delivery aborts, exact-run fencing, cleanup, and retry safety required by #96.
Out of Scope Changes check ✅ Passed The PR stays focused on critical delivery abort handling, tests, and related docs with no obvious unrelated work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 codex/issue-96-critical-delivery-abort

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request implements a fail-closed mechanism for critical briefing delivery failures, ensuring that team dispatches are cleanly aborted and returned to a ready state if initial delivery or reinjection fails. The review feedback identifies several critical issues in the abort flow: potential memory leaks of the issue key in the #criticalDeliveryAborting set if the abort process rejects or returns early due to lease loss, a potential live-lock of the queue if the optional clearStatus writeback method is missing, and a case-sensitivity bug when matching retrieved GitHub labels.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/orchestrator/factory.ts
Comment thread src/orchestrator/factory.ts Outdated
Comment thread src/orchestrator/factory.ts Outdated
Comment thread src/orchestrator/factory.ts Outdated
Comment thread src/writeback/github.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 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 @.agentworkforce/features/critical-paths.md:
- Line 242: Update the durable abort recovery documentation in
.agentworkforce/features/critical-paths.md at lines 242-242 to include
src/ports/state.ts and the durable state-store implementation alongside the
existing files. Also update .agentworkforce/features/manifest.yaml at lines
559-559 to include src/ports/state.ts and src/orchestrator/batch-tracker.ts.

In @.agentworkforce/features/verify/procedures.md:
- Line 244: Update the abort-state notice requirements in the “exhausted initial
delivery and exhausted reinjection” procedure so idempotency is scoped to each
correlated abort marker: allow one cleanup-pending marker and one final aborted
marker, while preventing duplicate emission of either marker during retries or
partial cleanup. Replace the single overall provider-visible failure constraint
without changing owner fencing, cleanup acknowledgement, or restart behavior.

In `@src/orchestrator/factory.test.ts`:
- Around line 7612-7635: The test around the idempotent GitHub abort notice must
exercise provider-backed reconciliation across a restart rather than relying on
the same instance’s in-memory notice set. Persist the abort state, create a new
Factory instance with a fake GitHub writeback implementing hasCommentMarker,
retry the failed status publication, and verify cleanup completes without
reposting the factory-critical-delivery-abort comment.
- Around line 7566-7588: The test around factory.dispatch must verify cleanup
ordering, not only eventual calls. Update RecordingGithubWriteback or the test
setup so clearStatus records/asserts that both expected releases in
fleet.releases have already completed when invoked, then retain the existing
release and writeback assertions.
- Around line 7827-7908: Seed the batch state in the late terminal failure test
before dispatch, using an occupied slot or queued record, then snapshot the
batch contents and capacity state. After the late event and duplicate event,
assert the seeded occupancy and queue remain unchanged and that no queued work
is promoted. Update the test around stateStore.getBatch and the late delivery
failure assertions while preserving the existing lifecycle and ignored-event
checks.

In `@src/orchestrator/factory.ts`:
- Around line 667-668: Update stop() to await all in-flight promises tracked by
`#criticalDeliveryAborts` after cancelling and clearing
`#criticalDeliveryAbortRetryTimers`, before calling fleet.dispose(). In the
follow-up dispatch logic around the critical-delivery abort completion handler,
guard the dispatch of the next queued issue with `#stopping` so no new work starts
during shutdown.
- Around line 3994-4004: Reorder the abort handling so
`#recordDispatchFailure`(record.issue) completes before the terminal abandoned
lifecycle is saved by `#saveDispatchLifecycle`. Preserve the existing watcher
cleanup and terminal waiter resolution, ensuring the dispatch-attempt fence is
released before any crash point after lifecycle terminalization.
- Around line 3889-3896: Update the critical-delivery abort flow around
`#doAbortCriticalDelivery` so unexpected rejections also schedule recovery before
propagating the failure. Preserve the existing cleanup of
`#criticalDeliveryAborts`, ensure the corresponding `#criticalDeliveryAborting`
entry is eventually cleared or retried through the established recovery
mechanism, and keep explicitly handled branches unchanged.

In `@src/ports/writeback.ts`:
- Around line 26-27: Make clearStatus in the writeback interface a required
method by removing its optional marker, and update any implementing writebacks
or mocks to provide the Promise<void> implementation so FactoryLoop can always
perform abort recovery.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7dc95137-bf7c-4720-89d2-fa53548e2528

📥 Commits

Reviewing files that changed from the base of the PR and between f419407 and e62b7b9.

📒 Files selected for processing (13)
  • .agentworkforce/features/critical-paths.md
  • .agentworkforce/features/manifest.yaml
  • .agentworkforce/features/verify/procedures.md
  • src/orchestrator/batch-tracker.ts
  • src/orchestrator/factory.test.ts
  • src/orchestrator/factory.ts
  • src/ports/state.ts
  • src/ports/writeback.ts
  • src/state/file-state-store.test.ts
  • src/state/file-state-store.ts
  • src/state/in-memory-state-store.ts
  • src/writeback/github.ts
  • src/writeback/writeback.test.ts

Comment thread .agentworkforce/features/critical-paths.md Outdated
Comment thread .agentworkforce/features/verify/procedures.md Outdated
Comment thread src/orchestrator/factory.test.ts
Comment thread src/orchestrator/factory.test.ts Outdated
Comment thread src/orchestrator/factory.test.ts Outdated
Comment thread src/orchestrator/factory.ts
Comment thread src/orchestrator/factory.ts
Comment thread src/orchestrator/factory.ts
Comment thread src/ports/writeback.ts Outdated

@khaliqgant khaliqgant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Live-tested this branch (ec8a958) against a real dispatch. The abort does not fire on the failure it's meant to catch.

Setup

Local build of this branch, factory dispatch on AgentWorkforce/relayed#50 (an issue deliberately unanswerable from the repo), config with babysitter.enabled: true + slack.channel. Internal backend.

What happened

The #95 gate fix worked — no false trip:

[factory] Slack sync soft-degraded but webhook delivery is healthy; continuing Slack writeback

Then the exact failure this PR targets:

[factory] error {"message":"Critical delivery failed to ar-50-impl-relayed: max delivery retries exceeded","issue":"50"}

And then nothing. Sampled every 20s for ~8 minutes after the failure:

[14:28:08Z] agents=2 abort_lines=0 file_written=no
...
[14:33:10Z] agents=2 abort_lines=0 file_written=no

Both agents still alive, no abort, no release, dispatch still running. The un-briefed implementer keeps going — the #96 behaviour, unchanged.

Likely mechanism (inferred, not proven)

#handleDeliveryFailed logs unconditionally via this.#error(...) — that's the line we see — but the abort is gated:

const critical = await this.#state.consumeCritical(this.#workspaceId, info.msgId ?? '')
...
if (critical && this.#fleet.waitForInjected) { ... #abortCriticalDelivery ... }

waitForInjected is implemented on InternalFleetClient (:332), so the suspect is critical. Criticals are recorded under ack.eventId (factory.ts:5397 et al) and looked up by info.msgId. In internal-fleet-client.ts, the message_delivery_failed branch (:527) has an explicit if (event.event_id) guard before using it — implying that kind can arrive without an event_id. If so, msgId is undefinedconsumeCritical(workspaceId, '')undefined → the if (critical && ...) guard is false → no abort, and the error is only logged.

max delivery retries exceeded reads like message_delivery_failed.lastError rather than delivery_failed.reason, which fits.

I haven't instrumented it to confirm which branch fired or whether event_id was actually absent — flagging the hypothesis rather than asserting it. Worth a log of info.msgId and whether consumeCritical resolved, since that would settle it in one run.

Why this matters for the fix

The PR's validation is unit-level (838 tests + a 17-case matrix), and the tests presumably construct a critical record so the guard passes. The observed production path doesn't get that far — so the suite can be green while the abort never fires in a real dispatch. That's the same shape as the --workspace-key lying-test problem in relay#1287.

Suggest either:

  1. Fail closed on a critical delivery failure regardless of whether consumeCritical resolves — the identity of the message matters less than the fact that a briefing for a known agent (info.to maps to a record) failed; or
  2. Make msgId reliably present on the message_delivery_failed path, and add a regression test that drives the failure through #emitDeliveryFailed with no event_id.

Context

The reproducible delivery failure also means #96's fabrication case is not a one-off: it has now failed to brief the implementer on 3/3 dispatches (relayed#48, #49, #50) in this environment. On #49 the un-briefed agent invented an on-call policy. Agents register a name (lastSeen current) but sit at status: "unknown" with no pid/cli — registered, never live/deliverable — while codex idles at ~0% CPU. Whether that non-registration is the root cause of the delivery failure is a separate thread worth pulling.

@khaliqgant

Copy link
Copy Markdown
Member

Recommend closing this as the wrong fix (not as "not a problem" — #96 is real and should stay open). Two independent reasons:

1. It doesn't fire in production

Live-tested against a real dispatch (branch ec8a958): the abort never triggered. #handleDeliveryFailed logs the failure but gates the abort behind critical && this.#fleet.waitForInjected, where critical = consumeCritical(this.#workspaceId, info.msgId ?? ''). On the observed path the delivery-failed event's msgId doesn't resolve a recorded critical, so critical is falsy and the abort is skipped — the agent keeps running un-briefed, exactly the #96 behavior this PR targets.

Sampled every 20s for ~8 minutes after Critical delivery failed to ar-50-impl-relayed: max delivery retries exceeded: agents still alive, abort_lines=0, dispatch still running. The 838-test suite is green because the tests construct a critical record so the guard passes; the real path never gets that far. Same shape as the --workspace-key lying test in relay#1287.

2. It guards a mechanism the correct fix removes

#97 aborts when the critical task injection fails. But that injection is itself the bug (#98): factory spawns codex with a task, codex starts working immediately, and the briefing injected on top races its interactive hold so delivery_injected never fires. The fix is to deliver the full briefing in the spawn task (validated: implementer spawned with the complete task incl. ask instructions, 0 injection failures, correct branch, no fabrication) — which eliminates the critical injection entirely. With no injection to fail, there's nothing for #97 to abort on. Under #99's escalation redesign there's no critical injection at all.

So even a working version of #97 would be guarding a code path that shouldn't exist.

What should happen

If for some reason the spawn-task fix is not landing soon and an interim guard is wanted, the useful shape is fail-closed at dispatch when the agent cannot be briefed — but that's better achieved by removing the failure mode than by reacting to it, and it would need the trigger above to actually fire first.

Refs: #98 (root cause + the spawn-task fix), #99 (escalation redesign), #96 (the real problem, keep open), #79 (release/resume — the reply half).

@khaliqgant

Copy link
Copy Markdown
Member

Closing per the rationale above: this doesn't fire in production (verified live) and guards a critical-injection mechanism that the spawn-task fix (#98) and escalation redesign (#99) remove. #96 stays open as the real problem — the fix there is reliable briefing delivery, not abort-on-injection-failure.

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.

A failed critical task injection leaves the agent running un-briefed — it guesses (observed: fabricated on-call policy) instead of aborting

2 participants