Hermes Honcho 记忆为什么总落在别的项目里


你有三个仓库:一个后端 monorepo、一个前端、还有一个放了两年多的运维脚本目录。上周你在 monorepo 里把退款流程重写了一遍,今天在运维脚本目录里打开 Hermes,随口问了一句「上次那个退款重构最后是怎么收尾的」——它答得头头是道,细节全是只存在于另一个项目里的东西。反过来也有更离谱的:刚从远端 clone 下来、还没写过一行代码的新仓库,第一次对话它就像认识你很多年。你按项目分开跑 Hermes,本来就是想让每个仓库有自己的记忆,结果 Honcho 一直在别的地方找答案。

这不是提示词写坏了,是记忆的落点错了。PR #112602(2026-09-16 合并)修掉了两条把会话路由带偏的路径,这篇把症状、原因、正确的配置方式,以及怎么确认你的记忆到底进了哪个会话讲清楚。

先把两个词说清楚

Honcho 是 Hermes 的 memory provider 之一:它跨会话长期维护「你是谁」的用户档案(user representation、peer card),而不是把一切都塞进单次对话的上下文里。

sessionStrategy 决定 Honcho 的 session(可以理解成「记忆桶」)跟你的工作怎么对应。默认值是 per-directory,一共四档:

sessionStrategy 含义
per-session 每次 hermes 启动都是全新 session,记忆靠工具按需取用。文档推荐新用户从这里开始
per-directory 每个工作目录一个 session,上下文跨多次运行累积(默认值)
per-repo 每个 git 仓库一个 session
global 所有目录共用一个 session

顺带一句上下文注入:Honcho 在 hybrid / context 模式下每轮会往 system prompt 里拼两层上下文——基础层(会话摘要、用户档案、peer card、AI 自我表示)和 dialectic 补充层(针对当前状态的 LLM 推理)。contextCadence 默认 1、dialecticCadence 默认 2(文档建议 1–5),两层拼完后按 contextTokens 预算截断。记忆存取和上下文注入是两件事,混起来看会很难判断问题出在哪。

如果你不想把记忆交给托管服务,本地那套走 LanceDB 的路线可以看 Hermes 的语义记忆是怎么落地的,两者解决的是同一个问题。

症状:三个仓库,一份记忆

典型的表现有三种,而且都发生在同一个策略配置下:

  • 你在 A 仓库里问 B 仓库的事,Honcho 把 B 的细节捞了出来。
  • 新仓库第一次对话就带着旧项目的偏好和结论。
  • 策略明明写的是 per-repo,但 agent 的行为像是 global。

三条表现指向同一个事实:所有项目都落在了同一个 session 上。而那个 session 的名字不是任何一个仓库名。

原因一:自动生成的标题会顶掉你的策略

Hermes 会替你给会话自动起标题——来源是 derived 或 llm(Desktop 分支的 lineage 标题也属于这一类)。这些标题本意只是给你在会话列表里认人的显示信息。

旧逻辑里不是这样:只要一个会话有标题,标题就会参与 session 名的推导。于是一次「Refactor the billing module」的自动标题,把会话从仓库对应的 session 上拽走,改名成了 Refactor-the-billing-module。你配的 sessionStrategy 还在,只是没人听它的了。

原因二:解析用的是启动目录,不是工作区

第二条更隐蔽。旧代码里,session key 的解析走 os.getcwd()——那是后端进程的启动目录。在 Desktop 或 messaging gateway 下,这个进程往往从 $HOME 启动,于是不管你当时在哪个仓库里对话,Honcho 看到的工作目录永远是家目录,所有项目一起塌进一个叫 home 的桶。

这就是为什么很多人「明明在项目里」,记忆却是全局的。

PR #112602 改了什么

修复落在解析链路上,两条问题各改一处:

  • plugins/memory/honcho/client.py::_resolve_session_name_base — 标题来源是 derived 或 llm 时直接跳过标题分支,它只是显示信息;显式敲的 /title(来源 user)仍然覆盖策略;标题没有记录来源时保持旧行为。
  • plugins/memory/honcho/__init__.py::_resolve_session_key — 改成对 kwargs["cwd"] 或 resolve_agent_cwd() 解析(pinned session cwd → TERMINAL_CWD → 启动目录),也就是 system prompt 和 context file 发现一直在用的那套合同,不再用 os.getcwd()。
  • hermes_state_common.py — TITLE_SOURCE_* 常量放在 cycle-safe 的兄弟模块里,SessionDB 和插件读同一套词表,不会各自理解一套。
  • agent/agent_init.py — AIAgent(cwd=...) 落到 agent.session_cwd;_memory_provider_init_kwargs 把 session_title_source 和 cwd 一起传给 provider。
  • tui_gateway/* — eager-resume、branch、compute-host 和延迟构建(deferred build)路径会传 cwd_override:这些路径的会话记录还没进 _sessions,不传就会被 _set_session_context 清掉 cwd。延迟构建期间发生的工作区搬动也能传到构建完成的 agent 上。

优先级这件事值得记一下:messaging gateway 的会话无论策略和标题是什么,始终用稳定的 per-chat key;其他会话按 per-session 身份 → 手动目录映射 → 显式标题 → 配置的策略 的顺序取第一个命中。

合并时用的是真实环境端到端验证(真实 SessionDB、真实 HonchoMemoryProvider,进程 cwd 设成伪造的 $HOME,会话 cwd 绑到一个叫 myapp 的 git 仓库,策略 per-repo):

场景 修复前 修复后
没有标题 home myapp
LLM 标题 “Refactor the billing module” Refactor-the-billing-module myapp
显式 /title My Explicit Name My-Explicit-Name My-Explicit-Name

第三行是本次修复刻意保留的行为:用户自己起的标题永远是有意为之的覆盖。

把 sessionStrategy 配对

装上 provider 最省事的方式还是向导,它会问你 gateway 那条分支的问题,并把 key 写进配置:

hermes memory setup    # 在 provider 列表里选 "honcho"

手动配置的话,config.yaml 里至少要有这一段:

# ~/.hermes/config.yaml
memory:
  provider: honcho
echo 'HONCHO_API_KEY=***' >> ~/.hermes/.env

API key 在 honcho.dev 申请。sessionStrategy 这类设置写在 honcho provider 的设置区里——也就是 hermes memory setup 写出来的那一段;不想手写、怕拼错的,在向导的提示处按 [e] 直接进编辑器设 key。

怎么选那一档:

  • per-repo:一个 monorepo 加几个独立仓库,每个仓库要自己的记忆。上面那个场景就该用这个。
  • per-directory:一个目录里长期干活,子目录之间不需要隔离。这是默认值,也是大多数人的合适选择。
  • per-session:每次都要干净起步,记忆靠工具主动取。想做实验或者排查串味问题时,这一档最好用,因为它把「会话级记忆」和「用户级档案」分得最清楚。
  • global:你真的想所有项目共用一份档案时才用。

其余几个设置项(多跟 gateway 相关,一起列出来方便对照):

设置 默认值 作用
sessionStrategy per-directory 上面那四档;目录类策略和手动的 sessions 映射都用逻辑会话工作目录,而不是后端的启动目录
a2aSessions true 其他 bot 发来的 DM 写进它自己按发送方区分的 session,绝不写进人类的;false 则完全跳过 bot 发起的回合
pinUserPeer false 仅 gateway。true 时每个平台用户都塌成 peerName
userPeerAliases {} 仅 gateway。运行时 ID 到 peer 的映射({"7654321": "alice"}),多对一
runtimePeerPrefix "" 仅 gateway。没有 alias 命中时给未知运行时 ID 加命名空间,例如 telegram_7654321

gateway 侧的 peer 解析顺序是自上而下、第一个命中生效:pinUserPeer → userPeerAliases[id] → runtimePeerPrefix + id → 原始运行时 ID → peerName → session-key 兜底。如果你在 Telegram 或 Discord 里发现别人的记忆粘到自己身上,看的就是这张表。顺带提醒一句文档里的警告:把 pinUserPeer 从 true 翻成 false 不会迁移数据,peerName 下面的历史留在原地。相关的命令细节可以对照 hermes memory 命令参考。

怎么确认记忆落在了哪个会话里

修复之后,session 名是可推导的,所以你可以反推:

  1. 看名字的形状。 目录/仓库类策略下,session 名就是那个目录或仓库的名字(验证里是 myapp);没有工作区信息时是启动目录名(home);显式标题则会被转成短横线形式(My-Explicit-Name)。如果你看到的是一个标题形状的名字,比如 Refactor-the-billing-module,那就是标题接管了会话。
  2. 做交叉测试。 在仓库 A 里让 agent 落一条只属于 A 的结论(honcho_conclude 写的结论,或者 honcho_profile 里的用户卡片),再切到仓库 B 问同一件事。per-repo 下 B 不应该答得出来;答得出来,说明两边进的是同一个 session。
  3. 确认工作目录本身。 让 agent 报出它这一次运行的工作目录,跟你想的那个仓库比一下。解析链是会话上下文 → scoped 的 terminal.cwd → 启动目录,只要链条中间某一环给的是 $HOME,落点就会跑到 home 桶里。
  4. 用 per-session 做对照实验。 临时切到这一档,每个新会话都应该是空的。如果还有内容冒出来,那它来自用户级档案(user representation / peer card),不是会话级记忆——这是设计如此,不是串味。

修复前建的会话怎么办

文档把这条写得很直白:在记录标题来源之前建的会话保留旧行为。因为一条旧的自动标题和一条旧的用户标题在数据上分不出来,所以没有记录来源的标题会被当成显式覆盖。换句话说,那些老会话不会因为你升级就自动回到仓库对应的 session 上。

另外这次修复不包含一件事:把已经写进错误 session 的消息迁移回来。已经落错地方的历史留在原处。实际做法是——需要干净记忆的仓库,在正确的目录下开新会话,它会落到正确的 session 名上;显式 /title 仍然继续生效,因为那是有意为之的覆盖,本次修复不动它。

如果你习惯把结论沉淀成笔记,让 agent 在正确的项目目录里写 Obsidian 笔记 也是一种绕开串味的方式:文件系统本身就是按项目分层的。同一条记忆链路在定时任务里的表现,可以看 cron 与记忆延续。

小结

  • Honcho 的记忆有明确的落点,而这个落点由 sessionStrategy 加会话工作目录共同决定。
  • 两个 bug 分别破坏了一半:自动标题(derived / llm)不该覆盖策略,现在不会了;解析该用会话的工作区而不是进程的启动目录,现在也是了。显式 /title 依然是你手里的覆盖开关。
  • 多仓库场景用 per-repo,单目录长期劳动用 per-directory,要干净起步或排查串味用 per-session。
  • 排查时先看 session 名的形状,再做一次跨仓库的交叉提问。
  • 老会话和已经落错的历史不会自动搬家,新会话从正确的目录开始建就行。