Skip to content

fix(windows): make redirected output UTF-8, independent of the codepage - #567

Merged
padak merged 2 commits into
mainfrom
fix/windows-redirected-output-utf8
Aug 11, 2026
Merged

fix(windows): make redirected output UTF-8, independent of the codepage#567
padak merged 2 commits into
mainfrom
fix/windows-redirected-output-utf8

Conversation

@padak

@padak padak commented Aug 11, 2026

Copy link
Copy Markdown
Member

kbagent semantic-layer --help and kbagent context exit 1 with UnicodeEncodeError on Windows the moment their output is piped or redirected, and any Rich table truncated by width emits a lone 0x85 where its ellipsis should be.

The split that matters is terminal vs not — not which codepage is active

I assumed at first this was a cp1250/cp852 problem and that the fix would have to trade a crash for mojibake on the console. Measuring it says otherwise. Since PEP 528, CPython writes to a real Windows console through the console API, so an interactive kbagent already reports utf-8 and renders anything:

stdout encoding "↔"
real console (isatty=True) utf-8 encodes fine
pipe / file (isatty=False) cp1252 UnicodeEncodeError

Redirect it and that path is gone — Python falls back to the locale encoding, which cannot represent an arrow, an em dash, or a box-drawing glyph. Running the same command under four different chcp values produces byte-identical output, because the console codepage has no say once stdout is a pipe.

So the people hit by this are scripts, CI, and AI agents capturing output — this CLI's stated primary audience — while the interactive users who would notice a change never see it.

The fix

Reconfigure stdout/stderr to UTF-8 only when the stream is not a terminal.

Terminals are deliberately left untouched. Forcing UTF-8 bytes at a console whose codepage is cp852 would replace a working display with mojibake, which is why the naive "just force UTF-8 everywhere" version of this fix is wrong. Redirected Windows output now matches POSIX byte for byte, including the box-drawing characters Rich had been downgrading to ASCII.

#546 fixed this same class for --json by writing bytes straight to sys.stdout.buffer. The human/Rich path cannot use that escape hatch, because Rich owns the writes — hence the stream-level fix.

The call sits at import rather than in the root callback: Click renders --help while parsing, before any callback runs, and --help is one of the crashing surfaces.

Verified on a real Windows 11 machine

Piped, before and after, same commands:

semantic-layer --help context
before CRASH, 14309 B of traceback CRASH, invalid UTF-8
after clean, 3962 B of actual help clean, valid UTF-8

Previously-mojibake surfaces (project list, permissions list, --help, --json changelog) all now emit valid UTF-8.

And the no-regression half — under a real console, with the fix in place:

console_handle=True
semantic-layer --help    rc=0 clean
context                  rc=0 clean
project list             rc=0 clean

Tests

Seven tests covering the contract on every platform: a redirected non-UTF-8 stream is switched; a terminal is left alone; an already-UTF-8 stream (POSIX, and a real Windows console) is untouched across three spellings; the error handler is preserved (surrogateescape is what round-trips undecodable filename bytes); and streams that lack reconfigure or refuse it do not take the CLI down.

Local suite: 5410 passed, 0 failed.

Notes


Open in Devin Review

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Open in Devin Review

Comment thread src/keboola_agent_cli/cli.py
Comment thread src/keboola_agent_cli/changelog.py Outdated
padak added a commit that referenced this pull request Aug 11, 2026
Same rule that was raised on #567: CONTRIBUTING requires a (since vX.Y.Z) entry
in the agent-facing behaviour log for anything an agent would not infer from
--help. Three of these qualify and none had one.

Two commands failed on EVERY Windows invocation before 0.80.1 and worked
everywhere else, so an agent seeing the report needs to know it is the
platform, not the project -- including what to do on an older kbagent, which
for --idempotency-key means not trusting it at all, since it never recorded an
entry there.

The doctor change is the one an agent can actually mis-handle: config_file went
from a permanent, unfixable warn to pass on Windows, so anything treating warn
as actionable was reporting a problem with no remedy.
padak added 2 commits August 11, 2026 17:35
`kbagent semantic-layer --help` and `kbagent context` exit 1 with
`UnicodeEncodeError` on Windows the moment their output is piped or redirected,
and any Rich table truncated by width emits a lone `0x85` for its ellipsis.

The split that matters is terminal vs not, not which codepage is active. Since
PEP 528 CPython writes to a real Windows console through the console API, so an
interactive kbagent already reports `encoding=utf-8` and renders anything --
measured on Windows 11:

    stdout        encoding   "arrow"
    console       utf-8      encodes
    pipe / file   cp1252     UnicodeEncodeError

Redirect it and that path is gone; Python falls back to the locale encoding,
which cannot represent an arrow, an em dash, or a box-drawing glyph. Scripts,
CI and AI agents capturing output are the ones affected -- this CLI's primary
audience -- while the interactive users who would notice never see it.

So: reconfigure stdout/stderr to UTF-8 only when the stream is NOT a terminal.
Terminals are deliberately left alone, because forcing UTF-8 bytes at a cp852
console would replace a working display with mojibake. Redirected Windows
output now matches POSIX byte for byte, including the box-drawing characters
Rich previously downgraded to ASCII.

human/Rich path cannot use that escape hatch because Rich owns the writes.

Verified on a real Windows 11 box, piped:

    before: semantic-layer --help -> CRASH   context -> CRASH (invalid UTF-8)
    after:  semantic-layer --help -> clean   context -> clean (valid UTF-8)

and under a real console, all three surfaces stay rc=0 and clean, confirming
the interactive path is untouched.

Ships in 0.80.1 alongside #566, rather than as its own version.
CONTRIBUTING requires a (since vX.Y.Z) entry in the agent-facing behaviour log
for anything an agent would not infer from --help, and this is squarely that:
before v0.80.1 an agent piping `semantic-layer --help` or `context` on Windows
got exit 1, while a human typing the same command saw it work. Raised in review.

Placed directly after the #546 entry, whose "human (Rich) output is unaffected"
line was about to become half-true, and linked from it. Documents the older-
version workaround verified against the installed build rather than copied from
the issue: PYTHONUTF8=1 and PYTHONIOENCODING=utf-8 each turn the crash into
clean output on their own.
@padak
padak force-pushed the fix/windows-redirected-output-utf8 branch from fac2262 to b411065 Compare August 11, 2026 15:39
@padak

padak commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Rebased onto main after #566 landed, and renumbered to ship inside 0.80.1 rather than as its own 0.80.2 — one release for the whole Windows batch.

Three things moved as part of that, none of them mechanical:

Version files, all six local gates and 5411 tests are green.

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.

1 participant