mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-13 19:43:27 +02:00
* 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>