fix: Better handle out of turn events (#780)

Claude has a lot of background tasks. If we wait for "idle", some people
experience turns that never end.
If we stop iterating the query after result, people end up with events
that never show up.

Given we already often have session_updates outside of the prompt (and
clients need to handle it for v2 if they aren't already) this moves the
setup to better handle how the SDK gives us this information.

I've been test-driving this build all day and it seems to be working
great.

Closes #773
Closes #679
Closes #435
Closes #497
Closes #688
Closes #630

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Brandt
2026-06-16 21:33:54 +02:00
committed by GitHub
parent b3640599ae
commit 4f273a20d8
3 changed files with 1406 additions and 498 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -216,6 +216,31 @@ describe("session config options", () => {
).rejects.toThrow("Invalid value for config option mode: invalid-mode");
});
it("rejects mode and config changes once the query stream has closed (husk session)", async () => {
// After an unexpected stream death the session lingers as a husk
// (queryClosed=true) so prompt() can answer with a clear error. The
// config/mode handlers must do the same rather than calling setModel/
// setPermissionMode on the closed query.
const session = (agent as unknown as { sessions: Record<string, { queryClosed?: boolean }> })
.sessions[SESSION_ID];
session.queryClosed = true;
await expect(
agent.setSessionConfigOption({
sessionId: SESSION_ID,
configId: "model",
value: "claude-sonnet-4-6",
}),
).rejects.toThrow(/start a new session/);
await expect(agent.setSessionMode({ sessionId: SESSION_ID, modeId: "plan" })).rejects.toThrow(
/start a new session/,
);
// Short-circuited before touching the (closed) query.
expect(setModelSpy).not.toHaveBeenCalled();
expect(setPermissionModeSpy).not.toHaveBeenCalled();
});
it("changes mode, sends current_mode_update but not config_option_update", async () => {
await agent.setSessionConfigOption({
sessionId: SESSION_ID,