Skip to content

fix(core): stop session work barrier from pinning a core at 100% CPU - #1084

Merged
code-yeongyu merged 3 commits into
mainfrom
fix/session-work-barrier-spin
Aug 22, 2026
Merged

fix(core): stop session work barrier from pinning a core at 100% CPU#1084
code-yeongyu merged 3 commits into
mainfrom
fix/session-work-barrier-spin

Conversation

@code-yeongyu

@code-yeongyu code-yeongyu commented Aug 22, 2026

Copy link
Copy Markdown
Owner

SessionWorkBarrier.waitForSettled ran a while(true) loop awaiting only already-resolved promises — never leaving the microtask queue (measured 25.6M iterations/sec at 100% CPU; a reproduction test hung past vitest's own 30s timeout with the process pinned at exactly 100.0%). This starves the event loop so RPC/socket IO (client acks) can't be serviced — the mechanism behind the desktop server's ack-timeout bursts under load.

Fix: keep the tight microtask re-sampling for the first 16 rounds (load-bearing for compaction recovery ordering — verified by test), and only a non-converging queue yields a real event-loop turn via MessageChannel (setTimeout/setImmediate deadlock under vi.useFakeTimers; process.nextTick runs ahead of the drain and never relieves starvation).

Verification: failing-first reproduction (hang/100% CPU) → fixed (0.4% CPU, 138ms); full @code-yeongyu/senpi suite 1059 files / 8662 tests green.


Summary by cubic

Stop SessionWorkBarrier.waitForSettled from pinning a core at 100% CPU by yielding a real event-loop turn when the event queue does not converge; previously it re-sampled only already-resolved promises in a tight loop that never left the microtask queue.

  • Behavior for converging sessions is unchanged; re-samples tightly for up to 16 rounds to preserve compaction/recovery ordering.
  • Uses MessageChannel to yield, keeping timers and IO responsive and working under mocked timers (e.g., vi.useFakeTimers()).
  • Adds a regression test and updates packages/coding-agent/CHANGELOG.md; full @code-yeongyu/senpi test suite remains green.

Written for commit b856e6d. Summary will update on new commits.

Review in cubic

`SessionWorkBarrier.waitForSettled` re-samples the agent event queue in a
`while (true)` loop that only ever awaits promises. When a session's subscriber
re-chains `_agentEventQueue` with already-resolved promises, the two samples in
a round never agree and the loop never leaves the microtask queue: timers and IO
are starved and the process pins one core at 100% CPU. Measured at 25.6M
iterations/sec, and reproduced here as a test file that hangs hard enough that
vitest cannot even fire its own 30s timeout — matching the intermittent spin
seen in `omo --mode rpc --multi-session` session children.

The first rounds still re-sample immediately, because that tight re-check is
what observes a continuation scheduled during the same turn and keeps queued
work from being reported as settled. Only once the queue proves it is not
converging does the loop yield a real event-loop turn, which bounds CPU without
changing when a converging session settles.

The yield uses `MessageChannel` rather than `setTimeout`/`setImmediate`: mocked
timers replace both, which would stall every caller that drives retry and
compaction recovery on a mocked clock, while `process.nextTick` runs ahead of
the microtask drain and would not relieve the starvation at all.

Verified: full `npm test` for @code-yeongyu/senpi green (8662 passed).
@code-yeongyu
code-yeongyu enabled auto-merge August 22, 2026 13:29
@code-yeongyu
code-yeongyu merged commit 6c44950 into main Aug 22, 2026
15 checks passed
@code-yeongyu
code-yeongyu deleted the fix/session-work-barrier-spin branch August 22, 2026 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant