Session 密钥、ids 和转录记录事件
会话键(sessionKey)¶
一个 sessionKey 用于标识你所在的会话桶(路由 + 隔离)。规范规则参见:/concepts/session。
| 模式 | 示例 |
|---|---|
| 主/直接聊天(按 agent) | agent:<agentId>:main |
| 群组 | agent:<agentId>:<channel>:group:<id> |
| 房间/频道(Discord/Slack) | agent:<agentId>:<channel>:channel:<id> 或 ...:room:<id> |
| Cron | cron:<job.id> |
| Webhook | hook:<uuid>(除非被覆盖) |
会话 ID(sessionId)¶
每个 sessionKey 都指向当前的一个 sessionId(即延续对话的 SQLite 转录身份)。决策逻辑位于 src/auto-reply/reply/session.ts 的 initSessionState() 中。
- Gateway 重置(
/new、/reset)会在已存在的持久化会话中记录一个重置边界,并保留其sessionId。尚不存在的会话则会被分配一个新的 id。 - 默认不自动重置。当前
sessionId继续使用,同时压缩机制将活跃模型上下文保持在有界范围内。 - 每日重置(
session.reset.mode: "daily")会在配置的本地小时边界(session.reset.atHour,默认4)之后的下一条消息上创建新的sessionId。 - 空闲过期(
session.reset.mode: "idle"搭配session.reset.idleMinutes,或旧的session.idleMinutes)会在空闲窗口之后有消息到达时创建新的sessionId。如果同时配置了每日和空闲两种模式,以先到期者为准。 - Control UI 重连恢复:当 Gateway 从操作员 UI 客户端收到匹配的
sessionId时,会为一次重连发送保留当前可见的会话。这是一个一次性信号;普通的过时发送仍会创建新的sessionId。 - 系统事件(心跳、cron 唤醒、exec 通知、gateway 簿记)可能会变更会话行,但绝不会延长每日/空闲重置的新鲜度。重置切换会在构建新的提示之前,丢弃队列中针对前一会话的系统事件通知。
- 自动父级派生策略:在创建线程或子 agent 派生时使用 OpenClaw 的活跃分支。如果该分支过大(超过固定的内部上限,目前为 100K token),OpenClaw 会以隔离上下文启动子级,而不是失败或继承不可用的历史记录。大小调整是自动的,不可配置;旧的
session.parentForkMaxTokens配置由openclaw doctor --fix移除。 - 操作员派生:
sessions.create { parentSessionKey, fork: true }从父级的当前状态进行分支。准入判断使用所选子模型的有效可用输入容量;如果模型容量不可用,则回退到 100K 安全上限。父级有活跃运行时会拒绝普通派生;添加forkFrom: "last-completed"则仅复制到最后一条已完成的助手消息,不包括进行中的尾部内容。与自动父级派生不同,操作员派生如果超过容量限制会被拒绝,而不是以隔离上下文接受。子级继承父级的模型选择,除非显式传入其他模型。响应会将其标记为forkedFromParent,token 计数器从零开始。 - 消息派生:
sessions.fork { sessionKey, entryId }会从所选用户消息之前的活跃路径前缀创建子级,并将该消息返回给编辑器进行修改。父级保持不变。隐身派生会保留父级的内存存储类别;重启 Gateway 会同时移除两个会话。Codex 派生验证会比较完整的、经见证的已提交提示,包括空白字符;有界的显示导入投影不能替代该证据。派生和回退操作请参见 Control UI。
会话存储结构¶
运行时存储将 SessionEntry 值保存在每个 agent 各自的 SQLite 中。值类型为 src/config/sessions.ts 中的 SessionEntry。关键字段(非详尽):
sessionId:当前转录 id,用于寻址 SQLite 转录行sessionStartedAt:当前sessionId的开始时间戳;每日重置的新鲜度使用该字段。旧行可以从 JSONL 会话头推导该值。lastInteractionAt:最后一次真实用户/频道交互的时间戳;空闲重置的新鲜度使用该字段,因此心跳、cron 和 exec 事件不会让会话保持存活。没有该字段的旧行会回退到恢复出的会话开始时间。updatedAt:最后一次存储行变更的时间戳,用于列表/修剪/簿记——不是每日/空闲新鲜度的权威依据。archivedAt:可选的归档时间戳。已归档会话保留在存储中,转录完整,并从正常的活跃列表中排除。pinnedAt:可选的置顶时间戳。活跃的置顶会话排在未置顶会话之前;归档会话会清除其置顶状态。- Codex 线程互操作:两个字段都遵循 Codex 线程管理的形态——传输中的
archived/pinned布尔值始终从时间戳派生并在服务端盖章,与 Codexthreads.archived_at语义和 camelCase 序列化保持一致。OpenClaw 时间戳为 epoch 毫秒,而 Codex 使用 epoch 秒,因此桥接层会在codex插件接缝处进行转换。Codex 线程方法仅涵盖归档(thread/archive/thread/unarchive),不包含置顶方法,因此置顶状态保留在 OpenClaw 侧。由于形态匹配,若 Codex 提供置顶方法,绑定会话就能以机制化方式往返传递置顶状态。 - Codex 监管仅列出未归档的原生线程。Gateway 本地活动状态未知(
idle或notLoaded)的线程,只有在操作员明确确认没有其他 Codex 进程拥有它之后,才能通过原生thread/archive归档;插件会先执行一次全新的进程本地状态读取,随后该线程从目录中消失。该读取无法证明另一个 App Server 进程未在使用该线程。OpenClaw 拒绝归档活跃行和错误行,并且在节点桥接能够拥有完整的流式线程生命周期之前,配对节点归档不可用。在原生 Codex 客户端中取消归档会使该线程重新有资格出现在列表中。 snoozedUntil/snoozedAt:可选的 epoch 毫秒唤醒时间和服务端盖章的休眠时间,仅存储在session_nodes.entry_json中。sessions.patch { snoozedUntil }接受未来的正整数或null来唤醒会话,且要求提供expectedSessionId。休眠会将符合条件的活跃根会话从 Control UI 的 Active 侧边栏隐藏,而不改变生命周期或工作准入。行记录暴露时间戳;客户端将唤醒时间与自身时钟进行比较,没有派生的snoozed布尔值或服务器定时器。已过期字段可以保留,直到后续写入将其清除。用户/频道交互以及更新用户可见活动的已完成运行会清除这两个字段;系统事件和保留状态的运行会保留它们。休眠会保留置顶,而置顶或归档会清除休眠。lastReadAt/markedUnreadAt:由sessions.patch { unread }在服务端盖章的已读状态时间戳——unread: false记录一次已读(设置lastReadAt,清除markedUnreadAt);unread: true记录markedUnreadAt并将会话标记为未读,直到下次激活或显式已读。会话行在派生的unread布尔值旁边暴露该标记,以便已经打开的客户端在确认新活动的同时保留手动提醒。支持所公布的未读确认契约的客户端发出的自动已读补丁会包含expectedMarkedUnreadAt(null表示没有标记);更新的标记会使该确认成为一次成功的空操作,而不是抹掉更新的意图。仅含unread: false的请求保留旧的清除行为,因此要在多个连接客户端之间提供保护,需要每个活跃客户端都支持该契约。从未标记为已读的会话保持unread: false,因此现有安装在升级后不会出现未读状态。lastActivityAt:最后一次完成的、算作值得未读的活动的 agent 运行时间戳(用户、频道和 cron 运行)。心跳和内部事件的轮次以及元数据补丁不会更新它;updatedAt不是活动信号。sessionFile:为迁移/归档兼容性而保留的旧标记;活跃运行时使用 SQLite 身份chatType:direct | group | roomlabel:显式自定义名称;始终具有最高优先级,包括那些标签看起来像自动设备名的旧记录。使用sessions.patch { label: null }清除它即可恢复自动命名。autoLabel:可选的自动设备标签,与自定义名称分离。Android 通过sessions.patch写入它;允许重复值,null清除它。它是保存在displayName之下的显示回退,不是唯一的会话标签。provider、subject、room、space、displayName:群组/频道标签元数据;displayName还存储生成的会话标题。- 开关:
thinkingLevel、verboseLevel、reasoningLevel、elevatedLevel、sendPolicy(每会话覆盖) - 模型选择:
providerOverride、modelOverride、authProfileOverride - Token 计数器(尽力而为/依赖提供方):
inputTokens、outputTokens、totalTokens、contextTokens compactionCount:此会话键的自动压缩完成次数memoryFlushAt/memoryFlushCompactionCount:上次压缩前内存刷新的时间戳和压缩计数
现有仅含 label 的记录会被保留:Gateway 不会根据文本来推断保存的 label 是否为自动生成。请显式清除或替换它,以改变其优先级。不支持 autoLabel 的旧版 Gateway 会拒绝该补丁字段;Android 不会将设备名重试为 label,以免覆盖自定义名称。Android 仍会将其本地已知的设备名作为仅用于显示的备用名称用于自己的会话,排在服务器提供的名称之后。此备用名称既不会发送给 Gateway,也不会存储在会话缓存中。
Gateway 是权威方:它可能会在会话运行期间重写或重新水合条目。对于旧版基于文件的后端安装,请使用 openclaw doctor --session-sqlite import --session-sqlite-all-agents 进行迁移,而不是编辑 sessions.json 并期望运行时继续读取该文件。
会话记录事件结构¶
会话记录由 OpenClaw 会话访问器管理,并通过基于身份的辅助函数暴露给运行时代码。事件流是仅追加的:
- 第一条目:会话头——
type: "session"、id、cwd、timestamp,以及可选的parentSession。 - 其后:带有
id+parentId的条目(树状结构)。
值得注意的条目类型:
message:用户/助手/toolResult 消息custom_message:扩展注入的消息,_确实_会进入模型上下文(在 TUI 中当display: true时渲染,当display: false时完全隐藏)custom:扩展状态,_不会_进入模型上下文(用于在重载之间持久化扩展状态)compaction:持久化的压缩摘要,包含firstKeptEntryId和tokensBeforereset:一个新的历史窗口,可选择性地保留自firstKeptEntryId起的消息branch_summary:在导航树分支时持久化的摘要
历史读取器会在后续压缩中保留最新的重置窗口:被显式保留的重置消息以及该重置之后的消息保持可见,但更早的消息和压缩摘要不会重新出现。模型上下文改为跟随最新的重置或压缩,因此压缩可以总结当前对话,而无需重新打开其更早的历史。
仅模型的调用方应等待 SessionManager.openModelContextAsync(target, { admission?, signal? }) 来创建一种独立的、不持久化的视图,而不会在持久化历史扫描上阻塞 Gateway 事件循环。openModelContext() 为同步消费者提供相同视图。读取器在 SQLite 中选择负载,并在模型窗口之外保留轻量级导航,不会引入历史大小上限。仅存储的原生提示文本和工具结果细节不会出现在该视图中;镜像身份、发送者与媒体事实、工具内容以及有效的提供商重放状态仍然可用。原生 fork 验证、重放、导出和 doctor 操作继续使用全保真证据读取器。
SessionManager.readSessionContext(target, read, { admission? }) 允许同步消费者在单个只读快照内处理全保真上下文消息。回调接收 (messages, header):一个惰性消息可迭代对象,以及未经校验的存储头。缺失的存储会提供空的可迭代对象且不提供头,并且不会创建数据库。可选的准入会排除当前已准入用户行及之后的事件。回调错误会传播;当回调返回或抛出时,迭代器即关闭;不能将其保留用于后续读取。这使重放消费者能够在获取期间强制执行其现有限制,而不会静默丢弃更早的历史。导航仍随会话记录规模扩展,且每个被选中的行都会整体解码;这不是固定的进程内存上限。
持久化的模型上下文读取在 worker 中运行。Codex 原生重放和已定轮次验证会将惰性读取及其消费者一起保留在 worker 中。Worker 读取按读取器串行化,并复用空闲的 worker。准入凭证会在读取快照内验证,并在结果被接受前再次验证;未带准入的读取则改为检查会话的重写代次和最后事件序列。已准入的读取保留其轮次边界,因此仅后续追加不会使其失效。失效的读取或已取消的信号会导致结果被拒绝。调用方会在上下文获取过程中携带其原始取消信号,并在调用钩子、启动模型运行或应用提案之前检查其所有者是否仍处于活动状态。隐身会话在 Gateway 进程中使用相同操作,因为它们的 SQLite 数据库保存在内存中。
OpenClaw 有意不“修补”会话记录;Gateway 使用 SessionManager 来读取/写入它们。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 cl/openclaw