- Extracted yield logic into a configurable YieldGate class to avoid process-global state.
- Injected time and sleep dependencies to support deterministic testing.
- Handled potential negative time progression by forcing a re-anchor instead of gating indefinitely.
- Maintained existing behavior for the public yieldIfDue export via a shared instance.
- Replaced the `keepaliveWhile` Promise wrapper with a new `EventLoopKeepalive` class that registers and disposes an interval timer through `Symbol.dispose`.
- Updated `Agent` to instantiate `EventLoopKeepalive` via `using` during prompt execution instead of manually managing an interval.
- Wrapped interactive mode's await path with the new helper and removed redundant `keepaliveWhile` usage from the CLI entrypoint.
- Removed the EventLoopKeepalive class and replaced it with a direct setInterval call.
- Added an unref call to the interval timer to prevent blocking the process exit.
Project convention (AGENTS.md) prohibits ReturnType<> — use the
concrete type name instead. NodeJS.Timeout matches the existing
pattern used throughout the codebase (e.g. interactive-mode.ts).
Root cause: Bun 1.3.x (JavaScriptCore) busy-waits when the only
pending work is an unresolved Promise. A setInterval keepalive
keeps the event loop in epoll_wait instead of userspace spinning.
- EventLoopKeepalive: setInterval-based keepalive (re-arms after each
firing, addressing the bot review concern about setTimeout expiry)
- keepaliveWhile(): wrapper to await a Promise with keepalive active
- Applied to getUserInput() in main.ts
- Retains yieldIfDue() and ExponentialYield from #1396
Idle CPU drops from ~100% to ~0% (wchan=do_epoll_wait).
- Replaced Bun.sleep with scheduler.wait for Node-compatible cancellable sleeps.
- Added module-level timestamp gate to skip yields within 50ms of the last one.
- Threaded AbortSignal through ExponentialYield.sleep to cancel losing timers in race.
- Added tests covering gate behaviour and stray-timer cancellation.
- yieldIfDue() uses compensated sleep (sleepAtLeast): retries Bun.sleep()
until the requested wall-clock duration has elapsed. This is necessary
because napi callbacks (uv_async_send) can wake the event loop
prematurely, causing Bun.sleep(N) to return after only ~1-2ms.
- ExponentialYield for bash-executor: starts at 20ms, doubles to 10s.
Closes#1384