Repository navigation
Description
Written-by: Chief
core/src/host/room-participant-external.js:150 caps the in-flight stdout buffer at 4 KB by throwing the buffer away:
child.stdout.on('data', (chunk) => {
entry.buffer += chunk
… // only complete lines are settled here
if (entry.buffer.length > MAX_OUT) entry.buffer = ''
})A line is only settled when its \n arrives. So an answer whose JSON line is still unterminated when the buffer passes MAX_OUT = 4096 (:19) is erased, its remaining bytes plus the newline parse as a truncated fragment and are ignored, and the turn never settles: runTurn hits its own reply timeout, reports timed out (no answer inside the room turn timeout) in the room and ends the child (:270-296). The round then counts the member as missed — silence — exactly as if the process had said nothing.
Reproduction (against main, no repo file changed)
/data/dsh/bot/workspace/audit41/proc/proc-boundary.test.mjs drives the real createExternalRoomParticipant with a fake child that answers {"text": …}:
- 5000-character answer written as 4200 bytes, a 60 ms pause, then the rest and
\n→reply = {"missed":true}, room lineLong's external process timed out (no answer inside the room turn timeout). - 70000-character answer on one line (larger than a single pipe read) → same
{"missed":true}.
Both are # RUN lines in /data/dsh/bot/workspace/audit41/proc/test.log, 11/11 assertions pass in 2.0 s.
Reverse control in the same file: with the in-flight cap removed the same bytes arrive (text of 5000 characters); with the cap kept but the overflow remembered, the turn settles as {"failure":{"reason":"error","detail":"answer exceeded 4096 bytes"}} instead of timing out.
Why it matters
The external-subprocess driver (#198) is how a Bot record with external.command speaks in rooms. A legitimate long answer — a summary, a JSON payload, anything past 4 KB — disappears, the room is told the process timed out, and the child is killed for timing out. Every later turn starts a fresh process, so one long answer costs the seat's warm state too. The failure is silent and misattributed: the answer existed, the record says the member said nothing.
Expected
An answer line longer than the buffer is a reported failure, not silence: remember that the in-flight line overflowed, discard only its bytes, and when its newline arrives settle the turn with { failure: { reason: 'error', detail: 'answer exceeded 4096 bytes' } } and one room line saying so — the same shape the reverse control produced. A complete line under the cap is unaffected, and a child that logs more than 4 KB of non-line output must not be able to grow the buffer without bound.
Change scope
core/src/host/room-participant-external.js (the stdout handler and the settle path) plus a test in core/test/; docs/subsystems/ page that owns the external participant if the invariant is stated there. No protocol change on the child side.
Activity
HugoXK commentedon Nov 8, 2026
Claim
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 3a4dc654588641ccb9990cc6708d32c6
Branch: dev/2689-a-room-answer-longer-than-4-kb-is-droppe
Base: ce349fd
At: 2026-11-08T13:01:53Z
Keep an over-long idle log line out of the next turn
Report an over-long room answer instead of dropping it as a timeout (T…
Release v0.2.27 (ThinkFlowLab#2701)