会话管理深入

会话管理与压缩(深入)

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

  • 会话路由(入站消息如何映射到 sessionKey
  • 会话存储sessions.json)及其追踪内容
  • 转录持久化*.jsonl)及其结构
  • 转录卫生(运行前提供商特定的修复)
  • 上下文限制(上下文窗口与追踪的 tokens)
  • 压缩(手动 + 自动压缩)以及在哪里钩入预压缩工作
  • 静默内务处理(例如不应产生用户可见输出的记忆写入)

如果你想要先获取高级概述,从以下开始:


事实来源:网关

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

  • UI(macOS 应用、Web 控制 UI、TUI)应查询网关获取会话列表和 token 计数。
  • 在远程模式下,会话文件在远程主机上;"检查你的本地 Mac 文件"不会反映网关正在使用的内容。

两个持久化层

OpenClaw 在两个层中持久化会话:

  1. 会话存储(sessions.json

    • 键/值映射:sessionKey -> SessionEntry
    • 小、可变、安全编辑(或删除条目)
    • 追踪会话元数据(当前会话 id、最后活动、切换、token 计数器等)
  2. 转录(<sessionId>.jsonl

    • 附加_only_ 转录,具有树结构(条目有 id + parentId
    • 存储实际对话 + 工具调用 + 压缩摘要
    • 用于为未来轮次重建模型上下文

磁盘上位置

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

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

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


存储维护和磁盘控制

会话持久化具有自动维护控制(session.maintenance),用于 sessions.json 和转录产物:

  • modewarn(默认)或 enforce
  • pruneAfter:陈旧条目年龄截止(默认 30d
  • maxEntries:限制 sessions.json 中的条目(默认 500
  • rotateBytes:当过大时轮换 sessions.json(默认 10mb
  • resetArchiveRetention*.reset.<timestamp> 转录归档的保留(默认:与 pruneAfter 相同;false 禁用清理)
  • maxDiskBytes:可选的会话目录预算
  • highWaterBytes:清理后的可选目标(默认 maxDiskBytes80%

磁盘预算清理的执行顺序(mode: "enforce"):

  1. 首先删除最旧的归档或孤立转录产物。
  2. 如果仍高于目标,驱逐最旧的会话条目及其转录文件。
  3. 继续使用直到使用量达到或低于 highWaterBytes

mode: "warn" 中,OpenClaw 报告潜在的驱逐但不改变存储/文件。

按需运行维护:

openclaw sessions cleanup --dry-run
openclaw sessions cleanup --enforce

Cron 会话和运行日志

隔离的 cron 运行也创建会话条目/转录,它们有专用的保留控制:

  • cron.sessionRetention(默认 24h)从会话存储中修剪旧的隔离 cron 运行会话(false 禁用)。
  • cron.runLog.maxBytes + cron.runLog.keepLines 修剪 ~/.openclaw/cron/runs/<jobId>.jsonl 文件(默认:2_000_000 字节和 2000 行)。

会话键(sessionKey

sessionKey 标识你处于哪个对话桶(路由 + 隔离)。

常见模式:

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

规范规则在 /concepts/session 记录。


会话 id(sessionId

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

经验法则:

  • 重置/new/reset)为该 sessionKey 创建新的 sessionId
  • 每日重置(默认网关主机本地时间凌晨 4:00)在重置边界后的下一条消息创建新的 sessionId
  • 空闲过期session.reset.idleMinutes 或遗留 session.idleMinutes)当消息在空闲窗口后到达时创建新的 sessionId。当每日 + 空闲都配置时, whichever 先过期获胜。
  • 线程父分支守卫session.parentForkMaxTokens,默认 100000)当父会话已经太大时跳过父转录分支;新线程从头开始。设置 0 禁用。

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


会话存储模式(sessions.json

存储的值类型是 src/config/sessions.ts 中的 SessionEntry

关键字段(非详尽):

  • sessionId:当前转录 id(文件名从此派生,除非设置 sessionFile
  • updatedAt:最后活动的时间戳
  • sessionFile:可选的显式转录路径覆盖
  • chatTypedirect | group | room(帮助 UI 和发送策略)
  • providersubjectroomspacedisplayName:用于群组/频道标签的元数据
  • 切换:
    • thinkingLevelverboseLevelreasoningLevelelevatedLevel
    • sendPolicy(每会话覆盖)
  • 模型选择:
    • providerOverridemodelOverrideauthProfileOverride
  • Token 计数器(最佳努力/提供商依赖):
    • inputTokensoutputTokenstotalTokenscontextTokens
  • compactionCount:此会话键自动压缩完成的次数
  • memoryFlushAt:上次预压缩记忆刷新时的时间戳
  • memoryFlushCompactionCount:上次刷新运行时的压缩计数

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


转录结构(*.jsonl

转录由 @mariozechner/pi-coding-agentSessionManager 管理。

文件是 JSONL:

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

值得注意的条目类型:

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

OpenClaw 故意"修复"转录;网关使用 SessionManager 读取/写入它们。


上下文窗口与追踪的 tokens

两个不同的概念很重要:

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

如果你在调整限制:

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

更多详见 /token-use


压缩:是什么

压缩将旧对话摘要为转录中持久化的 compaction 条目,并保持最近的消息完整。

压缩后,未来轮次看到:

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

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


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

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

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

contextTokens > contextWindow - reserveTokens

其中:

  • contextWindow 是模型的上下文窗口
  • reserveTokens 是为提示 + 下一个模型输出保留的余量

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


压缩设置(reserveTokenskeepRecentTokens

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

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

OpenClaw 也为嵌入式运行执行安全下限:

  • 如果 compaction.reserveTokens < reserveTokensFloor,OpenClaw 提升它。
  • 默认下限是 20000 tokens。
  • 设置 agents.defaults.compaction.reserveTokensFloor: 0 禁用下限。
  • 如果已经更高,OpenClaw 不管它。

为什么:在压缩变得不可避免之前,为多轮"内务处理"(如记忆写入)留下足够的余量。

实现:src/agents/pi-settings.ts 中的 ensurePiCompactionReserveTokens() (从 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 开始,并确认 /status 中的 sessionKey
  • 存储与转录不匹配?确认网关主机和 openclaw status 中的存储路径。
  • 压缩垃圾邮件?检查:
    • 模型上下文窗口(太小)
    • 压缩设置(reserveTokens 对于模型窗口太高可能导致更早压缩)
    • 工具结果膨胀:启用/调整会话修剪
  • 静默轮次泄露?确认回复以 NO_REPLY 开头(确切 token),并且你在使用包含流式传输抑制修复的构建。