Skip to content

feat: per-call conversation_id for MCP tools — let the model supply its session id #760

Closed

Description

@kuviki

背景

MCP(plugin)模式要求 shim 知道会话身份,现有三条通道都依赖客户端配合:

  1. 主机把会话 id 传进 MCP 子进程 env(claude 的 CLAUDE_CODE_SESSION_ID);
  2. launcher spawn 时注入 BILI_CONVERSATION_ID(codex,依赖"一进程一会话");
  3. MCP initialize 的 _meta.ui.sessionId(mcp.ts:181)。
    对单进程托管多会话的客户端(如 kimi web,workspace 级共享 MCP 进程、env 无插值、不给子进程传会话 id),三条路全部失效,只能退回纯 wire 模式。而 wire 与 plugin 模式语义不同(plugin 模式 agent 拥有压缩、摘要载体是自身历史里持久的工具调用、支持 absorb),值得为它补一条不依赖客户端的通道。

提案

让模型在每次工具调用时自己带上 conversation id:

  1. manifest 工具 schema 增加可选 conversation_id 参数(proxy manifest + shim 透传)。转发层本来就是 per-call 携带 id(mcp.ts:115),改动集中在参数解析。
  2. shim 支持 per-call id 覆盖 + 惰性注册:调用带 conversation_id 时优先于 env/默认绑定;首次遇到新 id 时执行现有 /__bili/plugin/register 流程(复用 mcp.ts:185-188),之后该 id 缓存可用。无 id 调用回退现有默认绑定,无任何绑定可用时报错并在错误信息中提示用法。
  3. proxy 在 wire 注入文本中携带当前 conversation id:wire 模式下 proxy 已在逐请求注入 acp 标签与 MARKER_INTEGRITY_NOTE(compress-tool.ts:102-109),追加一行 your bili conversation id: <id> 即可让模型知道该填什么——id 就是 proxy 自己解析出的会话身份(如 kimi 的 prompt_cache_key 值),零客户端改动,多会话天然正确。
  4. 纯 plugin 模式语义:首次带 id 调用完成注册后,会话按既有 pluginMode sticky 语义进入纯 plugin 模式——后续请求 proxy 不再注入 ephemeral 压缩工具与 nudge,模型可见的压缩工具只有 mcp__bili__* 一套,摘要载体为 agent 自身历史中持久的工具调用。部署形态就是纯 MCP,不考虑 wire ephemeral 工具混用。

设计要点 / 风险

  • 安全语义:工具参数可指向任意已有会话(与 session-id.ts:44 对 x-acp-session 的顾虑同源)。本地单用户 proxy 可接受;若将来 shim 暴露到非本地场景,需要加白名单或绑定校验。建议在注册时校验 id 对应的会话确实存在,拒绝为未知 id 凭空建会话(防止模型编造 id 污染状态)。
  • 模型可靠性:id 是长字符串(session_459f8cd2-…),模型从注入文本中复制一般可靠;漏带/带错时 shim 报错信息应包含正确 id 的提示(从最近的注入或默认绑定给出)。
  • 与 initialize meta 通道的关系:_meta.ui.sessionId(mcp.ts:181)是 per-process 绑定,本提案是 per-call 路由;两者并存不冲突,per-call 优先级最高。
  • 适用边界:第 3 点的 id 下发依赖 proxy 在流量路径上的逐请求注解通道(acp 标签/note,plugin 与 wire 两种模式下都会注入,与 ephemeral 工具注入无关)。direct-URL 无 proxy 的场景仍需 env/meta 通道,不在本提案范围。

验收标准

  • 共享 shim 进程(无 env id)下,模型带 conversation_id 调用 compress/acp_status 成功路由到对应会话,首次调用自动完成插件注册;
  • 注册完成后会话进入纯 plugin 模式:后续请求不再注入 ephemeral 压缩工具与 nudge,模型可见压缩工具仅 mcp__bili__* 一套;
  • 同一会话的 wire 请求上下文中可见 proxy 注入的 conversation id;
  • 两个不同会话的模型各自带各自 id 调用,互不串扰;
  • 带未知 id 调用被拒绝并给出明确错误,不创建新会话。

Activity

  1. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 已通读 issue 引用的全部代码位置(mcp.ts:115/181/185-188、compress-tool.ts:102-109、session-id.ts:44、server.ts 绑定链 :1370-1392、plugin.ts 各 handler),引用全部准确。设计评估:方案成立,四条通道失效的分析正确(per-call id 是唯一不依赖客户端配合的路径),验收标准可落地。开始实现前,先同步几个实现层面的判断(均为按 issue 意图的细化,不是改方向):

    1. 惰性注册的 identity 标志必须用 true,不能照抄 mcp.ts:185-188。
    现有注册体是 identity: IDENTITY_BINDING——非 claude 宿主下为 false,走 headless 队列(takePendingPluginRegister,server.ts:1376-1380):会在下一个任意新会话的首个请求上消费掉。共享多会话 proxy 上这会把不相干客户端的新会话翻成 plugin mode。per-call id 语义上是"模型自报、请求会原样携带"→ 用 identity: true(registeredIds 惰性匹配,server.ts:1370-1375):只有携带该 id 的请求到达才绑定;模型编造的 id 永远匹配不到任何请求 → 天然 inert,同时满足"拒绝未知 id、不建会话"。

    2. 首调路由有一个 map 缺口,proxy 侧补一个 fallback。
    conversations map 只在两个时机写入:plugin 绑定后(:1391)和 pck 无条件记录(:1402-1407)。header 类身份(x-session-affinity 等)在首次工具调用时 map 里没有条目 → 首调 404(注册是惰性的,要等下一个模型请求才绑定)。修法:handlePluginTool 在 map miss 时回退 peekSession(conversationId)——session id 本来就是 conversation value 逐字相等(#286 不变量),命中即执行并回填 map。这样所有身份来源(header/pck/affinity)的首调都能直接路由,不用等下一轮请求。pck 客户端(kimi web 场景)其实今天就能路由(map 已有条目),此 fallback 覆盖的是其余身份类型。

    3. 注入的 id 值 = session.id,放在静态 system prompt 段(MARKER_INTEGRITY_NOTE 同一位置,三处 wire:injectSystem :2805 / openai sysParts :2278 / responses :2469)。
    对一切 client-provided 身份,session.id === clientConv ?? conversation(绑定键),逐轮字节稳定 → prefix cache 安全;subagent 线程(subagentNamespace 的 |sub: 命名空间)也一致,因为注入的就是 session 自己的 id。注入与 pluginMode/ephemeral 工具开关无关(两种模式都注,符合提案第 3 点)。匿名 prefix-affinity 会话不会翻 plugin mode(:1370 排除了 anonAffinity)——边界情况,PR 里注明。

    4. nudge 措辞与既有 pluginMode 语义冲突,我按既有语义实现。
    验收标准写"后续请求不再注入 ephemeral 压缩工具与 nudge",但 #451 之后 plugin mode 保留 proxy nudge(agent 自身没有 nudge 通道,抑制掉就只剩 preflight 硬限触发,失去主动压缩信号;server.ts:2040-2046 注释明确)。提案第 4 点自己也说"按既有 pluginMode sticky 语义"——所以我的理解是:抑制 ephemeral 压缩工具,nudge 保留。如果你确实要连 nudge 一起关,说一声,改动是一行 gate 的事。

    安全语义:同意本地单用户信任假设。未知 id 调用被拒且不建会话由第 1+2 点共同保证(identity 注册不建条目 + tool 端点只读解析 peekSession,无任何创建路径);跨会话访问 = 持有该会话 id 的本地主体,与 x-acp-session 同档。

    manifest schema:只改 handlePluginManifest 的输出(深拷贝 kernel 常量后加可选 conversation_id 参数),wire 模式注入用的是 kernel 常量本身(server.ts:2812-2855),两条路互不污染。注意 pi/omp/opencode 原生插件拉的是同一个 manifest,它们的模型也会看到这个可选参数——shim 转发前剥离该字段,proxy 端点防御性再剥一次,body.conversationId(宿主自绑定)始终优先。

    开始动手了,完成后开 PR。

  2. kuviki commented on Sep 14, 2026

    @kuviki
    ContributorAuthor

    考虑下会话 fork 后会怎样?

  3. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 好问题。把 fork 路径完整推了一遍,结论:per-call id 对 fork 天然免疫,而且比现有三条通道都更抗 fork:

    1. 注入行是逐请求重新派生的,不持久化。 your bili conversation id: <id> 追加在出站 system/developer 消息的静态段(ephemeral,每个请求重建),取的 id 是该请求实际落到的 session 的 session.id。fork 后新会话的第一个请求就携带新 id——旧 id 不会进入重放的历史(system 不在 messages 数组里,客户端重放的只是自己的原始历史,proxy 每轮从原始历史重建出站请求并重新注入)。
    2. 旧 id 保持有效,且不构成串扰。 fork 后旧会话 A 仍在 store 里,模型若带旧 id 调用,路由到的是 fork 前快照——这是正确语义(那是另一个 conversation),不是污染。新 id 走惰性注册(identity 模式),在其首个模型请求上绑定到会话 B。两个会话各自独立、各自 sticky plugin mode,互不干扰。
    3. 对照:为什么现有通道在 fork 时失效——env/meta id 在 spawn 时冻结,宿主 resume-fork 出新 id 的瞬间 shim 手里就是死 id(MCP tools fail with 'unknown plugin conversation' after Claude Code --resume (shim holds a stale session id) #656 的 orphan-adoption 就是为这个打的补丁)。per-call 通道的 id 来源是 proxy 的逐请求注解,永远跟随当前会话,fork 自愈。所以实现里 adoption fallback 只对默认绑定调用启用:per-call id 指向未知会话时直接失败报错,绝不 adopt 兄弟会话(避免把 A 会话的调用路由到刚 fork 出来的 B)。
    4. 一个残留风险(低):如果模型某轮把旧 note 原文引用进了自己的输出,那段引文会随重放历史带进 fork 后的上下文,而活的 system 行已是新 id——模型可能有一轮困惑,但活的行为准,下一轮自纠。属于 instruction echo 类固有风险,与本改动无关,不额外处理。

    继续实现中(修代码 + 补测试),完成后开 PR。

  4. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 已实现,PR: #761

    实现要点(按提案四通道落地):

    1. manifest schema:conversation_id 可选 string 参数进入全部三种工具格式(anthropic/openai/responses)。kernel 常量被 clone 而非 mutate——wire 模式的 ephemeral 工具注入直接服务 kernel 常量,完全不受影响。
    2. shim per-call 路由 + 惰性注册:tools/call 读取 conversation_id,仅对该次调用覆盖默认绑定;首次遇到新 id 时 POST /__bili/plugin/register(**identity: true,issue-once);之后缓存可用。无 id 调用回退默认绑定,无任何绑定时报错并提示 per-call 用法。per-call 失败永不触发 MCP tools fail with 'unknown plugin conversation' after Claude Code --resume (shim holds a stale session id) #656 orphan adoption(adoption 会改写共享 shim 的默认绑定,影响其他会话的后续调用)。
    3. proxy 注入 id:wire 与 plugin 两种模式的静态 system prompt 部分都追加一行 [Your bili conversation id: <id>. …pass this value as the conversation_id argument…],id = session.id(对所有 client-provided identity 都等于绑定键、会话内稳定→逐请求字节不变,prefix cache 安全)。零客户端改动,kimi 这类客户端直接受益。
    4. 纯 plugin 模式 sticky:首次带 id 调用注册后,该会话下一次请求按既有 session.metadata.pluginAgent sticky 语义翻转为纯 plugin 模式——wire ephemeral 压缩工具不再注入,模型可见的压缩工具只剩 mcp__bili__* 一套。

    两个关键决策(提案"设计要点"中安全语义的落地方式):

    • 惰性注册用 identity 模式而非 headless。headless pending-register 会绑定"下一个任意新会话"——在共享 proxy 上那可能是别的客户端的新会话(把别人的会话翻转成 plugin 模式)。identity 模式只在该 id 对应的请求真正到达时才生效;模型编造的 id 匹配不到任何请求,天然惰性,同时满足验收标准"未知 id 拒绝、不建会话"。
    • 首次调用的路由缺口:/__bili/plugin/tool 原本要求 conversations map 已有条目(pck 记录或绑定时写入),header 类客户端(无 pck)的首次调用会 404。现在 map miss 时直接按 verbatim session-id(会话身份不稳定:credential/upstream 维度导致长会话状态孤儿化(sessions 缺失) #286 不变量)查内存会话,命中则补记映射;纯读操作,未知 id 不创建任何东西,no model request has arrived 404 文案原样保留(adoption 触发子串不能动)。

    一处与验收标准的偏差,请拍板:验收写"注册完成后不再注入 ephemeral 压缩工具与 nudge",但既有 pluginMode 语义是保留 nudge 的(#451:agent 侧没有自己的 nudge 通道,proxy 注入的 nudge 是唯一的主动压缩触发器)。我按 proposal 第 4 点自己写的"按既有 pluginMode sticky 语义"实现——nudge 保留。如果确认要连 nudge 一起抑制,告诉我,改动很小但要另开一个 issue 记录这个行为变更的影响。

    验证:

    • 新增 tests/issue760-percall-conversation.test.ts(6 个用例):manifest 三格式 schema + kernel 常量未被污染;wire 模式 id 行存在且跨轮字节稳定;无注册/无 pck 记录下首次调用即路由成功;未知 id 404 且不产生会话;spawned-shim e2e(共享进程、无 env/meta id:两个会话各自带各自 id 互不串扰、均翻转 plugin 模式、一个无关新会话插在注册和下一次请求之间验证了 identity-not-headless);mock-proxy shim 单测(参数剥离、register 载荷 {conversationId, agent:"mcp", identity:true}、issue-once、adoption guard)。
    • npm run typecheck ✅ / npm test 1465 pass / 0 fail / 2 skipped(gated e2e)/ npm run build ✅
    • E2E codex:E2E_CHECK=1 preflight ✅;完整跑不了——本环境的本地 sglang upstream(:8199)当前 down(ECONNREFUSED,所有 forward 失败,零模型完成,属环境问题与 diff 无关)。server.ts 的 prompt 注入路径有改动,合并前建议手动 dispatch 一次 .github/workflows/ci-e2e.yml。

    fork 场景的讨论见上面的回复(per-call id 对 fork 免疫,note 每请求从当前 session 重新派生)。

  5. kuviki commented on Sep 14, 2026

    @kuviki
    ContributorAuthor

    nudge 保留

  6. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 收到——当前实现就是保留 nudge 的(注册后仅抑制 ephemeral 压缩工具,wire/plugin 两种模式都继续注入 nudge,即 #451 的既有语义),无需任何改动。PR #761 保持原样,等合并即可。

  7. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    问ework,这个和proxy模式下前缀树有关系吗?

  8. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 直接关系没有,但有一个明确的交叉点,分三层说:

    1. 两个机制在不同层,互不读写。 前缀树(src/prefix-affinity.ts,#309)解决的是"无身份的匿名请求属于哪个会话"——模型请求路径上的会话身份解析(server.ts:1306-1339,内容哈希链最长前缀匹配 + 分叉)。per-call conversation_id(#760)解决的是"这次工具调用路由到哪个会话"——工具调用路径(/__bili/plugin/tool)。两者不共享代码、不共享状态,前缀树的匹配/分叉逻辑一行没动。

    2. 交叉点:注入的 id 就是前缀树铸出来的那个。 对匿名客户端(前缀树服务的正是这批无 header 的客户端),#760 注入的 session.id 恰好是前缀树 resolve/铸出的会话 id。模型把它带回来调工具时,handlePluginTool 的 verbatim session-id 查找直接命中(session id 本身就是键)——所以匿名会话的工具路由同样工作,零改动覆盖到前缀树服务的人群。

    3. 但匿名会话永远不会翻 plugin 模式(有意为之)。 server.ts:1378 的绑定链显式排除 anonAffinity:匿名客户端无法把铸出的 id 稳定地带回 header,identity 注册永远等不到匹配请求。所以对匿名会话,per-call id 退化为"路由可用、模式保持 wire":ephemeral 压缩工具与 nudge 继续注入,压缩继续由 proxy 侧执行。语义上这是对的——这类宿主在客户端侧没有稳定身份,宣称 agent 拥有压缩(plugin 模式)反而错误。

    fork 兼容性(顺带呼应 @kuviki 之前的问题):前缀树分叉时旧会话 id 保持有效、新分支拿新 id;注入行每请求从当前 session.id 重新派生,分叉后各分支只看到自己的 id,模型若引用分叉前的旧 id 会路由到分叉前快照——正确语义而非串扰。两个机制天然兼容,无需任何特殊处理。

  9. kuviki commented on Sep 14, 2026

    @kuviki
    ContributorAuthor

    ework,目前支持的客户端哪些是会话携带了 session id 的?

  10. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 按"会话身份从哪来"分三类(依据均为代码库内的实际行为):

    A. 客户端原生携带(不依赖 bili 任何注入)

    客户端 载体 出处
    claude x-claude-code-session-id header(CLI 原生 per-session UUID,最强信号);另外 Claude Code 会给 MCP 子进程设 CLAUDE_CODE_SESSION_ID env → shim 直接拿到身份 src/session-id.ts:45-48
    codex 每个 Responses 请求带 session-id header;subagent 线程另带 thread-id + x-codex-turn-metadata 做分区(#316)。注意:codex 不给 MCP 子进程传任何 session id(codex-cli 0.147.0 实测),shim 侧靠 launcher spawn 时注入的 BILI_CONVERSATION_ID(per-spawn UUID,"一进程一会话") src/session-id.ts:12, src/launcher.ts:859-877, 2370-2401
    opencode x-opencode-session header src/session-id.ts:13
    dsh body 字段 prompt_cache_key = dsh session uuid(bili 强制 PI_CACHE_RETENTION=long 保证它真的发出;否则 dsh 是匿名的会被 400) src/launcher.ts:2306-2317
    omp / 部分 codex builds 无 header、全量重放历史;若带 prompt_cache_key 则被提升为身份,否则匿名 src/session-id.ts:59-91

    B. bili 协作插件协议注入(launcher 的 agent 扩展)

    • pi / omp / opencode:扩展在工具注册完成后给模型请求打 x-bili-plugin: <agent> + x-bili-plugin-conversation: <自己的 session uuid>(+ context window header)。该 header 优先级压过所有其他信号,但仅在 marker 存在时才被信任。src/agent/pi.ts:392-413,src/session-id.ts:37-44,src/launcher.ts:2229-2257

    C. 完全不携带(匿名 → 前缀亲和 #309)

    • hermes:明确注释 "no identity carrier … sessions bind by persisted content-prefix affinity"。src/launcher.ts:2265-2267
    • qoder / trae / jcode / codebuddy:MITM 接入,launcher 不注入任何身份;代码库中也没有它们 wire 自带 session header 的证据——如果它们恰好发代理接受的任一通用 header(x-session-affinity、x-acp-session、x-session-id、session-id、session_id)或 body session_id/metadata.session_id,会自动生效(src/session-id.ts:50),否则落入匿名前缀亲和桶。

    和 #760 的关系:A/B 类的 shim 都有身份通道(claude 靠 env、codex 靠 spawn 注入、pi/omp/opencode 靠 agent 自己持有会话),#760 真正补的是 C 类里"模型流量本身带 id(如 kimi 的 prompt_cache_key)、但共享 MCP 子进程拿不到"的那批宿主——per-call conversation_id 让模型把 proxy 已解析出的会话身份回传给 shim。

  11. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    不明白 二者不是一样的吗?

    前缀树(src/prefix-affinity.ts,https://github.com/ranxianglei/billion-context/issues/309)解决的是"无身份的匿名请求属于哪个会话"——模型请求路径上的会话身份解析(server.ts:1306-1339,内容哈希链最长前缀匹配 + 分叉)。per-call conversation_id(https://github.com/ranxianglei/billion-context/issues/760)解决的是"这次工具调用路由到哪个会话"——工具调用路径(/__bili/plugin/tool)。两者不共享代码、不共享状态,前缀树的匹配/分叉逻辑一行没动。
    
  12. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 不一样,关键在输入不同:

    • 前缀树是"从内容推断身份"。它的输入是模型请求里重放的完整消息历史——把消息列表哈希成链(h_i = sha256(h_{i-1} || msg_i)),拿这条链去和已存会话的链做最长前缀匹配。它"知道"是哪个会话,是因为请求里带着整个对话本身。
    • per-call conversation_id 是"按 id 直接寻址"。它的输入是一个显式的 id 字符串。不做任何推断——给什么 id 就查什么会话。

    为什么不能统一成一个机制:工具调用路径上根本没有内容可推断。看两条路径的载荷:

    模型请求:  { system, messages: [50 条完整历史], ... }   ← 有内容,可以哈希、可以匹配
    工具调用:  POST /__bili/plugin/tool { tool, args }        ← 零历史、零对话字节
    

    一次 compress 调用就带 {start:"m00040", end:"m00060"} 两个参数,没有任何消息体——前缀树在这条路径上无物可哈希,物理上做不了。所以工具调用要找到会话,只能靠有人把会话 id 明说出来,这就是注入的 note + per-call 参数的作用。

    两者唯一的接触点是我上一条说的:对匿名会话,注入的 id 恰好等于前缀树铸出的那个 session id(前缀树的"输出结果"被复用为路由 token)——但 #760 只是复用了它的产物,从不调用它本身,匹配/分叉逻辑一行没动。

    一句话:前缀树回答"这个请求延续了哪个会话"(推断,需要内容);per-call id 回答"这次操作作用于哪个会话"(寻址,不需要内容)。前者只在模型请求路径成立,后者专门补上前者覆盖不到的工具调用路径。

  13. kuviki commented on Sep 14, 2026

    @kuviki
    ContributorAuthor

    ework,匿名会话前缀树铸出的 session id 会变吗?

  14. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 铸出后不变——只要该会话的链还在被跟踪,后续请求命中的都是同一个 id。变化的只发生在"请求不再匹配到这条链"的时候。具体机制(src/prefix-affinity.ts):

    id 如何铸出(一次性):新会话的 id 是确定性的——pfa- + sha256(tailHash ‖ 当前深度) 的前 16 位 hex(:270)。锚定在创建那一刻的请求尾部,故意做成确定性而非随机数:proxy 重启后同样的重放能重新贴回同一个 id(#499 P1a 还把链快照持久化到盘上,重启本身不丢跟踪)。

    为什么之后不变:后续请求走的是匹配路径,不是铸出路径——全前缀匹配(:188-204)和 tail-window 重接(:207-248)返回的都是已存条目里的那个 sessionId。note() 每轮更新条目的深度/尾部哈希/itemHashes(:285-294),但 Map 的键(sessionId)从创建起就固定。历史增长、滚动窗口截断重放,都拿回同一个 id。

    什么情况下会"变成另一个 id"(准确说是旧链不再匹配、新会话另铸):

    1. 分叉/编辑:历史内容分叉或任何一条旧消息被改字节 → 哈希链断裂 → 按新会话铸新 id(设计如此);回到原分支继续扩展则拿回原 id。
    2. LRU 驱逐:最多跟踪 256 条链,最久未见的被逐(:289-293)。被逐后的重放匹配不到 → 铸新 id;由于对话已增长,新尾部 ≠ 创建时尾部,所以新 id 和旧 id 不同。
    3. TTL 过期:7 天无活动停止匹配(:349-355),下次请求铸新 id。
    4. 会话删除:forget() 主动摘除跟踪。

    对 #760 的意义:注入行每请求取当前 session.id——匿名会话在亲和性保持期间 id 恒定 → 注入字节稳定,prefix cache 不受影响;一旦上述情况发生切到新会话,下一请求的 note 自动反映新 id,per-call 通道跟着切换,无需任何额外处理。旧 id 若仍指向内存中残留的旧会话状态,路由过去也是语义正确的(操作作用于那个快照)。

  15. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    有没有可能proxy模式下,同一个sessionid发送不同会话?是不是前缀树更靠谱

  16. 13 remaining items

  17. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    简述最新讨论下是否做出新的改动

  18. ranxianglei commented on Sep 14, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 有,一处新改动 —— 针对你 review 4b56101 时提的两个收尾点,新增 commit 5f1e0df(已 push,PR #761 head 更新,PR 描述同步重写)。分支现在共 3 个 commit:c4e6a0a(per-call conversation_id)→ 4b56101(unified canonical id + evidence flip)→ 5f1e0df(收尾)。

    5f1e0df 内容(2 文件,+22/−39):

    1. 删掉冗余的 per-call ensureRegistered()(src/mcp.ts):核对确认那次注册以 canonical pfa-* 为键、而 client-id 会话后续请求带 raw id 永远命中不了 → sticky flip 实际全由 successful-tool evidence 完成,这些条目还白占 identity-register LRU。连同已成死代码的函数一起移除;保留 initialize/register(单会话宿主)+ evidence flip(shared-host)两条机制。
    2. shared-shim E2E 改用真实 note 里的 canonical id:测试里三处 conversation_id 从 raw "conv-mcp-a/b" 改成 canonicalOf(...)(即模型抄到的 pfa-…),现在完整覆盖 raw client id → canonical note → shim → canonical 反查 → Session → evidence flip → 下一轮进 plugin mode;mock 单测同步改为断言 per-call 调用不再发任何 register。

    验证:typecheck ✅ · issue760 8/8 ✅ · 全套 1467 pass / 0 fail / 2 skipped ✅ · build ✅(E2E full run 仍受本环境 local upstream :8199 不可达限制,合并前建议手动 dispatch ci-e2e.yml)。按你的 review 判断无新 correctness blocker,PR #761 只剩人工合并。

  19. added a commit that references this issue on Sep 15, 2026
  20. added a commit that references this issue on Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions