当前位置: 首页 > news >正文

从一次误清理事故看 AI Agent 的 Session 生命周期治理

这篇文章记录一次真实的 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。

这样做有两个直接好处:

  1. 降低 token 成本:heartbeat 不需要每次带上主会话完整历史。
  2. 隔离后台噪声:后台工具输出、系统事件处理,不污染用户主聊天上下文。

从源码看,核心逻辑在 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

但从系统角色看,它们至少分成三类:

  1. 主 heartbeat 入口:长期巡检入口,应保留。
  2. channel-specific heartbeat sibling:通常由具体聊天中的 targeted wake、exec completion、提醒事件等生成,很多是低价值临时上下文。
  3. cron-run heartbeat sibling:通常与某次 cron run 后续事件有关,长期价值较低。

把三者混为一谈,就是这次误清理的根因。

4. 为什么没有造成严重后果

这次误清理没有演变成严重数据事故,主要因为执行清理时用了“可恢复归档”,而不是硬删除。

处理方式是:

  • sessions.json 中移除 session entry。
  • 把对应 transcript、trajectory 等文件移动到备份目录。
  • 保留原始 sessions.json 备份。

这意味着恢复主 heartbeat 的成本很低:

  1. 从备份的 session store 中取回目标 key 的 entry。
  2. 将对应 transcript 文件移回 sessions 目录。
  3. 写回当前 sessions.json
  4. 确认当前 active heartbeat 只剩应该保留的 main heartbeat。

最后我们只恢复了两个 main-level heartbeat:

  • agent:<main-agent>:main:heartbeat
  • agent:<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'

这段逻辑有几个有意设计的保守点:

  1. active 状态优先保留。
  2. main / chat / cron main / main heartbeat 优先保留。
  3. 自动归档只针对“确定是自动生成”的 session。
  4. unknown stale 不自动处理,只进入确认清单。
  5. 脚本输出结构化 JSON report,方便 cron 后续判断是否需要私聊确认。

8. Cron 编排:成功静默,疑难上报

weekly cleanup cron 也相应调整为:

  • 每周运行一次。
  • 执行 cleanup 脚本。
  • 如果没有确认项,返回 HEARTBEAT_OK 或静默。
  • 如果有 needsLexConfirmation,给负责人私聊留言。
  • 如果失败,输出 blocker 和错误信息。

这比“定时删除所有过期对象”安全得多。

Cron 的职责不应该是强行替人做不可逆判断,而应该是:

  1. 自动处理低风险对象。
  2. 把高风险、不确定对象整理成可决策清单。
  3. 保留恢复路径。

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 基础设施越来越强烈的感受:

真正可靠的自动化,不是“永远替你做决定”,而是知道哪些决定自己不该做。

http://www.jsqmd.com/news/754749/

相关文章:

  • MacBook上从零搞定VOSviewer:用文献可视化帮你快速定位研究热点(附Web of Science数据导出技巧)
  • 告别Hello World!用PySide6从零搭建一个带登录界面的桌面应用(附完整源码)
  • 开源项目国际化实战:从i18n到l10n的多语言文档建设指南
  • Timer-S1时间序列分析模型:原理与应用实践
  • 构建零幻觉RAG系统:基于ModernBERT与SPLADE的逐字问答引擎
  • VueHooks Plus状态管理完全指南:从基础到企业级应用
  • nli-MiniLM2-L6-H768真实作品:客服对话中用户诉求与解决方案匹配度热力图
  • Senta模型训练全流程解析:从数据准备到效果评估
  • OAuth2 授权码流程中如何验证 state 参数防止篡改?
  • 告别死记硬背!用AD画PCB时,这几个隐藏的交互技巧比快捷键还好用
  • FreeDictionaryAPI 终极指南:构建多语言词典查询服务的完整解决方案
  • VimCode:在VS Code中实现LazyVim风格的高效键位配置方案
  • 终极指南:如何用RunCat365在Windows任务栏实时监控系统性能
  • Tello无人机群飞还能这么玩?用多机视频流打造你的空中监控系统
  • 基于Next.js的全栈开发工具包orchard-kit:快速构建现代化Web应用
  • 告别手动排版!用Python+CPCL指令批量生成汉印HM-A300蓝牙打印机标签(附完整代码)
  • 拯救你的12800端口:Windows上因Hyper-V/Docker导致的‘幽灵端口占用’分析与修复实录
  • 2026届必备的六大降AI率方案横评
  • C++20 constexpr 调试实战手册(含17个真实崩溃案例+GDB 12.4+LLVM 16联合调试流程图)
  • Cheat Engine 6.8.1 保姆级通关教程:从精确值扫描到多级指针,手把手带你玩转内存修改
  • 告别逐帧重建:4D Gaussian Splatting如何用‘一个网络’搞定动态场景?技术解读与性能实测
  • 立项管理考点预测
  • 主构造函数从语法糖到生产力引擎,C# 13这6项增强正在重构.NET 8项目架构标准
  • C++动态数组vector全面解析
  • 智能代理系统记忆模块优化实战
  • WarpGPT:为AI大语言模型打造的网页内容抓取与解析中间件
  • 思源象棋v0.0.11 PWA 版正式上线!无需安装,点开即玩,支持添加到桌面/程序坞
  • egergergeeert效果展示:软光渲染下皮肤质感与布料纹理的细节表现
  • 田口法/灰关联分析
  • 别再写SQL了!MyBatis-Plus的remove()方法,一行代码清空Spring Boot项目里的表数据