Deep dive: session store + transcripts, lifecycle, and (auto)compaction internals

Read when…
  • You need to debug session ids, transcript JSONL, or sessions.json fields
  • You are changing auto-compaction behavior or adding “pre-compaction” housekeeping
  • You want to implement memory flushes or silent system turns

会话管理与压缩(深入探讨)

本文档解释了OpenClaw如何端到端地管理会话:

  • 会话路由(传入消息如何映射到一个 sessionKey
  • 会话存储 (sessions.json) 及其跟踪的内容
  • 对话持久化 (*.jsonl) 及其结构
  • 对话卫生(运行前的特定提供者修复)
  • 上下文限制(上下文窗口与跟踪令牌)
  • 压缩(手动+自动压缩)及预压缩工作的钩子位置
  • 静默维护(例如不应产生用户可见输出的内存写入)

如果您希望先获得高层次的概述,请从以下开始:


权威来源:网关

OpenClaw设计围绕一个单一的网关进程,该进程拥有会话状态。

  • 用户界面(macOS应用、Web控制界面、TUI)应查询网关以获取会话列表和令牌计数。
  • 在远程模式下,会话文件位于远程主机上;“检查本地Mac文件”不会反映网关正在使用的内容。

两层持久化

OpenClaw在两层中持久化会话:

  1. 会话存储 (sessions.json)
    • 键值映射:sessionKey -> SessionEntry
    • 小型、可变、安全编辑(或删除条目)
    • 跟踪会话元数据(当前会话ID、最后活动时间、开关、令牌计数器等)
  2. 对话 (<sessionId>.jsonl)
    • 具有树结构的追加式对话(条目具有 id + parentId
    • 存储实际对话 + 工具调用 + 压缩摘要
    • 用于重建未来轮次的模型上下文

磁盘上的位置

每个代理,在网关主机上:

  • 存储:~/.openclaw/agents/<agentId>/sessions/sessions.json
  • 对话:~/.openclaw/agents/<agentId>/sessions/<sessionId>.jsonl
    • Telegram主题会话:.../<sessionId>-topic-<threadId>.jsonl

OpenClaw通过 src/config/sessions.ts 解析这些路径。


会话密钥 (sessionKey)

一个 sessionKey 标识您所在的_哪个对话桶_(路由+隔离)。

常见模式:

  • 主/直接聊天(每个代理):agent:<agentId>:<mainKey>(默认 main
  • 群组:agent:<agentId>:<channel>:group:<id>
  • 房间/频道(Discord/Slack):agent:<agentId>:<channel>:channel:<id>...:room:<id>
  • 定时任务:cron:<job.id>
  • Webhook:hook:<uuid>(除非被覆盖)

规范规则记录于 /concepts/session


会话ID (sessionId)

每个 sessionKey 指向一个当前 sessionId(继续对话的对话文件)。

经验法则:

  • Reset (/new, /reset) 创建一个新的 sessionId 用于该 sessionKey
  • Daily reset (默认为网关主机本地时间凌晨4:00) 在重置边界之后的下一条消息时创建一个新的 sessionId
  • Idle expiry (session.reset.idleMinutes 或旧版 session.idleMinutes) 在空闲窗口后收到消息时创建一个新的 sessionId。当同时配置了 daily 和 idle 时,以先过期者为准。

实现细节:该决策发生在 initSessionState() 中的 src/auto-reply/reply/session.ts


会话存储架构 (sessions.json)

存储的值类型是 SessionEntrysrc/config/sessions.ts 中。

关键字段(不完整):

  • sessionId: 当前对话记录ID(除非设置了 sessionFile,否则文件名由此派生)
  • updatedAt: 最近活动时间戳
  • sessionFile: 可选的显式对话记录路径覆盖
  • chatType: direct | group | room(帮助UI和发送策略)
  • provider, subject, room, space, displayName: 分组/频道标签的元数据
  • 切换:
    • thinkingLevel, verboseLevel, reasoningLevel, elevatedLevel
    • sendPolicy(每个会话的覆盖)
  • 模型选择:
    • providerOverride, modelOverride, authProfileOverride
  • 令牌计数器(尽力而为/依赖提供商):
    • inputTokens, outputTokens, totalTokens, contextTokens
  • compactionCount: 此会话键自动压缩完成的频率
  • memoryFlushAt: 上次预压缩内存刷新的时间戳
  • memoryFlushCompactionCount: 上次刷新时的压缩次数

存储可以安全编辑,但网关是权威:它可能会在会话运行时重写或恢复条目。


对话记录结构 (*.jsonl)

对话记录由 @mariozechner/pi-coding-agentSessionManager 管理。

文件是 JSONL 格式:

  • 第一行:会话头 (type: "session",包括 id, cwd, timestamp, 可选的 parentSession)
  • 然后:带有 id + parentId(树)的会话条目

值得注意的条目类型:

  • message: 用户/助手/工具结果消息
  • custom_message: 进入模型上下文的扩展注入消息(可以在UI中隐藏)
  • custom: 不进入模型上下文的扩展状态
  • compaction: 带有 firstKeptEntryIdtokensBefore 的持久化压缩摘要
  • branch_summary: 导航树分支时的持久化摘要

OpenClaw 故意 “修复”对话记录;网关使用 SessionManager 来读取和写入它们。


上下文窗口与跟踪令牌

两个不同的概念很重要:

  1. 模型上下文窗口:每个模型的硬限制(模型可见的令牌)
  2. 会话存储计数器:写入 sessions.json 的滚动统计信息(用于 /status 和仪表板)

如果您正在调整限制:

  • 上下文窗口来自模型目录(并通过配置可以被覆盖)。
  • contextTokens 在存储中是一个运行时估计/报告值;不要将其视为严格的保证。

更多内容,请参阅 /token-use


压缩:什么是压缩

压缩将较旧的对话总结为一个持久化的 compaction 条目保留在记录中,并保持最近的消息不变。

压缩后,未来的回合会看到:

  • 压缩摘要
  • firstKeptEntryId 之后的消息

压缩是持久化的(与会话修剪不同)。请参阅 /concepts/session-pruning


自动压缩何时发生(Pi 运行时)

在嵌入式 Pi 代理中,自动压缩在两种情况下触发:

  1. 溢出恢复:模型返回上下文溢出错误 → 压缩 → 重试。
  2. 阈值维护:在一个成功的回合之后,当:

contextTokens > contextWindow - reserveTokens

其中:

  • contextWindow 是模型的上下文窗口
  • reserveTokens 是为提示词 + 下一个模型输出预留的空间

这些是 Pi 运行时语义(OpenClaw 消费事件,但 Pi 决定何时压缩)。


压缩设置 (reserveTokens, keepRecentTokens)

Pi 的压缩设置位于 Pi 设置中:

{
  compaction: {
    enabled: true,
    reserveTokens: 16384,
    keepRecentTokens: 20000,
  },
}

OpenClaw 还对嵌入式运行强制执行一个安全底线:

  • 如果 compaction.reserveTokens < reserveTokensFloor,OpenClaw 会提高它。
  • 默认底线是 20000 个标记。
  • 设置 agents.defaults.compaction.reserveTokensFloor: 0 以禁用底线。
  • 如果已经更高,OpenClaw 不会改变它。

原因:在压缩变得不可避免之前,为多回合“家务”(如内存写入)留出足够的空间。

实现:ensurePiCompactionReserveTokens()src/agents/pi-settings.ts (从 src/agents/pi-embedded-runner.ts 调用)。


用户可见的界面

你可以通过以下方式观察压缩和会话状态:

  • /status(在任何聊天会话中)
  • openclaw status(CLI)
  • openclaw sessions / sessions --json
  • 详细模式:🧹 Auto-compaction complete + 压缩计数

静默家务 (NO_REPLY)

OpenClaw 支持“静默”回合用于后台任务,用户不应看到中间输出。

约定:

  • 助手的输出以 NO_REPLY 开头,表示“不要向用户发送回复”。
  • OpenClaw 在交付层剥离/抑制此内容。

截至 2026.1.10,当部分块以 NO_REPLY 开始时,OpenClaw 还会抑制草稿/正在输入流,因此静默操作不会在回合中途泄露部分输出。


压缩前的“内存刷新”(已实现)

目标:在自动压缩发生之前,运行一个静默代理回合,将持久状态写入磁盘(例如代理工作区中的 memory/YYYY-MM-DD.md),以便压缩不会擦除关键上下文。

OpenClaw 使用预阈值刷新方法:

  1. 监控会话上下文使用情况。
  2. 当其超过“软阈值”(低于Pi的压缩阈值)时,向代理运行一个静默的“立即写入内存”指令。
  3. 使用 NO_REPLY 以便用户看不到任何变化。

配置 (agents.defaults.compaction.memoryFlush):

  • enabled(默认:true
  • softThresholdTokens(默认:4000
  • prompt(刷新轮次的用户消息)
  • systemPrompt(刷新轮次附加的系统提示)

注意事项:

  • 默认提示/系统提示中包含一个 NO_REPLY 提示以抑制交付。
  • 刷新每个压缩周期运行一次(在 sessions.json 中跟踪)。
  • 刷新仅适用于嵌入式Pi会话(CLI后端跳过此步骤)。
  • 当会话工作区为只读时跳过刷新(workspaceAccess: "ro""none")。
  • 有关工作区文件布局和写入模式,请参阅内存

Pi还在扩展API中暴露了一个 session_before_compact 钩子,但OpenClaw的刷新逻辑目前位于网关侧。


故障排除检查表

  • 会话密钥错误?从/concepts/session开始,并确认 sessionKey/status 中。
  • 存储与记录不匹配?确认网关主机和存储路径来自 openclaw status
  • 压缩垃圾信息过多?检查:
    • 模型上下文窗口(太小)
    • 压缩设置(reserveTokens 对于模型窗口太高可能导致更早的压缩)
    • 工具结果膨胀:启用/调整会话修剪
  • 静默回合泄露?确认回复以 NO_REPLY 开头(确切标记),并且你使用的构建版本包含流式抑制修复。