Repository navigation
Description
Summary
util.force_utf8_stdio() reconfigures sys.stdout and sys.stderr to UTF-8, but not sys.stdin. On a Chinese-locale Windows install Python decodes stdin with the legacy ANSI code page (GBK / cp936) plus surrogateescape. Any caller that speaks the JSON-lines API over stdio and writes UTF-8 bytes therefore gets silently mis-decoded text: no exception, no warning, just a query that matches nothing.
Reported by @SunnyRad in dsh-sessionflow PR #5. This is a Core-side defect and is not part of, and not caused by, dsh-sessionflow PR #5.
Environment
- Windows with a Chinese system locale (ANSI code page cp936 / GBK)
- Python without
PYTHONUTF8=1and withoutPYTHONIOENCODINGoverriding stdin - sessionFlow Core at
1.3.1(verified on the currentmain,ccfd33b) - A JSON-lines
voyager apicaller that writes UTF-8 (the DSH bridge, the VS Code client, or any MCP-style stdio client)
Reproduced in a controlled environment by forcing PYTHONIOENCODING=gbk:surrogateescape (equivalent to what a Chinese Windows console does by default). All data below is synthetic; no real user session content is involved.
Expected / Actual
| transport | query | expected | actual |
|---|---|---|---|
voyager search 会话 --json |
会话 |
1 hit | 1 hit (correct) |
voyager api stdio, {"op":"search","params":{"query":"会话"}} |
会话 |
1 hit | 0 hits, results: [] |
same api call, stdin forced to UTF-8 (control) |
会话 |
1 hit | 1 hit (correct) |
The failing response echoes the decoded query back, which exposes the misdecode:
{"id": 1, "result": {"query": "浼氳瘽", "count": 0, "results": []}}
会话 in UTF-8 is e4 bc 9a e8 af 9d; decoding those six bytes as GBK yields 浼氳瘽. The query never matches because the wrong characters are being searched for.
With a Chinese repo value the same misdecode is worse: overview returns a silent empty list (the filter matches no garbled row), while search surfaces 'utf-8' codec can't encode character '\udcaf' ... surrogates not allowed from the surrogate-escaped bytes. In both cases 0 results / [] is indistinguishable from "there genuinely is nothing".
Root cause
voyager/util.py:31-56 — force_utf8_stdio():
for stream in (sys.stdout, sys.stderr):
...
reconfigure(encoding="utf-8", errors="replace")sys.stdin is absent from that loop. voyager/api.py:544 (serve()) reads requests with for line in sys.stdin:, so the JSON payload is decoded with the process's legacy stdin encoding before json.loads ever sees it. The fix that shipped for the stdout side (DSH bridge decoding stdout as UTF-8) covered only the output half of the same transport boundary.
Adding sys.stdin to the reconfigured streams resolves all three observations above (verified: 会话 returns 1 hit, the CJK repo empty list and the surrogate error both disappear).
Impact
- Any UTF-8 JSON-lines
apiclient on a non-UTF-8 Windows locale: CJK / non-ASCIIquery,repo, and other string params are silently corrupted. - Read-mostly operations fail quietly as empty results — the worst failure mode, because it looks like an empty index.
voyager apiis the transport used by the VS Code client and stdio-based integrations; the DSH bridge is one such caller.- CLI argv-based commands are unaffected (Windows passes argv as UTF-16), which is exactly why
voyager search 会话 --jsonworks while the equivalent API call does not.
Acceptance criteria for a fix
Keep it minimal and at the process boundary; do not blanket-set an encoding for the whole program without checking every input path.
- stdin, stdout and stderr are all UTF-8 at the
api/ CLI entry points. voyager apiround-trips a CJKqueryand a CJKrepopath from UTF-8 stdin bytes.- Windows GBK code page: no silent misdecode, no surrogate escapes leaking into JSON.
- JSON special characters (quotes, backslashes, embedded
=, control characters) still parse. - Invalid UTF-8 input produces a clear error rather than a traceback or a silently empty result.
- MCP stdio entry point (
mcp_server.py, which also callsforce_utf8_stdio()) stays compatible. - stdout/stderr behavior from the existing
tests/test_utf8_stdio.pyregressions is unchanged. - Tests must spawn a real subprocess and assert on bytes, not monkeypatch
sys.stdinin-process: the bug lives in the child's locale-dependent decoding, which an in-processio.TextIOWrapperdouble cannot reproduce. Cover both the GBK-misdecode case and a UTF-8 control case.
Scope note
This issue is independent of dsh-sessionflow PR #5. PR #5 was a plugin-side fix to argv construction and argument validation; it neither introduced nor can fix this Core-side stdin decoding. It was found while running PR #5's integration tests against a real Core. Do not fold this into that PR.
No real session content is reproduced here — the reproduction uses a synthetic one-session index.
Activity
HarryHeYu commentedon Nov 11, 2026
@SunnyRad 这个 issue 是按你在 dsh-sessionflow PR #5 里的发现建的,已标
help wanted。原始评论链接和合成索引的字节级复现都写在正文里了(e4bc9ae8af9d按 GBK 解成浼氳瘽、api返回 0 行而 argv 路径命中 1 行)。如果你愿意接手,这个修复就交给你:
force_utf8_stdio()的 stdin 一侧、中文 query / 中文 repo 路径、stdout/stderr 与 MCP stdio 入口的回归,以及真实子进程的字节级测试。我这边不会同时提交同一修复,避免抢占你正在准备的工作;你什么时候方便就什么时候推,不需要固定时间。如果有更合适的做法(比如不动
force_utf8_stdio、改在api.serve()按字节读 stdin 再显式解码),也完全可以在 PR 里讨论。验收要点列在 issue 里。另外:PR #5 已 Squash Merge(
b3fc6b9),作者归属保留为 SunnyRad。谢谢你的实机证据和三个修复。HarryHeYu commentedon Nov 11, 2026
补充:核对 CI 时发现 @SunnyRad 已在 PR #19(
fix/utf8-stdin,87cd92a3)提交了修复,早于本 issue。本 issue 保留为缺陷与验收标准的记录,实现以 PR #19 为准,不需要重复认领或重复提 PR。PR #19 还额外报告了一个独立缺陷:文档与 VS Code 扩展都写
voyager api serve,但两个 parser 都不认这个动词(python -m voyager.api serve从引入起就 exit 2),导致 VS Code 侧边栏子进程在读到第一个请求前就退出。它和 stdin 编码是两件事,Review 时应分开评估,并在 CHANGELOG / release notes 里分别列明。本轮未合并 PR #19(未经审查的 PR 不在授权范围内),也未由维护者并行实现同一修复。Core
v1.3.1的发布门槛仍保持阻塞,直到 PR #19 完成 Review、CI 与合并。SunnyRad commentedon Nov 11, 2026
谢谢登记 issue #21,也谢谢把 dsh-sessionflow PR #5 Squash Merge(
b3fc6b9)并保留署名。按你第二条评论的最新状态确认一下,方便 Review:不重复认领、不动 main。 实现以 PR #19(
87cd92a3)为准,我不会另开 PR、不重复提修复、也不碰main。核对过你ae45580/02e0760f两个 commit 只改了 ROADMAP 与审计报告,没有并行改代码——「维护者不并行实现同一改动」这条你确实落实了,感谢。一个小更正:两个缺陷其实都已修,不只是「报告」。 你写「PR #19 还额外报告了一个独立缺陷……Review 时应分开评估,并在 CHANGELOG / release notes 里分别列明」。PR #19 里
serve动词在两个 parser(voyager/api.py::main()与voyager/cli.py的apisubparser)已作为 no-op 接受,未知动词仍 exit 2;CHANGELOG.md的### Fixed下两个缺陷也已并列成两条、各自成段。所以「分别列明」这部分已落地,Review 时按两条独立改动看即可;release notes 是发布侧产物,随你那边出。关于实现位置,issue #21 的验收标准倾向「不动
force_utf8_stdio、改在api.serve()按字节读 stdin 再显式解码」。PR #19 取的是另一条路:force_utf8_stdio()补上sys.stdin(幂等),并在api.serve()——三条入口唯一真正读 stdin 的位置——再调一次,这样voyager api/python -m voyager.api/python -m voyager.api serve全覆盖,MCP 等其它调用方也一并受益。如果你更希望按字节读 + 显式解码(能更直接满足「无效 UTF-8 明确报错」),我可以在 PR 里改,听你的。PR #19 对照验收标准的覆盖度(供 Review 取舍,不涉及改代码):回归测试用真实子进程、
input=request.encode("utf-8")、env PYTHONIOENCODING=gbk、断言count而非回显,另有test_force_utf8_covers_stdin与test_force_utf8_tolerates_none_stdin。已覆盖:中文query往返、stdin/stdout/stderr 全 UTF-8、fd 0 关闭、stdout 既有回归不变。未专门测:中文repo路径 /overview空列表、无效 UTF-8 输入 → 明确报错(当前errors="replace"会出 U+FFFD 而非报错)、JSON 特殊字符、MCP stdio 入口显式回归。这些要不要补进 PR #19、还是合并后另开小 PR,按你的节奏,我不会在你 Review 前主动扩范围。CI:PR #19 分支的
testsworkflow(42845178098)仍是action_required/jobs=0(首次外部贡献者待批准);main上42961042487的 success 是文档 commit 的,与 PR 分支无关。分支上的独立证据要等你批准 workflow 后才有。本机全量1376 passed, 7 skipped, 2 failed仅作旁证,2 个失败是test_scan_streaming.py硬编码E:/code/voyager(已在 issue #20 另记,pristine main 上同样复现),与 PR 代码无关。等你的 Review 结论,随时配合。
HarryHeYu commentedon Nov 11, 2026
Fixed by Core PR #19 —
fix(util): decode stdin as UTF-8, not the ANSI code page— which addssys.stdintoforce_utf8_stdio()so the JSON-linesapitransport decodes UTF-8 payloads correctly on non-UTF-8 Windows locales (GBK / cp936). With the fix, a UTF-8query/repoover stdin round-trips instead of being silently mis-decoded into a zero-result search.Thanks to @SunnyRad for the original real-world report against dsh-sessionflow PR #5 — that thread is what surfaced this Core-side stdin gap. Closing as resolved in v1.3.1.