Skip to content

fix(agent-invocations): Route child work by stored parent and fail it on the last try - #2500

Merged
sentry-junior[bot] merged 2 commits into
fix/child-agent-resume-routingfrom
fix/child-agent-terminal-failure
Nov 1, 2026
Merged

sentry-junior[bot] merged 2 commits into
fix/child-agent-resume-routingfrom
fix/child-agent-terminal-failure

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2499. Merge that first.

#2499 fixed the reproduced bug, where a child agent's watch-event resume went to the root worker and crashed. This PR fixes the two design gaps behind it, so the same kind of bug cannot happen again quietly.

1. The stored parent picks the worker (app/conversation-work.ts, agent-invocations/result.ts, cli/chat.ts)

Before, run picked a worker by trying a list of checks in order: message labels, the Turn Source, and agent invocation lookups. The bug came from that order. Now run reads the Conversation record once. If it has a parentConversationId, the work goes only to createChildConversationWorker. That worker always returns a result. Work that no agent invocation owns throws an error and does not fall through to a root worker. Root wakes also skip the agent invocation SQL reads they made before.

2. A resume attempt can be the final attempt (task-execution/worker.ts, agent-invocations/work.ts)

isFinalAttempt was set only from how many times a message had been delivered. A resume wake has no messages, so it was never final. When a resumed Turn kept failing, the worker gave up and dropped the mailbox, but no adapter got a chance to record the failure. For a child agent, the invocation stayed paused. The parent was never told. Later results from that child were also held back as replaced, because hasOpenChildWork still saw the stuck invocation.

Now the retry count also sets isFinalAttempt: an attempt is final when one more failure reaches CONVERSATION_WORK_MAX_RETRIES. On that attempt, the agent invocation worker fails the invocation for any error and reports it to the parent. This includes save errors without an AgentRunError wrapper, which it used to rethrow even on the final attempt. Root conversation Turns and dispatches use the same flag, so a resume that keeps failing now ends with their normal final-attempt failure path (a fallback reply or a failed dispatch), not a silent stall.

Worker crashes: this does not need a separate repair job. If a child's worker dies in the middle of a Turn, the next wake fails that Turn through projectTerminalTurn, well before the retry limit is reached.

Tests

The resume test in agent-invocation-work.test.ts goes through real production routing. It now has a fourth case: a child yields, then its resume throws on every attempt. Without the worker change, the last attempt gives up (conversation.work.retry.exhausted) and the invocation stays open. With the change, the invocation ends failed and its result is appended to the parent mailbox.

  • tsc, oxlint, depcruise, the test-architecture check, and the file-length check pass.
  • 25 test files / 274 tests pass: agent invocation, agent dispatch worker and recovery, task-execution component, conversation-turn, Slack runtime, and CLI.
  • durable-queue.test.ts (2 tests) and agent-dispatch-work.test.ts (1 test) time out in my sandbox. They time out the same way on the fix(agent-invocations): Route subagent resumes to subagent code #2499 head without this change.

Still open (not in this PR)

  • If the worker crashes several times before a child Turn even starts, the work can still give up and drop queued parent messages. That needs a heartbeat repair, and I have not seen it in production.
  • The message, nested, spawn, and stop tests still build worker inputs by hand instead of going through real routing.

via David Cramer.

--

View Session [Sentry]

sentry-junior Bot and others added 2 commits November 1, 2026 17:10
The server picked the worker for a Conversation by asking a list of
questions about the work, in order. A wrong order sent a child resume to
a root worker. Now the stored parentConversationId decides once: a child
Conversation only reaches the child worker, and work that no agent
invocation owns is an error instead of falling through. Root wakes also
skip the agent invocation SQL reads.

Co-Authored-By: David Cramer <[email protected]>
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]>
@vercel

vercel Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
junior-docs Ready Ready Preview Nov 1, 2026 5:12pm UTC

Request Review

@github-actions github-actions Bot added the risk: medium PR risk score: medium label Nov 1, 2026
@sentry-junior
sentry-junior Bot merged commit 8be6f2d into fix/child-agent-resume-routing Nov 1, 2026
7 checks passed
@sentry-junior
sentry-junior Bot deleted the fix/child-agent-terminal-failure branch November 1, 2026 17:32

This branch was successfully deployed

1 active deployment
Preview – junior-docs — 8be6f2d7 Deployed Nov 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants