fix(bot-mode): fail closed on transient group-session resume failures - #92870
fix(bot-mode): fail closed on transient group-session resume failures#92870pierrenode wants to merge 1 commit into
Conversation
ensureGroupChatSession's resume loop caught ANY session.resume error (stored sid, then title lookup) identically and fell through to session.create — the same bug findExistingCanonicalChat was fixed for hours earlier (87b645f) in the same file: a transient failure (the backend still warming up after a restart, a network blip on a cross-connection lookup, an oversized-resume refusal) read as "no session, mint a new one". That forks the member's real session AND silently overwrites room.sessions[key], making the original unreachable from the room. ensureGroupChatSession is actually more exposed than the 1:1 case: it runs every group turn (runGroupChatMemberTurn), with two independent swallow points. Distinguish "genuinely doesn't exist" from "transient failure" the same way the gateway itself does: session.resume's own handler (tui_gateway/methods_session.py) returns JSON-RPC code 4007 only when the target truly isn't found; every other failure (including 4130, "session too large to resume" — a session that DOES exist) now surfaces instead of being silently swallowed. The existing outer try/catch at the call site already treats a thrown error as "this member passes the round" (recordGroupActivity kind: 'failed'), so nothing new needs to catch it — a transient hiccup now costs one skipped round instead of a permanent fork.
Overall: correct and well-tested fail-closed fix — the fork-on-transient-failure consequence (silently overwriting
The error message including the member name and the explicit "not starting a new one" clause is exactly the right operator-facing wording. |
|
Merged via ring-2 consolidated PR #93430 — your commit cherry-picked with authorship preserved (group-session resume fails closed on transient errors; only 4007 means absent, mirroring your earlier findExistingCanonicalChat fix). Thanks @pierrenode — that's two solid halves of the bot-mode reliability train from you. |
Summary
Sibling-gap:
findExistingCanonicalChatwas fixed for exactly this bug class hours earlier in this same file (87b645f) — "FAIL CLOSED. A failed registry lookup MUST NOT read as 'no Bot Chat exists' — that is the one remaining way to fork a bot's forever chat."ensureGroupChatSession(group-member sessions, not the 1:1 canonical chat) still has the pre-fix shape: its resume loop (stored sid, then title lookup) catches anysession.resumeerror identically and falls through tosession.create.A transient failure (the backend still warming up after a restart — the exact window
findExistingCanonicalChat's fix calls out — or a network blip on a cross-connection lookup) reads as "no session, mint a new one." That mints a fresh hidden session AND silently overwritesroom.sessions[key]with the new id, orphaning the member's real prior history from the room.ensureGroupChatSessionis actually more exposed than the 1:1 case it mirrors: it runs on every group turn (runGroupChatMemberTurn), with two independent swallow points instead of one.Fix
Distinguish "genuinely doesn't exist" from "transient failure" the same way the gateway itself does.
session.resume's own handler (tui_gateway/methods_session.py) returns JSON-RPC error code 4007 only when the target truly isn't found (checked by id, then by title); every other failure — including 4130 ("session too large to resume",SessionResumeTooLargeError— a session that genuinely does exist) — now surfaces as a thrown error instead of being silently read as "doesn't exist."The call site already has a safety net for this:
runGroupChatMemberTurn's caller wraps it intry { ... } catch { recordGroupActivity(group, { kind: 'failed', ... }); reply = null /* a failed turn is a pass, never a room error */ }. So a transient hiccup now costs the member one skipped round (it retries from scratch next round, since nothing was persisted toroom.sessions[key]on failure) instead of permanently forking the session.Testing
apps/desktop/src/plugins/hermes-bots/tests/group-chat.test.mjs: two new tests —a transient resume failure fails closed instead of forking the member session: a.code = 5000(non-4007) resume error rejectsensureGroupChatSessionand leavesroom.sessionsand the session count untouched (mutation-verified: fails on pre-fix code with "Missing expected rejection" — it silently created a second session instead).a genuinely nonexistent stored session (4007) still falls through to the title lookup, then create: confirms the legitimate "nothing to resume yet" path still works.session.resume"not found" mocks in this test file plus the two sibling harnesses that independently mock the same RPC (group-activity.test.mjs,group-chat-attachments.test.mjs) to shape their thrown error like the real gateway'sJsonRpcGatewayError(a.codeproperty, perapps/shared/src/json-rpc-gateway.ts) — their old plain-Error, code-less mocks no longer matched what production now branches on, which surfaced as 3 failures across those two files until fixed the same way.apps/desktop/src/plugins/hermes-bots/tests/*.test.mjssuite: 413/413 passing.node --checkclean on all four changed files.eslintcould not run (pre-existing, unrelated to this change — workspace-scopedapps/desktopinstall is missing the root-onlyglobalspeer dependency this repo'seslint.config.mjsrequires).Competing PRs
Checked
gh pr list --searchacross several keyword combinations. #90343 ("bot sessions always follow the profile's current config") touchesensureGroupChatSessionbut only adds newsession.createparameters (room_plumbing,follow_profile_config) for a different bug (stale provider pin after a profile switch) — it doesn't touch the resume try/catch this PR changes. No other open PR addresses this gap.