Agent 对话状态管理机制对比调研报告
分析对象:Codex (OpenAI)、DeepSeek Harness (DSH)、OpenCode、Claude Code (Anthropic)、Hermes (Nous Research)
性质:基于各项目源码的实际机制调研(含源码路径佐证),非文档转述
视角:为 NLP/AI 团队 Agent 技术栈(Hermes/Claude Code)的对话/上下文管理设计提供依据,并给出 Hermes 的落地优化建议
说明:Codex/DHSH/OpenCode 基于完整源码;Claude Code 基于官方公开仓库(文档/插件/CHANGELOG),核心二进制闭源,可核实项与闭源项已明确区分
0. 结论速览:五种对话状态管理范式
各家对"对话历史"的底层抽象不同,决定了它们的可扩展性、可恢复性与上下文成本:
| Agent | 存储范式 | “给模型的上下文” | 压缩语义 | 一句话定位 |
|---|---|---|---|---|
| DeepSeek Harness | 事件溯源(append-only JSONL,zstd 帧) | 从日志 fold 的投影 | 投影替换,存储不动 | 存储无限、prompt 轻量、可逆可回放 |
| Codex | 三层:JSONL 权威 + SQLite 元数据 + thread_history 投影 | 重放 + 压缩点快照替换 | 检查点 append(存储不动)+ 活动上下文投影替换 | 存储完整 + 检索投影分层 |
| OpenCode | SQLite + 事件溯源(bus→投影两表) | 从最新压缩点投影读回 | 摘要 checkpoint(存储追加)+ 投影替换 | 事件 + 关系型混合,压缩可回滚 |
| Claude Code | JSONL transcript(闭源) | (闭源,推测 摘要+增量) | /compact + /autocompact 插摘要(闭源细节) | 官方闭源,本地 JSONL 会话 |
| Hermes | 关系型(SQLite sessions/messages) | 读回 active 消息 | 会话轮转 + lineage(旧会话保留可回放) | 存储即历史、压缩换上下文、原始可回放 |
核心分歧一:存储层是否保留完整历史
- DSH / Codex / OpenCode 的底层日志都是 append-only 或追加式,压缩只是"换掉送入模型的那份上下文",磁盘上的原始消息保留可回放;
- Hermes 自动压缩也保留原始历史:走"结束旧会话 + 新建子会话(parent_session_id)"的会话轮转,旧会话消息行不删,靠 lineage 沿谱系读回即可完整恢复(见 §5.3)。真正物理删消息的是
/retry、/undo等手动 transcript 改写(replace_messages),那是用户主动改写,不是默认压缩路径。
核心分歧二:是否用投影/派生分离"数据面"与"模型面"
- DSH 的 SurfaceManager、Codex 的 thread_history 投影、OpenCode 的 projector,都是"存一份完整数据 + 按需折出轻量模型上下文";
- Hermes 是"存储即模型上下文",没有独立的模型面投影层。
1. DeepSeek Harness(DSH)—— 事件溯源 + 投影派生
DSH 源码根:/netds2/userdata/447574/agent/deepseek-harness/deepseek-harness-master/
1.1 顶层架构
DSH 把每一次对话/工具/元数据变化都落成带序号(seq)的不可变事件,持久化到按 zstd 帧压缩的 JSONL 文件;模型看到的"消息历史"不是存储内容,而是从事件日志按 surfaceOp 标记 fold 出的投影。
Session(packages/core/session/src/index.ts:446):内存持有一棵 append-onlylog: SessionEvent[],每个事件带 seq/time/data/surfaceOp。SessionStore(行 925):进程内 Map,负责创建/恢复/分叉;刻意不持久化,持久化由独立插件订阅事件完成。- 数据面/模型面解耦:
SurfaceManager只保留标记为消息的节点,deriveMessages()(行 860)fold 成真正送进 LLM 的Message[]。
1.2 持久化
JsonlSessionPersistence(session-persistence-jsonl):每会话一目录,header + 事件逐行。- zstd 拼接帧(
zstd.ts:48):按帧追加、按帧恢复 torn frame,带 checksum;truncateTornTail(storage.ts:328)帧级修复崩溃半帧。 - 跨进程写锁:
SessionWriteLease(lease.ts:70)用 POSIXflock(Windows 信号量),持有者死亡自动释放。 - fail-closed 恢复:
session-checkpoint-policy在 LLM 请求 / 工具副作用执行前强制flush(session),保证"已记录即可恢复"。
1.3 上下文压缩(投影替换,存储不动)
BasicCompactionEngine(compaction-basic/src/index.ts:113):agent/pre-step按 tokenMeter 压力触发 + provider 返回CONTEXT_WINDOW_EXCEEDED时触发('context-overflow',带重试)。- 范围选择(region.ts:117):保留
retainTokens尾部;toolPairingBalancedBefore/After保证不拆开 tool-call/result 对;system/message 永不进压缩范围。 - 压缩事务:写 compaction/start(持锁)→ LLM 生成结构化摘要(检查点:Primary Request/Key Technical Concepts/Files/Errors/Jobs/Current Work/Next Step)→ 校验摘要确实更小 → 用
user/message + surfaceOp: replace替换被压缩范围 → 写 compaction/end。 - KV-cache 友好:摘要调用把对话原样回放、只在末尾追加压缩指令,复用 provider warm prefix cache。
- 手工
/compact调用compactNow。
1.4 DSH 的投影技巧清单 & 与 Hermes 的映射
先说清楚一个容易混淆的点:DSH 的"投影替换压缩"和 Hermes 的"会话轮转压缩",在"底账保留、只换上下文视图"这个理念上是一致的,但机制完全不同,不是同一套东西。 相同之处:两者压的都不是"固定 N 轮",而是按上下文 token 阈值触发 + 保留策略定范围,且压缩后原始历史都保留可回放;不同之处:DSH 是在运行时从事件日志 fold 出投影、在投影上做 surfaceOp:replace(底账是一棵 append-only 事件日志);Hermes 是结束旧会话、新建带摘要的子会话、沿 lineage 读回(底账是 SQLite 行)。映射时照搬 DSH 的技巧没问题,但要落在 Hermes 的"摘要 + 保留策略 + 投影渲染"这套形态上,而不是把事件日志机制搬过去。
DSH 的投影技巧(源码可证),逐条列出,并标注 Hermes 的对应现状/可借鉴点:
| # | DSH 投影技巧 | 原理 | Hermes 现状 / 可借鉴点 |
|---|---|---|---|
| 1 | 头尾保护(tail/head guards) | 压缩两端留原文:最近一段(尾部)必留,源头(头部)也保护,避免丢刚发生的关键上下文和首轮设定。按 token(retainTokens)而非固定轮数 | Hermes 已是 head(3)+tail(≈10%)保护——对应,但头部是固定 3 条,可考虑按 token 更灵活 |
| 2 | tool-call/result 对不拆分(toolPairingBalancedBefore/After) | 压缩边界绝不在"assistant 发起的工具调用"和"工具返回结果"之间切断,否则语义断裂 | Hermes 已守此条(压缩守卫),一致 |
| 3 | system/message 永不进压缩范围 | 系统提示词/角色骨架每轮必用,明确排除 | Hermes system prompt 走单独组装不在压缩范围,一致 |
| 4 | 固定结构化摘要模板(检查点) | 非自由发挥,而是固定结构:Primary Request / Key Technical Concepts / Files and Code / Errors and Fixes / Pending Jobs / Current Work / Next Step。信息密度高、机器可解析、续会话按字段直接接上 | Hermes 有结构化摘要(迭代更新+时间锚+focus),可对照补"Pending Jobs / Next Step"字段,便于子会话直接续接任务 |
| 5 | 压缩事务化 + 校验更小 | 先 compaction/start(持锁)→ 生成摘要 → 校验确实更小才提交 → end;失败不落半截,原子 | Hermes 有 compression_locks 防并发 + 真实 usage 校准,已覆盖;可明确加"校验摘要比原段小"一步 |
| 6 | KV-cache 友好的摘要调用 | 摘要调用把 system/tools/messages 原样回放、只在末尾追加压缩指令,是已路由请求的真前缀,复用 warm prefix cache,摘要几乎不额外算 | Hermes 未刻意这么做;对离线 Qwen3.5 这是实打实的推理成本节省,建议借鉴 |
| 7 | surfaceOp 标记驱动投影 | 压缩不物理删,而是写 surfaceOp:replace 事件;fold 时按标记换摘要,天然可回放、可逆 | Hermes 等价物是会话轮转 + lineage(旧会话行保留),已满足可回放;差异是无"模型面独立投影层"(见 §7.P0) |
给的落地映射(结合你们 Hermes + 内网 Qwen3.5 场景):
- 保留策略别用固定轮数,用"按 token 的 head + tail 保护"更稳妥;
- 压缩边界必须守 tool 对不拆分 + user/assistant 交替合法(Hermes 已有守卫,别丢);
- 摘要用固定结构化模板,重点补"待办/下一步"字段,子会话续接更顺;
- 摘要生成调用尽量复用 warm prefix cache(回放原文+末尾追加指令),省离线推理成本。
1.5 跨会话引用 / 溢出(spill)
SessionReferenceResolver(session-reference):解析@session提及,插入"## Referenced sessions"上下文块;referenceBudget默认contextWindow*4*0.2预算。- 预算截断时把完整投影转储到 spill 存储(
SpillStore,0600 私有文件 + 防路径穿越),prompt 里留SpillRef定位符 + 安全警告(防止跟随注入指令)。 - 图片 offload:溢出时最老图片打
offloaded:true跳过。
1.6 任务状态事件化
goal/plan/todo 以专用 session 事件写进同一棵日志,崩溃/分叉靠纯 fold 回放重建——与对话消息共享同一套持久化/恢复/压缩原子性。goal(goal/change)、plan(plan/mode)、todo(todo/write 全量快照 latest-write-wins)。
1.7 强项与局限
- 强项:数据面/模型面解耦(存储无限、prompt 轻量、可回放可逆);压缩可逆;goal/plan/todo 事件化;fail-closed + 单写者锁 + 帧级修复;FTS5 + 跨会话引用注入。
- 局限:单写者模型;lease 无过期;FTS 索引是分离的派生索引有同步窗口;状态 latest-write-wins 无多版本。
2. Codex —— append-only JSONL + 检索投影
Codex 源码根:/tmp/agent_src/codex/codex-main/(codex-rs/)
2.1 持久化模型:三层混合
- JSONL rollout(权威/规范历史源):每会话一个
~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<thread>.jsonl,append-only 打开(recorder.rs:1804),空闲后可后台压缩为.jsonl.zst(zstd),读取透明解压。 - SQLite state DB(线程元数据 + 任务/记忆状态):
threads表存元数据(id、rollout_path、cwd、title、source、tokens_used、archived 等),不存消息正文;另有 thread_goals(已删)、memories、projects 等表。 - SQLite thread_history DB(可查询投影):
thread_turns+thread_items把 JSONL 里的消息物化成关系表(带 rollout_ordinal),用于分页/检索。由"投影"机制从 JSONL 增量推进(单事务提交游标)。
Thread(= Session 持久形态):一个 UUID 对应一个 rollout 文件;Turn 是一次用户请求的执行周期(status: inProgress/completed/interrupted/failed);Item 是最小对话单元(挂某 turn 下)。
2.2 历史恢复:全量重放 + 压缩点快照替换
reconstruct_history_from_rollout从头重放 JSONL;遇到CompactedItem时直接读其replacement_history(压缩时保存的模型可读快照)重建该窗口,不重新展开所有原始 tool 消息。- 恢复形态:
InitialHistory::{New, Cleared, Resumed, Forked};分冷恢复(重放 rollout)与热 fork。
2.3 上下文压缩:检查点 append + 活动上下文投影替换
- 核心
compact.rs:发起压缩 turn 把完整历史喂给模型生成summary(SUMMARIZATION_PROMPT);build_compacted_history= 最近一批用户消息(≤20k token)+ 摘要;replace_compacted_history在内存活动历史上替换,同时向 JSONL append 一个CompactedItem。 - 磁盘是 append 检查点(不改写原历史);送入模型的上下文是投影替换。存储可回放、可审计。
- RetainedContext:独立于模型摘要之外、host 拥有的有界事实(用户原始消息、已投递消息、验证问答),硬上限(家族 8 条/16KB/64KB),超限淘汰最旧;是压缩后仍能注入"未压缩的原始授权证据"的手段。
- tool 对:压缩时一般不单独保留(折叠进摘要);显式保留的 tool output 按整条保/弃。
2.4 token 预算与溢出
- 计量:实际 usage 回传(
get_total_token_usage)。 - 两级:
model_auto_compact_token_limit(触发自动压缩)+resolved_context_window() * percent(硬上限)。 - 触发:turn 前后检查阈值(
CompactionPhase::PreTurn/PostTurn)→ 自动压缩;另有异步预压缩(async_compaction.rs):在上一轮 checkpoint 期间后台生成候选,阈值到时直接take_ready采用,低延迟。 - 溢出:压缩自身请求遇 ContextWindowExceeded 时移除最旧项后重试直到放下;普通错误指数退避。
2.5 会话生命周期
- 创建/恢复:
thread_manager.rscreate_thread / resume_thread_with_history / fork_*。 - Fork:复制父历史(
ForkPersistence::Copied)或引用式共享(Referenced),forked_from_id记录血缘。 - 归档:threads 表
archived/archived_at,文件移_ARCHIVED目录。 - 并发:跨进程每 thread 一个
.lock文件锁(单一 writer);同进程live_recorders+ in-memory writer_lock;记忆后台任务用jobs表lease_until/ownership_token分布式租约。
2.6 跨会话检索
- 无 SQLite FTS5:跨线程搜索用 ripgrep 扫 rollout 文件;线程内用 thread_history 表 + LIKE + 双指针子串匹配。大规模历史检索性能有限。
2.7 任务状态与记忆
- goal:早期有独立
thread_goals表,已删除(0034 migration);现在 goal/plan 作为消息 item 直接写进会话历史,压缩时捕获 goal id 以便保留。 - 记忆机制自成体系(
codex-rs/memories/):两阶段流水线——Phase 1 按线程并行从 rollout 抽取raw_memory+rollout_summary落库(stage1_outputs);Phase 2 全局整合到~/.codex/memories/(git 管理)。读取时注入为 developer instruction。
2.8 强项与局限
- 强项:存储分层(不可篡改事件源 + 可查询投影);压缩保留原始记录(可回放);异步预压缩降延迟 + RetainedContext 保证据;并发控制务实;长期记忆完整。
- 局限:JSONL 与 SQLite 两套非单库事务,有同步窗口;真正删改历史难(靠追加 truncation 标记);compaction 后工具细节被折叠,模型无法再直接引用;检索走 rg+LIKE 非 FTS。
3. OpenCode —— 事件溯源 + SQLite 投影
OpenCode 源码根:/tmp/agent_src/opencode/opencode-v2.1.4/
3.1 持久化模型:SQLite + 事件溯源
- 存储:
effect/sql+ Drizzle;bun:sqlite(本地文件,WAL)、Node 驱动、Cloudflare Durable Object(外键强制)。 - 核心表(projector 从事件投影):
session_v2— 会话头(project_id、parent_id、fork_session_id、title、token 计数、cost、revert、permission、resume_attempts)。session_message— 消息日志(append-only,唯一索引 (session_id, seq),整条消息 JSON 存data)。event+event_sequence— 事件溯源日志(每会话单调 seq)。instruction_state/entry/blob— 每会话指令。
- append-only 为主,但进行中消息原地 UPDATE(assistant/shell/compaction 消息 running→completed)。原始消息行从不删除。
3.2 历史恢复:从最新压缩点投影读回
session/history.ts的load:SELECT ... WHERE session_id=? AND seq >= 最新 completed compaction 的 seq ORDER BY seq一次性读回,再decodeMessageRow。- 分层渲染:compaction 消息被渲染成
<conversation-checkpoint>,内含<summary>+<recent-context>(最近 keep tokens 原文,用keepID引用原消息重建)。 - 当前对话部分仍按完整发送。
3.3 上下文压缩
- A. 摘要压缩(summarize/checkpoint):把旧对话喂给模型产结构化摘要(Objective/Requirements/Decisions/Work State/Next Move/Files/Context);摘要 + 最近 15k tokens 原文打包成一条
compaction消息(追加,不走删除);发送给模型时用 checkpoint 替换被压缩历史——投影替换,存储不变。 - B. 原生压缩(native):若 provider 支持,调用其 own compaction 端点,窗口 JSON 存
native.messages整体重放。 - 压缩级别 flatten(
flattenAndDropOldest):对话放不下时逐级缩短(全量→裁 tool 输出→删 tool 输出且裁输入→留 user 文本→省略),最新一条永不省略。 - tool 对:原始 tool-call/result 永远完整存 DB,只有投影成摘要文本时被裁减。
3.4 token 预算与溢出
- 计量:
Token.estimate = round(len/4)(粗略启发),精确值以 provider usage 为准。 - 预算:measured(provider 报告最近一次)+ estimated(新增按 4 chars/token);图片/PDF 固定估 token。
- 天花板
calculateCeiling = window - max(15%窗口, 16k);due判定超限自动压缩。 - 溢出:
isContextOverflowFailure时返回Outcome.Compacted→compact({reason:"overflow"}),按SHRINK_STEPS=[0.85,0.7,0.5,0.35]渐进缩小目标直到成功,保留RESERVE_MIN=16k。 - precompute(后台预压缩):接近阈值(85%)时后台生成摘要,届时就地换入,保 prompt cache。
3.5 会话生命周期
- 事件溯源
bus.ts:每会话是 aggregate,事件写event+event_sequence,projector.ts投影成两表。 - 崩溃恢复:
SessionStore.claim写time_suspended(执行声明),崩溃遗留用resume_attempts计数自动恢复。 - Fork:把父会话消息复制进新 session(
fork_session_id/fork_boundary)。
3.6 检索/记忆
- 无 FTS、无向量:会话搜索仅
title LIKE,无法检索正文。 - 无独立长期记忆库:记忆 = 压缩摘要 checkpoint + AGENTS.md/指令注入(
instruction-discovery.ts扫全局+项目指令文件),无跨会话学习。 - Todo:不是独立状态,
todowrite工具写进助手消息的 tool 输出 metadata;压缩摘要模板的 “Work State / Next Move” 承担目标追踪。
3.7 强项与局限
- 强项:事件溯源 + SQLite,崩溃可恢复、多实例一致(fork/transfer/claim 完整);压缩机制成熟(摘要 checkpoint + native + precompute + 溢出缩 + 投影不破坏原始日志);token 预算与自动触发/重试完整。
- 局限:追加 + 原地改写并存非纯 append-only;压缩后旧消息只是"隐藏"不减存储(长会话 DB 只增不减);token 估算粗糙;无 FTS/向量检索;无独立长期记忆。
4. Claude Code —— 官方闭源,本地 JSONL 会话
Claude Code 仓库 /tmp/agent_src/claude/claude-code-v2.1.310/ 不包含核心源码(是官方公开的文档/插件/示例/CHANGELOG 仓库,核心为闭源 npm 包)。以下分"可核实"与"闭源"两面。
4.1 可核实(来自插件源码/CHANGELOG/settings 示例)
- 会话持久化:transcript 是 JSONL 文件(每行一个 JSON,ralph stop-hook 用 jq 解析);hooks 通过 stdin 收到
transcript_path和session_id(UUID)。有cleanupPeriodDays保留期(HIPAA 示例=30 天)。 - 恢复:
--resume(含--resume <id>)、--continue、headless-p均存在;“resume of large compacted sessions … first request sent sooner”。 - 压缩:
/compact+/autocompact(可配置窗口,CLAUDE_CODE_AUTO_COMPACT_WINDOW=200k支持 k/M 后缀、--autocompact 100k);接近窗口/超限时自动压缩;压缩把旧对话总结成一条摘要插进 transcript(summary + boundary,sec-default README);会裁最旧消息;有PreCompacthook(可注入"需保留的关键信息");插件可session.compact接管压缩。 - token 预算:支持 1M 上下文模型(
[1m]别名、CLAUDE_CODE_DISABLE_1M_CONTEXT);autocompact 窗口即 token 预算;/cost、--max-budget-usd;5 小时用量限制。 - 生命周期:
SessionStart/SessionEnd/Stop/SubagentStop/PreCompacthooks;状态文件 GC(30 天 + flock 锁)。 - 记忆:
CLAUDE.md/AGENTS.md项目指令注入(agents-md mod,嵌套 CLAUDE.md 有限制);$CLAUDE_ENV_FILE跨会话持久化环境变量。 - Todo:
TodoWrite是内置工具。
4.2 闭源(无法核实)
存储目录布局、projects.db、恢复是"全量重放还是摘要+增量"、摘要 prompt、autocompact 默认窗口/百分比、每请求精确 token 计量、长期记忆向量库——本仓库不可见。
4.3 结论
Claude Code 的对话状态管理理念可确认为:本地 JSONL transcript + /compact+/autocompact 压缩插入摘要 + --resume/--continue 恢复 + CLAUDE.md 项目记忆;与 DSH/Codex/OpenCode 的"append-only + 投影替换"方向一致,但实现闭源,细节不可比对。
5. Hermes 对话状态管理 —— 源码深度解析
Hermes 源码根:/home/myuser/.hermes/hermes-agent/(排除 venv)
5.1 顶层架构:关系型存储 + 会话轮转压缩
Hermes 把对话历史以行形式持久化在 SQLite(sessions + messages),上下文窗口靠"采摘要 + 会话轮转(lineage)"腾挪——压缩时结束旧会话、新建子会话,旧会话原始消息保留可回放;会话恢复时沿 lineage 读回目标会话的全部 active 消息(含祖先可选)。
核心模块:
hermes_state.py(4941行)— SessionDB:SQLite + FTS5 + WALgateway/session.py(1501行)— SessionStore 生命周期状态机agent/context_compressor.py/conversation_compression.py— 压缩agent/memory_manager.py— 长期记忆agent/prompt_builder.py/system_prompt.py— prompt 组装与预算
5.2 持久化
- schema:
sessions(含 parent_session_id、token 记账、cost、title)、messages(tool_calls/reasoning/active 软删除标志/observed)、state_meta、compression_locks。 - 全文检索:
messages_fts(FTS5 unicode61 + 触发器)+messages_fts_trigram(trigram tokenizer 专为 CJK 子串搜索——中文检索强项)。 - WAL 降级 + 损坏自修复:
apply_wal_with_fallback(对 NFS/SMB 降级 DELETE journal)、repair_state_db_schema(备份重建)。 - 会话生命周期:
get_or_create_session(auto reset:idle/daily/suspended)、suspend_session。
5.3 上下文压缩:会话轮转 + lineage,原始保留
- 触发:
preflight(回合前预算检查)、回合内、API 错误(CONTEXT_WINDOW_EXCEEDED)、工具循环后;阈值默认 context×50%;真实 usage 校准。 - 策略:廉价 tool 裁剪 → head(3)+tail(≈10%)保护 → 结构化 LLM 摘要 → tool 对补齐 / 角色交替守卫。
- 持久化方式:会话轮转(session rotation),不是同一会话内删消息——
conversation_compression.py压缩后end_session(旧会话, "compression")(仅标记 ended,不删行)→ 生成新session_id→create_session(parent_session_id=旧会话)→ 新会话写入含摘要的新 system prompt。旧会话的所有原始消息行原样保留在 DB,只是模型上下文切到新会话的摘要表示。 - 恢复/导出:
get_messages_as_conversation(session_id, include_ancestors=True)沿parent_session_id链(_session_lineage_root_to_tip)把"根→当前"全部祖先会话的消息一次性读回,可完整回放/导出原始逐条历史(带去重,避免压缩边界重复用户消息)。 - 真正物理删消息的只是手动改写路径:
/retry、/undo等走replace_messages(DELETE FROM messages WHERE session_id=?再重插);这与"自动上下文压缩"是两条独立路径,不是默认压缩行为。 - 压缩锁:
compression_locks跨会话防并发压缩。
5.4 会话恢复(终止后重启,能还原什么)
结论速览:一个 Hermes 会话的状态,本质是两样东西——已落库(SQLite)的对话消息流水 + profile 级共享的 memory 文件。只要退出是"正常结束"(该落库的都已落库),重启就能把上下文还原出来。
| 状态 | 重启后 | 原因 |
|---|---|---|
| 完整对话历史(user/assistant/tool 消息) | ✅ 可恢复 | SQLite messages 表按 session_id 全量保存 |
| 长期记忆(MEMORY.md / USER.md) | ✅ 可恢复 | 文件持久 + profile 级共享,启动时重载快照 |
| todo 任务清单(内存态) | ⚠️ 有条件恢复 | 从恢复的历史里重放最近一条 todo 工具响应 |
| 压缩链的延续 | ✅ 自动重定向 | 拿恢复会话的 id 找到持有消息的子孙会话 |
| 进行中的工具执行 / 半成品输出 | ❌ 不恢复 | 未逐轮落库的执行态,进程退出即丢 |
| system prompt 冻结快照 | 🔄 重建 | 每会话启动按当前 profile 重新构建 |
数据源:SQLite messages 表。 session_id 列就是隔离/恢复的键;active 标记(默认 1)承载 /undo 软删——软删的行置 0,恢复时默认不读;tool_calls/tool_call_id 完整保存,所以恢复后 assistant→tool 的调用序列依然合法。
为什么能还原:逐轮落库。 Hermes 每轮会话数据都刷新到 SQLite 落盘(_last_flushed_db_idx + _flushed_db_message_ids 去重);正常退出 / 压缩 / /new / /resume 前都先落库再结束会话,所以数据库始终是最新权威副本。唯一可能丢的窗口:进程被强杀在"落库"之前——最后几回合的内存态丢失;正常 /quit 会先落库,不丢。
恢复核心:get_messages_as_conversation。 它读回指定会话的活跃(!)消息,include_ancestors=True 时沿 parent_session_id 把全部父会话也一起读回(全量、按时间序、只读活跃行)。
恢复后三类状态怎么重建:
- 对话上下文:全量从库里读回,直接当
conversation_history用; - todo 清单:先建一个空清单,下一轮从历史里把最近一条 todo 写回(所以"从未进历史的 todo 记录"重启后会消失——设计如此,不保证逐条);
- memory 快照:按当前 profile 重建(所以若 profile/模型配置变了,恢复后"人格/工具集"可能和之前不同——这不是 Bug,是按现配置重建的取舍)。
为什么不是 100% 原样恢复(边界): 强杀丢最后几句 / todo 只回放最近一条 / system prompt 按现配置重建而非保留旧快照 / 压缩产生的空头父会话会自动重定向到持有消息的子孙(resolve_resume_session_id)/ /undo 回滚的行恢复时不读(“回滚回合不复活”)。
三种触发方式:
hermes --resume <id>/-r:启动时按会话 id 读回历史填进上下文;hermes --continue:先解析最近会话,再转成--resume <id>;- 会话内
/resume <id>(中途切换):先结束当前会话(“resumed_other”)→ 解析目标会话(对压缩链做重定向)→ 读回目标历史 → 重开会话 → 重建 todo、重建 system prompt,下一轮就在恢复的上下文里继续。
小结:会话恢复 = “SQLite 时间线上读回消息 + 之后空 todo 重建 + memory/system 按现配置重建"三件套,不是全量重放一份完整快照,所以能保证"对话历史完整可回放”,但对 todo/system prompt 这类"派生状态"是重建而非原样。
5.5 多会话的隔离与共享
Hermes 的"多会话"要从两个维度叠加着看,这是最核心的一条认知:
- 一个维度是文件级作用域(通过
HERMES_HOME/ profile / 独立进程)——决定"谁和谁是物理隔离的一套世界",管的是谁能看到谁; - 一个维度是数据库记录级时间线(通过
session_id/parent_session_id)——决定"一条历史线怎么接着干/怎么分叉",管的是谁能从哪恢复。
一句话:Hermes 多会话 = 一套套隔离的"世界"(文件级) + 世界里一根根可串可分的"时间线"(数据库级)。前者管隔离,后者管历史,两者互相独立、可以叠加。
隔离与共享可拆成五个正交维度:
-
文件级物理墙(Profile /
HERMES_HOME) —— 所有持久状态都在~/.hermes下:改HERMES_HOME就等于换一个世界,config、skills、memory、state.db、会话列表各自独立,互相完全看不见;profiles/<name>/就是这种独立世界(本报告即跑在docs_reviewprofile 下)。唯一例外是内存里的 todo——它本来就按会话走,与 profile 无关。 -
进程级并发(同一世界多进程并存) —— 同一 profile 可以同时开多个 hermes 进程(多个终端、或
-wworktree 并行)。它们不是各留一份副本,而是同一份持久化文件上加锁:SQLite WAL +compression_locks(保证两个进程不会同时压缩同一个会话)、memory 文件用文件锁 + 写前重读(发现被姐妹进程改过就拒绝覆盖、另存备份)。净效果:多会话共享同一份 memory(看到的记忆一致),但各自的进行中 todo 互不干扰。 -
路由与缓存(单进程里怎么认人、怎么续上) —— 常驻 Gateway 一个进程服务多个会话:每条消息先映射成
session_key(解析出平台/聊天/线程),内存里用 LRU 的 Agent 缓存(_agent_cache)把"会话→Agent 实例"对应起来——命中就复用内存里的实例(保留 todo、memory 快照、循环状态),未命中或配置变了就新建;缓存条目带配置签名,配置/模型一变就失效重建,所以能随时热切模型。同一会话内串行处理(保证角色/状态不乱序),不同会话之间才真正并行。 -
时间线链(parent_session) ——
sessions表带parent_session_id,承载两种关系:压缩(新会话是旧会话的延续,旧的没删、沿 parent 链可拼回完整历史)和分支(/branch,新会话从旧会话岔出、独立演进)。/resume恢复正是沿这条时间线+todo 重放+memory 快照 重建,不是全量重放。 -
子任务会话(delegate_task / worktree) ——
delegate_task给子代理造一个隔离上下文 + 独立终端,只回传最终摘要(同步、可并行、可后台);共享 profile 级 memory,但进行中的 todo / 中间结果不共享,父会话被中断则子过程被取消。hermes -w则另走代码维度:每个会话一个独立 git worktree + 新分支,多个 agent 并行改同一仓库不冲突。
5.6 长期记忆的"冻结快照"机制
Hermes 的长期记忆(MEMORY.md / USER.md 两个纯文本文件,在 agent/memory_manager.py 之上由 tools/memory_tool.py 维护)不是简单的"每次请求都重新读文件贴进提示词",而是一套会话内固定、写实时另走一套的设计,业内就叫"冻结快照"(frozen snapshot):
一句话:冻结快照 = 会话启动、记忆加载时从 MEMORY.md 和 USER.md 各读一次、做一次严格注入清洗后固定下来的记忆渲染块。这块内容只进系统提示词,整场会话逐字节不变;会话中途的记忆增删走另一套"实时状态"并落盘,不触碰这份快照。
拆开有三大要点:
-
“冻结”= 会话内逐字节不变,为的是稳定前缀缓存(kv-cache)
Hermes 只在记忆加载时(session 启动)对 MEMORY.md / USER.md 各构造一次这块快照,之后整个会话即使改记忆,系统提示词里的记忆段落也保持逐字节一致。这样每轮请求的系统提示词前缀都完全相同,能命中模型侧的 prefix/kv-cache,避免每次请求都重复计算这段;这就是"冻结"背后的缓存友好动机,不是随便起的名字。会话内真正的增删改走的是另一套 live 状态并写回磁盘,要到下一场、重新加载时才进快照。 -
冻结快照 ≠ 实时状态,两者是分离的
- 冻结快照:进系统提示词的那份记忆渲染块,会话内固定;
- 实时状态(memory_entries / user_entries):由 memory 工具的增删改驱动、并落盘;工具响应(比如
memory(action=read))反映的是实时状态,不是快照。
也就是说,你会在会话中给 Hermes 加了条记忆,它写进了磁盘、也影响了后续工具读到的内容,但系统提示词里那份"冻结"的记忆块直到下个会话才会更新——这是刻意的取舍:保提示词前缀稳定,而不是实时刷进 system prompt。
-
构造快照时做一次性"严格"注入清洗
每条记忆在进入快照前都会做与"写记忆"同级(严格档)的提示词注入/投毒检测:- 命中威胁模式的条目,在快照里被替换成一个
[BLOCKED: ...]占位符,不进系统提示词; - 但实时状态仍保留原文 → 用户还能用
memory(action=read)看到被污染的条目、并memory(action=remove)删掉——故意不静默丢弃,免得把攻击藏起来让用户看不见。
动机:防止磁盘上被投毒(供应链/被攻破的工具/姊妹会话写入)的记忆文件把恶意指令注入系统提示词。因为清洗是从磁盘字节确定性推导的,所以快照在整场会话内保持稳定,与前两条相互支撑。
- 命中威胁模式的条目,在快照里被替换成一个
适用面提示:冻结快照属长期记忆在系统提示词里的呈现,和前面 §5.3 讲到的"对话历史/上下文压缩(会话轮转 + lineage)"是两条不同的子系统,不要混为一谈。干净状态下记忆不受影响;只有当 memory 文件可能被污染时,[BLOCKED] 才会出现——正常情况下用户看到的记忆内容与磁盘一致。
5.7 强项
- 中文全文检索(trigram);压缩会话轮转 + lineage 保留完整历史(可回放/可导出);压缩锁;会话生命周期状态机 + auto reset;WAL 降级 + 自修复;token/cost 记账完整;软删除 rewind(active=0 回退);长期记忆"冻结快照"机制(前缀缓存友好 + 注入清洗安全闸)。
6. 五家横向对比表
| 维度 | DSH | Codex | OpenCode | Claude Code | Hermes |
|---|---|---|---|---|---|
| 存储范式 | 事件日志(JSONL+zstd) | JSONL+SQLite 投影 | SQLite+事件溯源 | JSONL(闭源) | SQLite 关系型 |
| 压缩是否动存储 | 否(投影替换) | 否(检查点 append) | 否(摘要追加) | 闭源(插摘要) | 会话轮转+lineage,旧会话存留 |
| 完整历史可回放 | ✅ | ✅ | ✅ | (闭源) | ✅(lineage 读祖先) |
| 数据面/模型面解耦 | ✅ SurfaceManager | ✅ thread_history 投影 | ✅ projector | (闭源) | ❌ 存储即上下文 |
| tool 对不拆分守卫 | ✅ | 压缩时折叠 | ✅(投影裁剪,存储完整) | (闭源) | ✅ |
| 跨会话引用注入 | ✅ session-reference+spill | ✅ fork 血缘 | ✅ fork | (闭源) | ⚠️ 仅 session_search 文本 |
| 自动溢出压缩+重试 | ✅ | ✅(含异步预压缩) | ✅(渐进收缩) | ✅(autocompact) | ⚠️ API 错误驱动 |
| 精确 token 计量 | ✅ tokenMeter | ✅ usage | ⚠️ 4char/token | (闭源) | ⚠️ 4char/token 粗估 |
| 长期记忆 | ⚠️ 事件化 | ✅ 两阶段记忆库 | ❌(摘要+指令) | CLAUDE.md | ✅ MEMORY.md |
| 中文全文检索 | unicode61 | ❌(rg/LIKE) | ❌(title LIKE) | (闭源) | ✅ trigram |
| 并行并发一致 | flock 单写者 | 文件锁+租约 | claim/suspend | GC+flock | 压缩锁 |
| 会话回退 rewind | ❌ | 追加 truncation | revert 快照 | /rewind | ✅ active=0 |
7. Hermes 可优化点(与四家对标后)
基于四家对标,Hermes 的最优优化点。先说明一条被证伪的候选:早先认为"Hermes 压缩丢失完整历史、需要补投影替换式保留"——经源码核实不成立(见 §5.3):Hermes 自动压缩走会话轮转 + lineage,旧会话原始消息原样保留、可沿谱系完整回放/导出。真正与 DSH/Codex/OpenCode 的差距在于没有独立"数据面/模型面"投影层(读取时需整条 lineage 全量读回,再靠内存去重),而非丢失历史。据此重新排序:
P0(高价值,中低成本)
-
分层的会话恢复(投影渲染,而非 lineage 全量平铺)
- 现状:
get_messages_as_conversation读回目标会话全部 active 消息,include_ancestors=True时把整条 lineage 全量读回再内存去重;长会话/多代压缩后一次性拉全量,上下文成本高。 - 建议:对齐 DSH SurfaceManager / Codex thread_history 投影,恢复/组装上下文时按"最近 N 条原文 + 前置代压缩摘要"分层渲染,而非全量平铺——完整原始历史仍在 DB 可随时回看,只是不进每次模型上下文的默认渲染。
- 收益:长任务继承上下文时省 token 明显;原始历史保留,需回看细节时再按需取;不牺牲可回放/可审计(历史本就在 lineage 上)。
- 现状:
-
引入"跨会话引用 + 受预算投影注入"(对标 DSH session-reference / Codex fork)
- 现状:session_search 只能把旧会话读成文本回显,无法像 DSH 那样做受预算裁剪的投影注入,超限落盘 + SpillRef;且沿 lineage 全量重放是主要 token 开销。
- 落点:在
session_search之上做"引用解析 + 预算裁剪 + spill"层。 - 收益:长任务拆分会话后,子会话可引用父/兄弟会话结论,不用全量重放(含祖先代),大幅省 token。
P1(中价值,中成本)
-
精确 token 计量(对标 DSH tokenMeter)
- 现状:4 chars/token 粗估 + provider usage 回填;对离线 Qwen3.5 建议用对应 tokenizer 精确计量,减少误压缩/漏压缩。
-
自动溢出恢复 + 重试(对标 Codex 异步预压缩 / OpenCode precompute)
- 现状:Hermes 有 API 错误驱动压缩,但无"溢出即自动压缩 + 自动重试"闭环,更无后台预压缩。
- 建议:provider 返回 CONTEXT_EXCEEDED 时自动压缩该请求并重试(带重试上限);可选做 turn 间后台预压缩保 prompt cache。
P2(加分项)
- 任务状态事件化(goal/plan/todo)
- 现状:todo 是独立状态;建议如需"跨会话任务履历 + 崩溃回放",把 todo/plan 里程碑写成 session 事件,与会话共享一致性(对齐 Hermes 自身 kanban 体系)。
8. 给团队的落地建议
结合你们(NLP/AI 团队,Hermes + Claude Code + OpenClaw,内网部署、Qwen3.5-122B-A10B、FastAPI + Milvus + ES):
- 最优先做"分层恢复/投影渲染"而非"补完整历史":Hermes 自动压缩本就走会话轮转 + lineage,完整历史已保留、可回放可导出(见 §5.3)。真正要补的是恢复/组装上下文时的分层投影渲染(最近 N 条原文 + 前置代摘要),避免长任务沿 lineage 全量平铺。你们有 Milvus(向量)+ ES(全文),可把完整历史做可检索层,上下文保持轻量——正好复用现有 RAG 栈。
- 其次是"跨会话引用注入":多 Agent(orchestrator + 专门 agent)最需要子任务共享上下文;DSH/Codex 的"预算裁剪投影 + spill"思路在 Hermes 的 session_search 之上加一层即可落地,省 token 明显。
- 精确 token 计量:对离线 Qwen3.5 用对应 tokenizer,避免长任务误压缩。
- 中文检索已是 Hermes 强项:trigram FTS5 适配中文,继续保留;可叠加 Milvus 向量召回 + ES 全文做 RRF 混合检索。
- 守卫不可丢:无论做分层投影渲染还是引用注入,保证 tool-call/result 对不拆分、user/assistant 交替合法(Hermes 已有守卫)。
附:关键源码文件索引
DeepSeek Harness(/netds2/userdata/447574/agent/deepseek-harness/)
packages/core/session/src/index.ts— Session/Surface 投影派生(446,860,925)packages/session/session-persistence-jsonl/src/*— JSONL+zstd 持久化packages/session/session-checkpoint-policy/— 副作用前强制 flushpackages/compaction/compaction*— 压缩引擎/区域/摘要packages/context/session-reference/、packages/spill/spill*/— 跨会话引用+溢出packages/session-query/session-query-sqlite/— FTS5 检索
Codex(/tmp/agent_src/codex/codex-main/codex-rs/)
rollout/src/recorder.rs、compression.rs— JSONL append-only 记录与 zstdcore/src/compact.rs、session/mod.rs::replace_compacted_history— 压缩检查点history/src/{lib,retained_context}.rs— CompactedItem / RetainedContextthread-store/src/local/、local/thread_history.rs— 会话管理 + 历史投影state/migrations/*.sql— threads/memories/projects 表memories/— 两阶段长期记忆
OpenCode(/tmp/agent_src/opencode/opencode-v2.1.4/)
packages/core/src/session/{compaction,history,projector,store,runner}.ts— 压缩/恢复/投影packages/core/src/database/、session/sql.ts— SQLite schemapackages/core/src/session/recent-text.ts— keepID 引用重建packages/core/src/instruction-discovery.ts— AGENTS.md 注入
Claude Code(/tmp/agent_src/claude/claude-code-v2.1.310/ — 文档/插件仓库)
plugins/ralph-wiggum/hooks/stop-hook.sh— transcript JSONL 格式证据mods/sec-default/README.md— compaction summary+boundary 事件examples/settings/settings-hipaa.json— cleanupPeriodDays 保留期CHANGELOG.md— /compact、/autocompact、–resume/–continue 证据
Hermes(/home/myuser/.hermes/hermes-agent/)
hermes_state.py— SessionDB:SQLite schema、WAL 降级、压缩锁、lineage、rewind、检索、恢复gateway/session.py— 生命周期状态机agent/context_compressor.py/conversation_compression.py— 压缩策略与"会话轮转+lineage"持久化(旧会话保留可回放)agent/memory_manager.py— 长期记忆tools/session_search_tool.py— 跨会话检索(文本,非投影注入)
更多推荐

所有评论(0)