fix(sdk): re-dispatch a single in-flight user on recovery boot - #4768
Open
claude[bot] wants to merge 2 commits into
Open
fix(sdk): re-dispatch a single in-flight user on recovery boot#4768claude[bot] wants to merge 2 commits into
claude[bot] wants to merge 2 commits into
Conversation
The recovery-boot smart default spliced the partial assistant plus the user it was answering into the seed chain whenever a partial existed and there was at least one in-flight user. With exactly one in-flight user — the plain OOM / crash-mid-answer shape — that consumed the only message there was to dispatch: `recoveredTurns` came out empty, the boot queue stayed empty, the session.in cursor was advanced past the message anyway, and on a preload or continuation boot (no message on the wire payload) neither dispatch site fired. The interrupted question was never answered and the run idled until its timeout. Require two or more in-flight users for the splice, on both the chain and the recoveredTurns branch. With n >= 2 nothing changes. With n = 1 the orphan partial is dropped and the interrupted user is re-dispatched as a fresh turn, which is what the OOM-resilience docs promise. The submit-message boot is unaffected: the existing dedup skips a queued message identical to the one already on the wire payload. Also corrects the docstrings and the recovery-boot docs, which described the default as "re-dispatch every user" without mentioning the splice. Co-Authored-By: Claude <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: c3055c9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 27 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
matt-aitken
marked this pull request as ready for review
August 25, 2026 04:50
@trigger.dev/build
trigger.dev
@trigger.dev/core
@trigger.dev/python
@trigger.dev/react-hooks
@trigger.dev/redis-worker
@trigger.dev/rsc
@trigger.dev/schema-to-json
@trigger.dev/sdk
commit: |
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.
Requested by Matt Aitken · Slack thread
Before: a
chat.agentrun is killed mid-answer (OOM, crash, eviction) while the message it was answering is the only one still outstanding. The new run boots, puts that message and the half-written reply into its context, and then waits for a message that already arrived. Nobody ever answers the user; the run sits idle until it times out.After: the new run re-runs that message as a fresh turn and replies to it. The half-written reply is dropped. When two or more messages are outstanding, nothing changes — the interrupted one still goes into context and the newer ones are re-run, exactly as before.
✅ Checklist
Testing
New regression test in
packages/trigger-sdk/test/recovery-boot.test.ts— seeds a partial assistant plus exactly one in-flight user, noonRecoveryBoot, and asserts one turn fires for that user with the orphan partial dropped from the chain. It fails onmain(turnCount0, no turn at all) and passes with this change.pnpm exec vitest runinpackages/trigger-sdk— 373 passed, 1 skipped (31 files passed, 1 skipped)pnpm exec oxfmt --checkon the changed files — cleanpnpm exec oxlint packages/trigger-sdk/src packages/trigger-sdk/test— cleanpnpm run build --filter @trigger.dev/sdk— cleanWhat it does: with exactly one in-flight user on a recovery boot, re-dispatch that user as a fresh turn instead of splicing it into the seed chain, where it was never answered.
How: the recovery-boot smart default made one decision in two halves — the seed chain and the recovered-turn list — both gated on
partialAssistant !== undefined && inFlightUsers.length > 0. The splice consumesinFlightUsers[0]into the chain as "the question the partial was answering" and dispatches the rest. That only works when there is a rest: at n=1recoveredTurnscame out empty, the boot-injected queue stayed empty, thesession.incursor was advanced past the message anyway, and on apreloador continuation boot (nomessageon the wire payload) neither dispatch site fired. Both branches now requirelength > 1, so n=1 falls through to the documented default — chain =settledMessages, re-dispatch every in-flight user. The submit-message boot is unaffected: the existing dedup still drops a queued message identical to the one already on the wire payload.Also corrected alongside it: the two SDK docstrings and the
docs/ai-chat/patterns/recovery-boot.mdxdefaults section, which described the default as "re-dispatch every user" and never mentioned the splice.Follow-up (not in this PR): the webapp e2e OOM helper never streams a token before throwing, so it exercises the no-partial path only and would not have caught this. Worth a variant that emits a token first.
Changelog
Fixed a chat agent hanging after an interrupted turn: when a run was killed mid-answer and only the one message it was answering was still outstanding, the new run never replied to it. That message is now re-answered on the new run.