Skip to content

fix(win): put stdout in binary mode before the serve handshake (closes #748) - #749

Merged
JustVugg merged 1 commit into
devfrom
fix/serve-binary-mode-windows
Aug 1, 2026
Merged

fix(win): put stdout in binary mode before the serve handshake (closes #748)#749
JustVugg merged 1 commit into
devfrom
fix/serve-binary-mode-windows

Conversation

@JustVugg

@JustVugg JustVugg commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Kimi K3 and Inkling hang forever on Windows in coli chat, coli web and coli serve.

What happens

The engine announces itself with a byte sentinel:

\x01\x01READY\x01\x01\n

coli matches it byte-exactly. On Windows the CRT opens stdout in TEXT mode and rewrites \n as \r\n, so what actually arrives is \x01\x01READY\x01\x01\r\n — and the gateway waits for a byte that was mangled on the way out. No error is printed, because nothing failed: one side is waiting, the other already sent something else.

@brad-evony hit this in #748 on Windows 10: 93 layers loaded over 42 minutes, tokenizer read, then silence. His Ctrl-C traceback lands in read_engine_turn’s stream.read(1) — exactly where the handshake blocks.

Why it was there to hit

colibri.c has called _setmode(..., _O_BINARY) since #195, and its comment describes the symptom word for word:

stdout translates "\n"->"\r\n" (so the READY sentinel never matches and chat hangs)

engine had it
colibri.c ✅ since #195
inkling.c, kimi_k3.c ❌ never

Not a v1.4.0 regression — the bug predates it. v1.4.0 only made it reachable: before that the archives contained no kimi_k3.exe at all (#720), so no Windows user could get far enough to hang.

The fix, and where it lives

coli_serve_binary_mode() in compat.h, called by both serve loops before the sentinel.

In compat.h rather than pasted into each engine, because pasting is how it went missing. This is now the third Windows-specific behaviour found living in colibri.c alone — after select()/fd_set in #736. Each time, the newer engines were written without it and nothing noticed.

The test that should have existed

tests/test_serve_sentinel.c writes the handshake through a real stream and asserts the bytes contain no CR. Nothing in the tree had ever looked at those bytes, which is precisely why this reached a user instead of CI.

It can only pass on Linux/macOS — that is what a regression guard is, and CI has built every engine on Windows since #736, so it now runs where it can actually fail. It is picked up automatically by the rule-derived gate list from #733.

Scope

compat.h, kimi_k3.c (+2 lines), inkling.c (+2), one new test, one Makefile rule.

olmoe.c has no serve mode and is untouched. colibri.c is untouched — it is already correct, and 14 open PRs touch that file.

All four engines build; make test-c passes.

kimi_k3 and inkling never called _setmode(..., _O_BINARY). On Windows the CRT
opens stdout in TEXT mode and rewrites '\n' as '\r\n', so the engine emits

    \x01\x01READY\x01\x01\r\n

and coli -- which matches the sentinel byte-exactly -- waits for a byte that was
mangled on the way out. No error is printed, because nothing failed: one side is
waiting, the other already sent something else.

@brad-evony hit it in #748 on Windows 10: 93 layers loaded over 42 minutes, the
tokenizer read, and then silence. Ctrl-C landed in read_engine_turn's
stream.read(1), which is exactly where the handshake blocks.

colibri.c has done this since #195 and its comment describes the symptom word
for word ('the READY sentinel never matches and chat hangs'). The two newer
engines were written without it and nothing noticed for months. Not a v1.4.0
regression -- the bug predates it. v1.4.0 only made it reachable, because before
that the archives contained no kimi_k3.exe at all (#720).

It goes in compat.h rather than being pasted into each engine: pasting is how it
went missing, and this is the third Windows-specific behaviour to be found
living in colibri.c alone (after select()/fd_set in #736).

Adds tests/test_serve_sentinel.c, which writes the handshake through a real
stream and asserts the bytes contain no CR. Nothing in the tree looked at those
bytes before, which is why this reached a user rather than CI. It can only pass
on Linux/macOS -- that is the point of a regression guard, and CI builds every
engine on Windows since #736, so it runs where it can fail.

olmoe.c has no serve mode and is untouched. colibri.c is untouched: it is
already correct, and 14 open PRs touch that file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@JustVugg
JustVugg merged commit 113bc0f into dev Aug 1, 2026
13 checks passed
@JustVugg
JustVugg deleted the fix/serve-binary-mode-windows branch August 1, 2026 11:45
minne100 added a commit to minne100/colibri that referenced this pull request Aug 1, 2026
Addresses the main blocker on JustVugg#712: the engine built clean and touched no
existing files, but coli could not reach it.

c/coli:
- model_arch(): return 'qwen' for any model_type containing 'qwen'
- engine_for(): resolve 'qwen36' binary
- need_model(): qwen36 build target
- chat banner model_id: qwen36-colibri

c/qwen36.c (SERVE=1 mode):
- implements the colibri gateway wire protocol (READY + STAT + SUBMIT/
  CANCEL + DATA + DONE + PROF), mirroring kimi_k3.c / inkling.c so
  'coli chat' / 'coli web' / 'coli serve' can drive the engine
- READY/STAT handshake, SUBMIT payload -> encode_text -> step() prefill +
  per-token step() decode, stream decoded bytes as DATA frames, stop on
  EOS (Qwen3 151645, Q36_EOS override), DONE/STAT on completion
- temperature + top-p sampler (ported from kimi_k3.c, qsort O(V log V))
- Windows binary-mode fix inlined (JustVugg#748 hang: CRT rewrites n->rn and the
  gateway never matches READY). Uses _setmode(_fileno(stdout/stdin),
  _O_BINARY) before the sentinel; switch to compat.h's
  coli_serve_binary_mode() once JustVugg#749 lands (helper not in base yet).
ErikTromp added a commit to ErikTromp/colibri-hy3 that referenced this pull request Aug 3, 2026
Port JustVugg#749's shared helper so Hy3 (and inkling/kimi on this merge base)
do not hang forever on Windows when CRT TEXT mode rewrites the sentinel
as CRLF (JustVugg#748).

Co-authored-by: Cursor <cursoragent@cursor.com>
kreuzzelg pushed a commit to kreuzzelg/colibri that referenced this pull request Aug 3, 2026
Addresses the main blocker on JustVugg#712: the engine built clean and touched no
existing files, but coli could not reach it.

c/coli:
- model_arch(): return 'qwen' for any model_type containing 'qwen'
- engine_for(): resolve 'qwen36' binary
- need_model(): qwen36 build target
- chat banner model_id: qwen36-colibri

c/qwen36.c (SERVE=1 mode):
- implements the colibri gateway wire protocol (READY + STAT + SUBMIT/
  CANCEL + DATA + DONE + PROF), mirroring kimi_k3.c / inkling.c so
  'coli chat' / 'coli web' / 'coli serve' can drive the engine
- READY/STAT handshake, SUBMIT payload -> encode_text -> step() prefill +
  per-token step() decode, stream decoded bytes as DATA frames, stop on
  EOS (Qwen3 151645, Q36_EOS override), DONE/STAT on completion
- temperature + top-p sampler (ported from kimi_k3.c, qsort O(V log V))
- Windows binary-mode fix inlined (JustVugg#748 hang: CRT rewrites n->rn and the
  gateway never matches READY). Uses _setmode(_fileno(stdout/stdin),
  _O_BINARY) before the sentinel; switch to compat.h's
  coli_serve_binary_mode() once JustVugg#749 lands (helper not in base yet).
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