分析对象: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(存储不动)+ 活动上下文投影替换存储完整 + 检索投影分层
OpenCodeSQLite + 事件溯源(bus→投影两表)从最新压缩点投影读回摘要 checkpoint(存储追加)+ 投影替换事件 + 关系型混合,压缩可回滚
Claude CodeJSONL 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-only log: 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)用 POSIX flock(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 更灵活
2tool-call/result 对不拆分(toolPairingBalancedBefore/After)压缩边界绝不在"assistant 发起的工具调用"和"工具返回结果"之间切断,否则语义断裂Hermes 已守此条(压缩守卫),一致
3system/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 校准,已覆盖;可明确加"校验摘要比原段小"一步
6KV-cache 友好的摘要调用摘要调用把 system/tools/messages 原样回放、只在末尾追加压缩指令,是已路由请求的真前缀,复用 warm prefix cache,摘要几乎不额外算Hermes 未刻意这么做;对离线 Qwen3.5 这是实打实的推理成本节省,建议借鉴
7surfaceOp 标记驱动投影压缩不物理删,而是写 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.rs create_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);会裁最旧消息;有 PreCompact hook(可注入"需保留的关键信息");插件可 session.compact 接管压缩。
  • token 预算:支持 1M 上下文模型([1m] 别名、CLAUDE_CODE_DISABLE_1M_CONTEXT);autocompact 窗口即 token 预算;/cost、--max-budget-usd;5 小时用量限制。
  • 生命周期:SessionStart/SessionEnd/Stop/SubagentStop/PreCompact hooks;状态文件 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 + WAL
  • gateway/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 回滚的行恢复时不读(“回滚回合不复活”)。

三种触发方式:

  1. hermes --resume <id> / -r:启动时按会话 id 读回历史填进上下文;
  2. hermes --continue:先解析最近会话,再转成 --resume <id>;
  3. 会话内 /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 多会话 = 一套套隔离的"世界"(文件级) + 世界里一根根可串可分的"时间线"(数据库级)。前者管隔离,后者管历史,两者互相独立、可以叠加。

隔离与共享可拆成五个正交维度:

  1. 文件级物理墙(Profile / HERMES_HOME) —— 所有持久状态都在 ~/.hermes 下:改 HERMES_HOME 就等于换一个世界,config、skills、memory、state.db、会话列表各自独立,互相完全看不见;profiles/<name>/ 就是这种独立世界(本报告即跑在 docs_review profile 下)。唯一例外是内存里的 todo——它本来就按会话走,与 profile 无关。

  2. 进程级并发(同一世界多进程并存) —— 同一 profile 可以同时开多个 hermes 进程(多个终端、或 -w worktree 并行)。它们不是各留一份副本,而是同一份持久化文件上加锁:SQLite WAL + compression_locks(保证两个进程不会同时压缩同一个会话)、memory 文件用文件锁 + 写前重读(发现被姐妹进程改过就拒绝覆盖、另存备份)。净效果:多会话共享同一份 memory(看到的记忆一致),但各自的进行中 todo 互不干扰。

  3. 路由与缓存(单进程里怎么认人、怎么续上) —— 常驻 Gateway 一个进程服务多个会话:每条消息先映射成 session_key(解析出平台/聊天/线程),内存里用 LRU 的 Agent 缓存(_agent_cache)把"会话→Agent 实例"对应起来——命中就复用内存里的实例(保留 todo、memory 快照、循环状态),未命中或配置变了就新建;缓存条目带配置签名,配置/模型一变就失效重建,所以能随时热切模型。同一会话内串行处理(保证角色/状态不乱序),不同会话之间才真正并行。

  4. 时间线链(parent_session) —— sessions 表带 parent_session_id,承载两种关系:压缩(新会话是旧会话的延续,旧的没删、沿 parent 链可拼回完整历史)和分支(/branch,新会话从旧会话岔出、独立演进)。/resume 恢复正是沿这条时间线+todo 重放+memory 快照 重建,不是全量重放。

  5. 子任务会话(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 各读一次、做一次严格注入清洗后固定下来的记忆渲染块。这块内容只进系统提示词,整场会话逐字节不变;会话中途的记忆增删走另一套"实时状态"并落盘,不触碰这份快照。

拆开有三大要点:

  1. “冻结”= 会话内逐字节不变,为的是稳定前缀缓存(kv-cache)
    Hermes 只在记忆加载时(session 启动)对 MEMORY.md / USER.md 各构造一次这块快照,之后整个会话即使改记忆,系统提示词里的记忆段落也保持逐字节一致。这样每轮请求的系统提示词前缀都完全相同,能命中模型侧的 prefix/kv-cache,避免每次请求都重复计算这段;这就是"冻结"背后的缓存友好动机,不是随便起的名字。会话内真正的增删改走的是另一套 live 状态并写回磁盘,要到下一场、重新加载时才进快照。

  2. 冻结快照 ≠ 实时状态,两者是分离的

    • 冻结快照:进系统提示词的那份记忆渲染块,会话内固定;
    • 实时状态(memory_entries / user_entries):由 memory 工具的增删改驱动、并落盘;工具响应(比如 memory(action=read))反映的是实时状态,不是快照。
      也就是说,你会在会话中给 Hermes 加了条记忆,它写进了磁盘、也影响了后续工具读到的内容,但系统提示词里那份"冻结"的记忆块直到下个会话才会更新——这是刻意的取舍:保提示词前缀稳定,而不是实时刷进 system prompt。
  3. 构造快照时做一次性"严格"注入清洗
    每条记忆在进入快照前都会做与"写记忆"同级(严格档)的提示词注入/投毒检测:

    • 命中威胁模式的条目,在快照里被替换成一个 [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. 五家横向对比表

维度DSHCodexOpenCodeClaude CodeHermes
存储范式事件日志(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/suspendGC+flock压缩锁
会话回退 rewind❌追加 truncationrevert 快照/rewind✅ active=0

7. Hermes 可优化点(与四家对标后)

基于四家对标,Hermes 的最优优化点。先说明一条被证伪的候选:早先认为"Hermes 压缩丢失完整历史、需要补投影替换式保留"——经源码核实不成立(见 §5.3):Hermes 自动压缩走会话轮转 + lineage,旧会话原始消息原样保留、可沿谱系完整回放/导出。真正与 DSH/Codex/OpenCode 的差距在于没有独立"数据面/模型面"投影层(读取时需整条 lineage 全量读回,再靠内存去重),而非丢失历史。据此重新排序:

P0(高价值,中低成本)

  1. 分层的会话恢复(投影渲染,而非 lineage 全量平铺)

    • 现状:get_messages_as_conversation 读回目标会话全部 active 消息,include_ancestors=True 时把整条 lineage 全量读回再内存去重;长会话/多代压缩后一次性拉全量,上下文成本高。
    • 建议:对齐 DSH SurfaceManager / Codex thread_history 投影,恢复/组装上下文时按"最近 N 条原文 + 前置代压缩摘要"分层渲染,而非全量平铺——完整原始历史仍在 DB 可随时回看,只是不进每次模型上下文的默认渲染。
    • 收益:长任务继承上下文时省 token 明显;原始历史保留,需回看细节时再按需取;不牺牲可回放/可审计(历史本就在 lineage 上)。
  2. 引入"跨会话引用 + 受预算投影注入"(对标 DSH session-reference / Codex fork)

    • 现状:session_search 只能把旧会话读成文本回显,无法像 DSH 那样做受预算裁剪的投影注入,超限落盘 + SpillRef;且沿 lineage 全量重放是主要 token 开销。
    • 落点:在 session_search 之上做"引用解析 + 预算裁剪 + spill"层。
    • 收益:长任务拆分会话后,子会话可引用父/兄弟会话结论,不用全量重放(含祖先代),大幅省 token。

P1(中价值,中成本)

  1. 精确 token 计量(对标 DSH tokenMeter)

    • 现状:4 chars/token 粗估 + provider usage 回填;对离线 Qwen3.5 建议用对应 tokenizer 精确计量,减少误压缩/漏压缩。
  2. 自动溢出恢复 + 重试(对标 Codex 异步预压缩 / OpenCode precompute)

    • 现状:Hermes 有 API 错误驱动压缩,但无"溢出即自动压缩 + 自动重试"闭环,更无后台预压缩。
    • 建议:provider 返回 CONTEXT_EXCEEDED 时自动压缩该请求并重试(带重试上限);可选做 turn 间后台预压缩保 prompt cache。

P2(加分项)

  1. 任务状态事件化(goal/plan/todo)
    • 现状:todo 是独立状态;建议如需"跨会话任务履历 + 崩溃回放",把 todo/plan 里程碑写成 session 事件,与会话共享一致性(对齐 Hermes 自身 kanban 体系)。

8. 给团队的落地建议

结合你们(NLP/AI 团队,Hermes + Claude Code + OpenClaw,内网部署、Qwen3.5-122B-A10B、FastAPI + Milvus + ES):

  1. 最优先做"分层恢复/投影渲染"而非"补完整历史":Hermes 自动压缩本就走会话轮转 + lineage,完整历史已保留、可回放可导出(见 §5.3)。真正要补的是恢复/组装上下文时的分层投影渲染(最近 N 条原文 + 前置代摘要),避免长任务沿 lineage 全量平铺。你们有 Milvus(向量)+ ES(全文),可把完整历史做可检索层,上下文保持轻量——正好复用现有 RAG 栈。
  2. 其次是"跨会话引用注入":多 Agent(orchestrator + 专门 agent)最需要子任务共享上下文;DSH/Codex 的"预算裁剪投影 + spill"思路在 Hermes 的 session_search 之上加一层即可落地,省 token 明显。
  3. 精确 token 计量:对离线 Qwen3.5 用对应 tokenizer,避免长任务误压缩。
  4. 中文检索已是 Hermes 强项:trigram FTS5 适配中文,继续保留;可叠加 Milvus 向量召回 + ES 全文做 RRF 混合检索。
  5. 守卫不可丢:无论做分层投影渲染还是引用注入,保证 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/ — 副作用前强制 flush
  • packages/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 记录与 zstd
  • core/src/compact.rs、session/mod.rs::replace_compacted_history — 压缩检查点
  • history/src/{lib,retained_context}.rs — CompactedItem / RetainedContext
  • thread-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 schema
  • packages/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 — 跨会话检索(文本,非投影注入)
Logo

更多推荐