Repository navigation
fix(agent-invocations): Route subagent resumes to subagent code - #2499
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Member
|
Why aren't patent and child events handled identically? They're roughly the same systems? |
sentry-junior Bot
added a commit
that referenced
this pull request
Nov 1, 2026
After executeTurn records the terminal Turn, an error in the owner's complete step ran the stop step when a stop was pending, then acked. For a child, that skipped promoting a replaced result, so the parent never got it. Run complete outside the stop and failure handling, so an error there retries the attempt like it did before the shared lifecycle. Reported by Cursor Bugbot on #2499.
Contributor
|
2 screenshot changes — 2 changed · 0 added · 0 removed Review screenshots in Frameshift
|
isFinalAttempt came only from message delivery counts, so a resume attempt with no messages was never final. When a resumed Turn kept failing, the worker gave up without telling the adapter. A child agent then stayed paused, the parent was never told, and later results of that child were held back as replaced. The retry count now also marks the last attempt as final. On that attempt the agent invocation worker fails the invocation for any error, also a save error without an AgentRunError wrapper, and reports it to the parent. Root Turns and dispatches get the same final attempt. Co-Authored-By: David Cramer <[email protected]>
The message, nested, and stop tests built worker contexts by hand and called startAgentInvocationsForChildInput, createAgentInvocationWorker, and createChildConversationWorker directly. They did not run the production conversation work that selects the child worker. They now process queue wakes through createConversationWorkHarness, which moves to fixtures/conversation-work and wraps processConversationQueueMessage. Stops use the real stop request poll and stopConversationTurn instead of a local AbortController. Runners only read or abort runs, because tests in packages/junior/tests must not run the agent. The nested report case in the spawn test duplicated the nested test, so it is removed. The maxAgentDepth check moves to the component worker test, which owns that option. The stopped empty resume wake is now a case of the yielded child wake test. startAgentInvocationsForChildInput has no callers outside result.ts, so it is no longer exported. Co-Authored-By: David Cramer <[email protected]>
… child work Mailbox work and agent invocation work each had their own copy of the Turn lifecycle: input ack, stop checks, AgentRunError unwrap, lost-lease detection, and the retry-or-fail decision. The child-resume bugs came from drift between these copies. runWorkerTurn in task-execution/worker-turn.ts now owns that lifecycle. Each work owner supplies its Run, its result save, and its finish steps. Behavior changes from aligning the two copies: - An execution limit now fails a root Turn at once, as it already did for a child. A retry cannot pass the limit. - A root setup error on the last attempt now fails the Turn, as it did for a child, instead of leaving it open. - A child setup error while a stop is pending now ends the child as stopped. - A lost lease while a child records a stop returns lost_lease. Co-Authored-By: David Cramer <[email protected]>
After executeTurn records the terminal Turn, an error in the owner's complete step ran the stop step when a stop was pending, then acked. For a child, that skipped promoting a replaced result, so the parent never got it. Run complete outside the stop and failure handling, so an error there retries the attempt like it did before the shared lifecycle. Reported by Cursor Bugbot on #2499. Co-Authored-By: David Cramer <[email protected]>
A child agent invocation is a core Turn with no provider Delivery. Move the steps that root mailbox work and child work both ran into task-execution/core-turn.ts: - createTurnSandbox: read the saved sandbox ref and save each change. - createSteeringDrain: drain pending mailbox input, skip messages that an earlier drain already saved in the Turn, and accept the rest. - saveFinishedTurn: save the sandbox ref, the completed checkpoint, and the automated-turn limit. - recordAutomatedTurnLimit: count an automated Turn, or reset the count for input from the Conversation owner (a person, or the parent agent of a child). Child work also reads history with loadProjection, as root work does. Align three drifts: - A grandchild result now counts toward the automated-turn limit of a child, as a child result counts on a root. - A resumed Slack Turn now records the automated-turn limit, as a Slack Turn that finishes in one slice does. - A completed child checkpoint with no earlier record starts at slice 1, as on a root, instead of failing. The web and event worker now selects Delivery from the input owner (event mailbox input or a saved event Message), not from Source. Co-Authored-By: David Cramer <[email protected]>
Add one table to the durable queue tests. It runs each work owner through production routing with the real agent: a root web Turn, a root event Turn at a Slack Location, a root Slack mention, a dispatch, a child from a parent task, and a child from a watch event. Each owner gets new input, a yield and empty resume wake, a stop mid-Turn, an attempt that throws every time, and a resume that throws every time. The table found these bugs, which this commit fixes: - A stop that aborted a child Run saved the Run instead of ending the Turn as stopped. The stop check now runs in the shared worker lifecycle for root and child work. A Run that finished before the stop keeps its result. - A root event Turn that failed on its last attempt posted no error reply at its Slack Location. It now posts the standard failure reply. - A dispatch resume failed, because the wake and the resume read a Slack session Source that a dispatch Conversation does not have. They now use the saved Source of the dispatch. The table moves the durable queue helper onto the shared conversation work harness, so the file runs the agent from one place. The owner and signal rows are in tests/fixtures/turn-lifecycle.ts to keep the test file under the length limit. Dispatch stop and failure rows are not in the table yet. Dispatch work still runs its own lifecycle. The next commit moves it onto the shared one. Co-Authored-By: David Cramer <[email protected]>
Dispatch work had its own copy of the ack, lost lease, stop, and last attempt logic. It now runs through runWorkerTurn, like root and child work. Dispatch keeps its own steps: it reads and projects the dispatch record, and its Slack runtime runs the Turn. runWorkerTurn accepts a prepared Turn that its owner runs. The owner says whether the Turn completed or paused, and acks when the Turn saved its input. This fixes two dispatch rows of the Turn lifecycle table: - A stop mid-Turn now reaches the dispatch Run and ends its Turn as stopped. Before, the Run ignored the stop and the Turn failed. - When every attempt throws, the last attempt now records the failed Turn. Before, only the dispatch record failed and the Turn stayed running. Co-Authored-By: David Cramer <[email protected]>
core-turn.ts imported the Slack pause notice of the automated-turn limit. Core now takes the notice as a step from the owner of the Delivery. The Slack Turn and resume runners and the root mailbox worker pass the Slack notice. Child work has no Delivery, so it passes none, and core clears the notice claim as the Slack notice did for a Conversation without a Slack Location. Co-Authored-By: David Cramer <[email protected]>
…s turn limit A child Turn whose input is a result or message from its own child agent is work the child started. Restore the earlier rule: it neither counts toward nor resets the automated-turn limit. The child owner supplies this as a third TurnLimitInput state, so core has no child check. Root behavior is unchanged. Co-Authored-By: David Cramer <[email protected]>
The resumed Slack Turn started to count toward the automated-turn limit in this PR, with no test. Restore the main behavior. The change moves with the Slack runner conversion, where its test row belongs. Co-Authored-By: David Cramer <[email protected]>
…ed child Turns An error after the result save, such as a failed turn_completed write, went to the stop or fail step when a stop was pending. A child result that waited for an open grandchild then ended as skipped, and the parent never heard. runWorkerTurn now records the save, and any later error retries the attempt. The child report applies the stop-after-save rule, so the retry reports a replaced result and stops the grandchildren. Also: - A failed root or child Turn writes its idempotent start before its failure, so a setup error before the start leaves no lone turn_failed. - A failed or blocked child abandons its paused Turn record, as a stopped child did, so nothing can resume it. - The agent-invocations README names the latest invocation of any status as the owner of a late wake. The Turn lifecycle table gets a depth-2 child owner and rows for a stop before and after the Run, an error after the save with a pending stop, an execution limit, a setup error on the last attempt, and a child auth pause. Rows where the Slack runner differs are listed per owner as slackRunnerGaps. Co-Authored-By: David Cramer <[email protected]>
…im the lifecycle table A failed setup can end a Turn before its start write. Readers key turn_failed by turn id and do not need the start, so the extra start write is removed. The table rows now arrange their mocks in the row. Co-Authored-By: David Cramer <[email protected]>
…ered reply A model failure reply reaches the Location before the save. When the save then fails on the last attempt, the fail step posted another error message. The fail step now skips the Location message when the Location already has a reply. A provider post also counts as delivered when its commit fails. Co-Authored-By: David Cramer <[email protected]>
…ut a Run result A dispatch that failed on its last attempt or was blocked wrote a failed summary but kept a running or paused Turn record. A stop with no saved result only failed the dispatch. The fail step now fails the record, the block step abandons it, and the stop step abandons it and writes the cancelled Turn end, as root and child stops do. Co-Authored-By: David Cramer <[email protected]>
sentry-junior
Bot
force-pushed
the
fix/child-agent-resume-routing
branch
from
November 1, 2026 23:22
ef6dd67 to
b9cc6ef
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b9cc6ef. Configure here.
The row let the start write pass again after the retries, so a start write in a fail step was not covered. Each start write now fails, and the row checks that the failure still reaches the owner.
…ast attempt The fail and block steps wrote the Turn summary, the Turn record, and the Turn end before they marked the dispatch. When one of those writes failed, the dispatch stayed open with no retries left. The step now marks the dispatch first.
dcramer
reviewed
Nov 2, 2026
dcramer
left a comment
Member
There was a problem hiding this comment.
if you look at this in the context of the whole its still confusing... core-turn? conversation-turn? worker-turn? tf is the difference, and thats just one component...
… fix The shared Turn layer did not make the code simpler. It added worker-turn.ts and core-turn.ts with a seven-step callback interface, but the three Slack Turn copies still run their own lifecycle. The source grew by about 390 lines, and most review bugs on this PR came from that layer. This commit puts the branch back to the child fix (0b2d66b): - Child work goes to the child worker, picked from the stored parent. - The last resume attempt counts as final, so a failing child tells its parent. - Child routing tests run through production work. It also keeps one small fix: a failed or blocked child closes its paused Turn record, as a stopped child did. The yielded-child test checks this. The removed work is kept on ref/shared-turn-lifecycle-attempt. Co-Authored-By: David Cramer <[email protected]>
Main replaced the module READMEs with AGENTS.md files. The two routing rules from this PR move there as one bullet each. Main also turned the old yielded-child test into an approve plugin tool test. Its checks now run in the yielded-child table, which already goes through production routing. Co-Authored-By: David Cramer <[email protected]>
dcramer
approved these changes
Nov 2, 2026
This branch was successfully deployed
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.

What broke
A subagent was working on a GitHub event ("PR checks failed"). After 12 minutes it paused, as long jobs do. When it picked the job back up, Junior looked at the job's "came from an event" label and sent it to the code for top-level chats. That code needs a Slack thread to reply in. A subagent has none, so it crashed (
Invalid local conversation id, Sentry JUNIOR-C7). It retried 4 times, gave up, and sat "paused" with nobody told.What this PR changes
Tests
The subagent tests now run through the same routing code that production runs. Before, they built their own copy of it, which is why they missed this bug. The pick-up test covers: a normal task, a GitHub-event job (the prod case), a job that ended before the pick-up, a stopped subagent, and a pick-up that fails every time.
What I took out
An earlier version of this PR also added a shared "Turn lifecycle" layer. It made the code bigger and harder to follow, not simpler: two new files, a seven-callback interface, and the three Slack copies of the same logic were still there. Most review bugs on this PR came from it. It is removed. The old commits are on
ref/shared-turn-lifecycle-attemptfor reference.Still open
The real cleanup is to have fewer copies of the Turn logic and fewer places that store "is this job running, paused, or failed", not a new layer on top. That needs its own design first.
via @dcramer.
--
View Session [Sentry]