fix(x): make stopTurn reliable under concurrent advances on one turn

The sessions layer tracked live advances in a map keyed by bare turnId
with overwrite-on-set and unconditional delete-on-settle. A turn can
legally have several live advances at once — a running invocation plus
external-input invocations queued on the turn lock (e.g. a permission
response while an async result is pending). The overwrite made stopTurn
abort the wrong controller and await an outcome that couldn't settle;
the unconditional delete untracked a sibling advance, so a later
stopTurn missed the abort path, fell back to a cancel input, and could
reject with TurnInputError if the turn had completed meanwhile — a
user-visible stop failure. Durable state was never affected.

Track a set of advances per turn: each advance removes only its own
entry on settle, and stopTurn aborts every live controller and awaits
all outcomes. A cancel input that loses the race with a concurrent
settle now counts as a successful stop (the turn is already terminal)
instead of rejecting.

Adds four tests: concurrent invocations both aborted, an earlier settle
not untracking a later advance, the settle-race cancel treated as
success, and non-terminal rejections still surfacing. Updates
session-design.md §9.3 and §13.5 accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-10 22:46:06 +05:30
parent 284dc77f5e
commit e22d3b4796
3 changed files with 172 additions and 23 deletions

View file

@ -338,9 +338,14 @@ runtime's job; the session layer passes its errors through.
### 9.3 stopTurn and resumeTurn
- `stopTurn` cancels via the turn runtime: aborting the active invocation's
signal if one is running, else advancing a suspended turn with a `cancel`
input.
- `stopTurn` cancels via the turn runtime: aborting the signal of every
live advance the layer has started for the turn, else advancing the turn
with a `cancel` input. A turn can legally have several live advances at
once — one running invocation plus external-input invocations queued on
the turn lock — so the layer tracks them per turn and stop aborts them
all; the queued ones observe their aborted signal when the lock frees. A
cancel input that loses the race with a concurrent settle (the turn is
already terminal by the time it applies) counts as a successful stop.
- `resumeTurn` re-enters the latest turn with no input — the turn spec's
recovery entry point — for turns left `idle` by a crash. There is no
automatic resume sweep at startup: recovery re-issues interrupted model
@ -499,6 +504,11 @@ All tests use the in-memory/mocked turn runtime and repo fakes.
correct turn with the correct input type.
- Turn-runtime rejections (unknown call, terminal turn) pass through.
- `sendMessage` never routes to ask-human.
- `stopTurn` aborts every live advance when a turn has concurrent
invocations, and an earlier advance settling does not untrack a later
one.
- A `stopTurn` cancel input that lost the race with a concurrent settle
resolves as a successful stop; a non-terminal rejection still surfaces.
### 13.6 Index