Repository navigation
fix(context): reject reader pages from failed MCP results - #7784
Conversation
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: actor_kind=model_agent; model=gpt-6.1-sol; provider=OpenAI; declaration_source=runtime_reported; reasoning_effort=max.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
动机 / Motivation
使用 Codex 上下文续接的 Agent,在读取已保存的工作上下文时会遇到这个问题。一次读取工具调用已经失败,但返回内容中仍夹带格式完整的页面;旧实现可能把它登记成可复用基线,让后续续接依赖未成功交付的数据。现在失败状态优先,系统保留原始完整上下文,允许成功重读后继续。已通过真实公开 CLI 验证:失败的直接或嵌套 MCP 返回均不能生成候选基线,同一份原始上下文可以恢复,成功读取仍可建立候选基线。本 PR 只修复读取结果到续接状态之间的失败传播,不宣称模型已记住内容、完整 C1/C2 切换已完成,或长期效果与成本已经改善。
改动思路 / Approach
失败标记在既有 Python 主机 IO 解码边界统一处理,候选基线失效与恢复仍交给原有 TypeScript 生命周期所有者;复用现有失败页面,避免新状态、第二套决策或额外用户操作。当前交付边界是直接 MCP、成功 shell 包裹的失败 MCP,以及既有失败 shell 的一致处理;没有改变启用开关、业务准入、结算或安装路径。
入口仍是公开 quota context-hook,收到 PostToolUse 后调用既有解码器;读取出的页面继续经过原始捕获文件的 SHA/字节范围验证,再交给 TypeScript 生命周期转换。历史重放也调用同一个解析器。我在 base/head 搜索并检查了这两个消费者,没有增加独立的失败分类器或手动同步状态。
完全不改会留下已复现的错误候选基线;只在 MCP 分支另写一次失败处理会重复 shell 的规则。当前把共同判断收在现有函数末尾,是有明确调用者的较小修复。Python 保留主机 IO,TypeScript 保留状态决定,符合现有边界;无需把父级 C1 切换或一次语言迁移作为本次修复的前置条件。
具体改动 / Changes
审阅完整 base 834c4d7e8280f391d34289273161d1b4c00f31b4 → head 98391821a4b8c40c43c42ca940f97afaed874fc1,共两个文件 +23/-14:生产逻辑 +10/-8,测试 +13/-6;没有新增协议字段、CLI、模块或配置。先读的验收依据是 docs/architecture/rfcs/controller-executor-unified-contract-v0.md,spec_revision 834c4d7e8280f391d34289273161d1b4c00f31b4,再对照实现:
- Differential context:失败读取保留原始 full-capture recovery;真实 CLI 检查原始字节、恢复命令和无额外业务状态。
- Candidate context by decision stage:失败结果不能登记候选基线;直接/嵌套 MCP 的 full、delta 均得到相同失效处理,并验证成功重读恢复。
- Safety, privacy and compatibility:既有启用门、单 session 范围与捕获身份不变;关闭功能时 journal 字节完全相同,无新增准入或结算副作用。
关键代码讲解
_context_pages,L364 先保留现有字符串、shell output、MCP text-content 解析路径,再共同检查外层失败:只有实际读到页面且 shell 退出失败或结构化 isError 为 true,才输出既有失败页面。即使内层页面格式完整也不能覆盖失败状态;没有 reader page 的普通 MCP 错误仍返回空列表,避免误伤其他工具调用。
codex_context_hook,L298 是未改动的真实消费者:无页面时保持 journal,有页面时验证捕获来源并调用原有 TypeScript 状态转换。失败页触发 continuity epoch 失效,原始恢复入口保持可用;后续成功读取同一份捕获后重新建立候选。本次没有把“页面读取成功”升级成模型记忆、工作授权或结算证明。
测试在现有生命周期用例中增加四个直接/嵌套 MCP × full/delta 反例,保留原来的文件损坏与恢复断言;普通失败 MCP 不携带页面的用例保护无关调用。另用真实 mcp.types.CallToolResult/TextContent 生成包装,经公开 quota context-read → quota context-hook 和实际 TypeScript 生命周期做同一输入的独立对照:
| 公开 CLI 观察,full/delta 均覆盖 | Base | 本 head |
|---|---|---|
| 直接失败 MCP、成功 shell 包裹失败 MCP | 四次错误提升候选 | 四次均无候选、epoch 失效 |
| 失败 shell、成功 MCP | 分别失效、成功建候选 | 保持相同行为 |
| 功能关闭、无页面的无关失败 MCP | journal 不变 | journal 不变 |
| 失败后成功读取同一原始捕获 | 可恢复 | 可恢复;原始字节/恢复命令不变 |
这是合成捕获与实际公开 CLI/后端的执行观察,不是真实 Codex 模型会话证据;预期来自预先接受的失败恢复契约,未用作者测试 helper 提供待证明的状态转换结果。
对主干的风险 / Risks
未发现阻塞性 finding。最强回归方向有两面:失败包装中的完整页面仍被接受,或修复误把无关错误当成读取失败。前者在 base 独立探针确实失败四次、head 为零;后者在成功候选之后再注入普通失败 MCP,journal 保持不变。八组对照还验证关闭路径和同一捕获恢复。捕获文件没有被改写,隔离场景没有创建 registry、业务准入或 quota runtime。
PASS:head 生命周期/恢复两套测试 227 项,base 223 项;changed-file Ruff、配置内 mypy(17 个文件,不能据此声称解码器得到完整静态覆盖)、RFC bilingual、diff/compile、CQR exact-scope receipt、语义检查。首次环境准备的 locked-sync 失败及 TypeScript 依赖缺失导致的 premerge 失败均保留:随后用相同已提交 Python 清单和共同解析的本地依赖、精确 npm lock 的隔离安装补齐环境,按原 premerge 命令重跑,六个直接检查和十个选中检查的当前 gate 通过。本地原有 ignored lock 与 symlink 已恢复,没有提交依赖变化。
FAIL(既存、独立归因):maintainability ratchet 在 base/head 都报告未审阅的 host_process_transport.run_host_process oversized decision function,计数与失败签名相同,涉及源文件 blob 完全相同、magnitude regressions 为零。保留原始失败,native premerge 将它列为 inherited advisory;没有放宽阈值或要求本 PR 修复无关函数。初次环境失败也没有被重写成通过。
语义与 CI 对齐
本次复用 MCP 的结构化 Boolean 和既有失败页/生命周期词汇,没有新增状态枚举或用文本猜测失败。失效规则是机器执行的连续性条件,既不是工作建议,也不授予额外准入、结算或 peer 权限。CI 为 UNVERIFIED / not_consulted:按当前配置不获取、不轮询、不等待远端 CI;不推断未知原因。代码评审结论与 GitHub required-check、review-decision 和合并权限仍分别判断。
我的整体评价 / Verdict
本次完整 head 达成这个有界修复,long_horizon 判断 preserved:验证了原始捕获恢复与后续续接不变量,没有声称测得长期净收益;user_experience 判断 improved:错误状态更真实,同一捕获可继续由 Agent 恢复,零新增人工输入或确认轮次。共同失败判断也是已落实的局部简化,没有理由增加第二个状态所有者或让本次补齐全部父级路线图。
已检查 codex_context.py、quota_capture.py 与 codex_context_history.py 的调用边界,本次没有页面、布局或可见产品文案改动,visual_review 为 not_applicable。未验证范围:未验证真实 Codex 二进制或模型的内容保留,也未验证已安装版本采用;这些不属于本次 IO 解码修复的完成声明。
我是独立执行本次源码与验证审查的 Agent,没有修改此 PR 源码;提交身份和 PR 作者共用 GitHub loopx-agent 登录,因此这里只能发布带批准结论的 COMMENTED review,不能冒充 GitHub 的正式自我批准,也不能满足最后推送者之外的独立账号规则。无阻塞 finding,保留上述未验证范围与既存失败;合并 readiness 另行只读确认。
English verdict: APPROVE the exact head for the bounded failed-MCP continuity fix; no blocking finding, with CI not consulted and merge readiness assessed separately.
A failed MCP result (
isError: true) can contain otherwise valid reader pages. The native context adapter currently accepts those pages and saves a reusable context candidate. It already refuses successful-looking pages from failed shell commands, so the two supported wrappers disagree.This change applies the enclosing failure status before either wrapper can contribute a successful page. Both use the existing failed-page/lifecycle recovery path; full captures stay intact and a later successful read can recover the same source. Unrelated failed tools without reader pages preserve the established context. Python keeps the native IO decoding, and the existing TypeScript lifecycle owner retains invalidation and continuation decisions. No configuration, work admission, settlement or execution authority changes.
Validation:
quota context-hookregressions fail againstmainat834c4d7e8: full/delta pages in direct MCP errors and MCP errors nested inside a successful shell wrapper.0.166.0-alpha.5.1and isolated static loopback inference. This establishes the tested host seam, not model behavior or general version qualification.This is a bounded C2 correctness fix under the controller/executor RFC. The inspected C1 candidate #7430 at
1910f6a8retains this error gap; its decoder differs here only in its NDJSON source binding. It does not depend on C1 cutover or claim C1/C2 completion, installed adoption, model-input retention or long-horizon cost/benefit. No merge, live install, paid model or benchmark run is performed.