会话管理与压缩(深入探讨)
本文档解释了OpenClaw如何端到端地管理会话:
- 会话路由(传入消息如何映射到一个
sessionKey) - 会话存储 (
sessions.json) 及其跟踪的内容 - 对话持久化 (
*.jsonl) 及其结构 - 对话卫生(运行前的特定提供者修复)
- 上下文限制(上下文窗口与跟踪令牌)
- 压缩(手动+自动压缩)及预压缩工作的钩子位置
- 静默维护(例如不应产生用户可见输出的内存写入)
如果您希望先获得高层次的概述,请从以下开始:
权威来源:网关
OpenClaw设计围绕一个单一的网关进程,该进程拥有会话状态。
- 用户界面(macOS应用、Web控制界面、TUI)应查询网关以获取会话列表和令牌计数。
- 在远程模式下,会话文件位于远程主机上;“检查本地Mac文件”不会反映网关正在使用的内容。
两层持久化
OpenClaw在两层中持久化会话:
- 会话存储 (
sessions.json)- 键值映射:
sessionKey -> SessionEntry - 小型、可变、安全编辑(或删除条目)
- 跟踪会话元数据(当前会话ID、最后活动时间、开关、令牌计数器等)
- 键值映射:
- 对话 (
<sessionId>.jsonl)- 具有树结构的追加式对话(条目具有
id+parentId) - 存储实际对话 + 工具调用 + 压缩摘要
- 用于重建未来轮次的模型上下文
- 具有树结构的追加式对话(条目具有
磁盘上的位置
每个代理,在网关主机上:
- 存储:
~/.openclaw/agents/<agentId>/sessions/sessions.json - 对话:
~/.openclaw/agents/<agentId>/sessions/<sessionId>.jsonl- Telegram主题会话:
.../<sessionId>-topic-<threadId>.jsonl
- Telegram主题会话:
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)
存储的值类型是 SessionEntry 在 src/config/sessions.ts 中。
关键字段(不完整):
sessionId: 当前对话记录ID(除非设置了sessionFile,否则文件名由此派生)updatedAt: 最近活动时间戳sessionFile: 可选的显式对话记录路径覆盖chatType:direct | group | room(帮助UI和发送策略)provider,subject,room,space,displayName: 分组/频道标签的元数据- 切换:
thinkingLevel,verboseLevel,reasoningLevel,elevatedLevelsendPolicy(每个会话的覆盖)
- 模型选择:
providerOverride,modelOverride,authProfileOverride
- 令牌计数器(尽力而为/依赖提供商):
inputTokens,outputTokens,totalTokens,contextTokens
compactionCount: 此会话键自动压缩完成的频率memoryFlushAt: 上次预压缩内存刷新的时间戳memoryFlushCompactionCount: 上次刷新时的压缩次数
存储可以安全编辑,但网关是权威:它可能会在会话运行时重写或恢复条目。
对话记录结构 (*.jsonl)
对话记录由 @mariozechner/pi-coding-agent 的 SessionManager 管理。
文件是 JSONL 格式:
- 第一行:会话头 (
type: "session",包括id,cwd,timestamp, 可选的parentSession) - 然后:带有
id+parentId(树)的会话条目
值得注意的条目类型:
message: 用户/助手/工具结果消息custom_message: 进入模型上下文的扩展注入消息(可以在UI中隐藏)custom: 不进入模型上下文的扩展状态compaction: 带有firstKeptEntryId和tokensBefore的持久化压缩摘要branch_summary: 导航树分支时的持久化摘要
OpenClaw 故意 不“修复”对话记录;网关使用 SessionManager 来读取和写入它们。
上下文窗口与跟踪令牌
两个不同的概念很重要:
- 模型上下文窗口:每个模型的硬限制(模型可见的令牌)
- 会话存储计数器:写入
sessions.json的滚动统计信息(用于 /status 和仪表板)
如果您正在调整限制:
- 上下文窗口来自模型目录(并通过配置可以被覆盖)。
contextTokens在存储中是一个运行时估计/报告值;不要将其视为严格的保证。
更多内容,请参阅 /token-use。
压缩:什么是压缩
压缩将较旧的对话总结为一个持久化的 compaction 条目保留在记录中,并保持最近的消息不变。
压缩后,未来的回合会看到:
- 压缩摘要
firstKeptEntryId之后的消息
压缩是持久化的(与会话修剪不同)。请参阅 /concepts/session-pruning。
自动压缩何时发生(Pi 运行时)
在嵌入式 Pi 代理中,自动压缩在两种情况下触发:
- 溢出恢复:模型返回上下文溢出错误 → 压缩 → 重试。
- 阈值维护:在一个成功的回合之后,当:
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 使用预阈值刷新方法:
- 监控会话上下文使用情况。
- 当其超过“软阈值”(低于Pi的压缩阈值)时,向代理运行一个静默的“立即写入内存”指令。
- 使用
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开头(确切标记),并且你使用的构建版本包含流式抑制修复。