Files
multica/server
Bohan Jiang 6e6682539c fix(agent): make readOpenclawStdout's cut-short decision atomic (MUL-6024) (#6765)
* fix(agent): make readOpenclawStdout's cut-short decision atomic (MUL-6024)

TestReadOpenclawStdoutWaitsForCompleteResult failed on #6757's CI with
"cutShort = true after a clean EOF, want false" — a flake unrelated to
that PR, which only touched server/cmd/multica/.

The test's reader deliberately returns data and io.EOF from one Read so
that completion and clean EOF are a single observation, but the reader
goroutine split that observation across two critical sections, and the
poll loop split its own decision across two more. Either seam lets a
tick see a buffer that has just become parseable while atEOF is still
false, and report a stream that ended cleanly as cut short:

  - append bytes / set atEOF were separate lock regions, so a tick
    landing between them saw the completing bytes without the EOF;
  - the tick snapshotted (size, lastByte, atEOF), released the lock,
    then re-locked to copy acc — so bytes arriving in between could
    complete a buffer whose silence check had already passed. This one
    needs no starvation at all, just an unlucky interleaving, and is
    the likelier cause of the CI failure.

Both are now single critical sections. Injecting a 400ms delay at
either seam reproduces the exact CI failure before this change and
passes after it; the test is now deterministic rather than a race the
reader construction merely makes unlikely.

Production impact beyond the flake: cutShort on a cleanly exited
openclaw logs "openclaw delivered its result but did not exit" and
cancels an already-dead process. The delivered result was still
correct, so this was invisible to users but wrong in the logs.

Adds TestReadOpenclawStdoutCutsShortWhenCLILingers, which pins the
other half — a complete result on a stream that never reaches EOF must
still cut short. Without it the suite would pass if the shortcut
stopped firing entirely.

Verified: go vet; go test ./pkg/agent -run TestReadOpenclawStdout
-race -count=50; go test ./pkg/agent/... -race.

Co-authored-by: multica-agent <github@multica.ai>

* docs(agent): correct what kind of reader returns data with io.EOF

Review nit on #6765. The comment claimed a pipe returns its last bytes
and io.EOF from one Read; it does not. *os.File only reports io.EOF on
a zero-byte read, so os/exec's StdoutPipe always splits the final bytes
and the EOF across two adjacent reads.

The atomicity the comment sits on is still required — readOpenclawStdout
takes any io.Reader, and n > 0 with io.EOF is a legal return — so only
the justification was wrong, not the code. Says instead what production
actually sees: the same seam one Read wider, which still has to outlast
idleGrace to fool the poll, while the race that needs no starvation at
all is the one in the tick branch.

Comments only; no behaviour change.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-11 18:40:13 +08:00
..