| fix(session): 压缩后保住原始 prompt,/resume 不再开局就是 tool_call
bug: /resume 后滑到顶部看不见自己最初问的 prompt,直接是 list_directory /
bash / read_file 一串 tool 调用;session JSON 文件里也没有原始 prompt
的痕迹。
根因:hard_truncate_to_target (agent/mod.rs:3178) 找"last user
message"作为 sacred 锚点,但 agent 在 turn 过程中会以 Role::User 注入
3 种合成消息: [Additional context from user]: ...、 `Output limit
hit. ...、[Context was compressed. ...]`。这些合成消息让 last_user_idx
指向了某条注入而非真实原始 prompt,触发压缩时原始 prompt 在
drain(0..keep_from) 里被一并砍掉,落盘 JSON 也丢失。
修复(opencode 子集):
1. Message 加 synthetic: bool 字段。#[serde(default)] + skip-if-
false,旧 session.json 反序列化默认为 false,序列化时常见 false 不
写盘,无 bloat。新增 Message::synthetic_user() 构造器。
2. 3 个合成注入点改用新的 Conversation::add_synthetic_user_message,
该方法 merge 逻辑保留既有 synthetic 标(real + synthetic 文字 append
后不会被错误升级为 synthetic)。
3. hard_truncate_to_target sacred 集合从 {last_user} 扩展为
{first_real_user, last_real_user},两个 anchor 都 filter
!m.synthetic。first 保会话锚点供 /resume,last 保当前任务上下文。
单 prompt 场景下两者重合,compaction 宁可超 budget 也不丢 prompt
(tier 1/2 仍可降 token,tier 3 在这种场景退化为 no-op 是设计取舍)。
4. session.rs::auto_name_from_messages、event_loop::apply_session_messages
主信号改用 synthetic 字段,次信号保留 bracket-prefix 启发式作为旧
session 兜底,避免老 JSON /resume 标题退化。
参考:opencode 的 message-v2.ts synthetic part 字段 + replay 机制
是公认的"原始 prompt 保护"工程化做法;DeepSeek-TUI 只在 metadata.title
存截断版,不能恢复完整 prompt。我们抄了 opencode 的 synthetic 字段 +
双 anchor sacred,没抄 replay(那是单独的"压缩后给模型重新喂上下文"
机制,不在本 bug 范围)。
测试(12 个新):
- message.rs: 5 个 — 构造器 / serde 默认 false / 不序列化 false /
序列化 true / 反序列化兼容
- conversation/mod.rs: 3 个 — syn 注入标记 / syn 合并到 real 保 real /
syn 合并到 syn 保 syn
- agent/mod.rs: 3 个 — 复现 bug 场景验证原始 prompt 保留 / 多轮场景
验证 last real 跳过尾部 syn / 空 conv 不 panic
- event_loop session_naming tests 全过(legacy bracket fallback 还在)
跨 provider/render/test fixture 共 19 处 Message {} 字面量补全
synthetic: false(脚本批量,brace-aware,跳过 -> Message { 函数签名)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| 10 天前 |
| docs(undo): note real-prompt vs divider numbering trade-off in prompt_count
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| 1 天前 |
| fix(session): 压缩后保住原始 prompt,/resume 不再开局就是 tool_call
bug: /resume 后滑到顶部看不见自己最初问的 prompt,直接是 list_directory /
bash / read_file 一串 tool 调用;session JSON 文件里也没有原始 prompt
的痕迹。
根因:hard_truncate_to_target (agent/mod.rs:3178) 找"last user
message"作为 sacred 锚点,但 agent 在 turn 过程中会以 Role::User 注入
3 种合成消息: [Additional context from user]: ...、 `Output limit
hit. ...、[Context was compressed. ...]`。这些合成消息让 last_user_idx
指向了某条注入而非真实原始 prompt,触发压缩时原始 prompt 在
drain(0..keep_from) 里被一并砍掉,落盘 JSON 也丢失。
修复(opencode 子集):
1. Message 加 synthetic: bool 字段。#[serde(default)] + skip-if-
false,旧 session.json 反序列化默认为 false,序列化时常见 false 不
写盘,无 bloat。新增 Message::synthetic_user() 构造器。
2. 3 个合成注入点改用新的 Conversation::add_synthetic_user_message,
该方法 merge 逻辑保留既有 synthetic 标(real + synthetic 文字 append
后不会被错误升级为 synthetic)。
3. hard_truncate_to_target sacred 集合从 {last_user} 扩展为
{first_real_user, last_real_user},两个 anchor 都 filter
!m.synthetic。first 保会话锚点供 /resume,last 保当前任务上下文。
单 prompt 场景下两者重合,compaction 宁可超 budget 也不丢 prompt
(tier 1/2 仍可降 token,tier 3 在这种场景退化为 no-op 是设计取舍)。
4. session.rs::auto_name_from_messages、event_loop::apply_session_messages
主信号改用 synthetic 字段,次信号保留 bracket-prefix 启发式作为旧
session 兜底,避免老 JSON /resume 标题退化。
参考:opencode 的 message-v2.ts synthetic part 字段 + replay 机制
是公认的"原始 prompt 保护"工程化做法;DeepSeek-TUI 只在 metadata.title
存截断版,不能恢复完整 prompt。我们抄了 opencode 的 synthetic 字段 +
双 anchor sacred,没抄 replay(那是单独的"压缩后给模型重新喂上下文"
机制,不在本 bug 范围)。
测试(12 个新):
- message.rs: 5 个 — 构造器 / serde 默认 false / 不序列化 false /
序列化 true / 反序列化兼容
- conversation/mod.rs: 3 个 — syn 注入标记 / syn 合并到 real 保 real /
syn 合并到 syn 保 syn
- agent/mod.rs: 3 个 — 复现 bug 场景验证原始 prompt 保留 / 多轮场景
验证 last real 跳过尾部 syn / 空 conv 不 panic
- event_loop session_naming tests 全过(legacy bracket fallback 还在)
跨 provider/render/test fixture 共 19 处 Message {} 字面量补全
synthetic: false(脚本批量,brace-aware,跳过 -> Message { 函数签名)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| 10 天前 |