You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On a Windows host whose ANSI code page is not UTF-8 (a zh-CN host with cp936/GBK),
LoopX code paths that read structured state files through Path.read_text()without an
explicit encoding decode them with locale.getpreferredencoding(False), which is gbk.
JSON (RFC 8259) and TOML are UTF-8 by specification, so any file carrying a non-ASCII
character is either rejected or silently mis-decoded.
bytes GBK cannot decode -> UnicodeDecodeError, which is a ValueError subclass, so
callers that catch (OSError, ValueError) swallow it and report a misleading secondary error;
bytes GBK can decode -> silent wrong data: 用户任务 reads back as 鐢ㄦ埛浠诲姟.
Same class as #4155, different code path. #4396 pinned UTF-8 for every remaining text-mode
subprocess read and closed #4155; the Path.read_text() / Path.write_text() path was not
covered by that sweep.
Expected behavior
Text reads and writes of structured state files (.json / .toml) decode and encode as
UTF-8 regardless of the host's ANSI code page. Concretely:
loopx goal-portfolio --manager-view portfolio --format json succeeds on a cp936 host
whose registry.json carries a non-ASCII knowledge_root / state_file, instead of
returning {"ok": false, "error": "manager_evidence_unavailable_or_invalid", "rows": []};
a record written by collaboration/inbox.py can be read back by the same module on the
host that wrote it.
Reproduction — real command, real output
A project registered under a non-ASCII path is sufficient: registry.json carries filesystem
paths (project_record.knowledge_root, goal_record.state_file), and non-ASCII directory
names are ordinary for a zh-CN user.
Root cause: loopx/capabilities/manager_context/evidence_export.py:12 reads the registry
without an explicit encoding.
Scope — 10 sites in loopx/
Measured with an AST scan over the package (a grep count is misleading here, because the
tree uses multi-line calls):
Kind
Count
Path.read_text() without encoding
9
Path.write_text() without encoding
1
Most consequential:
loopx/control_plane/collaboration/inbox.py:56 — the module's own _write uses encoding="utf-8"andensure_ascii=False, so a record written by LoopX cannot be read
back by LoopX on a non-UTF-8 host.
loopx/capabilities/connector_registry/core.py:149 (write side) — save_connector_registry
writes with the locale encoding while load_connector_registry reads as UTF-8. On cp936
the write produces GBK bytes, the read raises, except (OSError, ValueError) swallows it,
and the loader silently falls back to built-in defaults.
Add an explicit encoding="utf-8" to those 10 product-code call sites — one keyword per site,
no control-flow change. I am not proposing to touch the ~449 matching call sites under tests/, examples/ and benchmark/ in the same change; they are a separate slice and I am
happy to take them separately if you would rather.
Separate decision, not in this change
CPython ships EncodingWarning for exactly this pattern (PEP 597). Run pytest with -X warn_default_encoding plus filterwarnings = error::EncodingWarning and the class is
caught rather than the instances. Measured on this host:
against the unfixed call sites under benchmark/tests/, it fails the same 5 tests that PYTHONUTF8=1 fixes — so it does detect exactly this defect;
against two test files whose modules are already fixed (17 passed + 18 passed before),
it reports 18 failed / 7 passed / 10 errors — the currently-passing tests go red, because
the ~449 unfixed sites under tests/, examples/ and benchmark/ are inside the same run.
So enabling the gate is a CI-wide decision that first requires that cleanup to land. Flagging
it here, not proposing it. (ruff's PLW1514 covers part of it — 6 of these 10 — but it is
preview-only today.)
If you would rather have the ~449 sites fixed first and the gate enabled in one go, I am happy
to take that as a follow-up slice instead of this one.
Summary
On a Windows host whose ANSI code page is not UTF-8 (a
zh-CNhost withcp936/GBK),LoopX code paths that read structured state files through
Path.read_text()without anexplicit
encodingdecode them withlocale.getpreferredencoding(False), which isgbk.JSON (RFC 8259) and TOML are UTF-8 by specification, so any file carrying a non-ASCII
character is either rejected or silently mis-decoded.
UnicodeDecodeError, which is aValueErrorsubclass, socallers that catch
(OSError, ValueError)swallow it and report a misleading secondary error;用户任务reads back as鐢ㄦ埛浠诲姟.Same class as #4155, different code path. #4396 pinned UTF-8 for every remaining text-mode
subprocess read and closed #4155; the
Path.read_text()/Path.write_text()path was notcovered by that sweep.
Expected behavior
Text reads and writes of structured state files (
.json/.toml) decode and encode asUTF-8 regardless of the host's ANSI code page. Concretely:
loopx goal-portfolio --manager-view portfolio --format jsonsucceeds on acp936hostwhose
registry.jsoncarries a non-ASCIIknowledge_root/state_file, instead ofreturning
{"ok": false, "error": "manager_evidence_unavailable_or_invalid", "rows": []};collaboration/inbox.pycan be read back by the same module on thehost that wrote it.
Reproduction — real command, real output
A project registered under a non-ASCII path is sufficient:
registry.jsoncarries filesystempaths (
project_record.knowledge_root,goal_record.state_file), and non-ASCII directorynames are ordinary for a
zh-CNuser.{ "ok": false, "error": "manager_evidence_unavailable_or_invalid", "rows": [] }Exit status
1. Nothing in the output points at encoding.The only variable is the encoding default. Same fixture, same command, same machine:
loopx goal-portfolio ...{"ok": false, "error": "manager_evidence_unavailable_or_invalid", "rows": []}PYTHONUTF8=1 loopx goal-portfolio ...{"ok": true, "view": "portfolio", ...}Root cause:
loopx/capabilities/manager_context/evidence_export.py:12reads the registrywithout an explicit
encoding.Scope — 10 sites in
loopx/Measured with an AST scan over the package (a
grepcount is misleading here, because thetree uses multi-line calls):
Path.read_text()withoutencodingPath.write_text()withoutencodingMost consequential:
loopx/control_plane/collaboration/inbox.py:56— the module's own_writeusesencoding="utf-8"andensure_ascii=False, so a record written by LoopX cannot be readback by LoopX on a non-UTF-8 host.
loopx/capabilities/connector_registry/core.py:149(write side) —save_connector_registrywrites with the locale encoding while
load_connector_registryreads as UTF-8. Oncp936the write produces GBK bytes, the read raises,
except (OSError, ValueError)swallows it,and the loader silently falls back to built-in defaults.
loopx/codex_cli_runtime_probe.pyL172/183/190/197 — the same file where fix(runtime): pin UTF-8 decoding for every remaining text-mode subprocess read #4396 pinnedL222/275 (subprocess) but left these four (file read) untouched.
Planned change
Add an explicit
encoding="utf-8"to those 10 product-code call sites — one keyword per site,no control-flow change. I am not proposing to touch the ~449 matching call sites under
tests/,examples/andbenchmark/in the same change; they are a separate slice and I amhappy to take them separately if you would rather.
Separate decision, not in this change
CPython ships
EncodingWarningfor exactly this pattern (PEP 597). Run pytest with-X warn_default_encodingplusfilterwarnings = error::EncodingWarningand the class iscaught rather than the instances. Measured on this host:
benchmark/tests/, it fails the same 5 tests thatPYTHONUTF8=1fixes — so it does detect exactly this defect;17 passed+18 passedbefore),it reports 18 failed / 7 passed / 10 errors — the currently-passing tests go red, because
the ~449 unfixed sites under
tests/,examples/andbenchmark/are inside the same run.So enabling the gate is a CI-wide decision that first requires that cleanup to land. Flagging
it here, not proposing it. (ruff's
PLW1514covers part of it — 6 of these 10 — but it ispreview-only today.)
If you would rather have the ~449 sites fixed first and the gate enabled in one go, I am happy
to take that as a follow-up slice instead of this one.