Skip to content

fix(mcp): recover tool calls when the shim's conversation id goes stale after host resume - #657

Merged
ranxianglei merged 2 commits into
ranxianglei:masterfrom
ranxueqing:fix/mcp-stale-session-id
Sep 9, 2026
Merged

fix(mcp): recover tool calls when the shim's conversation id goes stale after host resume#657
ranxianglei merged 2 commits into
ranxianglei:masterfrom
ranxueqing:fix/mcp-stale-session-id

Conversation

@ranxueqing

Copy link
Copy Markdown
Contributor

Fixes #656

Problem

After a Claude Code --resume (which forks a NEW session id after MCP children were spawned), the shim's env-captured CLAUDE_CODE_SESSION_ID is stale forever: every tool call 404s with unknown plugin conversation (no model request has arrived with this conversation id yet) while the proxy keeps serving the conversation under the new id. Only a full CLI restart heals it.

Root cause chain

  • src/mcp.ts captures the conversation id once at process start and never refreshes it;
  • the host never re-sends it, and the proxy registers conversations by the id derived from actual model requests — so after a resume the two sides hold different ids with no recovery path.

Fix

  1. src/mcp.ts — orphan adoption: when a tool call hits that exact 404, the shim asks status?conversationId=<stale>&fallback=latest, adopts the resolved conversation id, and retries once (one-time stderr note). Armed only for identity-bound hosts (CLAUDE_CODE_SESSION_ID); BILI_MCP_NO_ORPHAN_ADOPT=1 opts out for setups where several host sessions share one proxy and a resumed one must not adopt a sibling's conversation.
  2. src/plugin.ts — fallback now reports the adopted id (required for 1): the status fallback branch picks the latest active session but used to echo the caller's stale id back, so adoption was impossible. It now reverse-resolves the session's most-recently-seen conversation id.
  3. src/plugin.ts — 404 diagnostics: the tool endpoint's single error message covered two different failures; they are now distinct (never-registered id vs registered-but-session-not-resident), and rejections are logged — these 404s were previously invisible in bili.log.

Tests

  • New: #656: status fallback=latest resolves the active conversation for a stale shim id… (adoption resolution + retry with adopted id) and a wording assertion on the never-registered 404.
  • tests/plugin-protocol.test.ts: 12/12 pass; tsc --noEmit clean.
  • Full suite: 1242 pass / 5 fail — the same 5 fail identically on a clean master checkout in this environment (verified with git stash; environment-dependent, cf. test: resolveClientCommand codex/claude test is environment-dependent (fails when codex/claude live in /usr/bin) #642): 2× launcher-plugin-mode mcp stdio shell, 1× mitm whitelist, 2× plugin-agent.

Note

The commit was uploaded via the REST git API (local network can't run git push); tree content is identical to a normal push — 3 files, +123/−15.

…le after host resume

Fixes ranxianglei#656.

Claude Code resumes fork a NEW session id after MCP children are spawned,
so the shim's env-captured CLAUDE_CODE_SESSION_ID goes stale and every tool
call 404s with 'unknown plugin conversation' for the rest of the session
while the proxy keeps serving the conversation under the new id.

- mcp.ts: on that exact 404, adopt the proxy's most-recent active
  conversation (status?fallback=latest) and retry once. Identity-bound
  hosts only; BILI_MCP_NO_ORPHAN_ADOPT=1 opts out for multi-session setups.
- plugin.ts: status fallback now reports the RESOLVED conversation id
  (it used to echo the caller's stale id back, so adoption was impossible).
- plugin.ts: split the tool-endpoint 404 into its two real causes
  (never-registered id vs session-not-resident) and log the rejection -
  these 404s used to be invisible in bili.log.
- tests: fallback resolution + adopted-id tool call; never-registered
  wording assertion.
@ranxianglei

Copy link
Copy Markdown
Owner

接单

@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

已审核 PR #657(fixes #656)。结论:修复正确,可合并(合并由人工执行)。

根因验证(#656 — issue 引用的代码路径全部属实:

  • src/mcp.ts:61-64:shim 在进程启动时一次性捕获 CLAUDE_CODE_SESSION_ID,之后不再刷新。
  • src/session-id.ts:session id = 客户端提供的会话值原文(claude 走 x-claude-code-session-id),与 shim 的 env id 同源。resume fork 新 session id 后,模型请求按新 id 注册、工具调用仍带旧 id → 永久 404。

修复验证

  • orphan adoption 逻辑正确:仅在命中该特定 404(no model request has arrived)时触发;attempt === 0 保证只重试一次(无死循环);data.conversationId !== conversationId 兜底,防止采纳回自己的旧 id。
  • handlePluginStatus 的 fallback 分支改为经 conversationIdForSession 反解最近活跃 session 的 conversation id(不再回显调用方旧 id)——这是 adoption 能成立的前提,改动到位。
  • 404 拆成「从未注册」与「已注册但 session 不在本实例」两种文案并补了 warn 日志(此前在 bili.log 里不可见)。
  • 预检:tsc --noEmit 干净;tests/plugin-protocol.test.ts 12/12 通过;全量 1247 pass / 0 fail(本环境)。PR 提到的 5 个失败(2× launcher-plugin-mode、1× mitm、2× plugin-agent)在本环境不复现,属环境相关(cf. test: resolveClientCommand codex/claude test is environment-dependent (fails when codex/claude live in /usr/bin) #642),非本 PR 引入。

发现(次要,非阻塞)

  1. 恢复是启发式:采纳「最近活跃 session」的 conversation。对 launcher 场景(bili claude 每次起独立 proxy、单 host)正确;多 host 共享一个 proxy 时可能误采兄弟会话——BILI_MCP_NO_ORPHAN_ADOPT=1 已提供退出,取舍合理。
  2. 新增测试走的是协作式插件协议(x-bili-plugin + x-bili-plugin-conversation),覆盖了核心 fallback 逻辑(反解 + 采纳后工具可用),但没覆盖 claude 的 identity-binding(x-claude-code-session-id)resume 路径。核心逻辑已验证,identity-binding 用例属 nice-to-have。
  3. 我额外用 identity-binding 做了个探针:当旧 id 仍能解析到一个仍驻留的旧 session 时,工具调用不会 404,而是静默作用在那个旧 session 上;本修复只在 404 时触发,不覆盖这种情况。这是既有行为、不在本 PR 范围(in-app resume + proxy 未重启时可能出现),仅提示留意。

建议:可合并。若想更稳,可补一个 identity-binding 的 resume 回归用例(非必须)。

@ranxianglei

Copy link
Copy Markdown
Owner

小问题修复下

@ranxianglei

Copy link
Copy Markdown
Owner

继续

…stale-id recovery

The existing ranxianglei#656 test exercises the cooperative plugin protocol
(x-bili-plugin-conversation). Add a parallel test that drives the actual
Claude Code identity-binding path (x-claude-code-session-id + persistent
identity register): a stale pre-resume id 404s, status?fallback=latest
resolves the active conversation, and the adopted id then works.
@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

小问题已修,已推到 PR 分支(ranxueqing:fix/mcp-stale-session-id,新 head 919283e),PR #657 已更新。

修了什么(对应我 review 的发现 #2:测试缺口)
新增一个 identity-binding 的 resume 回归用例(tests/plugin-protocol.test.ts),走 Claude Code 真实路径(x-claude-code-session-id + 持久 identity register),而非原用例的协作式插件协议(x-bili-plugin-conversation):

  1. 注册旧 id 与新 id(queuePluginRegister(id, "mcp", true));
  2. 新 id 下发一次模型请求 → 绑定 plugin mode、进 conversation map,旧 id 无流量;
  3. 断言旧 id 工具调用 404(no model request has arrived);
  4. 断言 status?conversationId=<旧>&fallback=latest 解析到新 id(fallback:true);
  5. 断言用新 id 重试成功。

这样 #656 的核心恢复逻辑在 identity-binding 场景下也有覆盖了。

预检
tsc --noEmit 干净;tests/plugin-protocol.test.ts 13/13;全量 1248 pass / 0 fail(+1 新用例,无回归)。

未处理(发现 #3,仅说明)
「旧 id 仍能解析到一个仍驻留的旧 session 时,工具调用不 404、而是静默作用在旧 session 上」——既有行为、不在本 PR 范围,未改动;如需一并处理可另开 issue。发现 #1(启发式采纳 + 共享 proxy opt-out)是设计取舍,无需改动。

@ranxianglei
ranxianglei merged commit 1be5dad into ranxianglei:master Sep 9, 2026
5 of 6 checks passed
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.

MCP tools fail with 'unknown plugin conversation' after Claude Code --resume (shim holds a stale session id)

2 participants