You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When several background results finish while their conversation is busy, each one gets its own wake-up turn. When the busy turn ends, they all become deliverable at once. One wins, and the others fail generation creation with JobPredecessorMismatchError, defer, and run one after another. Four finished results cost four agent turns and four model calls, and the last one arrives only after the other three turns.
This is a follow-up to #16349, #16339, #16345 and #16343. Those made waiting cheap and delivery prompt; they don't change how many turns a group of results costs. Implementation is maintainer-directed canary work: branch from origin/canary and open the PR with --base canary.
Evidence
A local stress run on dev (7238be3: #16349 + #16339), with 8 conversations each finishing 4 background results behind a 5-minute turn:
32/32 results delivered, each on its first attempt, a median of 2 s after the busy turn ended
32 separate wake-up turns, and 43 JobPredecessorMismatchError logs from wake-ups colliding at turn creation (12 on the earlier dev baseline, 29 with 🐢 fix: Back Off Waiting Completion Wake-ups and Keep Their Trace Out of Requests #16349 alone). Prompt delivery releases a conversation's results at the same moment, so collisions grow as delivery gets faster.
On the demo deployment, real traffic produced 21 of these errors over 5 days. Most real results are collected by the agent's own same-turn poll, so the cost is concentrated in turns that park several results.
Why batching doesn't apply today
The message-backed path already claims a bounded sibling batch (completionResultBatchSize, default 8). The durable-receipt path, which is the normal path, claims exactly one result and reconciles that result's ownership against the parent message before dispatch. Extending bounded batching to durable receipts is the optimization. It's also where the delivery guarantees live, so it needs an explicit contract first.
Proposed contract
Ownership per result, not per batch. Each receipt claim is its own CAS; updateMany is atomic per row, not per batch. Concurrent roots may claim different subsets and start separate turns. Opportunistic batching reduces turns and collisions; it doesn't guarantee one turn per group.
Siblings stay responsible for their results. A sibling whose result a batch root claimed does not settle. It defers as a follower, and settles only once the root's continuation is confirmed applied. If the root dead-letters, is retired, or releases its claims, the follower claims and delivers its own result. Its row persists, so a crash at any point strands nothing.
Frozen membership. The root records its member list, the claims that actually succeeded, durably on its own row before dispatch. Retries rebuild the same input from that list and never add newly finished siblings. A lost write reply is idempotent: reading back by claimId returns the same set.
Explicit lifecycle for: partial claim success, lost write replies, definite pre-dispatch failure (release every member), dispatch that may already have started (keep ownership, reconcile), and root cancellation, dead-lettering and owner loss (followers take over).
Every member reconciled, with a precise scope. Message-backed ownership is checked for every selected result, including races with manual polls and late message persistence. The scope key is principal, tenant, conversation, parent branch and target agent, not just "same conversation".
Mixed-version gate. An older replica settles a claimed sibling the old way, which would reintroduce stranding. So batching must be gated: behind a new worker capability, or a flag that stays off until every replica runs it. Recovery is proven with a mixed-version test, not assumed.
Scope for a first version: ordinary background tools and code results. Subagent completions stay one per turn. No waiting window: only results already finished at claim time are folded in.
Per-branch election, meaning one turn per ready group, is a possible second phase if that becomes a hard requirement. It adds coordination and recovery complexity, so it needs its own design and estimate.
Acceptance
Barrier-controlled multi-worker races and crash injection at each step: after claim, after the membership write, after dispatch, and during retry.
The stress run reports unique results consumed, duplicates, stranded results, turns, and delivery latency. Target: 8 conversations × 4 results produce well under 32 turns, with zero duplicates, zero stranded results, and no latency regression against dev. The turn reduction is a benchmark target, not an established result.
JobPredecessorMismatchError from wake-up collisions drops substantially in the same run.
Summary
When several background results finish while their conversation is busy, each one gets its own wake-up turn. When the busy turn ends, they all become deliverable at once. One wins, and the others fail generation creation with
JobPredecessorMismatchError, defer, and run one after another. Four finished results cost four agent turns and four model calls, and the last one arrives only after the other three turns.This is a follow-up to #16349, #16339, #16345 and #16343. Those made waiting cheap and delivery prompt; they don't change how many turns a group of results costs. Implementation is maintainer-directed canary work: branch from
origin/canaryand open the PR with--base canary.Evidence
A local stress run on
dev(7238be3: #16349 + #16339), with 8 conversations each finishing 4 background results behind a 5-minute turn:JobPredecessorMismatchErrorlogs from wake-ups colliding at turn creation (12 on the earlierdevbaseline, 29 with 🐢 fix: Back Off Waiting Completion Wake-ups and Keep Their Trace Out of Requests #16349 alone). Prompt delivery releases a conversation's results at the same moment, so collisions grow as delivery gets faster.Why batching doesn't apply today
The message-backed path already claims a bounded sibling batch (
completionResultBatchSize, default 8). The durable-receipt path, which is the normal path, claims exactly one result and reconciles that result's ownership against the parent message before dispatch. Extending bounded batching to durable receipts is the optimization. It's also where the delivery guarantees live, so it needs an explicit contract first.Proposed contract
updateManyis atomic per row, not per batch. Concurrent roots may claim different subsets and start separate turns. Opportunistic batching reduces turns and collisions; it doesn't guarantee one turn per group.claimIdreturns the same set.Per-branch election, meaning one turn per ready group, is a possible second phase if that becomes a hard requirement. It adds coordination and recovery complexity, so it needs its own design and estimate.
Acceptance
dev. The turn reduction is a benchmark target, not an established result.JobPredecessorMismatchErrorfrom wake-up collisions drops substantially in the same run.