You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: per-call conversation_id for MCP tools — let the model supply its session id #760
shim 支持 per-call id 覆盖 + 惰性注册:调用带 conversation_id 时优先于 env/默认绑定;首次遇到新 id 时执行现有 /__bili/plugin/register 流程(复用 mcp.ts:185-188),之后该 id 缓存可用。无 id 调用回退现有默认绑定,无任何绑定可用时报错并在错误信息中提示用法。
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 值),零客户端改动,多会话天然正确。
安全语义:工具参数可指向任意已有会话(与 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 优先级最高。
[bot] 🏷 好问题。把 fork 路径完整推了一遍,结论:per-call id 对 fork 天然免疫,而且比现有三条通道都更抗 fork:
注入行是逐请求重新派生的,不持久化。your bili conversation id: <id> 追加在出站 system/developer 消息的静态段(ephemeral,每个请求重建),取的 id 是该请求实际落到的 session 的 session.id。fork 后新会话的第一个请求就携带新 id——旧 id 不会进入重放的历史(system 不在 messages 数组里,客户端重放的只是自己的原始历史,proxy 每轮从原始历史重建出站请求并重新注入)。
旧 id 保持有效,且不构成串扰。 fork 后旧会话 A 仍在 store 里,模型若带旧 id 调用,路由到的是 fork 前快照——这是正确语义(那是另一个 conversation),不是污染。新 id 走惰性注册(identity 模式),在其首个模型请求上绑定到会话 B。两个会话各自独立、各自 sticky plugin mode,互不干扰。
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 这类客户端直接受益。
Activity
ranxianglei commentedon Sep 14, 2026
[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。
conversationsmap 只在两个时机写入: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。
kuviki commentedon Sep 14, 2026
考虑下会话 fork 后会怎样?
ranxianglei commentedon Sep 14, 2026
[bot] 🏷 好问题。把 fork 路径完整推了一遍,结论:per-call id 对 fork 天然免疫,而且比现有三条通道都更抗 fork:
your bili conversation id: <id>追加在出站 system/developer 消息的静态段(ephemeral,每个请求重建),取的 id 是该请求实际落到的 session 的session.id。fork 后新会话的第一个请求就携带新 id——旧 id 不会进入重放的历史(system 不在 messages 数组里,客户端重放的只是自己的原始历史,proxy 每轮从原始历史重建出站请求并重新注入)。继续实现中(修代码 + 补测试),完成后开 PR。
ranxianglei commentedon Sep 14, 2026
[bot] 🏷 已实现,PR: #761
实现要点(按提案四通道落地):
conversation_id可选 string 参数进入全部三种工具格式(anthropic/openai/responses)。kernel 常量被 clone 而非 mutate——wire 模式的 ephemeral 工具注入直接服务 kernel 常量,完全不受影响。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 的默认绑定,影响其他会话的后续调用)。[Your bili conversation id: <id>. …pass this value as the conversation_id argument…],id =session.id(对所有 client-provided identity 都等于绑定键、会话内稳定→逐请求字节不变,prefix cache 安全)。零客户端改动,kimi 这类客户端直接受益。session.metadata.pluginAgentsticky 语义翻转为纯 plugin 模式——wire ephemeral 压缩工具不再注入,模型可见的压缩工具只剩mcp__bili__*一套。两个关键决策(提案"设计要点"中安全语义的落地方式):
/__bili/plugin/tool原本要求 conversations map 已有条目(pck 记录或绑定时写入),header 类客户端(无 pck)的首次调用会 404。现在 map miss 时直接按 verbatim session-id(会话身份不稳定:credential/upstream 维度导致长会话状态孤儿化(sessions 缺失) #286 不变量)查内存会话,命中则补记映射;纯读操作,未知 id 不创建任何东西,no model request has arrived404 文案原样保留(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 test1465 pass / 0 fail / 2 skipped(gated e2e)/npm run build✅E2E_CHECK=1preflight ✅;完整跑不了——本环境的本地 sglang upstream(:8199)当前 down(ECONNREFUSED,所有 forward 失败,零模型完成,属环境问题与 diff 无关)。server.ts 的 prompt 注入路径有改动,合并前建议手动 dispatch 一次.github/workflows/ci-e2e.yml。fork 场景的讨论见上面的回复(per-call id 对 fork 免疫,note 每请求从当前 session 重新派生)。
kuviki commentedon Sep 14, 2026
nudge 保留
ranxianglei commentedon Sep 14, 2026
[bot] 🏷 收到——当前实现就是保留 nudge 的(注册后仅抑制 ephemeral 压缩工具,wire/plugin 两种模式都继续注入 nudge,即 #451 的既有语义),无需任何改动。PR #761 保持原样,等合并即可。
ranxianglei commentedon Sep 14, 2026
问ework,这个和proxy模式下前缀树有关系吗?
ranxianglei commentedon Sep 14, 2026
[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 会路由到分叉前快照——正确语义而非串扰。两个机制天然兼容,无需任何特殊处理。
kuviki commentedon Sep 14, 2026
ework,目前支持的客户端哪些是会话携带了 session id 的?
ranxianglei commentedon Sep 14, 2026
[bot] 🏷 按"会话身份从哪来"分三类(依据均为代码库内的实际行为):
A. 客户端原生携带(不依赖 bili 任何注入)
x-claude-code-session-idheader(CLI 原生 per-session UUID,最强信号);另外 Claude Code 会给 MCP 子进程设CLAUDE_CODE_SESSION_IDenv → shim 直接拿到身份session-idheader;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,"一进程一会话")x-opencode-sessionheaderprompt_cache_key= dsh session uuid(bili 强制PI_CACHE_RETENTION=long保证它真的发出;否则 dsh 是匿名的会被 400)prompt_cache_key则被提升为身份,否则匿名B. bili 协作插件协议注入(launcher 的 agent 扩展)
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-2257C. 完全不携带(匿名 → 前缀亲和 #309)
x-session-affinity、x-acp-session、x-session-id、session-id、session_id)或 bodysession_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。ranxianglei commentedon Sep 14, 2026
不明白 二者不是一样的吗?
ranxianglei commentedon Sep 14, 2026
[bot] 🏷 不一样,关键在输入不同:
h_i = sha256(h_{i-1} || msg_i)),拿这条链去和已存会话的链做最长前缀匹配。它"知道"是哪个会话,是因为请求里带着整个对话本身。为什么不能统一成一个机制:工具调用路径上根本没有内容可推断。看两条路径的载荷:
一次 compress 调用就带
{start:"m00040", end:"m00060"}两个参数,没有任何消息体——前缀树在这条路径上无物可哈希,物理上做不了。所以工具调用要找到会话,只能靠有人把会话 id 明说出来,这就是注入的 note + per-call 参数的作用。两者唯一的接触点是我上一条说的:对匿名会话,注入的 id 恰好等于前缀树铸出的那个 session id(前缀树的"输出结果"被复用为路由 token)——但 #760 只是复用了它的产物,从不调用它本身,匹配/分叉逻辑一行没动。
一句话:前缀树回答"这个请求延续了哪个会话"(推断,需要内容);per-call id 回答"这次操作作用于哪个会话"(寻址,不需要内容)。前者只在模型请求路径成立,后者专门补上前者覆盖不到的工具调用路径。
kuviki commentedon Sep 14, 2026
ework,匿名会话前缀树铸出的 session id 会变吗?
ranxianglei commentedon Sep 14, 2026
[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"(准确说是旧链不再匹配、新会话另铸):
forget()主动摘除跟踪。对 #760 的意义:注入行每请求取当前 session.id——匿名会话在亲和性保持期间 id 恒定 → 注入字节稳定,prefix cache 不受影响;一旦上述情况发生切到新会话,下一请求的 note 自动反映新 id,per-call 通道跟着切换,无需任何额外处理。旧 id 若仍指向内存中残留的旧会话状态,路由过去也是语义正确的(操作作用于那个快照)。
ranxianglei commentedon Sep 14, 2026
有没有可能proxy模式下,同一个sessionid发送不同会话?是不是前缀树更靠谱
13 remaining items
ranxianglei commentedon Sep 14, 2026
简述最新讨论下是否做出新的改动
ranxianglei commentedon Sep 14, 2026
[bot] 🏷 有,一处新改动 —— 针对你 review
4b56101时提的两个收尾点,新增 commit5f1e0df(已 push,PR #761 head 更新,PR 描述同步重写)。分支现在共 3 个 commit:c4e6a0a(per-call conversation_id)→4b56101(unified canonical id + evidence flip)→5f1e0df(收尾)。5f1e0df内容(2 文件,+22/−39):ensureRegistered()(src/mcp.ts):核对确认那次注册以 canonicalpfa-*为键、而 client-id 会话后续请求带 raw id 永远命中不了 → sticky flip 实际全由 successful-tool evidence 完成,这些条目还白占 identity-register LRU。连同已成死代码的函数一起移除;保留 initialize/register(单会话宿主)+ evidence flip(shared-host)两条机制。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 只剩人工合并。
Merge pull request #761 from ranxianglei/2026-09-14_per-call-conversa…
feat: per-call conversation_id for MCP tools (ranxianglei#760)
feat: search_context targets historical sessions read-only via conver…
feat: search_context targets historical sessions read-only via conver…
feat: kimi three-mode alignment — native plugin mode (#963)
feat: zcode native plugin mode (`bili plugin install zcode`) (#1145)
feat: zcode native plugin mode (`bili plugin install zcode`) (#1145)
docs: codex posture — plugin mode needs a running bili first
fix: zero-injection session identity — route id-less MCP tool calls b…
fix: zero-injection session identity — route id-less MCP tool calls b…
feat: zcode native plugin mode (`bili plugin install zcode`) (ranxian…
fix(ranxianglei#4168): self-rebind the claude MCP binding when the ho…