乐于分享
好东西不私藏

Hermes 源码解读七:Session 与 SQLite,长期 Agent 的状态账本

Hermes 源码解读七:Session 与 SQLite,长期 Agent 的状态账本

全文约 1550 字,读完大约 3 分钟。

Hermes 源码解读七:Session 与 SQLite,长期 Agent 的状态账本

很多聊天机器人项目,第一版都会这样存历史:

一个 session 一个 JSON。

或者一个用户一个 JSONL。

简单、直观、够用。

但 Agent 跑久以后,历史不再只是“聊天记录”。

它还包括模型配置、tool calls、reasoning 信息、token 统计、成本估算、压缩后的 lineage、平台来源、恢复状态、搜索索引、重试痕迹。

这时候继续把它当聊天记录,就会很痛。

我看 Hermes 的 session 相关源码时,一个判断很明确:

Hermes 把会话状态当成账本,而不是聊天 transcript。

这篇主要看 hermes_state.pygateway/session.pydocs/session-lifecycle.mdsession-storage.md

为什么不是简单 JSONL

Hermes 现在把核心状态放在 ~/.hermes/state.db

里面至少有这些表和索引:

sessions
messages
messages_fts
messages_fts_trigram
state_meta
schema_version

这说明它已经从“保存聊天历史”升级成“保存运行时状态”。

sessions 不是只存 session_id。

它会记录:

  • • source;
  • • user_id;
  • • model;
  • • model_config;
  • • system_prompt;
  • • parent_session_id;
  • • started_at / ended_at;
  • • message_count;
  • • tool_call_count;
  • • input/output/cache/reasoning tokens;
  • • billing 信息;
  • • title;
  • • api_call_count。

messages 也不是只存 role 和 content。

它还会存 tool_call_id、tool_calls、tool_name、finish_reason、reasoning、Codex Responses message items 等。

这就不是聊天记录了。

这是 Agent 运行账本。

WAL、FTS5、trigram:SQLite 不是随便选的

Hermes 用 SQLite,并不是因为“轻量”两个字。

文档里列了几个明确设计:

设计解决的问题
WAL modeGateway、CLI、worktree agent 多进程共享时,读写更稳
FTS5在所有 session message 里做全文搜索
trigram tokenizer支持 CJK 和 substring 搜索
parent_session_id压缩、分支、恢复时保留 lineage
source tagging按 CLI、Telegram、Discord 等来源筛选
schema_version后续迁移可控

这些都是长期系统会遇到的问题。

尤其是 FTS。

Agent 的记忆不只是 memory 文件。很多时候你要找的是:

“上周哪个 session 里提过这个错误?”

“之前哪个工具调用失败过?”

“某个 Slack 群里的讨论是否已经处理过?”

如果历史只是散落的 JSONL,搜索和治理都会变得很差。

SessionStore 管的是会话入口,SessionDB 管的是账本

Gateway 里还有一层 SessionStore

它维护 sessions.json,主要保存 session_key 到 session_id 的映射,以及一些运行状态 flag。

这层和 SQLite 不是重复,而是职责不同。

主要职责
SessionStore当前 Gateway 会话映射、恢复状态、过期策略、reset/suspend/resume
SessionDB会话元数据、消息明细、搜索、成本、lineage、持久化账本

可以把 SessionStore 理解成“当前在线路由表”,把 SessionDB 理解成“长期账本”。

这两个职责如果混在一起,也会出问题。

Gateway 需要快速判断某个平台来的消息该进哪个 session。

而历史检索、成本统计、session lineage、transcript rewrite 又是另一类操作。

拆开以后,运行中路由和长期存储都更清楚。

session 生命周期不是只有 create 和 delete

docs/session-lifecycle.md 里有一个很值得看的状态机。

一个 session entry 有很多 flag:

flag语义
was_auto_reset因 idle/daily 策略自动 reset
is_fresh_reset用户显式 /new/reset
suspended硬重置,下次强制新 session
resume_pending软恢复,重启后保留同一个 session_id
expiry_finalized过期清理已经执行
reset_had_activityreset 前是否真的有对话

这说明 Hermes 把 session 当成运行中对象,而不是一条数据库记录。

尤其是 suspendedresume_pending 的区别很重要:

suspended
  → 硬切断,下一次新 session

resume_pending
  → 软恢复,下一次继续原 session

这就是长运行系统里的恢复语义。

进程重启、Gateway drain、用户 /stop,这些动作看起来都像“中断”,但它们对下一次会话的含义完全不同。

写入竞争也被当成生产问题处理

SQLite 很容易被误解为“单机小玩具”。

Hermes 的 SessionDB 里专门处理了写入竞争:

  • • SQLite timeout 设置较短;
  • • 应用层 retry;
  • • retry 加随机 jitter;
  • BEGIN IMMEDIATE
  • • 每 50 次写入做 WAL checkpoint。

这解决的是 gateway + CLI + 多 worktree agent 共享一个 state.db 时的竞争问题。

很多人遇到 SQLite lock,会直接把 timeout 调大。

但 timeout 太大可能让所有写入排队卡住。

Hermes 选择短 timeout + 应用层随机退避,更像服务端系统里的竞争控制。

这也是源码里很后端的一面。

这套状态设计给我的启发

如果你自己做 Agent,不一定一开始就上完整 SQLite schema。

但建议早一点把“聊天记录”和“运行账本”分开想。

最少要问:

问题为什么重要
这条消息来自哪个平台和用户?影响权限、恢复和上下文隔离
这轮用了哪个 model/provider?影响复现、成本和问题排查
哪些 tool call 被执行?影响审计和回放
token 和成本是多少?长期运行必须算账
session 是否压缩或分支?影响历史连续性
历史能不能搜索?影响记忆和问题定位
中断后是恢复还是新会话?影响用户体验和安全

我的判断是:

Agent 的 session 不是“聊天窗口”,而是一条可恢复、可审计、可搜索、可计费的运行轨迹。

Hermes 用 SQLite 承接这件事,是一个务实选择。

它没有一上来引入复杂数据库,也没有停留在散文件。

对个人和小团队 Agent 平台来说,这个取舍很有参考价值。

有了核心运行时、工具、Provider、Gateway 和 Session 之后,下一个问题就很自然了:

怎么让外部扩展改变系统行为,同时不把核心写成分支地狱。

如果你对 Agent 工程化感兴趣,可以继续收藏这个系列。越往后,越接近真实系统里最难维护的部分。