乐于分享
好东西不私藏

OpenClaw源码解析(十一):上下文运行时恢复链

OpenClaw源码解析(十一):上下文运行时恢复链

一个问题:

当上下文开始失控时,OpenClaw 到底怎么恢复,什么时候压缩,什么时候截断,什么时候放弃


1. 先区分 4 个动作

1.1 预处理

就是每轮固定会做的 sanitize / validate / limit / repair。

它的目标是让历史“结构合法”,不是恢复 overflow。

1.2 auto-compaction

发生在 attempt 内部。

可以理解成:

这一轮正在运行时,底层 session/runtime 自己先压缩了一次。

1.3 explicit compaction

发生在外层 run.ts

可以理解成:

attempt 已经结束了,外层判断还是 overflow,就会主动发起一轮正式压缩。

1.4 truncation

不是压缩整个会话,而是定向砍掉局部超大的内容,最典型是 tool result。


2. 恢复链什么时候开始

不是每轮都会进入恢复链。

正常路径是:

组装上下文-> 调模型-> 正常返回-> afterTurn

只有外层 run.ts 看到错误像下面这些情况,才会进入恢复判断:

  • context overflow
  • compaction failure
  • timeout
  • auth / overload / failover 问题

本篇只聚焦和上下文相关的主分支:

像 context overflow-> 看本轮是否已经 auto-compaction-> 必要时 explicit compaction-> 必要时 tool result truncation-> 仍失败才最终收敛成错误

3. auto-compaction 为什么和 explicit compaction 不是一回事

3.1 auto-compaction

它通过事件流暴露:

  • auto_compaction_start
  • auto_compaction_end

而且 attempt 不会因为 prompt 返回就直接结束,还会:

  • waitForCompactionRetry()

意思是:

只要 compaction 还在内部收尾,这一轮的上下文状态就还不稳定。

3.2 explicit compaction

外层在 overflow 恢复分支里会调用:

  • contextEngine.compact(…)

当前默认 legacy 会桥接到老的 compaction 实现。


4. 为什么本轮已经 auto-compaction 过了,外层还会继续重试一次

这是源码里一个很重要的保守策略。

  1. 如果这一轮已经 auto-compaction
  2. 但外层仍然看到 overflow 信号
  3. 外层先 retry 一轮
  4. 不马上 double-compact

原因是:

内部 compaction 已经可能改过 transcript,当前这个 overflow 信号可能只是本轮尾声的残留结果。这时立刻再压一次,可能是在重复压同一批内容。

所以系统先给“已经发生的压缩”一次重试的机会。


5. overflow 恢复主链

这条链是整个上下文恢复最重要的部分。

第一步:attempt 返回后,外层判断是不是 overflow

外层会看:

  • promptError
  • assistant error 文本

只要错误特征是 context overflow,就进入这条链路。

第二步:看本轮有没有 in-attempt compaction

如果本轮已经 auto-compaction 过:

  • 先 retry 一轮

如果本轮还没 auto-compaction:

  • 再考虑 explicit compaction

第三步:做 explicit compaction

这时会调用:

  • contextEngine.compact(…)

而且压缩前并不是直接拿原始 transcript ,而是会先经过整理。

第四步:如果还是 overflow,就检查 oversized tool results

  • truncateOversizedToolResultsInSession(…)

这不是整体压缩,而是定向裁剪工具调用结果。

第五步:仍不收敛,才给最终错误

这时才会真正返回:

  • context_overflow
  • 或 compaction_failure

6. “head / tail 截断”是什么意思

truncateToolResultText(…) 的策略可以用最通俗的话总结:

默认只保留开头;如果结尾看起来也很重要,就保留开头和结尾,中间整块省略。

为什么有时要保 tail

会在尾部检查这些迹象:

  • error
  • exception
  • failed
  • traceback
  • panic
  • exit code
  • summary
  • result
  • finished
  • JSON 收尾结构

7. pre-compaction snapshot 为什么重要

这也是恢复链里的关键点。

在 prompt 结束后、等待 compaction retry 前,attempt 会先抓一份当前消息快照。

但它不会盲信这份快照,而是会确认:

  • 抓取前没有正在 compaction
  • 抓取后也没有正在 compaction

只有这样,才把它当作:

  • preCompactionSnapshot

它的作用是:

如果后来 timeout 恰好发生在 compaction 过程中,那当前 session 里的消息可能处在“压到一半”的过渡状态。这时优先退回到 compaction 前抓到的那份稳定快照。

这就是 selectCompactionTimeoutSnapshot(…) 的意义。


8. hidden lifecycle

这里只保留最关键的几个:

bootstrap(…)

旧 session 重新进入运行时,engine 有一次冷启动机会。

afterTurn(…)

这一轮结束后的正式生命周期钩子。

ingestBatch(…) / ingest(…)

如果 engine 不支持 afterTurn,还可以退回到更弱的消息级接入。

prompt-local 图片

图片不是普通历史文本,它们是本轮级输入,受单独检测和治理。

session file repair

上下文工程成立的前提, transcript 至少能被读出来。


9. 最终怎么收敛成“放弃”

真正报错之前,系统通常已经尝试过:

  1. 等待 auto-compaction 收尾
  2. retry 一轮
  3. explicit compaction
  4. tool result truncation

只有这些都没收住,才最终收敛成:

  • context_overflow
  • compaction_failure

10. 总结

  1. 预处理、auto-compaction、explicit compaction、truncation 是四种不同动作。
  2. overflow 恢复链不是单次 retry,而是一棵分支恢复树。
  3. tool result truncation 是定向补救,不是整体上下文压缩。
  4. head / tail 的本质就是“保留开头”和“保留结尾”,中间部分省略。
  5. pre-compaction snapshot 是为了防止“压缩压到一半超时”时拿到半成品上下文。

谢谢你的阅读,我们 下次再见!