fix(streaming): bound SSE payloads, sockets, streams and subprocesses (salvage #759, #762, #765) - #789
Merged
Conversation
Salvages drafts #759, #762 and #765 onto current main: - eventHub (#762): MAX_EVENT_PAYLOAD_BYTES frame cap, MAX_LOG_LINE_CHARS / MAX_CHAT_TEXT_CHARS field bounds, per-client SSE backpressure counters (WeakMap, one drain listener per socket) with disconnectClient. - chatStream / chatBackend / LogLine (#762): retention caps for partial SSE frames, parsed chunks, assembled content, CLI stdout/stderr and rendered log lines. - rollback (#759): resolve the checkpoint stash by EXACT subject match (stash@{N} is a position, and 'abc' must not match 'abcd'). - gitStatus (#759): 10 MiB maxBuffer and reject-on-error so a failed git call can no longer masquerade as a clean tree. - httpBody (#759): streaming TextDecoder so a multi-byte char split across chunks is decoded correctly. - workSessionRoutes (#759): diff I/O through the containment-validated canonical worktree. - inputDebug (#759): contain diagnostic writes to ~/.openswarm. - gitInfo (#759): sentinel-prefixed churn parser that cannot read a numeric filename as a timestamp nor the next commit's timestamp as a path. - ciWorker / fixCommand (#765): subprocess timeouts and buffer bounds.
This was referenced Sep 28, 2026
Merged
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.
Summary
Salvages the streaming/execution-bounds work from three abandoned draft PRs into one reviewable change on current
main:rollback.tsexact stash match,gitStatus.tsmaxBuffer + reject-on-error,httpBody.tsstreaming decoder,workSessionRoutes.tscanonical-worktree I/O,tui/inputDebug.tssandbox containment,knowledge/gitInfo.tschurn parser (+ tests)knowledge/graphqlExporter.ts(imports nonexistent./repoSchema.js, callsgraph.getNodes()/getEdges()thatKnowledgeGraphdoes not define — dead code), junk (tmp-*,run-tests-workaround.sh,cursor-*.json,hooks.json)core/eventHub.tspayload/backpressure bounds,adapters/chatStream.tsretention caps,support/chatBackend.tsoutput caps,tui/components/LogLine.tsxrender bound (+ tests)discord/discordPair.ts(net −226 lines; callsagentPair.getPairStats(), an export that does not exist, and 6 nonexistent locale keys — dead code), junk (.agt3429-*,.cliRunner-from-main.ts,tmp-hook-test.txt)processRegistry.tsescalation-timer clearing,ciWorker.tsgh run reruntimeout + maxBuffer,fixCommand.tscheck timeout,support/dev.tscancel-until-closeadapters/base.tsandcli/workCommand.tsrewrites (destructively revert main's 6 intervening commits and do not compile against their own tree:settle()passesstdoutTruncated/stderrTruncated/signal/timedOutthatCliRunResultdoes not define;broadcastEventused but never imported)Nothing was taken from main's own hardening: every hunk that conflicted with work main already shipped keeps main's version.
What was fixed during the salvage
The drafts' code was not taken verbatim where it was broken; each of these was reproduced and fixed:
rollback.ts) was non-functional as drafted. fix(repository): harden filesystem, Git, and verification boundaries — prevent race conditions, ambiguous rollback, and unsafe execution #759 usedgit stash list --pretty=format:%gd: %gsand requiredmsg === message, but%gsis the reflog subject and actually readsOn <branch>: openswarm-checkpoint-abc, so the equality could never hold. Running fix(repository): harden filesystem, Git, and verification boundaries — prevent race conditions, ambiguous rollback, and unsafe execution #759's own tree (bf7ccae) gives 4 failed | 2 passed inrollbackStashIdentity.test.ts. The port matches the subject exactly (whole-subject, else exact: <message>tail), which also stopsabcfrom matchingabcd.gitInfo.ts) misread real git output. The draft's state machine expected an empty NUL token between commits; realgit log -zoutput has none, so the next commit's timestamp was parsed as a filename (inventing a file and dropping the real one). The port prefixes timestamps with an ASCII record separator (--format=%x1e%ct) so a timestamp token is self-identifying, and a numeric filename is never read as a date. Verified against a real repo: exact match on per-file counts, last-commit dates, and the set of paths.falseand disconnected at 64, butbroadcastEventis synchronous — one 23 KB stdout chunk fans out ~400 log events with no chance todrainin between, so an actively-reading dashboard client was destroyed mid-burst (verified over a real socket). Replaced the write count with a 4 MiB queued-byte cap plus a 30 s stall window; a healthy reader now survives a 400-frame burst and a stalled one is still dropped.function.arguments(the tool layer then got unparseable JSON) and returned long replies as a suffix that re-entered the conversation asassistantcontent. Now the reduced shape is accumulated incrementally — bounded content, intact tool calls — and exceeding the tool-call cap throws instead of silently corrupting.drainlistener per write, trippingMaxListenersExceededWarningon a slow client; now one listener per socket.chatBackendhead-truncated its partial line buffer, so a single >64 KB CLI event line removed the newlinesflushLinesneeds and live streaming stopped for the rest of the turn; both the line buffer and stdout now keep the tail (which is also whatextractChatResponsereads).inputDebug.tswas lexical only, so a symlinked directory inside~/.openswarmcould still redirect the write outside it. It now canonicalizes the nearest existing ancestor withrealpath— and tolerates a first run, where~/.openswarmdoes not exist yet and a barerealpathSyncwould silently drop the diagnostic line.Verification
npx tsc --noEmit— clean.src/core/**,src/support/**,src/adapters/chatStream,src/tui/**→ 132 files, 1455 tests passed.eventHub,chatStream,gitStatus,inputDebug,rollback*,httpBody,gitInfo,LogLine,ciWorker→ 182 tests passed.1→0); a truncated log line arrives at exactlyMAX_LOG_LINE_CHARSwith the ellipsis, and an event overMAX_EVENT_PAYLOAD_BYTESafter field bounding is dropped rather than sent.src/core/eventHub.tsburst disconnect,src/adapters/chatStream.tschunk eviction,src/tui/inputDebug.tsfirst-runrealpath,src/support/chatBackend.tstail truncation) were each reproduced, fixed, and covered by a regression test that fails on the un-fixed code.Deliberately not included
Files owned by sibling salvage groups were dropped rather than duplicated here, with the exact hunks handed to their owners:
src/runners/cliRunner.tsandsrc/adapters/codexResponses.ts(output-sanitize group — frame limits into the reducer, truncation into the runner output path),src/adapters/processRegistry.tsandsrc/support/dev.ts(concurrency-locking group — the #766 version ofkillProcesssubsumes #765's timer fix, and that group ships the unioncancelTask/finalizeform).