fix(coding-agent): persist kernel stderr to disk and bound the in-memory tail - #1947
Merged
Conversation
xeophon
previously approved these changes
Sep 1, 2026
kernelStderr accumulated the kernel's stderr in memory for its lifetime while every reader shows at most the last 1 KiB. Hand the spawned kernel an fd onto kernel-stderr.log in the session artifact directory (rotated once per spawn) so the full pre-ready stderr stays inspectable, read failure tails from that file, and bound the in-memory diagnostics tail to 8 KiB (ENG-5832).
snimu
force-pushed
the
fix/kernel-stderr-log
branch
from
September 1, 2026 13:58
2b0f40f to
b951f55
Compare
…up failures stderrTail() loaded the whole log to slice 1KB off the end; a kernel that spews to stderr until the 30s ready timeout can leave a log far too large to buffer in the host, so the failure report now does a positional read of the last 1KB only.
sethkarten
self-requested a review
September 3, 2026 22:39
…udget Handing the log fd straight to spawn left pre-ready spew only time-bounded (30s ready timeout x disk speed), and rotation then kept the oversized file as `.old` indefinitely. Spawn now always pipes stderr and the data handler is the single write path: it appends under a 5 MiB per-spawn budget, writes one exhaustion marker, then keeps draining but discards, so per-session disk is hard-capped at ~2x the budget while a blocked pipe can never wedge a pre-ready kernel. On exit the stream drains to quiescence before being destroyed (its EOF can be held hostage by orphaned grandchildren), keeping the kernel's last words in the tail; the file-tail read is gone since the in-memory tail now sees every logged byte.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 52ac831. Configure here.
A kernel that dies right after an incomplete UTF-8 sequence left those bytes buffered in the StringDecoder forever, so the startup error's tail silently dropped the truncated final character (the log file keeps the raw bytes either way). Flush decoder.end() on both 'end' and 'close': natural EOF can beat 'exit' while its queued 'close' emission lands after the tail is built, and a drain-destroyed stream never emits 'end'.
Rotation and open shared one try, so a rotation failure (a locked or undeletable `.old` target) dropped the log entirely for that spawn. Scope the failure to rotation itself: record a diagnostic and keep appending to the oversized file instead of losing the log.
The quiescence loop re-derived away: after the kernel exits, the tail only needs the bytes it wrote before dying, which one event-loop turn delivers from the bounded pipe buffer — output arriving later is a surviving grandchild's post-mortem noise, not the kernel's last words, and re-checking for it let such a writer defer teardown indefinitely in principle. Destroy after a single setImmediate instead. The log fd close in the stream's 'close' listener is also caught now: an fs error there was an uncaught exception in an event handler, killing the host.
fs.writeSync may write fewer bytes than asked (partial ENOSPC, signal interruption); the single call dropped the unwritten suffix while the budget still decremented by the full chunk. Loop until each chunk (and the exhaustion marker) is fully on disk. Also unpins the close-failure diagnostic from the startup error text: the 'close' listener that appends it can run after the tail is built (the closed-flag ordering already documented for the decoder flush), which CI hit; that pin's essence is host survival, which the unhandled-error check carries.
…und teardown drains The per-spawn write budget was a fresh 5 MiB regardless of the file's size, so a repeatedly restarted noisy kernel grew an unrotatable log without bound (rotation failure kept the file AND re-granted a full budget), and even with rotation working the strict over-cap check let current + .old transiently reach ~4x. The budget is now the file's remaining capacity after the rotation attempt, restoring the ~2x per-session cap in every path. Teardown regains its stderr bound: a still-alive child may ignore the kill signal and never emit 'exit', so the post-exit drain would never run — cleanupResources destroys its stderr again in exactly that case, while an exited child keeps the drain that preserves its last words.
sethkarten
approved these changes
Sep 4, 2026
Dmatut7
added a commit
to Dmatut7/prime-agent
that referenced
this pull request
Sep 5, 2026
…g uptake (FIFO nonblock, tail quota, rotation catch, spawn offset) Hardens this fork's fd-direct kernel stderr log: - open the log with O_NONBLOCK and assert a regular file, so a planted FIFO cannot block the spawn inside the global kernel-boot permit with the ready timeout not yet armed - keep a rotation failure in an inner catch, so a leftover .old directory no longer disables the session log and reports "cannot open" - create the log's parent directory through ensurePrivateDirectory (0700) - cap the report at 768 file bytes plus 256 host diagnostic chars and put the kernel traceback last, so a long host diagnostic cannot push the traceback out of the error message - record the log window start per spawn and read only the current incarnation's bytes; when no window is known (no log path configured, or the descriptor could not be opened and stdio degraded to a pipe) the file is not read at all, because its bytes belong to a previous incarnation - use readSync's return value so a short read cannot emit NUL padding The fork tracks PrimeIntellect-ai#1947 as of 2026-09-02 (ca7f26c). Upstream redesigned the same area on 09-04 (head 5698258, still open) around a pipe plus a write budget; those upstream commits are not taken here, so the pipe-fallback path keeps the limitations of the revision this fork took.
Dmatut7
added a commit
to Dmatut7/prime-agent
that referenced
this pull request
Sep 5, 2026
audit-findings.md: add the PrimeIntellect-ai#1947 staleness finding and the B task that subordinated it; record the F75 empirical conclusion and the three F76 facts; add the known-gaps list (4) to §2.2 (2) covering the two deferred corrections and the two new tasks. FORK_NOTES.md: update the F75 todo from a plan to a completed empirical result; add the three F76 facts; record the omitStreamingMessages line-number correction; add the PrimeIntellect-ai#1947 staleness note and the B task. local-axes.md: add gate item 7 covering the streaming line-number correction and the pre-enable trap (daemon-mode.ts:4184-4185 unconditional inheritance).
Dmatut7
added a commit
to Dmatut7/prime-agent
that referenced
this pull request
Sep 5, 2026
The S9 todo item 8 was written with a join over a string, which iterates its characters, so the whole paragraph landed as 590 one-character lines. The text was intact but unsearchable: grep for "staleness" returned nothing, and that note is a key input for the next upstream sync. Rejoin it into the single markdown line it was meant to be. Verified: grep -c staleness goes 0 -> 1, one-character lines go 470 -> 43 (all blank), the byte delta is exactly the 589 removed newlines, and items 1-8 plus the following section are intact.
Dmatut7
added a commit
to Dmatut7/prime-agent
that referenced
this pull request
Sep 5, 2026
R3 merge-back. F4 cleared first: the parallel lane's uncommitted preserve_thinking/enable_search WIP was committed at aa80279 (it was breaking the repo type check), which unblocked this merge. Brings in 237 files from the upstream 0.9.x line: daemon schema 26->27 (digest 589a2219bc8b), version 0.8.1->0.9.1, the mermaid rendering feature (grok-mermaid), and cherry-picked upstream PRs PrimeIntellect-ai#2027/PrimeIntellect-ai#1947/PrimeIntellect-ai#1896. All 264 local-only files and 54 local-only tests preserved (merge, never whole-tree checkout). openai-completions.ts auto-merged (R3 changes and the WIP compat feature touch disjoint regions). FORK_NOTES.md conflict resolved by folding the R3 sync detail into the running update log. Installed grok-mermaid (new upstream dep, already in the merged lockfile) into node_modules. Verified on the merged tree: biome 1036 files clean, tsgo 0 errors, installer + browser-smoke pass, openai-completions tests 20/20.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

When a Python kernel failed to start, the failure was often undiagnosable: its stderr lived only in an in-memory string that died with the process, and the startup error showed just the last kilobyte of it. Nothing stayed on disk to inspect after the fact — and on the other end, a long-lived chatty kernel grew that same string without bound for its whole lifetime, even though every reader only ever showed the tail.
The log is startup-shaped because the kernel's stderr is startup-shaped: the runtime dup2's fd 2 into its protocol pipe before it reports ready (repl.py
_setup_fds), so the process-level stderr only ever carries pre-ready bytes — interpreter, venv, and import failures; everything after ready already flows through the protocol into the bounded per-execution stderr. The kernel now always spawns with stderr piped, and the manager's data handler is the single write path intokernel-stderr.login the session's artifact directory: it appends raw chunks under a 5 MiB per-spawn write budget, and once the budget is spent it writes one[stderr log budget exhausted]marker, then keeps draining the pipe but discards — so a kernel that spews until the 30s ready timeout (a corrupted venv looping import warnings, multiplied by repair retries) cannot land more than the budget on disk per spawn, and a full pipe can never wedge a pre-ready kernel. The file rotates to.oldwhen a spawn finds it over the budget, hard-capping per-session disk at roughly 2× the budget.Startup failure messages read their 1 KiB tail from the in-memory string — bounded to an 8 KiB tail, decoded across chunk boundaries, and fed by the same pipe as the file, so it sees every logged byte plus host-side
[kernel]diagnostics. On kernel exit the pipe is drained to quiescence before the stream is torn down, so the kernel's last words reach both the tail and the log even when an orphaned grandchild holds the pipe's write end open past the parent's death. Sessions without an artifact directory (or whose log open fails) keep the memory-tail behavior; the log persists across kernel restarts within the rotation cap.Validation: three pins — a kernel that writes CR progress output and a split multi-byte character with no trailing newline before dying produces a log byte-identical to what it wrote, with that tail in the startup error; a kernel that spews 6 MiB before dying leaves a log capped at the budget plus the exhaustion marker while the startup error still shows its final words and the script runs to completion (the pipe stayed drained); and a log pre-filled past 5 MiB rotates to
.oldat the next spawn, with the new incarnation's stderr starting a fresh file. Kernel/ipython/repl suites, tsgo, and rootnpm run checkgreen.Linear: RES-1246 https://linear.app/primeintellect/issue/RES-1246/persist-python-kernel-stderr-to-disk-and-bound-the-in-memory-buffer
LOC
Total src: +123/−6 (net +117); tests: +244/−3 (net +241).
Note
Persist kernel stderr to disk with per-spawn budget and bound the in-memory tail
stderrLogPathtoKernelManagerOptions; when set,ReplKernelManager.doStartopens an append log and rotates the prior file when it exceeds the size threshold.appendKernelStderrTextandappendKernelDiagnostic, replacing unbounded accumulation for the kernel lifetime.IpythonKernelProvisioner.startKernelconfigures the log path under the session snapshot/artifact directory when it exists.shutdownand the pre-ready exit path inwaitForReadynow wait for stderr stream closure before finalizing, instead of discarding buffered data.ReplKernelManager.shutdownno longer destroys the child stderr stream immediately; any caller relying on synchronous stderr teardown during shutdown may see delayed closure until the exit-drain completes.Changes since #1947 opened
ReplKernelManager.openStderrLogFdwith try/catch error handling that logs a diagnostic message and continues appending to the existing log file when rotation fails, instead of throwing an error [86de757]child.stderrinReplKernelManager.startto flush buffered partial UTF-8 character data from theTextDecoderand close the stderr log file descriptor [86de757]child.stderrtype in theconfiguredManagertest utility to include aoncemethod implementation [86de757]ReplKernelManagerkernel process exit handler [78a1822]ReplKernelManagerstderr handling edge cases [78a1822]writeFullySyncutility function and replaced directfs.writeSynccalls inReplKernelManagerkernel stderr data handler withwriteFullySyncto handle partial writes, including writing the budget-exhausted marker as a Buffer [5698258]node:fsmock inrepl-kernel-startup.test.tsto simulate short writes and updated test assertions to verify byte fidelity under partial write conditions [5698258]ReplKernelManager.openStderrLogto return both file descriptor and remaining write budget based on existing log file size, and implemented rotation logic that attempts to move oversized logs to.oldbefore resetting budget [2a056a9]ReplKernelManager.startto track stderr write budget from log file initialization and enforce capacity limits by writing a budget-exhausted marker and discarding further pre-ready stderr bytes when exceeded [2a056a9]ReplKernelManager.shutdownto conditionally destroy child stderr stream only when the child process has not exited, preserving buffered output for already-exited processes [2a056a9]repl-kernel-shutdown.test.tsverifying thatkill()destroys stderr only when the child process is alive and not when it has an exitCode [2a056a9]repl-kernel-startup.test.tsto verify that rotation-failure scenarios respect capacity limits and that write budget equals remaining file capacity at MAX size [2a056a9]Macroscope summarized e9a7d24.
Note
Medium Risk
Changes kernel subprocess stderr lifecycle and synchronous file I/O on every spawn; behavior is heavily tested but affects startup-failure diagnostics and teardown timing.
Overview
Pre-ready Python kernel stderr is now persisted and memory-safe instead of living only in an unbounded in-memory string.
ReplKernelManageraccepts optionalstderrLogPathand, when set, appends raw pre-ready stderr tokernel-stderr.logwith rotation to.oldwhen the file exceeds 5 MiB, a per-spawn write budget based on remaining file capacity, and continued pipe draining after the budget is exhausted (with a one-time marker) so spew cannot wedge startup. The in-memory diagnostics tail is capped at 8 KiB with UTF-8 decoding across chunk boundaries.Startup failures wait for stderr stream closure before building the error tail; exit/teardown avoids destroying stderr on already-exited children so last bytes are not dropped, while still destroying the pipe for live children that ignore kill.
IpythonKernelProvisionersets the log path under the session artifact directory when a snapshot dir exists. Tests cover byte fidelity, rotation, budget caps, rotation failure, grandchild stderr noise, and shutdown stderr destroy rules.Reviewed by Cursor Bugbot for commit 2a056a9. Bugbot is set up for automated code reviews on this repo. Configure here.