[Feat] Durable admission: interrupted Fast turns resume on the next process - #2016
Merged
Conversation
Contributor
|
No code issues found. See task
Reviewed 4226a14 |
daniel-lxs
force-pushed
the
feat/fast-durable-turn-admission
branch
from
September 2, 2026 00:19
0eeab7b to
2053c56
Compare
Every human Fast turn (Slack, Discord, web, Teams, Telegram) is now persisted as an inline-admitted parent-event row under a claim lease before the accepting process runs it. The turn renews the claim as it works, revokes replay durably before the first action a re-run could duplicate (task launches, task messages, cancellations, memory writes, closeouts, non-read MCP calls), and settles the row on completion. When the process is interrupted by a restart or a lost conversation lock while the turn is still replay-safe, it releases the claim and posts nothing; the parent-event queue re-runs the turn through the same path busy follow-ups already use, with a resumed-turn marker so the model continues without re-acknowledging. A newer human message supersedes a pending interrupted turn. Interruptions after a non-replayable action keep the honest restart closeout.
…doff The turn's own human-steer poll treated its inline-admitted row as a pending follow-up, injected it as a steer, and settled it, leaving an interruption with nothing to resume. Inline rows are whole turns and are now excluded from steering. The abort path also logs whether the claim release and queue wake landed, which is what proves a handoff in production logs.
- The resumed queue run now receives its row as durableAdmission, so it revokes replay before any non-replayable action and a worker death after such an action cannot drain the row again. - Replay revocation fails closed: if the discard write does not land, the native or MCP action is refused with a tool error instead of running unrecorded. - Inline admission and supersession of an older pending inline row commit in one transaction, so recovery never sees the new turn without the superseded one retired. - The initial human turn of a new web Fast session is durably admitted, not only replies.
… pending A guarded revocation that matches no pending row means another executor already settled this turn, so this execution is a stale duplicate and must not act. Propagate that result and reject the tool call.
Plain-text and error closeouts post through the adapter directly rather than a tool call, so they never revoked replay. A restart in the gap between that post and settlement could re-run the turn and post the closeout twice. Every terminal closeout path now revokes first and is skipped when the revocation does not land.
…rable The deliberate-cancellation branch revoked replay but ignored the result and posted the interruption closeout anyway; if the revocation had not landed, the still-recoverable row could re-run the turn after that closeout. The closeout is now gated on a successful revocation.
…withdrawn from replay Skipping a terminal or error closeout because the revocation did not land left settlement marking the row delivered, so the queue could not resume it and the user got no answer. Settlement now releases the claim and wakes the queue in that case instead of marking delivery, so the resumed run delivers the outcome.
The drain marked every delivered row as delivered, including an inline row whose resumed run had just released its claim because its terminal revocation did not land. That settled a row meant to stay recoverable. For inline rows the drain now checks the row after delivery and stops the pass when it is still pending, leaving it for the next sweep.
… needs after rebase
daniel-lxs
force-pushed
the
feat/fast-durable-turn-admission
branch
from
September 2, 2026 00:44
852d2aa to
3b5cf40
Compare
A pre-acknowledgement work call is refused by the gate, so it must not discard the durable row first; otherwise a restart before any real side effect could no longer resume the turn.
… setup A turn interrupted before it reached inference never ran its own abort handling, so its durable claim stayed held and the row waited out the full claim lease before recovery. Handlers now bind the durable row to the turn lock, and abortActiveFastAgentTurns releases every bound claim after aborting; the release is a guarded no-op for rows the turn already revoked or settled. Found by the local smoke: a SIGTERM during setup now hands the turn to the recovery sweep within seconds.
Unknown Web API methods now return HTTP 200 with ok:false and unknown_method, as Slack does, instead of a 404 that the WebClient retries with backoff for minutes while holding Fast turn locks. The agents.sessions status and rename calls made by the Fast session activity adapter are accepted and ignored.
R_FAST_DURABLE_ADMISSION_DISABLED=true stops persisting human turns for automatic resumption; turns run inline exactly as before this change and the queue's claim logic stays inert. Default remains enabled.
A turn interrupted before it reached its own abort handling had its claim released by the shutdown path but nothing woke the queue, so recovery waited for the periodic sweep. Handlers now bind the wake alongside the row id, and the shutdown path wakes the queue after a successful release.
This was referenced Sep 2, 2026
This was referenced Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
An ordinary human Fast turn exists only in the memory of the API process that accepted it. When that process is restarted or loses its conversation lock, the turn is gone: the user gets a resend closeout and has to ask again. Deploys are the most common trigger. #2004, #2006, #2007, and #2010 shrank the blast radius (attribution, drain, no false stamps, the next nudge resumes the request), but nothing made the turn itself survive the process. This is Phase 1 of the durable-execution plan: durable admission with the accepting process as first claimant.
Change
Every human Fast turn on every surface (Slack, Discord, web, Teams, Telegram) is persisted as an inline-admitted
fast_agent_parent_eventsrow under a claim lease before the accepting process runs it. The existing parent-event queue, which already runs busy follow-ups through the same turn path on every surface, becomes the recovery executor.persistFastAgentInlineHumanTurn(andadmitFastAgentHumanFollowUpon its idle path) writes the row withadmission='inline'and a 15-minuteclaimed_untilin one transaction with the retirement of any older pending inline row for the conversation (a newer human message supersedes an interrupted one). The queue is not woken; the live owner runs the turn.resumedAfterInterruption(a<resumed_turn>marker plus a prompt rule not to re-acknowledge) and with the row as its own durable admission, so the resumed run fences itself the same way. Deliberate cancellations and post-action interruptions keep today's behavior (honest restart closeout, row discarded).R_FAST_DURABLE_ADMISSION_DISABLED=truestops persisting turns; everything else goes inert and turns run exactly as before this PR.Schema: two nullable columns on
fast_agent_parent_events(additive, N-1 safe). No new tables. Also includes a mock Slack harness fidelity fix: unknown Web API methods now returnok:false unknown_methodlike Slack instead of a 404 the WebClient retries for minutes.Validation
Unit: cloud-agents fast-agent suites, SDK queue/admission/delivery/surface-reply suites, api Slack+Discord handler suites, web fast-sessions suites, all green;
lint:fast,check-types:fast,knippass. Six Roomote review rounds, each closing a real gap in the fail-closed chain.Local smoke (exact head, PM2 from this checkout verified by cwd, mock Slack as the only external boundary with the real API, Postgres, Redis turn lock, BullMQ worker and recovery sweep in path, owner PID validated by port, command line, and cwd before every signal, drain set to 0 so SIGTERM is the deploy transition):
interruptionReason=api_shutdown, nothing resumed.Not exercised