这篇文章记录一次真实的 OpenClaw Session 清理排障过程。它不是一篇“脚本怎么写”的流水账,而是一次从现象、源码、运行数据到自动化治理策略的复盘。
文中涉及的 session key、用户 ID、群 ID、内部路径和备份目录均已脱敏或泛化。
1. 背景:为什么要清理 Session
OpenClaw 这类自托管 AI Gateway / Agent Runtime,通常会保留大量 session:
- 用户与 Agent 的主会话,例如 IM 私聊、群聊、Web Chat。
- Cron 任务会话,用来保存定时任务上下文与运行痕迹。
- Subagent 会话,用于 Map-Reduce、并行调查、长任务拆分。
- Heartbeat 会话,用于主动唤醒、系统事件处理、后台完成通知、提醒等。
- 一些临时 isolated session,用来降低上下文成本,避免把很长的主会话历史塞进后台任务。
随着 cron、subagent、heartbeat 被频繁使用,session store 很容易变得臃肿。很多 session 本身是自动生成的、短期有用的;但也有一些 session 承担长期上下文或审计价值,不能简单按名字删除。
这次事故正是从一个看似合理的目标开始的:清理不再需要的 heartbeat sessions。
2. 初始判断:heartbeat session 看起来都像可清理对象
当时我们看到很多 session key 以 :heartbeat 结尾,直觉上它们像是临时 sibling session。
Heartbeat isolated session 的生成逻辑大致是:当配置启用了 isolatedSession 时,heartbeat 不直接复用主 session,而是创建一个轻量的 base:heartbeat sibling session。
这样做有两个直接好处:
- 降低 token 成本:heartbeat 不需要每次带上主会话完整历史。
- 隔离后台噪声:后台工具输出、系统事件处理,不污染用户主聊天上下文。
从源码看,核心逻辑在 heartbeat runner:
const useIsolatedSession = heartbeat?.isolatedSession === true;if (useIsolatedSession) {const configuredSession = resolveHeartbeatSession(cfg, agentId, heartbeat);const { isolatedSessionKey, isolatedBaseSessionKey } =resolveIsolatedHeartbeatSessionKey({sessionKey,configuredSessionKey: configuredSession.sessionKey,sessionEntry: entry,});const cronSession = resolveCronSession({cfg,sessionKey: isolatedSessionKey,agentId,nowMs: startedAt,forceNew: true,});cronSession.sessionEntry.heartbeatIsolatedBaseSessionKey = isolatedBaseSessionKey;cronSession.store[isolatedSessionKey] = cronSession.sessionEntry;await saveSessionStore(cronSession.storePath, cronSession.store);
}
这段逻辑非常关键:
- isolated heartbeat 的 key 通常是
baseSessionKey + ':heartbeat'。 - session entry 中会写入
heartbeatIsolatedBaseSessionKey,指回 base session。 resolveCronSession(... forceNew: true)会为 heartbeat 创建新的 isolated session entry。
当时错误的清理条件大概是:
if key.endswith(':heartbeat') or entry.get('heartbeatIsolatedBaseSessionKey'):archive(session)
这个条件能找到 heartbeat,但它不能判断“这个 heartbeat 是否应该被清理”。
3. 事故:把 main heartbeat 也清掉了
实际清理后,active session store 中所有 heartbeat entry 都消失了,其中包括两类不该自动清掉的 session:
agent:<agent>:main:heartbeat- 另一个 agent 的
agent:<agent>:main:heartbeat
它们虽然从实现上也是 isolated heartbeat sibling,但在系统角色上并不等同于普通聊天 heartbeat sibling。
这就是问题的核心:
实现形态相同,不代表业务角色相同。
从实现看,它们都可能有:
:heartbeat后缀heartbeatIsolatedBaseSessionKey- isolated transcript
但从系统角色看,它们至少分成三类:
- 主 heartbeat 入口:长期巡检入口,应保留。
- channel-specific heartbeat sibling:通常由具体聊天中的 targeted wake、exec completion、提醒事件等生成,很多是低价值临时上下文。
- cron-run heartbeat sibling:通常与某次 cron run 后续事件有关,长期价值较低。
把三者混为一谈,就是这次误清理的根因。
4. 为什么没有造成严重后果
这次误清理没有演变成严重数据事故,主要因为执行清理时用了“可恢复归档”,而不是硬删除。
处理方式是:
- 从
sessions.json中移除 session entry。 - 把对应 transcript、trajectory 等文件移动到备份目录。
- 保留原始
sessions.json备份。
这意味着恢复主 heartbeat 的成本很低:
- 从备份的 session store 中取回目标 key 的 entry。
- 将对应 transcript 文件移回 sessions 目录。
- 写回当前
sessions.json。 - 确认当前 active heartbeat 只剩应该保留的 main heartbeat。
最后我们只恢复了两个 main-level heartbeat:
agent:<main-agent>:main:heartbeatagent:<secondary-agent>:main:heartbeat
没有恢复聊天 heartbeat sibling,也没有恢复 cron-run heartbeat sibling。
这是一个很重要的工程纪律:
清理默认应该是 archive,而不是 delete。尤其当对象角色和后果不能百分百判断时。
5. 源码层面的关键认识
5.1 Heartbeat 的 prompt 不应该复读旧上下文
OpenClaw 默认 heartbeat prompt 的设计本身很克制:
export const HEARTBEAT_PROMPT ="Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.";
这说明 heartbeat 的定位不是“把旧聊天重新跑一遍”,而是读取当前 workspace 中的 heartbeat 指令,处理当前到期任务或系统事件。
这也解释了为什么 isolated heartbeat 有价值:它应该是低上下文、任务导向、可重复唤醒的后台机制。
5.2 resolveIsolatedHeartbeatSessionKey 会折叠重复后缀
源码里有一段专门处理 :heartbeat:heartbeat 之类重复 suffix 的逻辑。
它的设计目的不是判断 session 是否有业务价值,而是避免 wake-triggered re-entry 时无限叠加 heartbeat 后缀。
抽象后逻辑类似:
function resolveIsolatedHeartbeatSessionKey(params) {const storedBaseSessionKey = params.sessionEntry?.heartbeatIsolatedBaseSessionKey?.trim();if (storedBaseSessionKey && params.sessionKey startsWith storedBaseSessionKey) {// collapse repeated :heartbeat suffixreturn {isolatedSessionKey: `${storedBaseSessionKey}:heartbeat`,isolatedBaseSessionKey: storedBaseSessionKey,};}return {isolatedSessionKey: `${params.sessionKey}:heartbeat`,isolatedBaseSessionKey: params.sessionKey,};
}
这里再一次提醒我们:
- key pattern 是 routing / normalization 层的实现细节。
- cleanup policy 不能只依赖 routing 层的实现细节。
5.3 resolveCronSession(... forceNew: true) 是成本隔离工具,不是“可删除”标签
Heartbeat isolated session 复用了 cron isolated session 的创建模式,并传了 forceNew: true。
这容易造成一个误解:既然是 force new,它是不是一次性临时对象?
答案是不一定。
forceNew 只说明“本次运行需要新 session / 干净上下文”,不说明“这个 session 没有审计价值,也不说明它可以随便删除”。
尤其是 main heartbeat,它虽然会被重新创建,但旧 session 仍可能包含:
- 运行轨迹
- 工具调用记录
- 最近一次 heartbeat 的状态
- 排障线索
因此“可再生”不等于“可丢弃”。
6. 修正后的清理模型:三分类,而不是一刀切
事故后,我们把 weekly cleanup 机制改成三分类:
6.1 Auto Archive:确定可归档
自动归档必须同时满足几个条件:
- 确定是自动生成。
- 非长期上下文。
- inactive。
- 超过阈值。
- 状态是 done / timeout / failed / unknown 中的安全集合。
典型对象:
- 已完成的 subagent sessions。
- cron-run isolated sessions。
- cron-run heartbeat sibling。
- channel-specific heartbeat sibling。
这些对象不限于某个 agent。未来 secondary agent 如果也产生自动生成的 subagent 或 cron-run session,也可以按同一规则处理。
6.2 Preserve:明确保留
明确保留的对象包括:
- agent main session。
- agent main heartbeat。
- Feishu / IM 主聊天 session。
- cron main session。
- running / pending / in_progress session。
- 最近仍在更新的 session。
保留规则的本质是:只要它承担长期上下文、主入口、审计入口、活跃任务,就不自动动它。
6.3 Needs Confirmation:需要人工确认
超过阈值但无法可靠判断的 session,不自动归档,而是列入确认清单。
典型情况:
- key pattern 不认识。
- entry 字段异常。
- 看起来像长期上下文但长时间未更新。
- 状态不是 done/timeout/failed,也不是 running。
- 无法判断业务归属。
这些对象会在 weekly cleanup 后给负责人发送私聊确认,而不是直接处理。
7. 实现:一个保守的 cleanup classifier
修正后的 cleanup 脚本核心是 classifier,而不是简单过滤器。
简化版如下:
ArchiveClass = Literal['auto_archive', 'preserve', 'confirm']
AUTO_DONE_STATUSES = {'done', 'timeout', 'failed', None}
ACTIVE_STATUSES = {'running', 'in_progress', 'pending'}def classify_session(key, entry, now, auto_older_days, confirm_older_days):if not isinstance(entry, dict):return 'confirm', 'non-dict session entry'status = entry.get('status')days = age_days(entry, now)if status in ACTIVE_STATUSES:return 'preserve', f'active status={status}'if is_agent_main_session(key):return 'preserve', 'agent main session'if is_main_heartbeat(key):return 'preserve', 'agent main heartbeat'if is_primary_chat(key):return 'preserve', 'primary chat context'if is_cron_main_session(key):return 'preserve', 'cron main session'if is_cron_run_heartbeat(key) and days >= auto_older_days:return 'auto_archive', 'cron-run heartbeat older than threshold'if is_channel_heartbeat(key) and days >= auto_older_days:return 'auto_archive', 'channel heartbeat sibling older than threshold'if is_cron_run_session(key):if days >= auto_older_days and status in AUTO_DONE_STATUSES:return 'auto_archive', 'cron-run isolated session older than threshold'if days >= confirm_older_days:return 'confirm', 'stale cron-run session but not confidently removable'return 'preserve', 'recent cron-run session'if is_subagent(key):if days >= auto_older_days and status in AUTO_DONE_STATUSES:return 'auto_archive', 'subagent older than threshold'if days >= confirm_older_days:return 'confirm', 'stale subagent but not confidently removable'return 'preserve', 'recent subagent'if days >= confirm_older_days:return 'confirm', 'unknown stale session older than threshold'return 'preserve', 'not old or not confidently auto-generated'
这段逻辑有几个有意设计的保守点:
- active 状态优先保留。
- main / chat / cron main / main heartbeat 优先保留。
- 自动归档只针对“确定是自动生成”的 session。
- unknown stale 不自动处理,只进入确认清单。
- 脚本输出结构化 JSON report,方便 cron 后续判断是否需要私聊确认。
8. Cron 编排:成功静默,疑难上报
weekly cleanup cron 也相应调整为:
- 每周运行一次。
- 执行 cleanup 脚本。
- 如果没有确认项,返回
HEARTBEAT_OK或静默。 - 如果有
needsLexConfirmation,给负责人私聊留言。 - 如果失败,输出 blocker 和错误信息。
这比“定时删除所有过期对象”安全得多。
Cron 的职责不应该是强行替人做不可逆判断,而应该是:
- 自动处理低风险对象。
- 把高风险、不确定对象整理成可决策清单。
- 保留恢复路径。
9. 最终得到的工程原则
这次事故留下了几个非常实用的原则。
原则一:不要把实现标记当成业务语义
key.endswith(':heartbeat') 和 heartbeatIsolatedBaseSessionKey 只能说明对象如何生成,不能说明对象能不能删。
实现标记只能作为候选发现手段,不能作为最终决策依据。
原则二:可再生不等于可丢弃
main heartbeat 可以重新生成,但它的历史仍可能有排障价值。
自动化系统里很多对象都“可再生”,但它们在某个时间窗口内可能承担审计、回滚、解释系统行为的作用。
原则三:清理脚本应该是 classifier,而不是 grep
安全 cleanup 的第一步不是 delete where pattern matches,而是:
- preserve
- auto_archive
- needs_confirmation
只有进入 auto_archive 的对象才应该被脚本处理。
原则四:默认 archive,不默认 delete
这次能快速恢复,是因为清理默认走归档:
- store 有备份。
- transcript 文件被移动而不是删除。
- 恢复只需要合并 entry 和移回文件。
对于 AI Agent 这类长上下文系统,硬删应该是最后选项。
原则五:自动化要有“求确认”的出口
不确定对象不应该让脚本猜。
更好的方式是每周整理一份确认清单,私聊发给负责人,让人做最后判断。
这不是降低自动化程度,而是把自动化用在更合适的位置:减少人工整理成本,而不是替人承担不可逆决策。
10. 小结
这次 session 清理事故本身不大,但它暴露了一个常见问题:我们太容易把“看起来像临时对象”的东西当成“可以清理的对象”。
在 Agent Runtime 里,session 不只是 transcript 文件;它可能同时承担上下文、路由、审计、任务状态、后台事件消费等多个角色。
因此,一个成熟的 session maintenance 机制应该满足:
- 有明确 preserve 规则。
- 有保守的 auto-archive 规则。
- 有 needs-confirmation 分支。
- 默认可恢复归档。
- 输出结构化报告。
- 定期运行,但不替人做高风险判断。
最终我们得到的不是一个更激进的清理脚本,而是一个更可靠的治理机制。
这也是我对 AI Agent 基础设施越来越强烈的感受:
真正可靠的自动化,不是“永远替你做决定”,而是知道哪些决定自己不该做。
