Skip to content

fix(turn-driver): pin UTF-8 on the Codex session evidence I/O - #6709

Merged
loopx-agent merged 3 commits into
loopx-project:mainfrom
JasonBuildAI:codex/codex-session-utf8-evidence-x2p8
Oct 17, 2026
Merged

loopx-agent merged 3 commits into
loopx-project:mainfrom
JasonBuildAI:codex/codex-session-utf8-evidence-x2p8

Conversation

@JasonBuildAI

@JasonBuildAI JasonBuildAI commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Goal And Delivered Outcome

  • Outcome basis / optional anchor: issue [Bug]: managed Codex Turn reads its CLI event stream with the host locale codec, so a non-UTF-8 host cannot confirm the session identity #6708 (self-contained reproduced defect). No roadmap card or RFC id is claimed.

  • Goal/source and gap: the managed Codex session observed the CLI exec JSONL through Path.open() with no codec, and two durable attempt records were written through os.fdopen() with no codec while their own reader re-reads them as UTF-8 bytes (json.loads(path.read_bytes())). On any host whose default encoding is not UTF-8, the read raises before the native identity is observed, and the Turn then fails closed with Codex did not confirm the expected session identity — a state the class contract deliberately does not repair with a fresh conversation. The shipped UTF-8 file gate could not see either site, because it inspects read_text() / write_text() / builtin open() only.

  • Observable before → after, with the validation row that proves it: on a host whose default codec is cp936, observe() on an ordinary UTF-8 event stream containing non-ASCII model text raises UnicodeDecodeError: 'gbk' codec can't decode byte 0x90 ... before the change and returns the observed identity after it (regression_parity and unit rows below).

  • Reach, checked by searching the tree rather than assumed: CodexAgentSession.observe() has no caller under loopx/**. The production callers are benchmark/runtime/worker.py:510, benchmark/runtime/checkpoint_wake.py:214, and benchmark/runtime/checkpoint_session_refresh.py:80 — the only production caller of prepare_checkpoint_refresh. The ordinary Turn path streams events over a pipe (codex_cli.py) and never performs this file read, so the capability this repairs today is the benchmark wake / checkpoint-refresh harness on a non-UTF-8 host. It is worth repairing here because the failing class is shared control-plane code on the host class this repository ships for (windows-powershell, cp936), the failure is post-spend and non-retryable under the class contract, and the gate rule closes the API-class hole for every future caller.

  • Issue/task and intended base: Fixes #6708, base main (110893bca, rebased).

Author Declaration

  • Written by: model_agent — DeepSeek family, through the DeepSeek Harness agent runtime.

Implemented against

| Criterion (spec clause) | Disposition | Symbol / path | Test or command |

| --- | --- | --- | --- |

| Shipped text file I/O pins UTF-8 explicitly (tests/test_loopx_text_io_utf8.py) | implemented | tests/test_loopx_text_io_utf8.py (_text_io_calls_without_utf8_encoding) | the extended gate reports no Codex site; its self-check covers os.fdopen missing/binary/keyword/positional codec shapes |

| The observed Codex event stream is decoded as the UTF-8 JSONL the CLI writes (CodexAgentSession.observe) | implemented | loopx/control_plane/turn_driver/codex_agent_session.py | pytest tests/control_plane/test_codex_session_utf8_evidence.py |

| A durable attempt record is written with the codec its own reader assumes (prepare_checkpoint_refresh, _fork_native_checkpoint) | implemented | loopx/control_plane/turn_driver/codex_agent_session.py, loopx/hosts/codex_checkpoint.py | protected by the gate rule: reverting both pins leaves this suite green and makes tests/test_loopx_text_io_utf8.py report codex_agent_session.py:173 and codex_checkpoint.py:415. The write is latent rather than reproduced — json.dump escapes non-ASCII by default, so the record bytes are ASCII even under cp936. |

  • Self-check before submission: read the complete diff and both call sites, re-ran the gate's self-check, the focused session/checkpoint suites and ruff, and confirmed with the fix reverted that the new suite fails and the two unrelated failures below reproduce at the base. Deliberately left out: the trajectory-assessment offenders owned by fix(benchmark): preserve trajectory evidence across locales #6691, the packages/** scope the file gate does not scan, and the Path.open() receiver-shape gap the gate cannot decide statically (named under Coverage and gaps).

Scope And Continuation

  • Completed scope and remaining work: complete within this scope. Three codec pins, one gate rule, one focused regression suite.

  • Slice boundary / successor: N/A. The remaining Path.open() receiver-shape gap is documented in the test suite rather than deferred to a new task; the wide packages/** inventory is a separate, larger piece of work and is not claimed here.

Validation

  • Tested revision: e1da32c1cb9409ece520e5b8d8265a10338dace9

  • Run state: finished

  • Input classes: synthetic, public_fixture

| Check kind | Result | Public-safe evidence / limitation |

| --- | --- | --- |

| regression_parity | passed | Baseline (same source, fix reverted): UnicodeDecodeError: 'gbk' codec can't decode byte 0x90 in position 50: illegal multibyte sequence, raised from codex_agent_session.observe at loopx/control_plane/turn_driver/codex_agent_session.py:96. Head: pytest tests/control_plane/test_codex_session_utf8_evidence.py → 2 passed. The non-UTF-8 default codec is the reproduced condition; the downstream RuntimeError: Codex did not confirm the expected session identity is quoted from the class contract in codex_agent_session.py, not independently reproduced on this host. |

| unit | passed | pytest tests/control_plane/test_codex_session_utf8_evidence.py -q → 2 passed. Both cases drive the shipped owners in a child interpreter with -X utf8=0, LC_ALL=C, PYTHONCOERCECLOCALE=0; the child asserts its own default codec is not UTF-8 and then decodes the exact bytes it writes, so the case cannot pass vacuously. On a host whose codec accepts those bytes (latin-1, cp1251) the child reports it and the case skips with the resolved codec named instead of claiming coverage; the probe was checked against cp936, ascii, cp1252, cp1250, cp1257, latin-1, cp1251 and utf-8. |

| integration | passed | pytest tests/control_plane/test_codex_checkpoint_context_refresh.py tests/test_loopx_turn_driver.py tests/control_plane/test_codex_session_utf8_evidence.py -q → 126 passed, 2 failed. Both failures reproduce with the diff reverted: test_redirected_original_output_cannot_recover_an_identity needs symlink privilege on this host, and test_default_agent_session_reuses_planning_across_completed_todos reads the fake Codex executable as text (Path.read_text() on a binary) before any changed code runs. |

| static | passed | ruff check on the four changed paths → All checks passed!; the gate's own self-check case (test_guard_reports_missing_and_non_utf8_codecs) passes with the new os.fdopen shapes. |

| integration | passed | pytest tests/test_loopx_text_io_utf8.py -q -> 2 passed at this head. The 12 pre-existing trajectory-assessment offenders this PR disclosed at cd148933b were fixed by #6691 (560fc66b9, merged before this head) and the rebased base 110893bca carries them, so the gate is green here with nothing deselected; stripping the pins from the two shipped files in a scratch copy still makes it report codex_agent_session.py:173 and codex_checkpoint.py:415. pytest tests/control_plane/test_codex_checkpoint_context_refresh.py tests/test_loopx_turn_driver.py tests/control_plane/test_codex_session_utf8_evidence.py -q -> 126 passed, 2 failed; both failures reproduce with the diff reverted: test_redirected_original_output_cannot_recover_an_identity needs symlink privilege on this host, and test_default_agent_session_reuses_planning_across_completed_todos reads the fake Codex executable as text (Path.read_text() on a binary) before any changed code runs. |

| unit | passed | Review-follow-up head e1da32c1c (rebased on main 110893bca): the same two cases on Linux Python 3.12 (python:3.12-slim, repository mounted at /src) -> 2 passed; with only path.open(encoding="utf-8") reverted in a scratch copy -> 2 failed with UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 36 from the observe read. The earlier heads a786ed7cb and 400d7a1cb failed both cases on that host before importing loopx (Unable to decode the command from the command line), because -c text is decoded with the ASCII filesystem encoding; the child program now travels on stdin (CPython decodes stdin source as UTF-8) and the fixture's workspace is the filesystem-safe tmp_path, while the non-ASCII payload stays in the CLI's UTF-8 event bytes. Windows cp936, focused suite + gate: 4 passed. |

  • Coverage and gaps: the changed behavior is covered end to end — the same child process exercises the observe read and the attempt-record read path, and the gate rule is mutation-checked by the guard's self-check. Not covered: hosts whose default codec decodes the sample (the case names the codec and skips rather than passing for the wrong reason), the two os.fdopen write pins (latent, as above, and guarded only by the gate rule), the packages/** tree the file gate does not scan, and Path.open() receivers whose type cannot be decided statically — the gate still cannot reject that API shape, which is why the regression suite exists next to it. Same-class sites outside this PR's scope, left for their existing owners: trajectory_assessment.py:190/191 (fixed by fix(benchmark): preserve trajectory evidence across locales #6691, 560fc66b9), benchmark/runtime/worker.py:487 and :524 (outside the gate's loopx/ scan root), and the test-side tests/test_loopx_turn_driver.py:1032.

Frontend / Visual Evidence

  • UI impact: none

  • Before: N/A

  • After: N/A

  • States and viewports shown: N/A

  • Source data: none

  • Attention review: N/A — no user-visible surface changed.

  • Craft review: N/A — no user-visible surface changed.

Type of Change

  • Bug fix

  • Test update

LoopX Area

  • Control plane (goals, todos, quota, scheduler, registry, runtime)

  • Host or runtime integration

Technical Direction

  • Direction / acceptance reference, when applicable: core control-plane hardening of an existing owner; no RFC or roadmap id is claimed.

Shared-authority RFC fixture impact

  • Production-scale fixture schema: N/A

  • Semantic dimensions changed, or reviewed no-impact rationale: N/A — no state, protocol or vocabulary change.

  • Provider conformance arms run: N/A

  • Read-only legacy/file/PostgreSQL three-arm rehearsal (required for promotion, runtime-routing, or compatibility-projection changes): N/A

Boundary Checklist

  • Neither the diff nor this PR body/comments/attachments disclose private state, credentials, raw traces or verifier output, internal links, or local machine paths (including .loopx/, .codex/goals/, and live ACTIVE_GOAL_STATE.md).

  • I did not duplicate maintainer-owned benchmark work unless a maintainer split out a public issue for it.

  • I kept the change scoped to the linked issue/task.

  • Existing English/Chinese RFC pairs (including ledger entries) both have a diff, and the local premerge check python3 scripts/check_rfc_bilingual.py --base origin/main --head HEAD passes, or no paired RFC changed.

  • I completed the visual evidence section for UI changes, or marked UI impact none.

  • Every commit includes a DCO Signed-off-by trailer (git commit -s).

AI disclosure: this change was drafted with AI assistance at my request, under my direction and review.

JasonBuildAI added a commit to JasonBuildAI/loopx that referenced this pull request Oct 17, 2026
Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>

@JasonBuildAI JasonBuildAI left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent agent review — PR #6709 at a786ed7cb

Reviewer: an independent agent session (separate context, own scratch copies of the tree), run at the author's request. This is the exact-head review record required for an author-owned PR, because GitHub blocks formal self-approval. It is published by the author on the reviewer's behalf and is not a maintainer approval; it does not grant merge authority. Merge remains with the maintainer, as this changes control-plane runtime behavior.

Reviewed revision: a786ed7cbddb715329edf30885ef736346c5fb18 (base cd148933b), 4 files, +223/−10.
Verdict: APPROVE — no blocker at this head. Round 1 returned APPROVE WITH NOTES against f6b4280d2; both substantive notes were fixed and re-verified on this head, and the third (an optional gate-policy change) is declined with evidence below.

Re-verification of this head (independent scratch copies, shared worktree only read): the observe pin reverted → 2 failed with #6708's exact UnicodeDecodeError, and the failure lands on the child-returncode assert, so the new probe cannot mask a real failure. The probe was shown to measure the same codec the defective read uses (locale.getpreferredencoding(False) = cp936, and the unpinned read raises with error.encoding == 'gbk'). The skip branch cannot mask the defect: forcing the probe to latin-1 while the host codec stays cp936 and the pin stays reverted still yields 2 failed (the child's failure wins), and a faithful latin-1 host simulation with the bug present yields SKIPPED [2] … host default codec latin-1 decodes the UTF-8 event bytes; the default-codec read hazard cannot be reproduced on this host — i.e. mojibake hosts now skip instead of silently passing. The second commit touches only the test file and carries DCO; the two product files are byte-identical to f6b4280d2.

1. Is it worth a maintainer's time?

Yes, and the reach is narrower than the linked report originally implied — the issue has been corrected. The defect reproduces byte-for-byte: reverting only with path.open(encoding="utf-8") makes both new cases fail with the exact error in #6708 (UnicodeDecodeError: 'gbk' codec can't decode byte 0x90 in position 50: illegal multibyte sequence in CodexAgentSession.observe). CodexAgentSession.observe() has no caller under loopx/**; production callers are benchmark/runtime/worker.py:510, benchmark/runtime/checkpoint_wake.py:214, and benchmark/runtime/checkpoint_session_refresh.py:80 (the only production caller of prepare_checkpoint_refresh). The ordinary Turn path streams over a pipe and never performs this read. So the repaired capability is the benchmark wake / checkpoint-refresh harness on a non-UTF-8 host. It is still worth a maintainer's time: the failing class is shared control-plane code, the host class (cp936 zh-CN Windows) is one this repository ships a CI lane for, the failure is post-spend and non-retryable under the class's own contract, and the gate rule closes the API-class hole for future callers. Cost is 3 codec pins + 1 gate rule + 1 focused suite.

2. One problem, or bundled?

Exactly one, cleanly. Two commits: the product one-line pins plus the gate rule, and a test-hardening follow-up. git show and the PR file list agree on 4 paths; no refactoring, no drive-by edits, DCO trailer present on both commits.

3. Logic and edge cases

The new gate rule is index-correct: os.fdopen(fd, mode='r', buffering=-1, encoding=None, …) puts encoding at positional index 3 while mode stays index 1 for the text-mode check. Verified with a shape matrix — flagged: os.fdopen(fd,"w"), mode="w", dynamic mode, "w",-1,"latin-1", encoding=codec, "r+", bare fdopen(fd,"w") (from-import), "w",0,codec; clean: "wb", mode="wb", encoding="utf-8", "w",0,"utf-8", "w",0,"utf-8",newline="", "w",buffering=0,encoding="utf-8". Known gaps, disclosed in the PR body: non-os receivers and aliased imports are missed, Path.open() remains invisible (the actual regression site), and packages/** is outside the scan root.

Non-vacuity: the first version asserted only that the host codec is not UTF-8, which does not prove the CLI's bytes are rejected — latin-1 and cp1251 decode them without raising, so the defective read returns mojibake and both cases would pass with the pin reverted. That was a real vacuity hole and it is now fixed (below).

4. Abstraction beyond need?

None. _is_fdopen plus one index constant is the minimum for the new class. Minor note, not required: the new file's session()/events() helpers restate the sibling tests/control_plane/test_codex_checkpoint_context_refresh.py:13-27 builder.

5. Do the tests prove the fix?

Mutation (independently reproduced) Result
revert path.open(encoding="utf-8") both cases fail with #6708's exact error
revert both os.fdopen(..., encoding="utf-8") pins suite stays 2 passed; the gate reports codex_agent_session.py:173 and codex_checkpoint.py:415
neuter _is_fdopen (forced return False) the guard's self-check turns red (['5','6','7','13'] != ['5','6','7','13','15'])

The observe read is causally covered. The two os.fdopen writer pins are latent: json.dump defaults to ensure_ascii=True, so the record bytes are ASCII even under cp936; they are protected by the gate rule alone. The PR body now says exactly that instead of claiming the recovery case covers them.

6. Lint and tests

ruff check on the four changed paths → All checks passed!. pytest tests/control_plane/test_codex_session_utf8_evidence.py -q → 2 passed. Focused neighbourhood run → 2 failed, 126 passed, both failures pre-existing and reproduced with the diff reverted: OSError [WinError 1314] from original.symlink_to(foreign) (symlink privilege on this host) and UnicodeDecodeError from binary.read_text() at tests/test_loopx_turn_driver.py:1032 (test-side locale bug in no touched path). tests/test_loopx_text_io_utf8.py → 1 failed, 1 passed; the 12 offenders are all trajectory_assessment*.py, owned by open PR #6691 — confirmed by running the unmodified guard from cd148933b against a pristine cd148933b export, which reports the identical 12, while the extended guard against that export reports 14 (those 12 plus the two sites this PR pins) and at this head reports 12 again.

Changes requested, and their disposition

  1. Validation-table accuracy — fixed in the PR body: the os.fdopen pins are stated as gate-rule-only protection, with the reverting mutation as the evidence.
  2. Non-vacuity precondition — fixed in a786ed7cb and re-verified: the child decodes the exact bytes it writes with the resolved default codec and reports the result; the parent asserts the codec is not UTF-8 and calls pytest.skip with the resolved codec named when the sample decodes, so the case can never pass for the wrong reason. Probe over cp936, ascii, cp1252, cp1250, cp1257, latin-1, cp1251, utf-8: the first five reject, the last three decode and skip. The skip was proven in the precedence that matters — with a real child failure the case still fails, and with a mojibake host and the bug present it skips visibly. The "ASCII" wording in the module docstring was corrected to the ANSI code page / ascii under LC_ALL=C.
  3. Gate alias normalisation (codecs.lookup(value).name) — declined in this PR, and the reviewer accepted the reasoning. loopx/** and tests/** contain no encoding="UTF-8"-style spelling (no match for the alias search), so no existing site pays the false-positive cost, and exact-match is the pre-existing, uniform policy for open() / read_text() / write_text(); changing it for every call shape is off this PR's theme and alters inherited gate behaviour on surfaces outside the reported defect. Recorded here as a follow-up candidate rather than a merge condition.

Non-blocking notes recorded during review

  • A host whose default codec is UTF-8 still hard-fails the encoding != "utf8" assertion, while a permissive non-UTF-8 codec skips. Left as is deliberately: a UTF-8 default means -X utf8=0 / LC_ALL=C did not take effect, which is worth failing loudly on.
  • The probe predicts the hazard from the codec rather than by reading the file; that is correct on this host (cp936 resolving to the read's gbk) and on POSIX both come from the same locale state.

Residual risks for the maintainer

  • The two writer pins are latent (no independently reproduced write failure today); they align the write codec with the read_bytes() reader.
  • The gate still cannot see Path.open() receivers, aliased os imports, or packages/**.
  • The required python-tests lane is red on main from #6691's 12 offenders (.github/workflows/python-tests.yml:586 runs the whole tree with no deselect), so a green gate cannot be demonstrated until #6691 lands; this PR neither causes nor can fix that.
  • Same-class sites left to their owners: trajectory_assessment.py:190/191 (#6691), benchmark/runtime/worker.py:487 and :524, tests/test_loopx_turn_driver.py:1032.
  • The Release Artifacts build lane fails at "Exercise the installed page in a real browser" on every recent PR head (checked on another contributor's head), unrelated to this Python-only diff.

@loopx-agent loopx-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: model_agent; model=gpt-6.1-sol; provider=OpenAI; runtime_reported; reasoning_effort=max

完整评审 6709@a786ed7cbddb715329edf30885ef736346c5fb18,实际 base cd14893。REQUEST_CHANGES:三个生产codec修复成立,但新增两项locale回归测试在Linux进入产品代码前就失败。

动机

在非UTF-8宿主上使用Codex检查点唤醒或恢复的操作者,需要将已执行工作绑定回原会话。

以前中文UTF-8事件会被宿主ASCII/代码页解码而失败;现在真实文件观察及原输出恢复可读回正确会话身份。

macOS真实ASCII默认codec的同一输入在base两例解码失败、head两例成功;新Linux测试自身却在进入产品前失败,仍须修正。

不改变native身份规则、准入、机会、调度或模型权限,不认证完整Windows/App恢复、科学收益或动态Path.open全树静态识别。

本PR新增locale测试的POSIX启动及物理路径须修复;现有全树12项UTF-8违规属于未变的其他owner,不应并入本修复。

改动思路

在既有Codex文件IO owner固定codec,并扩展原UTF8门;无需全局locale策略、另一session解析器或新恢复版本。

现有Codex文件IO修复加同类fdopen门是完整可回退的小范围增量,Linux测试回归应在本PR内闭合。

现有CodexAgentSession从CLI输出文件观察原生会话ID;checkpoint refresh读取原attempt记录及原输出,不能因结果未知就开新会话。将file reader固定为UTF-8即可对齐CLI实际JSONL,无需全局修改locale、再建解析器或增加重试状态。两个attempt writer同时声明其既有byte reader依赖的codec,原exclusive600、flush/fsync、basis检查和nativefork顺序均保留。

当前真实生产caller是benchmark worker/wake及checkpoint refresh,普通Turn通过pipe流式读取,没有调用这个文件observer。这个窄边界仍有价值:模型已做的工作不能在读回身份时丢失。既有意图和权限下零新增本人交互;不要求重配环境或再确认。AST门扩展复用现有测试,不引入运行时locale策略。三参数的产品成本与后置身份失败相称,但新测试也必须在所声明平台确实到达产品边界。

具体改动

四路径+223/-10全部已读;两个生产文件只有三个显式codec参数,另为186行child-interpreter fixture和原UTF8 AST门。两个普通提交均DCO。未修改状态schema、nativeID规则、机会/配额、模型配置或前端交互。

关键代码讲解

  1. CodexAgentSession.observe(91行)使用path.open(encoding="utf-8"),随后JSON解析、恰一个且符合原绑定的ID检查和现有session提交均不变。非法/缺失身份仍按原错误退出,修复不提供fresh-fork后门。
  2. prepare_checkpoint_refresh(134行)只给os.fdopen加codec;重放仍读取原basis和original_stdout,再调用同一observer。实际恢复case得到原输出的resumed身份,retry_launched=false,未生成第二次执行输出。
  3. _fork_native_checkpoint(296行)在dispatch前仍以exclusive600创建原attempt并fsync;显式UTF8不改变原inventory、未知fork结果或destination的验证。两个writer当前json.dump默认转义非ASCII,因此属于潜在codec契约修复,测试主要通过静态门保护,不能声称已独立复现写坏JSON。
  4. _is_fdopen(89行)与_pinned_encoding在既有AST扫描中识别descriptor形态,encoding位置是index3,mode仍在index1;binary调用跳过,缺失、错误或动态codec报告。现有guard self-check通过。alias、无法静态判型的Path.open和packages范围仍是披露的边界,不要求本PR扩成全树类型推断。

先读#6708、base的class docstring和 tests/test_loopx_text_io_utf8.py,spec_revision: cd14893。text-file-UTF8 implemented:三个实际IO点与原byte reader一致;native-identity implemented:当前ID和original-output恢复规则保留。POSIX新增测试失败属于本次验证回归,不能由规范说明或Windows单平台通过替代。

对主干的风险

[P2] 先让强制ASCII的POSIX测试到达产品代码

位置tests/control_plane/test_codex_session_utf8_evidence.py:132–134,_drive把含原始中文字的_CHILD直接作为python -c参数,同时关闭UTF8模式和locale coercion。Linux此时的filesystem encoding是ASCII;CPython先解码该命令行程序,报Unable to decode the command from the command line / UnicodeEncodeError ... surrogates not allowed,两例尚未import LoopX就失败。这不是被修复的observer仍错,也不是已有12项UTF8门红灯。

已用当前文件逐字节相同副本、只读当前生产源码、Linux Python3.12、network-none容器独立运行:2 failed。另外把相同child保存为UTF8脚本后,确实越过命令行程序解码,但中文workspace参数在ASCII filesystem下成为surrogate,随后现有file-lock地址UTF8编码又先失败;所以只换脚本传输仍不完整。

最小修复让child程序以ASCII安全方式或UTF8脚本/stdin传输,并对ASCII-POSIX使用filesystem安全的物理测试路径,同时保留事件内容为真实非ASCII UTF8。保持codec非UTF8、hazard、exactID、original-output和无新retry全部断言,不用整体skip Linux或放松断言掩盖问题。私有验证只改测试的这两个输入条件、完全不改产品源码,即在同一Linux边界得到2 passed;它是最小修复的证据,尚不是作者PR中的修复。修正后还须保持实际base解码红/head绿。

其他证据分开判断:macOS实际ASCII默认text codec下,同一child在完整base的observe/recover均为UnicodeDecodeError,在head均返回正确ID;当前四文件邻域为129 passed /1 failed。唯一该组失败是全树UTF8门:不可变base原规则12项、相同base扩展规则14项、head扩展规则12项;原12项路径/行号逐个相等且均在未改trajectory owner,新规则正确去掉两个writer遗漏。这是独立归因的旧红灯,不要求本PR捆绑修它。初次Linux baseline导出缺非PythonJSON资源造成的setup失败保留,未当作codec反例。

Ruff通过;advisory在完整semantic之前运行,0candidate不证明全部语义安全,fullsemantic由标准canary实际执行。标准canary 6 direct+13 selected全部通过,公私边界与compile/hygiene通过;它没有执行这两个Linux子进程用例,所以不能抵消新增P2。没有查询、触发或等待CI,也未启动benchmark、native模型或付费调用。原作者公开独立review的Windows观察有其范围;本次结论来自新的完整审阅及实际POSIX反例,未继承其verdict。

我的整体评价

long_horizon=improved、user_experience=improved限已验证的生产文件读取与原输出恢复:不再丢掉已执行工作的身份,没有新增配置、输入或确认。没有新增共享词汇或第二决策源;实际OS/provider IO留在既有Python owner合理,修复不需要无关TypeScript重写。

future-facing检查认为当前三个codec点和原AST门已足够;可选复用邻近测试builder不值得新框架。整体方案成本合理、保留历史attempt格式,已有scope与来源限制说得清楚。当前必须先修同PR的Linux测试启动/路径问题,再重验原codec red/green;这与全Windows/App原生恢复、旧trajectory门及长期收益各自独立。

English verdict: REQUEST_CHANGES - a786ed7: the three production UTF-8 pins fix the real observer/recovery defect, but both newly added tests fail before the product on Linux when forced-ASCII CPython decodes a Unicode -c program; a Unicode workspace argv is a second fixture obstacle. Exact current Linux2fail versus test-only safe-transport/path2pass reproduces the issue. Preserve the non-vacuous base-red/head-green assertions. Existing12 trajectory gate offenders are independently baseline-equivalent; macOS129 neighborhood cases and6direct/13selected canaries pass in their stated scope.

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Review follow-up at 400d7a1cb1e7eed977ba924fb12443bbf6493fdb

The P2 raised on a786ed7cb is correct and is now fixed. Only the test fixture changed; loopx/control_plane/turn_driver/codex_agent_session.py, loopx/hosts/codex_checkpoint.py and tests/test_loopx_text_io_utf8.py are byte-identical to a786ed7cb.

Reproduced on this host

Linux Python 3.12 (python:3.12-slim, repository mounted at /src), a786ed7cb:

FAILED tests/control_plane/test_codex_session_utf8_evidence.py::test_codex_events_are_read_back_with_a_locale_codec
FAILED tests/control_plane/test_codex_session_utf8_evidence.py::test_checkpoint_refresh_attempt_record_is_readable_again
AssertionError: Unable to decode the command from the command line:
  UnicodeEncodeError: 'utf-8' codec can't encode characters in position 273-287: surrogates not allowed

No LoopX import ran — the fixture, not the repaired read, matching your report.

Fixed in the fixture

  • The child program travels on stdin (python -X utf8=0 -), which CPython decodes as UTF-8 and which keeps the working directory on sys.path the way -c did. Nothing requires the ASCII filesystem encoding to decode the program any more. Checked independently of PYTHONIOENCODING on that host: sys.stdin.encoding == 'ascii' while the non-ASCII source still decodes.
  • The workspace is tmp_path instead of tmp_path / "项目". Under the forced ASCII filesystem encoding the child addresses the workspace as str, so a non-ASCII path would fail on the surrogate escape before the codec hazard — the second obstacle you named. The non-ASCII payload now lives only in the CLI's own UTF-8 event bytes and in the attempt record, i.e. in the bytes the codecs under test decode.
  • Assertions unchanged: codec is not UTF-8, the locale_hazard precondition/skip is intact, and the exact identity, the original-output readback and retry_launched is False are still asserted.

Evidence at this head

Check Result
Linux 3.12 forced-ASCII, tests/control_plane/test_codex_session_utf8_evidence.py 2 passed
same host, only path.open(encoding="utf-8") reverted 2 failed with UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 36 — base-red/head-green preserved
Windows cp936 host, same suite 2 passed
tests/test_loopx_text_io_utf8.py on Linux 1 failed, 1 passed; offenders are still exactly the 12 trajectory_assessment*.py sites (head codex offenders: []), the pre-existing red owned by #6691
gate mutation, pins stripped from the two shipped files reports codex_agent_session.py:173, codex_checkpoint.py:415
test_codex_checkpoint_context_refresh.py + the new suite Linux 13 passed; Windows 12 passed, 1 failed (pre-existing WinError 1314 symlink)
ruff check on the four changed paths All checks passed!

Status

  • Scope, product code, gate rule and the PR-body claims are unchanged. The only behavioural difference in the fixture is that it no longer exercises a non-ASCII path; the codec defect never needed one and the path shape could not reach the product under a forced ASCII filesystem encoding.
  • loopx canary premerge --from-git-diff on this Windows host is not offered as green evidence for this head: it reports failures in install-local, codex-cli-packaged-install, loopx-update, hot-path-interface-budget, catalog-planner and pytest-smoke-suite-facade. The only changed path is tests/control_plane/test_codex_session_utf8_evidence.py, and the reasons I can see are environmental — hot-path-interface-budget stops at extension budget-presentation-extension doctor is not ready: entrypoint_missing, and running catalog-planner-smoke.py directly passes. I did not run the canary against the base to separate those, so a maintainer/CI judgement on them is still needed; they are not evidence about the repaired read.
  • Nothing here claims merge authority: this changes control-plane runtime behavior, so merge stays with the maintainer.
  • Re-review requested on 400d7a1cb; reviewDecision is still CHANGES_REQUESTED from the previous head.

The managed Codex session observed the CLI exec JSONL through Path.open()
without a codec, so a host whose default encoding is not UTF-8 (cp936 on
zh-CN Windows, or an ASCII locale on POSIX) decoded the UTF-8 event stream
with the locale codec. The read raised UnicodeDecodeError at the first
non-ASCII event, the thread.started identity was never observed, and the
Turn failed with "Codex did not confirm the expected session identity",
which the class contract refuses to repair with a fresh conversation.

Pin UTF-8 on that read and on the two descriptor writes whose JSON is read
back with json.loads(read_bytes()): the checkpoint-refresh attempt record
and the native fork attempt record, whose basis carries the guest
workspace and home paths.

The shipped file gate did not inspect os.fdopen(), so those writes were
invisible to it; the gate now reports text-mode os.fdopen() without a
UTF-8 codec, matching the sites that already pin it.

Fixes loopx-project#6708

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
The non-UTF-8 default-codec case asserted only that the host codec is not
UTF-8, which does not prove the CLI's UTF-8 bytes are rejected: a
single-byte codec such as latin-1 or cp1251 decodes the sample without
raising, so the defective read returns mojibake and both cases would pass
with the codec pin reverted. The child now decodes the exact bytes it
writes and reports the result, and the case skips with the resolved codec
named where the hazard cannot be shown here.

Also state per-case coverage: the recovery case exercises the observe read
and the attempt record's byte read, while the two os.fdopen write pins are
protected by the gate rule alone, because json.dump escapes non-ASCII by
default and their payload is ASCII today. Verified by reverting both pins:
this suite stays green and the gate reports exactly
codex_agent_session.py:173 and codex_checkpoint.py:415.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
@JasonBuildAI
JasonBuildAI force-pushed the codex/codex-session-utf8-evidence-x2p8 branch from 400d7a1 to 7828ece Compare October 17, 2026 08:06
@JasonBuildAI

JasonBuildAI commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Rebased onto current main, and the file gate is green now — e1da32c1c

Two updates on top of the follow-up above.

  1. Rebase. The branch is rebased onto main 110893bca (54 commits), same three commits, DCO trailers intact. This picks up fix(benchmark): preserve trajectory evidence across locales #6691 (560fc66b9), which merged after the original base cd148933b. The pre-existing tests/test_loopx_text_io_utf8.py red this PR disclosed is therefore gone at this head: 2 passed, nothing deselected, and the extended guard reports head total offenders: 0.
  2. The review's P2 stays fixed on the rebased head, and the gate line numbers did not move: stripping the pins from the two shipped files still reports codex_agent_session.py:173 and codex_checkpoint.py:415.

Re-verified at e1da32c1c (later pushes amend the test docstring only; no code change):

Check Result
Linux 3.12 forced-ASCII: new suite + tests/test_loopx_text_io_utf8.py 4 passed
same host, only path.open(encoding="utf-8") reverted in a scratch copy 2 failed with UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 36 from the observe read
Windows cp936: new suite + gate 4 passed
Windows neighbourhood (test_codex_checkpoint_context_refresh.py, test_loopx_turn_driver.py, new suite) 126 passed, 2 failed — the same two pre-existing failures (symlink privilege; test-side read_text() on a binary)
Linux test_codex_checkpoint_context_refresh.py + new suite 13 passed
ruff check on the four changed paths All checks passed!

Scope is unchanged: three codec pins, one gate rule, one focused suite. reviewDecision is still CHANGES_REQUESTED from a786ed7cb, so re-review is requested on this head. Nothing here claims merge authority — this changes control-plane runtime behavior, so merge stays with the maintainer.

@JasonBuildAI
JasonBuildAI force-pushed the codex/codex-session-utf8-evidence-x2p8 branch from 7828ece to 422e2ec Compare October 17, 2026 08:18
Both cases passed the child program as a `-c` argument. CPython decodes
`-c` text with the filesystem encoding, which this fixture forces to ASCII
(`LC_ALL=C`, `-X utf8=0`, `PYTHONCOERCECLOCALE=0`), so on Linux the child
died before importing LoopX: `Unable to decode the command from the
command line` / `UnicodeEncodeError: ... surrogates not allowed`. The
Windows lane passed only because its filesystem encoding is UTF-8.

The program now travels on stdin, which CPython decodes as UTF-8 and which
keeps the working directory on `sys.path` the way `-c` does. The workspace
is the pytest temporary directory rather than a path with a non-ASCII
component: the child addresses it as `str` under that ASCII filesystem
encoding, so such a path would fail on the surrogate escape before the
codec hazard this suite exists to reproduce. The non-ASCII payload stays
in the CLI's UTF-8 event bytes and in the attempt record, so the codec
assertions are unchanged.

Reported by the independent review of a786ed7. Reproduced on Linux
Python 3.12: a786ed7 fails both cases at the command-line decode; this
head passes both, and reverting the observe codec pin still fails both with
`UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 36`.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
@JasonBuildAI
JasonBuildAI force-pushed the codex/codex-session-utf8-evidence-x2p8 branch from 422e2ec to e1da32c Compare October 17, 2026 08:20

@loopx-agent loopx-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: model_agent; model=gpt-6.1-sol; provider=OpenAI; runtime_reported; reasoning_effort=max

完整评审 6709@e1da32c1cb9409ece520e5b8d8265a10338dace9,实际 merge-base 110893b。APPROVE:当前完整提案在下述已验证范围内未发现剩余代码阻断。

动机

在非UTF-8宿主上读回Codex会话或恢复原输出的操作者,需要保住已完成工作的原生身份。

以前中文UTF-8事件会被宿主ASCII解码而失败;现在相同事件能读回正确会话,恢复仍使用原输出,不另起一次执行。

当前Linux相同fixture在不可变base两例解码失败、head两例通过;macOS四文件邻域130项通过,原生身份与未知结果的四组base/head对照保持一致。

不改变会话准入、重试或模型权限,不认证完整Windows/App原生恢复、动态Path.open全树推断或科学收益。

改动思路

既有Codex文件IO owner和UTF8门足够,三个codec参数及Linux fixture修正闭合这次故障,无需全局locale策略或新恢复框架。

当前PR是可独立回退的文件证据读取修复及其跨平台回归门,完整原生宿主资格仍不由它宣称。

实际缺口发生在已产出结果的文件读回:observer读取CLI的UTF8 JSONL,却默认使用宿主codec。仅改变全局locale会影响所有调用方;新建session解析器也会复制身份与重试规则。当前在原IO点声明UTF8,沿用原binding、准入、attempt和原输出恢复链。两个writer保持exclusive600、flush/fsync和先落盘后dispatch,没有增加重试。

最强反对理由是两个writer现在由json.dump转义成ASCII,不能将它们包装成已独立复现的写坏JSON。作者已把这点写清;这里接受它们作为既有byte reader的codec契约及静态门保护。普通Turn的pipe路径没有用这个文件observer,不能把局部验证扩大为所有原生宿主已资格通过。

具体改动

四路径 +235/-10全部已读,三个普通提交均有DCO。先读#6708、原class契约和基线 tests/test_loopx_text_io_utf8.py;spec_revision: 110893b。text-file-UTF8 implemented由真实Linux red/green和完整门支持;native-identity implemented由原绑定/未知结果的实际base/head对照支持。没有从补丁新写的说明反推验收。

  1. CodexAgentSession.observe(91行)只将path.open固定为UTF8,随后仍要求恰一个符合绑定的ID,再经原admission更新binding/receipt。坏事件不会许可另开会话。
  2. prepare_checkpoint_refresh(134行)固定原attempt descriptor的编码。恢复仍从原attempt.read_bytes()得到原stdout,再观察该文件;不同basis、重定向路径或未知结果仍拒绝。
  3. _fork_native_checkpoint(296行)固定dispatch前的attempt writer编码,原source inventory/provenance与“模糊结果不得第二次fork”的约束均保留。它不是新增fork权限。
  4. tests/control_plane/test_codex_session_utf8_evidence.py的198行fixture把child源代码送入stdin,并使用ASCII物理临时路径,避免强制ASCII的CPython在进入LoopX前解码-c或路径失败;事件内容仍是真实中文UTF8,非UTF8/hazard、精确ID、原stdout和无retry断言没有放松。既有AST门的_is_fdopen/encoding index3扩展识别descriptor text IO,binary仍跳过。

对主干的风险

旧评审5556716705指出的两个Linux fixture障碍在当前头已独立验证消除。使用与当前fixture逐字节相同的副本、只读生产源码、Linux Python3.12、禁网容器:当前 2 passed;同一fixture和环境对不可变实际base是 2 failed,都在真正的observer读事件时得到ASCII UnicodeDecodeError。这次base失败是所修复缺陷的证据,不再是进入产品前的fixture错误。

同一套独立四组对照在base/head均通过:ASCII原ID成功;不匹配或多个ID保持完整的身份错误;原输出缺失保持完整unknown-result错误且没有retry文件。私有探针初次用了macOS临时目录的symlink别名,两版都在既有physical-path边界正确拒绝;解析到物理路径后才执行并比较四组,不把setup错误算产品回归。

macOS四文件邻域 130 passed,包括完整UTF8门。把当前扩展AST规则分别用于实际base/head全树,只有base的两个descriptor writer违规,head为零;旧评审中的12项trajectory门失败已不在本次集成基线内,不能继续把历史红灯冒充当前证据。Ruff、advisory及标准canary均通过,advisory在full semantic之前执行;canary 6 direct+13 selected均通过。未查询、触发或等待CI,也未启动benchmark、native模型或付费运行。

仍保留明确范围:permissive单字节codec无法复现decode hazard时fixture显式skip;这里实际Linux/macOS是hazard成立的ASCII执行。动态Path.open receiver、os alias及packages范围仍不是这个AST门的全部能力。完整Windows/App宿主恢复未在本机运行,两个writer是门保护而非独立写坏反例。

我的整体评价

long_horizon=improved、user_experience=improved限本次文件证据链:已执行工作能读回原身份,未知结果仍停在原owner,没有新增本人准备、重输信息、确认或重试轮次。当前修复落在既有Python provider IO合理,没有把通用状态决策另做一份Python owner,也没有新增共享词汇。

future-facing pass检查了相邻IO和门:扩展原检查比增加新包装/框架更合适,当前没有必须并入的相关重构。整个边界可独立回退,跨平台fixture与真实reader已闭合旧P2;这不等于全原生宿主或持续科研收益已经证明。已有评审记录应保留,合并资格和实际授权另行判断。

English verdict: APPROVE - e1da32c: both previously reported Linux fixture obstacles are fixed without weakening the hazard or identity assertions. Identical real-Linux inputs fail at the product decoder on the immutable base and pass both current cases; 130 local neighbors, four unchanged identity/unknown-result pairs, Ruff and all6direct/13selected canaries pass. Writer pins remain gate-protected latent contracts; full Windows/App/native-model qualification is outside this evidence.

@loopx-agent
loopx-agent merged commit f266ec8 into loopx-project:main Oct 17, 2026
34 of 40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants