[#975] De-block the event loop#985
Merged
Merged
Conversation
QuadWork is single-process, so any synchronous block on the main thread freezes every agent's WS/HTTP/timers. Removes the 3 confirmed stalls, behavior-preservingly (only the blocking removed). 1. file-chat.js appendMessage — the retry loop slept via Atomics.wait(...,100), a synchronous 100ms block (up to 300ms per append) on EVERY chat + system message. Dropped the sleep; retries now run immediately (attempt count unchanged). appendFileSync transient failures are rare and not time-dependent, so the busy-wait bought nothing. Message ordering + ids are unaffected — the id is assigned synchronously before the append and appendFileSync writes in order (verified: 50 appends → contiguous monotonic ids, readback ordered). 2. index.js spawnButlerPty — the pre-trust ran `claude -p "echo ok"` synchronously with a 15s timeout on the event loop; a hung claude blackholed the server for up to 15s. Dropped it: the interactive trust prompt is already auto-answered by the onData trust-listener installed right after (the same mechanism agent PTYs rely on). 3. routes.js setup route — gh clone / git fetch / git worktree add / claude -p ran synchronously in the request handler. ensureGitHeadForSetup and createAgentWorktree are now async (awaiting an injectable execFn, default the new execAsync = promisified execFile); the /api/setup handler is async and awaits them. Express 5 forwards a rejected async handler to the default 500 handler, same as the prior sync throws. Removed the now- dead synchronous exec() wrapper. Tests: both setup test files migrated to inject an async fake exec and await (setupWorktrees 4/4, setupHardening 6/6). Full suite 60 passed / 0 failed. Server boots + shuts down cleanly on this branch. Note (not implemented here, per ticket): JSONL rotation is a separate future ticket. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
project7-interns
approved these changes
Jul 6, 2026
project7-interns
left a comment
Collaborator
There was a problem hiding this comment.
Verdict: APPROVE
Epic Alignment: PASS
EPIC #967 Phase 3 calls for minimal local hardening without regressing the orchestrator; #975 specifically requires behavior-preserving removal of chat/butler/setup event-loop stalls, and the diff does exactly that.
Checked (evidence)
- Structural gate: PR body contains
Closes #975, filled## EPIC Alignment (#967), and filled## Self-Verification. - Context:
gh issue view 975andgh api repos/realproject7/quadwork/issues/967read live; contract checked: behavior unchanged, only blocking removed, use async execFile path in routes. - Chat path:
server/file-chat.js:146still assigns ids synchronously before append;server/file-chat.js:166keeps 3 attempts;server/file-chat.js:178removes theAtomics.waitsleep. - Butler path:
server/index.js:1009removes the blocking pre-trustclaude -p;server/index.js:1077keeps the trust prompt listener. - Setup path:
server/routes.js:3691uses promisified async execFile;server/routes.js:3711andserver/routes.js:3777make setup helpers async;server/routes.js:3884makes/api/setupasync and awaits shell-outs atserver/routes.js:3892,server/routes.js:3906,server/routes.js:3920,server/routes.js:3937. - Tests updated for async setup helpers:
server/routes.setupWorktrees.test.jsandserver/routes.setupHardening.test.jsmigrated to awaited fake execFns. - Riskiest part of this diff: chat write path ordering; acceptable because id/seq assignment remains before append, append remains synchronous, and only inter-retry sleep was removed.
- Kill-list: scanned all items — clean.
- CI:
gh pr checks 985→testpass, 52s.
Findings
None.
Decision
Approved. The changes are scoped to the three #975 stalls, preserve the relevant contracts, and CI passes on current head 8d1b61dfb40863b899d2080178faf61eed3bab56.
Owner
Author
|
APPROVE at Full independent review — issue #975 De-block the event loop (EPIC #967 Phase 3, last fix ticket). Behavior-preserving removal of 3 synchronous main-thread stalls; comment-based approval (shared reviewer GH identity blocks a formal Approve). Checked (evidence)
Verification
No findings. |
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.
Closes #975
De-block the event loop. QuadWork runs as a single Node process, so any synchronous block on the main thread freezes every agent's WebSocket, HTTP, and timers at once. This removes the 3 confirmed stalls — behavior-preservingly (only the blocking is removed; outcomes are unchanged).
Changes
file-chat.jsappendMessagebusy-wait — the append retry loop slept viaAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100), a synchronous 100ms block on the main thread (up to 300ms across 3 attempts) on every chat + system message. Dropped the sleep — retries now run immediately, attempt count unchanged.appendFileSynctransient failures are rare and not time-dependent, so the busy-wait bought nothing but latency.index.jsspawnButlerPtypre-trust — ranexecFileSync("claude", ["-p", "echo ok"], { timeout: 15000 })synchronously on the event loop; a hungclaudeblackholed the whole server for up to 15s. Dropped it: the interactive trust prompt is already auto-answered by theonDatatrust-listener installed a few lines below (the same mechanism the agent PTYs rely on), so no synchronous pre-trust is needed to reach a trusted session.routes.js/api/setuproute — the setup shell-outs (gh repo clone,git fetch,git worktree add,claude -p) ran synchronously in the request handler.ensureGitHeadForSetupandcreateAgentWorktreeare now async, awaiting an injectableexecFn(default the newexecAsync, backed by the already-imported_execFileAsync = promisify(execFile)); the/api/setuphandler isasyncand awaits them. Express 5 forwards a rejected async handler to the default 500 handler — the same result the prior synchronous throws produced. The now-dead synchronousexec()wrapper was removed (execFileSyncis still used directly by the smallgh apihelpers).EPIC Alignment (#967)
EPIC #967 Phase 3, the last fix ticket. Isolated to three de-block sites; the one change on the chat write path (file-chat) is behavior-preserving with ordering verified below. No agent-spawn / steady-state runtime change beyond removing blocking. JSONL rotation is explicitly out of scope (a separate future ticket, per the issue).
Self-Verification
npm test(server suite, what CI runs) → 60 passed, 0 failed, 2 skipped. Both setup test files were migrated to inject an async fakeexecFnandawaitthe now-async functions:routes.setupWorktrees.test.js4/4 (stale/empty/clone HEAD paths),routes.setupHardening.test.js6/6 (slug normalization, origin match/mismatch, worktree fresh/stale-reuse/detached).node --checkon all 5 files → clean.HOME, free port) → printslistening, exits on SIGINT with no errors.state.nextId++) before the append, andappendFileSyncwrites in call order — removing the inter-retry sleep changes neither. Verified at runtime: 50 sequentialappendMessagecalls → ids1..50contiguous and strictly monotonic, and the on-disk JSONL reads back in ascending id order. The existingfile-chat.test.js/file-chat.system-messages.test.js/file-chat.loopguard.test.jsall still pass.🤖 Generated with Claude Code