Skip to content

fix(swarm): keep a Windows lane alive on the stdin pipe it watches - #151

Merged
rn7s2 merged 1 commit into
mainfrom
fix/windows-swarm-lane-stdin-eof
Oct 20, 2026
Merged

rn7s2 merged 1 commit into
mainfrom
fix/windows-swarm-lane-stdin-eof

Conversation

@rn7s2

@rn7s2 rn7s2 commented Oct 19, 2026

Copy link
Copy Markdown
Owner

Every lane of an evo-swarm on Windows died about a second after it started listening, and the coordinator reported each one as:

[lane 1] init-failed — ERROR 12029: Cannot connect

12029 is ERROR_WINHTTP_CANNOT_CONNECT, so this read as a networking fault — but the lane's ready.json and its own lane.log both showed a listener that had come up and then gone. The lane's process was gone before the coordinator's first request could be answered.

Cause

A lane is launched with --watch-stdin and stdin a pipe the coordinator holds, so a lane can never outlive the swarm: end of file on that pipe is its shutdown. watch-stdin-eof parks a thread on the stream make-stdin-stream returns and reads it with read-byte.

On Windows make-stdin-stream returned a character stream whenever stdin was not a console — which is exactly a lane's case, a pipe. read-byte on a character stream signals a type error rather than returning a byte, and the handler around that read treated any error as end of file, so every lane shut itself down at once.

Measured, with the lane's stdin a pipe held open by the parent:

with --watch-stdin without
old code exits within ~1s stays up
fixed stays up stays up

Changes

  • make-stdin-stream's Windows pipe branch is now a byte stream (:element-type '(unsigned-byte 8)), the only kind its one reader can read. The console branch is untouched: on a console it still returns the console input handle, which drain-console-input reads as key events. The character branch had no remaining caller — read-available-input's Windows character path only ever saw a console handle — so untranslated-newlines and that branch are gone, and read-available-input reads bytes everywhere but the console.
  • watch-stdin-eof calls its thunk only on a genuine end of file. A read that fails for any other reason leaves the thread with nothing to watch and returns; this signal is a lane's whole life, and a stream that cannot be read is not a parent that went away.

Verification (real Windows lanes, not the harness)

  • With --watch-stdin, the lane stays up and answers.
  • Both lanes of a two-lane swarm reach idle, restarts: 0, with a ready model.
  • A delegated task runs in the lane with a real model call.
  • The contract the flag exists for still holds: closing the coordinator's end of the pipe shuts the lane down cleanly (exit 0, ready file deleted).
  • test-stdin-stream-for-watching fails on the old element type and passes on the new one.

Co-authored-by: EvoAgent evo@ruiqilei.com

Every lane of an evo-swarm on Windows died about a second after it
started listening, and the coordinator reported each one as

  [lane 1] init-failed — ERROR 12029: Cannot connect

12029 is ERROR_WINHTTP_CANNOT_CONNECT, so this read as a networking
fault, but the lane's ready file and its own lane.log both showed a
listener that had come up and then gone: the lane's process was gone
before the coordinator's first request could be answered.

A lane is launched with --watch-stdin and stdin a pipe the coordinator
holds, so the lane can never outlive the swarm: end of file on that pipe
is its shutdown. watch-stdin-eof parks a thread on the stream
make-stdin-stream returns and reads it with read-byte.

On Windows make-stdin-stream returned a *character* stream whenever
stdin was not a console — which is exactly a lane's case, a pipe.
read-byte on a character stream signals a type error rather than
returning a byte, and the handler around that read treated any error as
end of file, so every lane shut itself down at once. Measured: with
--watch-stdin the lane exits within a second of writing its ready file;
without it, it stays up.

Two changes, one cause each:

- make-stdin-stream's Windows pipe branch is now a byte stream
  (:element-type '(unsigned-byte 8)), which is the only kind of stream
  its one reader can read. The console branch is untouched: on a console
  it still returns the console input handle, which drain-console-input
  reads as key events. The character branch had no remaining caller —
  read-available-input's Windows character path only ever saw a console
  handle — so untranslated-newlines and that branch are gone, and
  read-available-input reads bytes everywhere but the console.

- watch-stdin-eof calls its thunk only on a genuine end of file. A read
  that fails for any other reason leaves the thread with nothing to
  watch and returns; this signal is a lane's whole life, and a stream
  that cannot be read is not a parent that went away.

Verified against real lanes on Windows: with --watch-stdin the lane now
stays up and answers, both lanes of a two-lane swarm reach idle with
restarts 0 and a ready model, a delegated task runs in the lane with a
real model call, and — the contract the flag exists for — closing the
coordinator's end of the pipe still shuts the lane down cleanly (exit 0,
ready file deleted). The regression check is
test-stdin-stream-for-watching, which fails on the old element type.

Co-authored-by: EvoAgent <evo@ruiqilei.com>
@rn7s2

rn7s2 commented Oct 19, 2026

Copy link
Copy Markdown
Owner Author

The build-and-test (ecl) failure here is pre-existing on main, not from this PR.

Unmodified main at 449363e fails with the identical ECL internal error:

Internal or unrecoverable error in:
Unable to mprotect environment.
  [22: Invalid argument]
Aborted (core dumped)
make: *** [Makefile:74: test] Error 134

Reproduced by dispatching CI on main itself (run 39290253272): build-and-test (sbcl) and build-and-test-windows pass, build-and-test (ecl) fails the same way. No test assertion fails in either run — ECL dies inside its own compiler/GC (cl_read_line → mprotect), after the OAuth unit tests, which is a known ECL 24.5.10 failure on newer kernels that restrict PROT_EXEC.

build-and-test (sbcl) and build-and-test-windows both pass on this PR, which are the configurations this change actually affects (the Windows make-stdin-stream branch is SBCL-only — ECL is not supported on Windows).

Co-authored-by: EvoAgent evo@ruiqilei.com

@rn7s2
rn7s2 merged commit cf2459b into main Oct 20, 2026
10 of 15 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.

1 participant