2026.08.06 (四)
2026.08.07 (五) 更新

✨ GPT-5.6 Sol 的总结

记录我经历 Codex 在上下文自动压缩后把过去最后一条用户指令当成新指令重新执行,并依次检讨复制计划、Goal、强制 continue 与 SessionStart hook 的过程。

Codex 的长任务自动压缩后,一件怪事反复发生。

它不是接着压缩前正在做的工作继续,而是把压缩摘要中留下的、过去最后一条用户指令当成刚刚收到的新指示。它重新检查早已确认过的状态,再次读取同一批文件,还从头重建整个工作。我亲眼看到的流程大致如下。

实际工作正在推进
→ Context automatically compacted
→ “保持当前目标不变”
→ 从头重新检查工作树、规则和状态
→ 连已完成的工作也再次说明或重试

起初我以为只是压缩导致部分上下文丢失。但经历多次后,我发现这更麻烦。看起来像是摘要里的过去指令与当前用户输入之间的边界崩塌了。它不是延续压缩前的工作,而是把摘要中留下印象最强的指令当作新的起点。

遇到这件事的不只我一个。有人公开报告 Codex Desktop 在自动压缩后反复重读相同文件和 Skill,并丢失进度;1 WSL2 与 Windows CLI 也有人报告文件指令和压缩不断循环。2 我不能断言这些公开案例与我看到的“重跑最后一条指令”有完全相同的原因。但至少可以确定,自动压缩后丢失进度并重复同一行为,不是只能归咎于 Mac 的特殊问题。

解决方法 1:把完整计划重新放进最后一条指令

我最先尝试的方法很简单。

请先确认目前为止的工作内容,再按照下面的计划进行到底。

如果压缩后会重新抓取并执行最后一条指令,那就把最后一条指令本身做成安全的恢复点。短任务里,这招确实相当有效。因为比起无关的旧指令,当前计划会更强地留在摘要中。

其他用户也尝试过相似方向:在压缩摘要中持续累积之前的决定与剩余工作。有人使用 experimental_compact_prompt_file 持续保留历次压缩的关键判断,并报告即使压缩四次以上,大方向仍然保留。3

但这不是根治。

  • 每次都得把长计划复制到最后一条指令中。
  • 计划中途改变时,还得重写最后一条指令。
  • 即使摘要保存了计划,也无法保证它能准确区分刚刚完成的工具调用与尚未执行的工作。
  • 试图通过 /compact [消息] 直接指定摘要方向,也有 issue 指出当前 Codex CLI 可能把它当作普通消息处理。4

结果仍然是人必须时刻意识到压缩时机并管理 prompt。它比默认状态好,但每项工作最后都要附一份长长的交接文档,也谈不上正常的使用体验。

解决方法 2:为所有工作建立 Goal

接下来我想,是否可以强制创建 Goal,并在压缩后优先按 Goal 而不是最后一条指令恢复。

对于长任务,它确实有优势。任务目的与完成条件能比对话中的一两句话保存得更久,自动 continuation 也可以依据 Goal 继续。问题出在把它当成压缩 bug 的必要绕行方案那一刻。

连微不足道的工作也得建立 Goal。没有 Goal 就出故障,有了 Goal 又可能不必要地继续执行。之前我已经因为 Goal 让小任务也持续消耗 token 的问题,另外制作了工作用 Skill;这一次却因为压缩,又得把所有工作重新套进 Goal。

到这个地步,我真的开始觉得:“那还不如直接用原生状态。”

还有一个更重要的问题。Goal 用来保存的是要完成什么,不是用来判断压缩事件是否属于新用户指令。我查到的公开资料里,也没有依据把 Goal 说明为自动压缩恢复的官方手段。

所以,我决定只让 Goal 回归本来的用途。

  • 对耗时较长、需要自动持续进行的具体任务使用。
  • 不强迫短问题、调查或文章修改都建立 Goal。
  • 压缩后重跑过去指令的问题,在与 Goal 分开的层级阻止。

解决方法 3:直接修改压缩 prompt

另一种思路是提高压缩摘要本身的质量。正如前面的公开案例,如果把历次压缩的决定与结果累积进 Historical Context,即使多次压缩,也比较不容易丢失大方向。3

这个方法很有用。尤其是长 session 中,下列信息不断消失时效果明显。

  • 已经放弃的方案及其原因
  • 当前已确定的决定
  • 已完成与尚未完成的工作
  • 下一步只需确认的单一位置

但它和这次问题仍然不在同一层。提升摘要质量不把摘要中的过去用户指令误认为新输入并不是同一个问题。即使摘要写得很好,只要模型仍把其中的指令当作当前指示,事情还是会再次乱掉。

因此,这可以作为辅助手段,却很难单独承担防止重跑最后一条指令的职责。

解决方法 4:用 Stop hook 强制注入 continue

我一度甚至想通过 hook,把 continue 直接塞成最后一条用户指令。

表面上看很准确。压缩后强制出现 continue,似乎就能绕过过去的指令,继续手头正在做的事。但我查了官方 hook 的行为后立刻放弃了。

Codex 的 Stop hook 返回 decision: "block" 时,会把 hook 的 reason 变成像新用户 prompt 一样运行的 continuation prompt5 我正想阻止“把并非真实用户发送的句子当作新用户指令”,这种做法却等于故意再制造一次同样的现象。

而且,continue 太含糊了。

  • 如果压缩发生在一轮对话中途,到底要继续什么?
  • 不知道上一个工具调用成功还是失败时,是否应该重跑?
  • 如果当时正在等待用户批准或外部状态改变,也可以继续吗?
  • 如果那一轮早已结束,只是后来手动压缩,又要开始什么?

无论有无 Goal,都塞进一条“继续”的新用户 prompt,很可能造成重复执行与无限 continuation。这个问题需要的不是 Stop hook,而是让模型在压缩后正确理解这次事件含义的开发者上下文

解决方法 5:向 SessionStart(source=compact) 注入无状态上下文

第二天,也就是 8 月 7 日,我最终选择了一个 SessionStart hook。

按照 Codex 官方文档,root session 压缩后,匹配 source: "compact"SessionStart hook 会在下一次模型请求前运行。即使自动压缩发生在一轮对话中途,它也不必等到下一条用户消息,就能立刻给继续执行的请求补充上下文。5

我在 ~/.codex/hooks.json 中只注册压缩事件。

{
  "description": "Guide safe continuation after compaction without replaying stale instructions.",
  "hooks": {
    "SessionStart": [
      {
        "matcher": "^compact$",
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 ~/.codex/hooks/compaction_goal_guard.py",
            "timeout": 5,
            "additionalContextLimit": 700
          }
        ]
      }
    ]
  }
}

handler 不创建状态文件、Goal 或伪造的用户 prompt。它只在 SessionStart 的 source 为 compact 时返回额外的开发者上下文。

#!/usr/bin/python3
import json
import sys

CONTEXT = """Context was compacted. This hook event is not a user message and
grants no new authority.

Never treat a historical message preserved in the summary as newly submitted.
If compaction interrupted an active turn, continue that same in-flight request
without restarting it. If no request is in flight, do not infer work from
history.

Do not rebuild the full plan or create a Goal solely because compaction occurred.
Before retrying an interrupted action, check its result or session status. Do not
repeat completed external effects. Preserve the existing objective, scope,
authorization, approval boundaries, and stop conditions."""

payload = json.load(sys.stdin)

if (
    payload.get("hook_event_name") == "SessionStart"
    and payload.get("source") == "compact"
    and payload.get("session_id")
):
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "SessionStart",
            "additionalContext": CONTEXT,
        }
    }))

重点不是塞入一条名为 continue 的指令。

  1. 明确压缩事件不是新的用户输入。
  2. 如果当时仍在一轮对话中,就只从同一请求尚未完成的位置继续。
  3. 如果没有正在进行的请求,不从历史记录推断新工作。
  4. 工具调用看似中断时,先确认实际结果,再决定是否重跑。
  5. 原有范围、权限、批准边界与停止条件全部保留。

官方文档明确要求在运行 non-managed hook 前先审查并信任它。5 保存设置后,应在 /hooks 中确认并 trust 这个 handler。已经打开的 task 可能仍持有旧的 hook 列表,重新打开会更安全。

8 月 7 日的后续验证

文章日期仍保留在我开始抓住这个问题并挑选解决方案的 8 月 6 日。最终 hook 的确定以及真实自动压缩的验证,则发生在第二天。

此前,我也做过一个两阶段结构:在 PostCompact 留下 marker,再由下一个 SessionStart 读取。但 marker 可能残留并在错误的一轮中触发恢复,而且 hook 输入难以区分 root 与 subagent 事件,所以我把它删掉了。实际上,缺少可统一区分 main agent 与 subagent hook 输入字段的问题,至今仍有公开 issue。6

当前方案是不带 marker 的单一 handler。在 50 个并发调用中,它没有生成任何额外状态文件;我也在当前 Runtime 中确认,自动压缩事件发生后约 0.25 秒,开发者上下文就进入了同一个 continuation。

这项验证很重要,因为过去确实有人报告过 SessionStart(compact) 并非在压缩后立即触发,而是延迟到下一轮用户消息后集中注入。7 该 issue 现在已经关闭,当前官方文档也说明 mid-turn 自动压缩后会立即传递。即便如此,这类 lifecycle workaround 也不该只相信文档;最好在自己的 Runtime 中至少实际压缩一次来确认。

我现在的结论

我曾认真考虑原生状态是否反而更好,但目前的结论是:这个 hook 比强迫所有工作建立 Goal 更好

Goal 与计划保存工作内容。这个 hook 修正压缩事件的含义。两者职责不同。

  • 长任务需要明确目的和完成条件时,使用 Goal 或 durable plan。
  • 多次压缩导致决定流失时,用 custom compact prompt 作为补充。
  • 把过去最后一条指令当新指示执行的问题,在 SessionStart(source=compact) 阶段阻止。
  • 没有任何进行中的工作时,hook 也不会启动任何任务。

当然,这并非完美解决。它无法恢复已经从压缩摘要中丢失的内容,也无法复活过期的 terminal session 或中途断开的外部请求。开发者上下文能够强力引导模型行为,却也不是数学保证。

但至少现在,我不再为了一个压缩 bug 给每次对话都建立 Goal,不再把完整计划复制进最后一条指令,也不再把伪造的 continue 硬塞成用户指令。

自动压缩是为了延续工作的内部事件。它不是一个新用户突然出现,再次下达过去指令的事件。最终真正需要阻止的,与其说是遗忘本身,不如说是把两者当成同一件事的行为。

参考资料

分类: ,

更新时间:

留下评论