What happened? / 问题描述
PI-Desktop 0.17.0 在新会话经过少量交互/工具调用后也会出现 EMPTY_MODEL_RESPONSE,随后点击继续会出现 CONTEXT_COMPACTION_FAILED。
本地排查发现 system transcript journal 重复持久化相同系统消息:一条只有 5 条用户消息的会话存了 1,025 条 system 记录,但 modelSystem.messageJson 只有 5 个不同值。其中 4 个值各重复 256 次,另一个出现 1 次;重复项包含相同的原始 timestamp。新会话因此也会很快膨胀。
对 system journal 做内容去重和回放去重的本地补丁后,新会话、连续工具调用及原先失败的旧会话均恢复。下文区分实际观察和代码推断;未捕获修复前完整的 HTTP 请求体,因此不把推断的输出参数当成抓包结论。
Steps to reproduce / 复现步骤
实际触发路径:
- 使用 0.17.0 创建 Agent 会话,进行多轮交互及工具调用。
- 检查该会话 JSONL 中
meta.modelSystem.messageJson 的总数与不同值数;本例出现上述 1,025 / 5 的差异。
- 出现空响应后继续,随后会在模型请求前遇到自动压缩失败。
尚未在另一台干净安装上验证固定的端到端复现轮数。下面是可用于源码回归测试的最小 journal 复现(无供应商或网络依赖):
const journal = new SystemTranscriptJournal();
const entries: MessageEntry[] = [];
const rows: UiMessage[] = [];
const original: SystemMessage = {
role: "system",
content: "",
sections: { runtime: "Test instructions" },
timestamp: 100,
};
for (let i = 0; i < 8; i++) {
await journal.persist(
[structuredClone(original)],
entries,
async row => { rows.push(row); },
);
}
// Same durable system event, not eight new instruction changes.
expect(rows).toHaveLength(1); // Original implementation appends 8.
本地使用从已安装 sidecar 提取的 journal 实现做隔离测试:原版写入 8 条,补丁版写入 1 条。此代码片段是供维护者加入现有测试的等价示例;没有运行仓库完整 Vitest/构建套件。
Expected behavior / 预期行为
- 系统消息经过 clone、恢复、上下文重建后仍被识别为同一条已经落盘的事件。
- 相同正文但不同 timestamp、真正不同的指令/工具声明不得被误删。
- 对已有重复记录的会话提供非破坏性的回放恢复,保留全部用户、助手、工具消息及其顺序。
- 不能因内部 journal 重复记录而把短会话当作超长上下文。
Actual behavior / 实际行为
EMPTY_MODEL_RESPONSE 时 HTTP 状态为 200,实际记录的 outputTokens 为 1;MiniMax 正文为空,DeepSeek 有极短 thinking、无正文。
- 随后的继续请求以
CONTEXT_COMPACTION_FAILED 结束。
- 上述问题在 MiniMax 和 Cline Pass 两条线路出现。
App version / 应用版本
0.17.0,检查源码为 tag v0.17.0 / commit 72b5e826cb7a9928467091ccf745aa9b225eeb04。
Operating system / 操作系统
macOS
Extra environment / 其他环境信息
- macOS 15.5,Apple Silicon / arm64,已安装桌面应用,protocol 11。
- MiniMax:Anthropic Messages,
MiniMax-M3.1-Flash-Preview。
- Cline Pass:Chat Completions,
cline-pass/deepseek-v4.1-flash。
- 修复后的真实请求验证使用 MiniMax;未另发 DeepSeek 请求验证。
根因分析与建议修复
相关代码:
SystemTranscriptJournal.isPersisted() 检查对象身份 WeakMap,以及用于 checkpoint 的序列化集合。但常规 restore()、成功 persist()、remember() 只登记对象身份。内容不变的新对象不被视为已持久化,可能再次写入;setAgentMessages() 也依赖这个判断重建系统消息。重复记录中相同 timestamp 和 256 倍重复,与这种反馈累积相符。
estimateOutputCapInputTokens() 会统计 system sections / tool declarations,clampOutputToContext() 将剩余输出预算最小夹到 1。重复记录令估算膨胀、进而把输出额度压到 1,是结合代码和日志的故障链路推断,未通过原始请求抓包直接核实。
本地最小修复方向:
- 在 restore、成功持久化、remember 时登记包含原 timestamp 的完整序列化身份,避免仅依赖对象引用;成功持久化时同时登记运行时对象和归一化序列化对象。
- 仅在持久化成功后登记,不能让失败写入被误当作已落盘。
- 在
orderSystemRows() 中对完全相同的解析后 system 消息去重,保留首次记录及锚点,再按原规则排序。此修复只改变回放,不删除/改写原 JSONL 或 SQLite。
- 建议为 clone 后 persist、restore 后 clone、不同 timestamp、指令变化、失败写入重试、checkpoint 恢复和已有重复历史加入回归测试。
本地验证结果
- 原版:同一条消息 clone 后保存 8 次,产生 8 条记录;补丁版:1 条。
- 修复前历史的只读回放:1,025 条 system 记录还原为 5 条,用户/助手/工具消息及顺序保留。
- 失败写入重试、不同指令/不同 timestamp、checkpoint 回放的隔离检查通过。
- 本地修复副本:新聊天正常回复;连续 6 次
Bash 调用只执行 printf pi_runtime_ok,全部成功。
- 该测试会话仅存 2 条 system 记录,2 条均唯一,没有重复膨胀。
- 此前失败的旧会话成功回复恢复测试,回合状态
completed、无 error code。
- 未修改原始聊天文件;未做全仓库测试、长时间压力测试或跨平台验证。
Logs / 日志
以下为脱敏字段摘录(不是完整原始日志):
{
"code": "EMPTY_MODEL_RESPONSE",
"model": "MiniMax-M3.1-Flash-Preview",
"details": {
"phase": "stream",
"providerStatus": 200,
"requestMessages": 345,
"requestBytes": 93055,
"providerWaitMs": 1079,
"streamMs": 122
},
"usage": { "outputTokens": 1 }
}
CONTEXT_COMPACTION_FAILED
Automatic context compaction failed before the model request
未附完整聊天、数据库、系统提示词、项目路径或凭据。
Related / 相关报告
#918 也报告空响应,但这里提供的是 system journal 重复持久化这一具体机制;#504 是完成通知的静默结束问题,触发路径不同。
What happened? / 问题描述
PI-Desktop 0.17.0 在新会话经过少量交互/工具调用后也会出现
EMPTY_MODEL_RESPONSE,随后点击继续会出现CONTEXT_COMPACTION_FAILED。本地排查发现 system transcript journal 重复持久化相同系统消息:一条只有 5 条用户消息的会话存了 1,025 条 system 记录,但
modelSystem.messageJson只有 5 个不同值。其中 4 个值各重复 256 次,另一个出现 1 次;重复项包含相同的原始 timestamp。新会话因此也会很快膨胀。对 system journal 做内容去重和回放去重的本地补丁后,新会话、连续工具调用及原先失败的旧会话均恢复。下文区分实际观察和代码推断;未捕获修复前完整的 HTTP 请求体,因此不把推断的输出参数当成抓包结论。
Steps to reproduce / 复现步骤
实际触发路径:
meta.modelSystem.messageJson的总数与不同值数;本例出现上述 1,025 / 5 的差异。尚未在另一台干净安装上验证固定的端到端复现轮数。下面是可用于源码回归测试的最小 journal 复现(无供应商或网络依赖):
本地使用从已安装 sidecar 提取的 journal 实现做隔离测试:原版写入 8 条,补丁版写入 1 条。此代码片段是供维护者加入现有测试的等价示例;没有运行仓库完整 Vitest/构建套件。
Expected behavior / 预期行为
Actual behavior / 实际行为
EMPTY_MODEL_RESPONSE时 HTTP 状态为 200,实际记录的outputTokens为 1;MiniMax 正文为空,DeepSeek 有极短 thinking、无正文。CONTEXT_COMPACTION_FAILED结束。App version / 应用版本
0.17.0,检查源码为 tag
v0.17.0/ commit72b5e826cb7a9928467091ccf745aa9b225eeb04。Operating system / 操作系统
macOS
Extra environment / 其他环境信息
MiniMax-M3.1-Flash-Preview。cline-pass/deepseek-v4.1-flash。根因分析与建议修复
相关代码:
SystemTranscriptJournal.isPersisted()检查对象身份WeakMap,以及用于 checkpoint 的序列化集合。但常规restore()、成功persist()、remember()只登记对象身份。内容不变的新对象不被视为已持久化,可能再次写入;setAgentMessages()也依赖这个判断重建系统消息。重复记录中相同 timestamp 和 256 倍重复,与这种反馈累积相符。estimateOutputCapInputTokens()会统计 system sections / tool declarations,clampOutputToContext()将剩余输出预算最小夹到 1。重复记录令估算膨胀、进而把输出额度压到 1,是结合代码和日志的故障链路推断,未通过原始请求抓包直接核实。本地最小修复方向:
orderSystemRows()中对完全相同的解析后 system 消息去重,保留首次记录及锚点,再按原规则排序。此修复只改变回放,不删除/改写原 JSONL 或 SQLite。本地验证结果
Bash调用只执行printf pi_runtime_ok,全部成功。completed、无 error code。Logs / 日志
以下为脱敏字段摘录(不是完整原始日志):
{ "code": "EMPTY_MODEL_RESPONSE", "model": "MiniMax-M3.1-Flash-Preview", "details": { "phase": "stream", "providerStatus": 200, "requestMessages": 345, "requestBytes": 93055, "providerWaitMs": 1079, "streamMs": 122 }, "usage": { "outputTokens": 1 } }未附完整聊天、数据库、系统提示词、项目路径或凭据。
Related / 相关报告
#918 也报告空响应,但这里提供的是 system journal 重复持久化这一具体机制;#504 是完成通知的静默结束问题,触发路径不同。