ARTICLE · 1116719
OpenClaw 2.0 | 02 跨会话记忆:你一问起以前的事,它先派个子 agent 去翻别的聊天记录
「OpenClaw 2.0」系列第二篇。上一篇讲引擎收回了自己手里,这一篇讲记忆:你在一个会话里说过的事,换个会话,agent 凭什么还想得起来。
比方说,你昨天在 Telegram 上告诉它"下周二我去上海",今天在网页端新开了一个 thread,问它"我下周有什么安排"。这是两个会话,互相看不见。它要答得出来,得有东西把昨天那段话带过来。

OpenClaw 的办法是三步:把旧会话的内容留下来,需要的时候搜出来,回复之前想起来。先把"会话"本身讲清楚,不然后面全是黑话。
先分清两个名字 -- sessionKey 和 sessionId
"会话"就是一份独立的聊天记录,带着它自己的上下文。OpenClaw 给每个会话起了两个名字,混着用最容易晕。
sessionKey 是地址。官方文档的说法是,它标识你在哪一个对话桶里,管路由和隔离。同一个来源的消息,总是落到同一个 key 上,比如你所有的私聊,默认都是 agent:main:main。
sessionId 是转录的编号。每个 key 指向一个当前的 sessionId,也就是存在 SQLite 里的那份转录的身份,消息一条条追加在它下面。
两者的关系只有一条:一个 key 同一时刻,只指向一个 id。
打个比方,key 是门牌号,id 是门后那本笔记本的编号。

那 id 什么时候会换?新会话一定有新 id:会话还不存在,就新建一个,id 随机生成。但换 id 不一定是新会话。最常用的 /new、/reset,对一个已经存在的会话,key 不变,id 也不变,官方文档的写法是 Gateway 重置"records a reset boundary in an existing persisted session and keeps its sessionId",在已有会话里记一个重置边界,保留 sessionId。例外是 ACP 会话,它们的重置会换本地 id;分叉 fork 则是分出一个子会话,父会话不动。

一个 agent 能开很多会话 -- 会话是怎么来的
能。 一个 agent 名下可以同时有很多会话,每一个都有自己的 key、自己的 id、自己的转录,互不串。
会话从两条路来。一条是消息从一个新来源进来:一个私聊、一个群、一次定时任务、一个 webhook,key 由来源决定。另一条是你在控制台里点"新建 thread":gateway 的 sessions.create 会生成一个 agent:<id>:dashboard:<uuid> 形状的 key,uuid 是随机的。无痕 thread 的 key 里带 incognito-,会话只放在内存里,Gateway 重启就没了。
控制台侧栏把它们分了类。Home 是主会话,群聊在 Groups,从主会话分出去的在 Threads,编码和命令行的会话在 Coding。

跨会话记忆的全景 -- 留下来,搜出来,想起来
先看整张图。上排是四种写入:每条消息追加进转录;重置时 session-memory 钩子写一份摘录;压缩前模型把值得记的写进日记;转录还可以建索引,不过默认不开。中间是三个存放处:转录、memory/ 下的笔记文件、记忆索引。下排是三种读取:sessions_search 逐字搜原文,memory_search 搜笔记,Active Memory 在回复前自动召回。

有一条事实决定了整个设计,文档原话:日记、导入的转录和会话转录,"remain available through explicit memory tools ... but are never injected automatically"。它们不会自动进提示词,要 agent 主动调工具去取。 三条读取路径里,只有 Active Memory 是自动的。
留下来 -- 旧会话的东西怎么写进记忆
重置时。/new、/reset,或者自动重置之后,session-memory 钩子往工作区的 memory/ 写一个文件,文件名带日期和时分,YYYY-MM-DD-HHMM.md。写的不是模型摘要,是过滤后的最近 15 条 user 和 assistant 原文,斜杠命令、工具消息都滤掉。文档的说法是 "a filtered excerpt, not a complete transcript or a model-written summary"。它默认不调模型,只有打开 llmSlug 才会调模型起个文件名。onboarding 时这个钩子默认是勾上的,实际开没开,要看你的配置。
压缩前。 对话长到快要压缩的时候,系统先让模型静默跑一回合,提示词开头是 "Pre-compaction memory flush.",要求把值得长期记的内容只追加到当天的 memory/YYYY-MM-DD.md。MEMORY.md、DREAMS.md、SOUL.md、AGENTS.md 这一回合是只读的。触发的线是压缩阈值减去 4000 token,或者转录超过 2MiB,每个压缩周期只跑一次。默认开启,无痕会话、心跳、CLI 运行时、沙箱不可写时跳过。这一步是模型在写,不是规则。
转录入索引。 第三条是把转录本身建进记忆索引,默认不开。要么打开 experimental.sessionMemory 并把 sessions 加进 sources,要么打开 rememberAcrossConversations,它会自动把 sessions 加进来。转录更新后防抖 5 秒触发后台同步,切成约 400 token 的块,每条消息渲染成 User: …、Assistant: … 的行,先脱敏;有向量 provider 才做 embedding,没有就只存文本做全文检索。dreaming、cron、心跳会话和被 memory forget 清掉的会话不进索引。文档承认:"Session indexing is opt-in and runs asynchronously. Results can be slightly stale."
上面第一条写出来的笔记,也会被记忆索引收进去,所以同一段对话可能从 memory 和 sessions 两处都搜到。

想起来 -- 回复之前,它先派个子 agent 去翻
前面几条,都得 agent 自己去调用。自动的是 Active Memory:你每发来一条消息,在提示词拼好之前,它先跑一遍。
先过资格检查:得是用户消息,不是会话间的消息,不是 harness 会话,没被 /active-memory off 暂停。headless、心跳、子 agent 都不跑。
然后是车道 1,确定性检索,不调模型。只搜笔记,只收晋升过的可信条目,得分要到 0.65,最多注入 3 条、1800 字符。有强命中,就直接注入,到此为止。
没有强命中,才轮到车道 2。默认的 escalate 模式下,只有消息像在问过去的事,才会升级。文档原话:"The default escalate mode runs its blocking recall sub-agent only when the message asks about the past and the deterministic memory lane found no strong trusted trigger match." 判断"像在问过去的事"靠的是正则,不是模型,覆盖英文、中文、日文、韩文,还带"将来词排除",免得"我明天去上海"被当成回忆。
升级之后启动的是一个阻塞式的记忆子 agent。它只能调 memory_search 和 memory_get,提示词开头是 "You are a memory search agent.",接着是 "Do not answer the user directly."。它的输出只有两种,NONE,或者不超过 220 字的一行摘要。这行摘要包在 <active_memory_plugin> 里,拼在你的消息前面,不是系统消息,主 agent 才开始回复。
rememberAcrossConversations 在这条流程里多出来的,就是让子 agent 的搜索范围扩到同一个 agent 的其他私聊的转录。默认值有条件:session.dmScope 没设或者是 main,而且没有 binding 覆盖过 dmScope,个人安装下默认开;配了任何一档 DM 隔离,默认关;显式写 true 或 false 永远优先,而且得开着 Active Memory。过滤很严:群聊、频道、cron、心跳、hook、子 agent、当前会话,以及别的 agent 的转录,都不会被召回;同一份转录只要有一个别名不是私聊,整条丢弃。
代价写在文档里,"Retrieval adds a bounded blocking step to eligible replies"。默认最多等 15 秒,连续超时 3 次会熔断 60 秒,gateway 刚重启后的第一次召回可能超时返回空。还有一点要知道:它只回一行摘要,不是原文。要原话,得再用 sessions_search。

要原话,用 sessions_search -- 重置前的也搜得到
重置到底做了什么?往同一份转录里追加一条"重置边界"事件,仅此而已。sessions_history 读转录时,不带 messageId,只给最近一次重置之后的那一截;带上一个落在重置边界之前的 messageId,它就把那个已经关上的区间原样打开,不会混进重置之后的新对话。所以重置不是删除。

找回来靠两个工具。sessions_search 搜可见会话里 user 和 assistant 的文字,query 最长 4096 个字符,limit 默认 10、最多 25。每条命中带 sessionKey、sessionId、messageId、角色、时间和一段片段。工具结果、推理块、图片不进索引,只索引转录的当前分支。底下是每个 agent 的 SQLite 库里一张 FTS5 表,消息写进转录和写进索引在同一个事务里,索引不会落后于正在进行的对话。
搜到之后,把 sessionKey、sessionId、messageId 一起交给 sessions_history,就能把那一段重新打开,包括重置之前的。读出来的内容有上限,每个文本块 4000 个字符,总共 80KB,先脱敏。2.0 的发布说明把这条列在新功能里,叫"找回过去的对话"(#105057)。
中文要注意一点:索引用的是 unicode61 分词,标点之间的一整段汉字算一个词,仓库里没有中文切词。我用同款分词器复现过,没跑 OpenClaw 本体:搜整段能命中,搜片段零命中,见下图。所以中文要原样搜标点之间那一整段,或者带上英文词、文件名;按意思找,用 memory_search。

谁能看到谁 -- dmScope 和可见范围
私聊怎么分会话,由 session.dmScope 决定,四档,默认是 main:所有人的所有私聊,共用一个 agent:<id>:main。另外三档按发件人、渠道、账号逐步分细,官方推荐 per-channel-peer。session.identityLinks 能把同一个人在不同渠道的身份并成一个 peer,但 dmScope 是 main 时它不生效。

一旦有第二个人能给你的 agent 发消息,默认值就成了问题:不隔离,Alice 的私聊 Bob 看得见(文档原话 "Alice's private messages would be visible to Bob")。配了 DM 隔离,上一节的跨对话召回默认也跟着关。
还有一处容易漏:分开 DM 路由,不等于分开会话工具的可见范围。tools.sessions.visibility 默认是 "all",没沙箱的调用方照样能搜到别人的对话,要收窄得改成 agent、tree 或 self。
收尾
跨会话记忆这件事,OpenClaw 的取舍很明确:先留下来,要用的时候再取,只有你问起过去的时候,才付出那一步阻塞等待的代价。默认当你一个人用,所有私聊汇进一个大脑,要隔离得自己配。省事的默认值,和得自己担责的配置,是绑在一起的。这是我的判断。
你的 agent 现在是你一个人在用,还是已经有第二个人能给它发消息了?dmScope 你设的是哪一档?评论区聊聊。
下一篇聊上下文:压缩摘要要是漏了待办怎么办,compaction 现在会自己验货。