原生会话的上下文记忆
每个对话的原生 CLI 会话,Cloud Agent 都保存在这个对话的保留沙箱里。对话历史由 CLI 自己管理,Agent 只是把用户的每条消息原封不动地转过去。
"上下文丢了"其实要分成两个问题看:同一个原生会话有没有保住?模型有没有根据历史正确回答?九月的调查发现,两种问题都有。调查复现了原生会话持久化的 bug,但也发现:有时请求里的历史明明是完整的,模型却说自己看不到之前的内容。
会话的整体架构见原生 Fork 与共享工作区设计。本页依据的合成复现和本地验证结果,保存在一份带日期的调查记录里;那些结果不代表任何远程部署的现状。
上下文状态
每个轮次都会报告这个对话的原生上下文是否还在:
上下文状态 |
含义 |
|---|---|
|
这个成员之前还没有完成过任何原生轮次 |
|
这一轮用的是仍在运行、且兼容的 CLI 进程 |
|
新起的进程接上了保留下来的原生会话 |
|
无法确认所需的原生状态或容器完整性,拒绝执行 |
停止容器时,它的可写层会保留下来。但如果容器本身丢了,或者 Docker 的数据目录丢了,所需的原生状态也就跟着丢了。唯一的例外是:这个沙箱组之前是正常停止的,并且有一个最新的可恢复检查点。这样当原来的主机被移除、被回收,或者在租期结束前迁出时,另一台主机就能把它恢复出来。靠重放对话记录来恢复,从来都不在考虑之列。
其他原生 CLI
五个内置 CLI 都支持在查询失败后保留会话,供后续消息继续使用,前提是三件事都能确认:原生轮次确实已经结束,激活时的清理已经完成,传输通道和原生定位信息都还能用。
CLI |
判断原生轮次已结束的依据 |
|---|---|
Claude Code |
|
Codex |
当前原生轮次 ID 对应的 |
Pi |
|
OpenCode / Kilo |
原生会话空闲时,bridge 为当前轮次发出的 |
命令被拒绝、传输断开、超时或被取消,都不算原生轮次正常结束。Pi 遇到终止性的 API 错误时报告为失败,而不是一个成功但内容为空的轮次。OpenCode 和 Kilo 依据消息的角色元数据,只收集 assistant 自己的文本;轮次结束时再处理迟到的元数据,并排除之前 bridge 轮次留下的事件。这样用户的文本不会混进 assistant 的输出,原生模型的历史也不受影响。
固定版本的 Claude 2.1.220 CLI 有一个已复现的缺陷,出在重试时的父消息链上:一个迟到的 system/api_error,可能让之后的恢复跳过一条重试成功的 assistant 回复。所以 driver 一看到 system/api_retry,就结束当前轮次、保存原生定位信息,然后让这个进程正常退出。等退出中的进程把历史写完,下一轮就在同一个容器里接上同一个会话;如果退出时没法确认进程已经结束,这一轮照样结束,下一次启动前会先回收这个成员的槽位。没有发生重试的成功轮次,进程照常保留。这个处理无法修复旧版本已经弄坏的对话记录。
沙箱里的辅助程序必须固定为 http-agent-live-bridge=4,主机证明会检查这一点。版本 4 要求每个轮次都挂在当前有效的 SSE 订阅上;这条流无法重放,一旦断开,就中止正在进行的原生轮次。它还会把 OpenCode 和 Kilo 的自动权限回复,扩展到根轮次明确报告的 Task 子会话上;一次性的权限回复失败时会重试,再失败就中止,不会让请求一直挂着。版本 3 加入了与原生提问一一对应的回复。runner、Sandbox Manager 和沙箱镜像要一起部署;旧版辅助程序的镜像不能对新主机报告就绪。
这次发布解决了什么,还剩什么
这次的实现解决了已复现的、由重试和刷新导致的历史丢失;五个 CLI 中,凡是能确认已结束的失败轮次,会话都会保留下来。同时还处理了 Pi 的错误报告、OpenCode/Kilo 的 assistant 消息归属,以及 runner 和 Sandbox Manager 两侧的会话身份。本地的 Responses 夹具和冒烟检查也在这项工作里一并修正了。
调查记录里还有两项另外的发现:
发现 |
证据与局限 |
|---|---|
发出的消息完整,Flash 仍可能说看不到历史 |
在原生调用和直接的合成 API 调用里都复现了。调高推理强度、换新版 CLI 都不能稳定解决;仅凭客户端的证据,也查不出原因到底在上游的推理引擎、模板还是模型本身。 |
网关快照在转换时丢掉了推理强度配置和历史中的 thinking 块 |
已在本地复现。本仓库以外的几个独立补丁,这次调查没有在部署环境里验证过;它们也不能说明 Flash 的记忆问题已经解决。 |
HTTP 请求成功,不代表模型真的记住了内容;原生会话保存下来了,也不代表这一点。这些修复不会迁移已经损坏的对话记录,也不会处理旧的一次性对话。如果旧对话的历史没有原生 shared-live 成员关系,请新开一个对话。
上线验收前的检查
CI Gate 必须通过,其中包括固定版本原生 CLI 的请求历史回归测试。这些测试覆盖几种场景:仍在运行时发送后续消息、立刻关闭再恢复、调用失败后重试、内部重试成功后再做原生恢复;在每种场景下逐一比对用户和 assistant 的原文、角色、顺序以及重复出现的次数。Sandbox Manager 和 runner 的检查还必须做到:无论是成功的轮次,还是已结束的失败轮次,只要定位信息变了就拒绝。回环夹具不需要推理凭据,也不产生费用。
按部署可靠性里的方法,核对主机和 API 的确切 SHA、反向 sandbox manager 探测,以及最近一次心跳。模型能正常调用,不代表正在处理任务的就是你预期的那个主机版本。
在目标版本的本地仓库里,对目标推理网关运行这项可选的行为预检:
CLOUD_AGENT_TEST_API_KEY_FILE=/path/to/test-key \ uv run python -m deploy.preflight.check_model_context \ https://gateway.example.com --repetitions 3
CLOUD_AGENT_TEST_PROXY可以指定一个 HTTP 代理,不设也行。密钥只留在受信任的测试进程里,不会进入沙箱凭据,也不会出现在命令参数中。脚本会向deepseek-v4-flash发送合成消息,打印回答和请求 ID,不打印认证头。用例分五类:打招呼、重复提问、任务、事实及其更新、没有历史;每重复一次产生五次调用,默认重复三次,共 15 次调用,按正常推理用量计费。只要出现非 200 响应、流错误、内容被截断、缺少应有的事实,或者识别到"看不到历史"之类的说法,检查就失败。脚本会请求原生的
adaptive/high设置,并且要求能观察到 thinking。结果里recall_passed和thinking_observed分开报告;即使没有 thinking,回答也会保留。观察到 thinking,不能证明引擎收到了准确的推理强度;没有 thinking,也不能证明历史丢了。检测器比较保守,失败时要人工看一下:模型如果为了纠正一个错误说法而引用了它,也可能被判失败。只看到 HTTP 200 是远远不够的。在部署好的应用里实际试一遍:新开对话、在运行中发送后续消息、空闲后恢复。确认用的是同一个保留沙箱和同一个原生定位信息。如果某次回答说上下文丢了,先比对发出去的请求历史和轮次结束的依据,再判断是不是持久化出了问题。
这项预检需要主动运行,部署工作流不会自动跑。CI 拿不到推理密钥。网关的模型路由、模型部署或 CLI 的固定版本变了,就要重新跑一次。有限的样本只能检验已经报告过的那个问题,没法保证模型以后不会再说错。
实现和检查代码:runner.py、drivers.py、shared_session_supervisor.py、shared_protocol.py、check_model_context.py 和 test_claude_driver_context.py。