Skip to content

fix(server): add sessionId to user_message echo#347

Merged
dimakis merged 2 commits into
mainfrom
fix/user-message-session-bleed
May 29, 2026
Merged

fix(server): add sessionId to user_message echo#347
dimakis merged 2 commits into
mainfrom
fix/user-message-session-bleed

Conversation

@dimakis
Copy link
Copy Markdown
Owner

@dimakis dimakis commented May 20, 2026

Summary

  • user_message WebSocket events were echoed to clients without sessionId, bypassing the frontend's v2 session filter (store.ts wsListener)
  • The filter treated them as global events (like task_state), causing user input from other sessions to bleed into the active session view
  • Added sessionId + v: 2 to the echo payload at all three emission sites in chat.ts (resume, send, interrupt)
  • Updated UserMessageMsg type in ws-messages.ts to include optional sessionId

Root cause

Every other session-scoped v2 event (message_end, session_end, etc.) goes through sendOrBuffer() which auto-enriches with sessionId. The user_message echo bypassed that path entirely, using direct send() + broadcastToObservers().

Test plan

  • Open two sessions, send messages in both — verify no message bleed
  • Refresh page — verify messages stay correct
  • Resume a session — verify resumed user message appears in correct session only
  • Interrupt a running session — verify interrupt message scoped correctly

🤖 Generated with Claude Code

…ion bleed

user_message events were echoed to clients without sessionId, bypassing
the v2 session filter in the frontend store. The filter treated them as
global events, causing user input from other sessions to appear in the
active session view. Adding sessionId + v:2 to the echo payload at all
three emission sites (resume, send, interrupt) aligns user_message with
every other session-scoped v2 event.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dimakis dimakis force-pushed the fix/user-message-session-bleed branch from 4c4f0af to 681c987 Compare May 29, 2026 17:13
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dimakis
Copy link
Copy Markdown
Owner Author

dimakis commented May 29, 2026

Centaur Review

Found 4 issue(s) (2 warning).

server/chat.ts

Correct direction — adding sessionId to user_message echoes fills a real protocol gap. Main gaps: no test coverage for the new fields, and the frontend parser doesn't consume the sessionId, so cross-session bleed prevention is server/observer-only for now.

  • 🟡 regressions (L957): Inconsistency between echo paths: resume path unconditionally sets sessionId: options.resume, while sendToChat (line 1084) and interruptChat (line 1118) conditionally spread it. The resume path also doesn't guard against a falsy options.resume, though in practice it should always be set when this branch executes. More importantly, this creates a semantic inconsistency — the resume echo always has sessionId while the other two may not. [fixable]
  • 🔵 style (L957): The echo object is missing the ts field that the event store record includes (line 948-954 persists ts: Date.now()). Other v2 messages emitted by query-loop.ts include ts via the v2() helper. Consider adding ts: Date.now() to the echo for consistency with other v2-tagged messages and with the persisted event store record. [fixable]

server/__tests__/send-to-chat.test.ts

Correct direction — adding sessionId to user_message echoes fills a real protocol gap. Main gaps: no test coverage for the new fields, and the frontend parser doesn't consume the sessionId, so cross-session bleed prevention is server/observer-only for now.

  • 🟡 missing_tests (L44): Existing tests set session.sessionId but do not assert that the echo includes sessionId or v: 2. The first test (line 44) asserts toMatchObject({ type: 'user_message', text: ... }) — this passes but doesn't verify the new fields. Add assertions like expect(userMsgEvents[0]).toHaveProperty('sessionId', 'sess-123') and expect(userMsgEvents[0]).toHaveProperty('v', 2) to both sendToChat and interruptChat test suites. [fixable]

packages/client/src/protocol-parser.ts

Correct direction — adding sessionId to user_message echoes fills a real protocol gap. Main gaps: no test coverage for the new fields, and the frontend parser doesn't consume the sessionId, so cross-session bleed prevention is server/observer-only for now.

  • 🔵 unsafe_assumptions (L431): The protocol parser ignores the new sessionId on user_message events (line 431-437). For other message types like message_end and session_end, the parser extracts sessionId and uses it for session assignment (lines 339-343, 359-363). If the PR goal is to prevent cross-session bleed on the frontend, the parser should also extract and use sessionId from user_message — otherwise the sessionId is only useful for server-side logging or observer filtering. This may be intentional as a first step, but worth calling out. [fixable]

@dimakis dimakis merged commit 43a1635 into main May 29, 2026
1 check passed
@dimakis dimakis deleted the fix/user-message-session-bleed branch May 29, 2026 17:18
Copy link
Copy Markdown
Owner Author

@dimakis dimakis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Centaur Review

Found 3 issue(s) (1 warning).

server/__tests__/send-to-chat.test.ts

Correct fix — sessionId is added consistently across all three echo sites with proper conditional handling, and the type change is aligned with existing v2 conventions. Main gap is that no test asserts the new sessionId field is actually present in the echo.

  • 🟡 missing_tests: Existing tests for sendToChat and interruptChat assert toMatchObject({ type: 'user_message', text: '...' }) but never verify the new v: 2 or sessionId fields. Since the entire point of this PR is preventing cross-session bleed by adding sessionId, at least one test should assert that sessionId appears in the echoed message when session.sessionId is set (which it is in all test cases — e.g. line 33: session.sessionId = 'sess-123'). A simple expect(userMsgEvents[0]).toMatchObject({ v: 2, sessionId: 'sess-123' }) would lock in the fix. [fixable]
  • 🔵 missing_tests: No test covers the case where session.sessionId is falsy (before SDK resolves it). The conditional spread ...(session.sessionId ? { sessionId: session.sessionId } : {}) should be exercised with a test that omits setting session.sessionId and asserts the echo has no sessionId property. This would guard against regressions where someone changes the conditional to always include it (potentially sending undefined). [fixable]

server/chat.ts

Correct fix — sessionId is added consistently across all three echo sites with proper conditional handling, and the type change is aligned with existing v2 conventions. Main gap is that no test asserts the new sessionId field is actually present in the echo.

  • 🔵 style (L1095): The conditional spread ...(session.sessionId ? { sessionId: session.sessionId } : {}) appears identically in both sendToChat and interruptChat. This is fine for two occurrences, but if you wanted consistency with the resume path (line 962: sessionId: options.resume), you could use the simpler ...(session.sessionId && { sessionId: session.sessionId }). Minor style nit — current form is perfectly correct. [fixable]

Comment thread server/chat.ts
v: 2,
messageId,
text: fullPrompt,
...(session.sessionId ? { sessionId: session.sessionId } : {}),
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 style: The conditional spread ...(session.sessionId ? { sessionId: session.sessionId } : {}) appears identically in both sendToChat and interruptChat. This is fine for two occurrences, but if you wanted consistency with the resume path (line 962: sessionId: options.resume), you could use the simpler ...(session.sessionId && { sessionId: session.sessionId }). Minor style nit — current form is perfectly correct. [fixable]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant