| 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 天前 |
| feat(datalog): record session_id; unify session id generation
Tag datalog with the same session id that rides the request header and
telemetry, and collapse the two session-id generators into one.
- DatalogWriter gains a session_id (set via the agent's SetSessionId
handler). It's written into each turn's .md env header (session=…)
and the _requests.jsonl dump.
- log_llm_request (the llm/*.json writer) takes the session id from the
provider (new LlmProvider::session_id() getter) and records it, so the
per-call json — the one used for cache-hit analysis — is attributable to
a session.
- Agent::new no longer mints its own raw uuid; it reuses the single
generator SessionId::new() as a bootstrap value (the agent is built
before the Session exists, so the UI's SetSessionId still supplies the
real session-file id). Net: one generation site, and header / datalog /
telemetry / hooks all share the session-file id at runtime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| 2 天前 |
| fix(provider): unwrap JSON envelope when surfacing upstream HTTP errors
Gateways wrap the real error message in JSON shapes (AtomCode
{"detail":{"message":...}}, OpenAI {"error":{"message":...}},
FastAPI {"detail":"..."}). Showing the raw body buried the message
in JSON noise — extract the known shapes and render just the message.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| 16 天前 |
| fix(prompt): 冻结会话级 system 提示 + 锁定 assistant 序列化确定性,稳住前缀缓存
Part 2,承接 read_file 历史冻结(86e73592)。线上 920 个"好轮→坏轮"逐字节
比对:system(第0条)占缓存塌缩 19.8%,assistant 历史占被改写消息 23.3%。
== A. system 提示逐轮改写(已修) ==
build_system_prompt 每轮用 live 输入重建:working_dir 在模型每次 cd
(tool/cd.rs、tool/bash.rs 直接写 working_dir)后变化、被插进 4 行
(SCOPE/CONFIG/Working directory);plan_mode 切换增删 PLAN MODE 段;
memory / layered-instructions 每轮重读磁盘。system 是 messages[0],变 1 字节
整条会话缓存归零。
修复:会话级冻结——build_system_prompt 首轮 assemble 后缓存,后续原样复用;
仅在显式契约边界失效重建(SetPlanMode / ClearConversation / ReloadConfig /
change_dir 即 /cd)。模型自己的 cd 工具不失效——最新 cwd 仍通过 cd/bash
工具结果到达模型,冻结 system 不致盲。hook 扩展仍每轮收集(行为不变),但冻结后
只在冷缓存(首建/失效后)被消费;刻意保持无条件收集,避免某构建路径(如
RefreshContextStats)在 SystemPromptHook 收集前抢先冻结。
== B. assistant 历史重新序列化(排查结论:openai 路径本就确定,无需改) ==
provider/openai.rs format_messages 复用存储的 tool_call.arguments 原始字符串
(不重新 stringify)、serde_json 键序确定、无任何按 recency 裁剪历史
reasoning_content 的逻辑。故 deepseek-v4(Include)下历史 assistant 序列化
本就逐字节稳定;23.3% 主要是 system/tool 断点后的下游位移。加测试锁死该不变量
防回归(有人重编码 args / 重排键 / 裁剪历史 reasoning)。
回归测试:
- agent/mod.rs::system_prompt_is_frozen_across_model_cwd_change
改 working_dir 后 system 逐字节不变;显式失效后重建并反映新 cwd。
- provider/openai.rs::format_messages_is_deterministic_and_prefix_stable
同输入两轮字节一致;会话增长后旧前缀消息序列化不变。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| 1 天前 |
| fix(provider): regenerate signature per retry — eliminates SIG_REPLAY/SIG_STALE 403 on atomgit gateway
When the inner reqwest-level retry (send_with_retry) replayed the same
request bytes on transient 5xx / network errors, the server's signing
middleware rejected the second attempt: either SIG_REPLAY (server's
nonce cache held the first attempt's nonce) or SIG_STALE (cumulative
backoff pushed ts past the 300s freshness window).
Server returned 403 with body "请升级到最新版 AtomCode 后再试" for
both cases, which atomcode surfaced verbatim — users saw it, ran
/upgrade, found themselves already on latest, then ran /login and the
issue "resolved" (the new login triggered a fresh chat_stream which
re-signed naturally on first attempt).
Fix: add send_with_retry_resign that rebuilds the request via a
factory closure on each attempt. The atomgit-gateway caller in
openai.rs uses it; the factory regenerates fresh nonce+ts+signature
per attempt via build_codingplan_headers. Non-atomgit providers keep
using send_with_retry unchanged.
Trade-off accepted: on 5xx where backend already started processing
the first attempt, a re-signed retry will be processed again
(potentially double-billing). The previous accidental SIG_REPLAY
rejection acted as primitive idempotency. Long-term fix is an
Idempotency-Key header; for now the user-visible benefit (no more
false-positive 'upgrade' message, transparent recovery from network
blips) outweighs the rare double-process risk.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| 9 天前 |