Skip to content

[Bug] v0.17.0 系统消息重复持久化导致上下文膨胀、空响应及压缩失败 #1477

Description

@VanZY-CN

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 / 复现步骤

实际触发路径:

  1. 使用 0.17.0 创建 Agent 会话,进行多轮交互及工具调用。
  2. 检查该会话 JSONL 中 meta.modelSystem.messageJson 的总数与不同值数;本例出现上述 1,025 / 5 的差异。
  3. 出现空响应后继续,随后会在模型请求前遇到自动压缩失败。

尚未在另一台干净安装上验证固定的端到端复现轮数。下面是可用于源码回归测试的最小 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,是结合代码和日志的故障链路推断,未通过原始请求抓包直接核实。

本地最小修复方向:

  1. 在 restore、成功持久化、remember 时登记包含原 timestamp 的完整序列化身份,避免仅依赖对象引用;成功持久化时同时登记运行时对象和归一化序列化对象。
  2. 仅在持久化成功后登记,不能让失败写入被误当作已落盘。
  3. 在 orderSystemRows() 中对完全相同的解析后 system 消息去重,保留首次记录及锚点,再按原规则排序。此修复只改变回放,不删除/改写原 JSONL 或 SQLite。
  4. 建议为 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 是完成通知的静默结束问题,触发路径不同。

Activity

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