Repository navigation
fix(chat): keep interrupted turns in history and hand waits over with a message - #73
Merged
Merged
Conversation
Since the abort is rethrown (#70), a turn cut short by the person speaking during a wait, or by Stop, left before its messages reached history: the next request carried two user messages and nothing between, and the assistant denied work it had done and redid it. Track the messages of finished steps and commit them before the next turn pushes its own, with the still-open step recorded and its pending calls answered as interrupted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The wait tool's only argument was described as shown to the user, and models took the waiting chip for their reply: judged runs handed buttons over with no text, and the person had to ask whether they could press. The tool now requires the message itself, shown as the step's text when the step wrote none, and kept in history as the call's argument. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On a dataset page a data table and a rich-text toolbar filled the head of the capped outline, the metadata fields below landed in the cut, and the persona told the assistant a field it had just filled did not exist. Rows now keep their name without repeating their cells, tables list 20 rows and count the rest, toolbars fold to one line, and unnamed images, link targets and dividers are dropped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sendMessage returned once Send was clicked. In a judged run three persona messages stayed in the composer while a wait was armed, no request followed, and the run still counted nine turns and was judged. The chat empties its composer when it takes a message, so a send now waits for that and throws otherwise, voiding the run instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each persona turn is a fresh query remembering only the chat, so it could not tell its own actions from the assistant's instructions: judged runs had it report reloading a page it never reloaded. The prompt now lists its clicks and typing so far and says nothing else was done. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t stopped onStepFinish runs on the SDK's side of the stream and can report a step finished before the loop has read its parts, so the open step could hold calls already in the finished messages and put the same call id in history twice. Reset the open step on the finish-step part the loop reads and drop calls already finished. Results that arrived in the open step go through their tool's toModelOutput as the SDK would, and Stop gets its own interruption text. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ady says it
A step that wrote a few words ("Voilà.") before declaring the wait hid its
message, and the handover went unsaid again.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The persona asks a question before pressing the button it was handed, so each run interrupts a declared wait and checks that the assistant answers from what it already did instead of rebuilding it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… wait An interrupted wait's result said only that the person had written. A judged run answered the question and never waited again, so it never learned the list it had prepared was created. The result now names what the wait was for and asks to declare it again if it is still to come. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The runner ignored whether a turn ended or was holding a declared wait, so a persona that clicked the handed-over button and stopped ended the run before the assistant reacted. Port data-fair's hand-over handling, let the persona read the resumed reply before stopping, and have speak-during-wait expect to be told of the creation unprompted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The cap cut at a fixed character count, and in a judged run it fell inside a filled textbox: the persona read the start of the field's value as all of it, told the assistant its terms were missing and never saved. Lines are now shown whole or not at all, the marker counts the lines left out, and a root can take its own budget. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
albanm
added a commit
that referenced
this pull request
Oct 6, 2026
All three PRs changed the in-browser loop and the gateway, which this branch deletes, so this merge resolves conflicts mechanically and the behaviour is carried over onto the server loop in the commits that follow - one per behaviour, each with its tests: - #73/#75 wait handover: `wait_for_user_action` requires a `message`, repairs a small-model slip, refuses a wait with no message. Merged here (it lives in shared/host-events.ts); the server's rendering of the message in the transcript follows. - #73/#75 interrupted turns: an aborted turn's open calls keep a result saying why, and the next turn carries a reminder of what the wait was for. Follows. - #74 self-service consumption, redacted quota refusals, conversation cost. Follows. Resolved here: - Deleted on this branch, modified on main: kept deleted - api/src/gateway/router.ts, ui/src/composables/{use-agent-chat,agent-stream-parts}.ts and their two specs. - New on main and built on the deleted code, set aside to come back adapted: interrupted-turn.ts and its spec, gateway-cost.ts and its spec, the two self-usage specs, the "speaking during a wait" e2e (it reads the browser's /chat/completions request), and two moderation-cost api tests (gateway-shaped, and anonymous). - api/src/usage/enforce.ts: this branch's `checkAccountCreditCap` (re-checked between steps of a long run) and main's `accountViolation` (the cap plus the untrusted pool, also read by the self-usage view) compose - the cap check stays one definition. - shared/host-events.ts: main's `repairWaitInput` moves here from the deleted stream-part builder, beside the tool that uses it; verified byte-identical. - dev/resources/organizations.json: main's `user1` -> `user` role fix, plus this branch's NHI members. Takes effect when simple-directory reloads its fixtures. - The debug dialog shows its Consumption tab only when the chat can report usage, which the #74 port provides; until then its e2e keeps the two-tab expectation. - The wait-tool specs pass the now-required `message`. - ui/dts/auto-imports.d.ts: the running dev server wrote entries for main's set-aside files, and the committed copy already declared 11 modules this branch had deleted; pruned to what exists. host-events.md and quotas-usage.md merged main's text, which describes the browser mechanisms; each port commit rewrites its section for the server. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
albanm
added a commit
that referenced
this pull request
Oct 6, 2026
…port of #73, #75) Main's #73 gave `wait_for_user_action` a required `message` - the model's words to the person, what is ready and what to press - because models read the `expecting` status label as their reply and handed buttons over in silence. The browser loop rendered it; that loop is gone, so the server now adds the message to the step's own text when it records the call (`waitHandover`), just before the call and unless the step already says it, and streams it. Stored, not only streamed, so a reloaded or reviewed thread shows the same handover. Before the call rather than after it, because text after a tool call is replayed to the model as a separate assistant message it never wrote. The rule is a pure function in shared/host-events.ts beside `repairWaitInput` (#75's repair of a small-model slip), unit-tested with main's cases; an api test checks the server streams it, stores it once, and puts it before the call. The first version also cleared the activity label, as the first text-delta does. That erased "Waiting for ...": the SDK starts executing a tool before the loop reads its call from the stream, so the wait had already set its label. The host-events e2e caught it (9 of 13 failed); the handover now leaves the activity alone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
albanm
added a commit
that referenced
this pull request
Oct 6, 2026
#73, #75) A turn the person stops - by writing during it, or with Stop - must stay legible to the model. Without that, judged runs saw the person's two messages with nothing between, denied work they had done and redid it. Main fixed it for the browser loop; this is the same behaviour on the server. The server stores every turn as it goes, so most of an aborted turn is already in the record. The gap is the calls still open when it stopped: a call with no result is dropped on replay (`ignoreIncompleteToolCalls`), so the model never learned it had been waiting. The abort path now settles every open call with a result saying why (`settleInterruptedParts`, main's wording): the interrupted wait says what it waited for, any other call that the person spoke, and after Stop that the reply was stopped. The next request carries main's #75 reminder - what the wait was for, and to declare it again after answering - last in the hidden block, just before the person's words, and only for the turn right after (`pendingWaitReminder`, read from the settled wait in history; the conversation lock guarantees it is settled by then). Speaking writes no stop notice: the person's message answers it, and the notice would be replayed as the assistant's own words. Three things the port surfaced, fixed here: - Calls were persisted only on the throttled text clock, so the abort path - which reads the store - could miss an open wait entirely. They are now persisted the moment they open or settle, which also means a page reloaded mid-wait shows the wait. - On a failed or stopped turn, `turn-end` went out BEFORE the message was settled, unlike the success path. Anything reacting to it read the turn half-finished; the new api spec passed alone and failed in sequence. `turn-end` is now last on both paths. - The SDK runs a tool ahead of the loop reading the stream, so the loop's late "Analyzing tool result..." overwrote the wait's "Waiting for ...". While a wait is pending, the wait now owns the activity label. Tested at the api level with a mock seam reporting what the model was sent - the server-side counterpart of main's e2e, which read the browser's /chat/completions request. The module lives in api/src, not shared/: it is server-only, and the shared-contract spec enforces that. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Fixes found by the data-fair judged simulation baseline, in the chat and in the simulation harness.
wait_for_user_actiontakes a requiredmessage, shown as the reply to the person (after the step's own text unless it already says it); models used to read the waiting chip as their message and hand buttons over in silence.speak-during-waitcase.Why: in data-fair's baseline three of six cases failed on these defects; with them fixed, all seven data-fair cases pass (Sonnet, one pass).
Heads-up:
messageis now required onwait_for_user_action: hosts that tell the model to "tell the user, then wait" should point at the message instead (data-fair does so in its companion change).toolResultOutputmirrors the SDK's unexportedcreateToolModelOutput; recheck onaiupgrades.