Summary
On dev push CI, test (windows-latest, 1/2) failed in parent-wake-empty-turn-requeue.test.ts: "BackgroundManager parent wake empty-turn recovery > #given parent history contains the empty assistant turn #when idle flushes the requeued wake #then one retry prompt is delivered" (run 36300728869, 4440 ms, waitUntil at :98 called from :270).
Root cause
This is a product race that Windows timer granularity exposes, not only a slow runner.
- The first parent-wake dispatch leaves a promptAsync post-dispatch hold on the session:
DEFAULT_PROMPT_ASYNC_POST_DISPATCH_HOLD_MS = 2_000 (packages/utils/src/prompt-async-gate/timing.ts:1). The hold's expiry is checked lazily with Date.now() (reservations.ts pruneExpiredReservations).
- When a retry meets that hold,
parent-wake-prompt-dispatch.ts requeues it with scheduleFlush(2_000), a delay equal to the whole hold length.
- The retry is armed a few ms after the hold starts. If the timer fires even a tick before
Date.now() crosses expiresAt (Windows timers and Date.now() tick on coarse, different clocks), the gate still reports reserved and the wake waits a second full hold (~4 s). In production that is a needless extra 2 s of parent-wake latency. The test's 4 s sleep-polling window turns it into a red job.
Expected
- A wake that meets a gate hold retries when that hold actually expires.
- The test waits on the exact event (the retry
promptAsync call) instead of a sleep-poll loop.
Acceptance criteria
- The gate's
reserved result carries the hold's expiresAt. The parent wake schedules its retry for expiresAt - now (a fixed 2 s stays only as the fallback when there is no expiry).
- A deterministic regression test pins that retry delay with a frozen clock.
- The empty-turn test has no polling loop. A focused Windows soak of the file passes 3 x 10 iterations.
Refs #8324
Summary
On
devpush CI,test (windows-latest, 1/2)failed inparent-wake-empty-turn-requeue.test.ts: "BackgroundManager parent wake empty-turn recovery > #given parent history contains the empty assistant turn #when idle flushes the requeued wake #then one retry prompt is delivered" (run 36300728869, 4440 ms,waitUntilat :98 called from :270).Root cause
This is a product race that Windows timer granularity exposes, not only a slow runner.
DEFAULT_PROMPT_ASYNC_POST_DISPATCH_HOLD_MS = 2_000(packages/utils/src/prompt-async-gate/timing.ts:1). The hold's expiry is checked lazily withDate.now()(reservations.tspruneExpiredReservations).parent-wake-prompt-dispatch.tsrequeues it withscheduleFlush(2_000), a delay equal to the whole hold length.Date.now()crossesexpiresAt(Windows timers andDate.now()tick on coarse, different clocks), the gate still reportsreservedand the wake waits a second full hold (~4 s). In production that is a needless extra 2 s of parent-wake latency. The test's 4 s sleep-polling window turns it into a red job.Expected
promptAsynccall) instead of a sleep-poll loop.Acceptance criteria
reservedresult carries the hold'sexpiresAt. The parent wake schedules its retry forexpiresAt - now(a fixed 2 s stays only as the fallback when there is no expiry).Refs #8324