[eric] streaming: a finished reply no longer hands a still-thinking bubble to the next socket; the live thought is dropped when the answer starts and at every turn end, and a snapshot is only built for a turn in flight

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9zwUaHucUgrdxvK8FvjYT
This commit is contained in:
ciregenz
2026-09-03 16:14:53 -07:00
co-authored by Claude Fable 5.1
parent 09f839496f
commit 5b7c8ee2ba
11 changed files with 99 additions and 4 deletions
@@ -39,3 +39,11 @@ test('the socket handles the snapshot ABOVE the replay-skip guard that drops pre
const dashboardSkip = src.indexOf('if (this.skipStreamEvents) {');
assert.ok(snapshot < dashboardSkip, 'the snapshot handler decides skipStreamEvents itself, above the generic skip');
});
test('the socket refuses a snapshot for a session it already knows is finished', () => {
const src = fs.readFileSync(path.join(process.cwd(), 'src/shared/ws/WebSocketManager.ts'), 'utf8');
const i = src.indexOf("event === 'agent:stream_snapshot'");
const block = src.slice(i, i + 900);
assert.match(block, /agents\.sessions\[session_id\]\?\.status/);
assert.match(block, /!finished/);
});
+4 -1
View File
@@ -495,7 +495,10 @@ class WebSocketManager {
// Sent once per (re)connect, after the resume ack, so it must not sit behind the replay-skip guard below that drops pre-ack stream frames.
if (event === 'agent:stream_snapshot') {
if (session_id && data.message_id && typeof data.text === 'string' && !this.skipStreamEvents) {
// A snapshot is only ever for a turn in flight; a finished session that still gets one is a leak, not a reply.
const snapStatus = session_id ? store.getState().agents.sessions[session_id]?.status : undefined;
const finished = snapStatus !== undefined && snapStatus !== 'running' && snapStatus !== 'waiting_approval';
if (session_id && data.message_id && typeof data.text === 'string' && !this.skipStreamEvents && !finished) {
store.dispatch(streamSnapshot({ sessionId: session_id, messageId: data.message_id, role: data.role, text: data.text }));
}
return;