What happened? / 问题描述
PI-Desktop 0.15.3 存在一个可复现的按需工具恢复缺陷:ToolSearch 写入 details.activated,但新用户回合的恢复函数读取 details.addedToolNames。因此,已激活但尚未实际调用过的工具,会在下一用户回合从激活集合中丢失,即使成功的 ToolSearch 结果仍在有效上下文中。
这也是 #225 的后续问题:恢复函数已经存在,但生产端与消费端的数据字段不一致。实际调用过的工具可以通过自己的 toolResult.toolName 恢复,因此“激活后立即调用”的测试可能掩盖该缺陷。
排查连续对话中 prompt-cache 命中突降时,还发现子代理完成会重新组合系统提示。下文将已验证的工具恢复 bug与需要进一步量化的缓存前缀影响分开说明,不将所有 cache miss 归因于客户端。
App version / 应用版本
- 实际观察及本地隔离复现:0.15.3。
- 另检查了 v0.15.4 tag 的公开源码,相同的字段不一致仍存在;未在 0.15.4 上运行端到端复现。
Operating system / 操作系统
Windows,已安装的桌面应用。
Extra environment / 其他环境信息
Agent 模式,按需工具及子代理;缓存现象来自 OpenAI-compatible Responses API 工作流。字段恢复缺陷本身可在不调用模型、不联网的隔离环境中验证,不依赖特定供应商或模型。
Steps to reproduce / 复现步骤
- 在 Agent 模式中启用一个按需工具目录。
- 使用
ToolSearch 成功激活一个或多个工具,但先不要实际调用其中至少一个工具。
- 在同一会话发送新的用户消息。保留成功的 ToolSearch 结果,不触发压缩或切换模式。
- 检查
resetDeferredToolsForPrompt() 之后的 activeDeferredToolNames 和工具定义。
- 已激活但没有自己的成功调用记录的工具未被恢复;有独立成功调用记录的工具则可以恢复。
Synthetic fixture / 合成复现数据
下面的名称仅为测试占位符,不是用户的实际工具或配置:
const deferredToolNames = new Set(["ToolA", "ToolB"]);
const messages = [
{
role: "toolResult",
toolName: "ToolSearch",
isError: false,
content: [{ type: "text", text: "Activated on-demand tools: ToolA, ToolB." }],
details: {
query: "synthetic fixture",
matches: ["ToolA", "ToolB"],
activated: ["ToolA", "ToolB"],
},
},
{
role: "toolResult",
toolName: "ToolA",
isError: false,
content: [{ type: "text", text: "ok" }],
},
];
// Clear activeDeferredToolNames, then run restoreDeferredToolsFromContext()
// with the above effective context and deferred catalog.
// Expected: ToolA, ToolB
// Actual: ToolA
验证方式:在隔离 harness 中执行安装包内实际的 restoreDeferredToolsFromContext() 方法,仅提供有效工具结果及必要辅助函数,不执行整个应用或网络请求。观察到“用过的工具恢复、仅激活的工具丢失”。以上 fixture 是同一字段问题的脱敏最小表达,不是完整端到端测试。
Expected behavior / 预期行为
- 成功的 ToolSearch 激活记录仍处于有效上下文、且工具仍被当前目录和模式允许时,应在新回合恢复相同工具集,无须先调用工具或重新搜索。
- 不应因为激活元数据字段不一致而无故增删工具定义。
- 尽可能保持固定系统提示和工具声明前缀稳定;运行中的动态状态不应无必要地改写历史前面的稳定内容。
Actual behavior / 实际行为及源码证据
以下链接均为公开的 v0.15.4 tag 源码;本地 0.15.3 安装包也包含相同字段不一致:
因此,仅激活的工具丢失,而用过的工具可通过另一条恢复路径保留。工具集变化可能影响后续请求的可缓存前缀;具体影响范围取决于适配器序列化和上游缓存实现。
Related cache-prefix behavior / 相关缓存前缀行为
另一个值得独立评估的实现路径:
在所观察的 0.15.3 连续会话中,子代理完成附近出现过缓存命中从“数万 token”降到“数千 token”,随后恢复的情况。系统提示的更新机制已从代码确认,时间顺序与现象相符;但没有保存当时完整出站请求的前缀指纹,不能将每次 cache miss 与该路径建立排他的因果关系。上游缓存路由、驱逐或保留策略仍可能有影响。
会话分支导致会话 ID、缓存 key 或提示内容变化,不作为本 Issue 的缺陷证据。也不要求缓存命中量在所有请求间严格单调增长。
Suggested fix / 建议修复及验证
- 统一 ToolSearch 激活结果的写入、序列化、持久化和恢复字段;兼容历史中的
activated / addedToolNames 表示,避免只修复新写入而无法恢复旧历史。
- 保留当前安全边界:只信任成功、非占位的工具结果,并限制为当前目录及模式允许的工具。
- 增加回归测试:激活但未调用;激活后只调用一部分;新用户回合与历史恢复;失败或占位结果不恢复;已不在有效上下文或当前目录中的工具不恢复。
- 对子代理动态信息评估追加消息等方案,尽量避免重写固定系统提示;具体行为需要结合适配器兼容性验证。
- 可增加默认不包含正文的缓存诊断:工具集合/定义、系统提示和消息前缀的哈希,cache-key 哈希,以及上游返回的缓存用量。借此区分客户端前缀变化与上游缓存失效。
Logs / 日志与隐私处理
未附原始日志、对话、配置或截图。已省略用户/设备名称、本地绝对路径、项目与业务信息、会话/请求/子代理 ID、供应商和模型标识、端点、IP、凭据、精确时间及账单数据。报告仅保留应用版本、操作系统、公开源码链接、函数/字段名称和合成复现数据。
What happened? / 问题描述
PI-Desktop 0.15.3 存在一个可复现的按需工具恢复缺陷:
ToolSearch写入details.activated,但新用户回合的恢复函数读取details.addedToolNames。因此,已激活但尚未实际调用过的工具,会在下一用户回合从激活集合中丢失,即使成功的 ToolSearch 结果仍在有效上下文中。这也是 #225 的后续问题:恢复函数已经存在,但生产端与消费端的数据字段不一致。实际调用过的工具可以通过自己的
toolResult.toolName恢复,因此“激活后立即调用”的测试可能掩盖该缺陷。排查连续对话中 prompt-cache 命中突降时,还发现子代理完成会重新组合系统提示。下文将已验证的工具恢复 bug与需要进一步量化的缓存前缀影响分开说明,不将所有 cache miss 归因于客户端。
App version / 应用版本
Operating system / 操作系统
Windows,已安装的桌面应用。
Extra environment / 其他环境信息
Agent 模式,按需工具及子代理;缓存现象来自 OpenAI-compatible Responses API 工作流。字段恢复缺陷本身可在不调用模型、不联网的隔离环境中验证,不依赖特定供应商或模型。
Steps to reproduce / 复现步骤
ToolSearch成功激活一个或多个工具,但先不要实际调用其中至少一个工具。resetDeferredToolsForPrompt()之后的activeDeferredToolNames和工具定义。Synthetic fixture / 合成复现数据
下面的名称仅为测试占位符,不是用户的实际工具或配置:
验证方式:在隔离 harness 中执行安装包内实际的
restoreDeferredToolsFromContext()方法,仅提供有效工具结果及必要辅助函数,不执行整个应用或网络请求。观察到“用过的工具恢复、仅激活的工具丢失”。以上 fixture 是同一字段问题的脱敏最小表达,不是完整端到端测试。Expected behavior / 预期行为
Actual behavior / 实际行为及源码证据
以下链接均为公开的 v0.15.4 tag 源码;本地 0.15.3 安装包也包含相同字段不一致:
details: { query, matches, activated }。message.details.addedToolNames;其它工具结果则读取message.toolName。因此,仅激活的工具丢失,而用过的工具可通过另一条恢复路径保留。工具集变化可能影响后续请求的可缓存前缀;具体影响范围取决于适配器序列化和上游缓存实现。
Related cache-prefix behavior / 相关缓存前缀行为
另一个值得独立评估的实现路径:
refreshResumablePrompt(),更新该系统提示。在所观察的 0.15.3 连续会话中,子代理完成附近出现过缓存命中从“数万 token”降到“数千 token”,随后恢复的情况。系统提示的更新机制已从代码确认,时间顺序与现象相符;但没有保存当时完整出站请求的前缀指纹,不能将每次 cache miss 与该路径建立排他的因果关系。上游缓存路由、驱逐或保留策略仍可能有影响。
会话分支导致会话 ID、缓存 key 或提示内容变化,不作为本 Issue 的缺陷证据。也不要求缓存命中量在所有请求间严格单调增长。
Suggested fix / 建议修复及验证
activated/addedToolNames表示,避免只修复新写入而无法恢复旧历史。Logs / 日志与隐私处理
未附原始日志、对话、配置或截图。已省略用户/设备名称、本地绝对路径、项目与业务信息、会话/请求/子代理 ID、供应商和模型标识、端点、IP、凭据、精确时间及账单数据。报告仅保留应用版本、操作系统、公开源码链接、函数/字段名称和合成复现数据。