Skip to content

[Bug] 摘要失败后仍只留 1 条用户消息,当前长回合无真摘要 / Failed compaction still keeps one user line and drops the active turn #827

Description

@YahooYuan666

What happened? / 问题描述

Follow-up to #543 / PR #563 (ADR 0282 / D445). Do not reopen #543 — the UI and retry/preflight slice shipped; the remaining hole is the fallback contract.

On PI-Desktop 0.15.2, a long Agent session still lost almost all working memory after automatic compaction #3. The transcript row now correctly says:

上下文已压缩 · 第 3 次 摘要生成失败 · 已保留近期上下文

That label is the #563 UI fix. It is not a successful summary. After that row, the model no longer knew decisions, paths, or unfinished work from the active turn. The user had to re-steer from scratch.

#563 explicitly deferred:

So a failed attempt still omits the current long turn from the next model request, even when a previous real summary exists.

这是 #543 / PR #563(ADR 0282 / D445)的后续,不要重开 #543。UI 和重试/预检那一层已经落地;剩下的是回退契约。

PI-Desktop 0.15.2 上,一条长 Agent 会话在自动压缩第 3 次后几乎丢掉全部工作记忆。转录行现在会正确显示:

上下文已压缩 · 第 3 次 摘要生成失败 · 已保留近期上下文

这正是 #563 的 UI 修复,不是一次成功摘要。该行之后,模型不再知道当前回合里的决策、路径、未完成工作,用户只能重新引导。

#563 明确没做:分段 map-reduce、Codex 形 keepRecent 存活(#203)、回退尾部大小不变(仍最多最后一条用户消息)。所以失败时,即使上一份真摘要还在,当前长回合仍会从下一次模型请求里拿掉。

Steps to reproduce / 复现步骤

  1. Open PI-Desktop 0.15.2 (Linux AppImage / WSL2), Agent mode, a long coding session with many tool results.

  2. Let automatic compaction succeed at least once (a real 4–9k-character summary).

  3. Continue the same turn until the context hits the hard limit again (tokensBefore here ≈ 140k, mid-turn, retainedTailMode: active_turn).

  4. Watch 上下文已压缩 · 第 N 次 摘要生成失败 · 已保留近期上下文.

  5. Ask about files, decisions, or unfinished work from after the last successful summary but before the failure row — or simply continue the same task without restating it.

  6. 打开 PI-Desktop 0.15.2(Linux AppImage / WSL2),Agent 模式,跑带大量工具输出的长会话。

  7. 先让自动压缩成功至少一次(4–9k 字的真摘要)。

  8. 同一回合继续做到再次撞硬上限(本机 tokensBefore ≈ 14 万,回合中,retainedTailMode: active_turn)。

  9. 看到 上下文已压缩 · 第 N 次 摘要生成失败 · 已保留近期上下文。

  10. 再问「上一份成功摘要之后、失败行之前」的文件、决策、未完成工作,或不重述任务直接让它接着干。

Expected behavior / 预期行为

Compaction is a summarization pass. A failure must not wipe the active turn.

  • Transient failure: retry the same summary request (this part is in fix(agent-runtime): retry and right-size the compaction summary before falling back #563).
  • Input still too large after the one reduced pass: do not skip the model. Chunk / map-reduce / keep truncating tool output until a new structured summary covers the range this checkpoint claims.
  • Last resort only: persist a bounded recent original tail on the order of keepRecentTokens (thousands of tokens of recent user + assistant + tool text), not a single user sentence.
  • If a previous real summary exists, keep it and cover the delta since that summary. Do not tell the model that one leftover user line is the source of truth for the whole continuation.
  • The UI label “摘要生成失败 · 已保留近期上下文” is fine only if “近期上下文” is actually large enough to continue the task.

压缩的本质是调模型总结。失败不得清掉当前回合。

  • 瞬时失败:重试同一次摘要请求(fix(agent-runtime): retry and right-size the compaction summary before falling back #563 已做)。
  • 缩一次后仍塞不下:不要跳过模型。分段 / map-reduce / 继续截工具输出,直到产出覆盖本检查点范围的新结构化摘要。
  • 只有重试耗尽,才允许保 keep-recent 量级的近期原文(数千 tokens 的最近用户/助手/工具文本),而不是一句用户话。
  • 若已有上一份真摘要,应保留它并补上之后的增量。不要告诉模型「剩下的那一句用户消息就是整段续写的唯一真相」。
  • 「摘要生成失败 · 已保留近期上下文」这句 UI 可以留,前提是「近期上下文」真的够接着干活。

Actual behavior / 实际行为

Inspected the session JSONL and installed pi-host/0.15.2/agent-runtime/sidecar.js (this machine).

Session had 3 compaction entries:

gen time (UTC) result summary usage retainedTail
1 2026-09-21T18:48:41Z real summary 5802 chars input 82503 1 user (original task)
2 2026-09-21T19:00:55Z real summary 8620 chars input 41917 1 user (original task)
3 2026-09-21T20:55:00Z fallback 9027 chars = gen-2 summary copied forward + failure notice none 1 user (the in-progress follow-up only)

Generation 3 details:

{
  "failureCode": "CONTEXT_COMPACTION_FAILED",
  "fallback": "retained_tail",
  "generation": 3,
  "retainedTailMode": "active_turn"
}
  • tokensBefore: 139667
  • No usage object → this attempt did not complete a summary call (budget-reduced input still too large, or retries exhausted). Contrast gen 1–2, which both have provider usage.
  • retainedTail is a single user line (the task that was in flight). Codex-shaped prep still folds all history into messagesToSummarize and keeps at most that one user message (active_turn) or none (completed_turn). keepRecentTokens still does not decide survival.
  • createFallbackCheckpoint appends the previous summary (good vs 0.14.8’s 112-token stub) and instructs: “Use the one retained latest user request as the source of truth for that continuation.” After the row, the model treated that one sentence as the whole job and forgot the rest of the turn (~168 messages between gen 2 and gen 3: 3 user / 34 assistant / 131 tool).
  • Full JSONL remains on disk (~6.5MB). Only the next model request is stripped. The UI looks intact; the agent is amnesiac.

retainedTailForContext on 0.15.2 is still:

const latestUser = messages.at(-1);
return latestUser ? [latestUser] : [];

v0.15.3 changelog does not mention this path.

对照本机会话 JSONL 和已安装的 pi-host/0.15.2/agent-runtime/sidecar.js。

该会话共 3 次压缩:第 1、2 次是真摘要(5802 / 8620 字,有 usage);第 3 次是回退(failureCode: CONTEXT_COMPACTION_FAILED,fallback: retained_tail,tokensBefore: 139667,没有 usage)。第 3 次摘要 = 第 2 次原文照搬 + 失败声明(9027 字),retainedTail 只剩当前回合那一句用户消息。

相对 0.14.8,「空壳 112 tokens 伪装成成功摘要」已经没了;上一份真摘要也会带上。但 Codex 形仍把全部历史折进 messagesToSummarize,最多留 1 条 user。createFallbackCheckpoint 还写着「用这一条用户请求作为续写的唯一真相」。失败行之后模型就围着那一句转,丢掉 gen2→gen3 之间约 168 条(3 用户 / 34 助手 / 131 工具)。磁盘 JSONL 还在,被拿掉的是下一次模型请求。0.15.3 changelog 未再动这条路径。

App version / 应用版本

0.15.2

Operating system / 操作系统

Linux

Extra environment / 其他环境信息

Logs / 日志

Sanitized compaction checkpoints (no transcript body). Generation 3 is the failure:

{
  "generation": 1,
  "strategy": "summary",
  "retainedTailMode": "active_turn",
  "tokensBefore": 146922,
  "summaryChars": 5802,
  "usage": { "input": 82503, "output": 1715, "totalTokens": 84346 }
}
{
  "generation": 2,
  "strategy": "summary",
  "retainedTailMode": "active_turn",
  "tokensBefore": 147171,
  "summaryChars": 8620,
  "usage": { "input": 41917, "output": 2617, "totalTokens": 44662 }
}
{
  "failureCode": "CONTEXT_COMPACTION_FAILED",
  "fallback": "retained_tail",
  "generation": 3,
  "retainedTailMode": "active_turn",
  "tokensBefore": 139667,
  "summaryChars": 9027,
  "usage": null,
  "retainedTailCount": 1
}

Fallback notice appended to the carried gen-2 summary (sidecar COMPACTION_FALLBACK_MARKER):

[automatic context recovery: older context was omitted after summary generation failed]

The automatic summary request did not complete. Older messages before this checkpoint are omitted from the next model request.

The complete transcript remains available in the session. The provider is continuing the active turn. Use the one retained latest user request as the source of truth for that continuation.

No full transcript attached (privacy). Happy to add redacted sidecar traces if useful.

Screenshots / 截图

No screenshot. The UI row is the #563 failure label, which is correct; the bug is what the next model request contains.

无截图。界面行已经是 #563 的失败文案,文案没问题;问题是下一次模型请求里实际还剩什么。

Suggested fix (for maintainers)

In packages/agent-runtime compaction fallback (0.15.2 sidecar.js: fitSummaryInputToBudget / recoverCompactionFailure / codexShapedPreparation / retainedTailForContext):

  1. After the one reduced pass still overflows: chunk the range and still call the model; merge partial summaries until the checkpoint covers throughMessageId. Do not fall back solely because compactionSummaryWouldExceedBudget is true.
  2. Last resort: persist a bounded recent original tail (keep-recent / 8k–20k tokens of recent user+assistant+tool text), not one user message. selectRetainedUserMessages already exists; Codex shaping currently throws that budget away.
  3. Stop telling the model that the leftover user line is the source of truth when the new summary was not generated. Carry the previous summary without overriding the active-turn goal.
  4. Optional: persist build.message (why the summary failed: budget vs provider error vs abort) on details, so the next report is not usage: null with no reason.

fitSummaryInputToBudget / recoverCompactionFailure / codexShapedPreparation / retainedTailForContext:

  1. 缩一次仍超窗:把范围切开继续调模型,合并部分摘要直到检查点覆盖 throughMessageId。不要只因为 compactionSummaryWouldExceedBudget 就回退。
  2. 最后手段:留下 keep-recent / 8k–20k tokens 的近期原文(用户+助手+工具),不是一条用户消息。selectRetainedUserMessages 已在,Codex 形把这个预算丢掉了。
  3. 没产出新摘要时,不要把「剩下那句用户话」写成续写唯一真相;带上上一份摘要,也不要覆盖当前回合目标。
  4. 可选:把 build.message(预算 / 供应商 / 中止)写进 details,避免再出现只有 usage: null、没有原因的检查点。

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