fix(controller): isolate sessions across bots to stop cross-channel transcript leak#1122
Open
anthhub wants to merge 1 commit into
Open
fix(controller): isolate sessions across bots to stop cross-channel transcript leak#1122anthhub wants to merge 1 commit into
anthhub wants to merge 1 commit into
Conversation
listSessions emitted session.id = file.name (the sessionKey with .jsonl), which is not unique across bots. When two bots had the same sessionKey (e.g. WeChat bot and Web bot both using "shared-session"), getSession matched by id alone and returned the session with the most recent updatedAt — so opening one user's session could silently serve another bot's transcript and message count, causing the cross-channel message leak and wrong counters reported in #980. - Encode session.id as base64url({botId, sessionKey}) so it is unique per bot. The id stays URL-safe and opaque to the frontend. - getSession decodes the id and routes through getSessionByKey, which now matches on (botId, sessionKey) instead of the filename alone. - Legacy "<sessionKey>.jsonl" ids are honored only when a single session matches; ambiguous legacy ids safely return 404 instead of returning the wrong bot's transcript. - Add a regression test that seeds duplicate "shared.jsonl" under two bots and asserts isolated ids plus per-bot transcript contents. - Ship a repro script (scripts/repro/issue-980-verify.mjs) that seeds fixtures, drives the desktop renderer, and captures before/after screenshots for manual verification. Closes #980
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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.
What
Fix conversation record isolation so that two bots sharing a
sessionKeynever serve each other's transcripts or message counts.Why
Closes #980.
P0 user report (微信群): a WeChat session card showed 「3 条消息」 while the user only exchanged 2, and its history contained an English-language Web conversation that did not belong to them.
Root cause in
apps/controller/src/runtime/sessions-runtime.ts:listSessionsassignedsession.id = file.name(the bare<sessionKey>.jsonl). When two bots happened to use the samesessionKey, both rows had the same id.getSession(id)ransessions.find(s => s.id === id)and returned whichever bot's session had the most recentupdatedAt. Opening the WeChat card therefore fetched the Web transcript, and the count came from the wrong file.File layout (
agents/<botId>/sessions/<sessionKey>.jsonl) is already bot-namespaced at the filesystem level, so nothing on the write side was wrong. The bug was purely read-side id resolution.How
session.idassession:<base64url({botId, sessionKey})>so every card carries an unambiguous identity.base64urlkeeps the value safe for React keys and URL paths (used directly in/workspace/sessions/:id).getSession(id)decodes the id and delegates togetSessionByKey(botId, sessionKey), which now matches on both fields instead of the filename alone.<sessionKey>.jsonlids are honored only when exactly one session matches. Ambiguous legacy ids (the exact condition that caused [P0][微信群(nexu-微信龙虾交流群 10)] 会话记录显示异常:条数不符并混入非本人 Web 对话,疑似会话隔离或归属错误 #980) return 404 rather than silently serving the wrong transcript.updateSession,resetSession,deleteSession,getChatHistory) go throughgetSession(id), so they inherit the fix.shared.jsonl, and asserts unique ids plus fully isolated transcript reads.scripts/repro/issue-980-verify.mjsthat seeds fixtures, drives the desktop renderer, and captures screenshots so reviewers can reproduce or regress-test the UX path locally.Affected areas
Checklist
pnpm --filter @nexu/controller typecheckpassestests/sessions-runtime.test.ts(new + existing isolation coverage) passespnpm generate-types— no API route/schema changes, response shape is unchangedanytypes introducedScreenshots / recordings
Seeded two bots (
repro-web-bot,repro-wechat-bot) with the sameshared-sessionsessionKey. After the fix:1 条消息.This is the WEB-only repro message.with1 条消息.On
mainthe same fixture would serve the WeChat transcript to both cards (the newerupdatedAtalways wins), perfectly reproducing the #980 symptoms.Notes for reviewers
getSessionbranch that only honors<sessionKey>.jsonlwhen there is a single match) is intentional — it avoids re-introducing the [P0][微信群(nexu-微信龙虾交流群 10)] 会话记录显示异常:条数不符并混入非本人 Web 对话,疑似会话隔离或归属错误 #980 silent mis-routing while still serving old bookmarks when they are unambiguous. If you prefer a hard cutover, happy to remove it.Webas the channel label for a WeChat session. That is a display-layer bug separate from the isolation fix and should be filed as a follow-up.