Context
PR #17128 landed the poll-cadence lever for test/playwright/unit/ai/daemons/wake/daemon.spec.mjs and took the file off CI's slow-file list (dev baseline flagged it at 5.9m as the only slow file; the branch run carries no annotation, and the unit suite moved 15.7m → 14.0m at an identical 13316 passes).
That work deliberately did not touch the fixed sleeps, and #17123's own AC-2 required a decomposition receipt naming where the remaining wall clock lives before any conversion was attempted. This ticket is that remainder, and it exists because the decomposition inverted the earlier conclusion.
Live latest-open sweep: checked the latest 20 open issues at 2026-08-14T19:45Z; no equivalent found. A2A in-flight claim sweep over the last 30 messages: no competing claim; the only adjacent claim is #17124's lint lane (see Interaction below), forked to me by @neo-opus-grace at 15:09 and already resolved as complementary.
The Problem
I told @neo-opus-vega during #17123 intake that "it is not the sleeps," and I had measurements behind it: deleting a sleep made the spec twice as slow (6.4s → 12.2s), with every assertion still green.
That was true only at the shipped 3000ms cadence. The cadence quantized the file's wall clock — a wait shortened anywhere else landed before the same poll boundary and recovered nothing, and removing a boot sleep pushed DB injection before the daemon read its watermark, costing an extra full cycle.
With the cadence injectable at 50ms, that masking is gone and the sleeps are no longer hiding behind a tick. A source census of the current file:
| class |
count |
nominal |
setTimeout(resolve, N) with N >= 1000 |
82 |
142.9s |
N < 1000 (100 / 250 / 500) |
3 |
0.8s |
| total |
85 |
143.8s |
Largest groups: 63 × 1000ms, 8 × 4000ms, 3 × 5500ms, 2 × 5000ms, 2 × 4500ms, 2 × 3500ms.
Against the ~270s the file still costs locally after #17128, the fixed sleeps are ~53% of what remains — the single largest term. Vega's original #17123 premise was right about magnitude and wrong only about ordering: the cadence had to land first for de-sleeping to pay anything at all.
The generalizable finding, which is why this is worth a ticket rather than a line item: a masked cost is not a refuted cost. A measurement that shows "removing X does not help" is only valid at the configuration it was taken in; when a quantizing term is removed, X can become dominant. The first measurement did not falsify the sleeps, it measured them through a filter.
The Architectural Reality
test/playwright/unit/ai/daemons/wake/daemon.spec.mjs — the whole surface. 68 tests, 39 daemon spawns, 85 fixed sleeps.
- The conversion target already exists in-file:
waitForDaemonReady() (added by PR #17128) polls the daemon's own readiness line [Wake Daemon] Started. Tail-syncing from GraphLog ID: N, emitted immediately after lastSyncId = getLastSyncId(...) and immediately before pollLoop() — the exact instant the watermark exists, which is the ordering the boot sleeps were protecting. It measured ~90ms against a 1000ms sleep.
- The 5ms-interval
waitForCondition pattern in TextEmbeddingService.retry.spec.mjs is the established sibling precedent for non-boot synchronization.
AiConfig.orchestrator.wakeDispatch.pollIntervalMs (PR #17128) is what makes the conversion pay; specs pin it to 50ms per spawn. One test stays pinned at the shipped 3000ms because its 4s gap must straddle exactly one poll boundary — that one is genuinely wall-clock-under-test and must not be converted.
The Fix
Convert each fixed sleep whose purpose is state synchronization into a bounded condition poll or an event the daemon already exposes. Where elapsed wall-time IS the property under test, the sleep stays and carries the justification comment #17124's lint requires.
Work is confined to the one spec file; no production file changes, no new modules, no relocation.
Interaction with #17124 (sequencing, not duplication)
#17124 AC-3 adds a mechanical lint failing any unit spec with a bare fixed setTimeout(resolve, N), N >= 1000, lacking an explicit justification comment. @neo-opus-grace baselined this file's sites per-site as a burndown precisely so her gate could not block this work (her fork message, 2026-08-14 15:09; her census read 83 sites against my 82 — same class, counts taken at different heads).
The two tickets are complementary and ordered: #17124 freezes the class, this ticket burns down the baseline. Landing this will require editing that baseline down in the same PR — an expected, already-agreed edit, not a gate violation.
Acceptance Criteria
- Every fixed-duration sleep in
daemon.spec.mjs whose purpose is state synchronization becomes a bounded condition poll (the in-file waitForDaemonReady shape, or the 5ms waitForCondition pattern) or an event/promise the daemon already exposes.
- Any sleep that remains carries an inline comment stating that elapsed wall-time is the property under test — satisfying
#17124's lint without a blanket exemption.
#17124's per-site burndown baseline is reduced in the same PR to exactly the surviving sites; no site is silently exempted.
- Identical assertion coverage: no test deleted, no assertion weakened, pass count unchanged at 68 locally and unchanged in CI.
- Verification runs at the directory level (
test/playwright/unit/ai/daemons/wake/), not the single file — a single-file run is structurally blind to the cross-file worker state that produced the leak PR #17128 had to fix.
- Evidence: before/after from CI's own slow-file report plus the unit-suite total at identical pass counts, in the PR body. State the nominal recovered figure separately from the measured one — they will differ.
Out of Scope
- Daemon spawn/teardown cost. After the sleeps, ~126s across 39 spawn cycles is the next term. That number is arithmetic, not instrumentation — nobody has measured spawn cost directly. Sharing or batching daemon fixtures is a separate ticket with a different risk profile (shared fixtures reintroduce exactly the cross-test coupling this suite has been fighting).
- Production
ai/daemons/wake/daemon.mjs behaviour. The cadence leaf landed in PR #17128; nothing here changes the daemon.
- The two live ADR-0019 A1 constants in
daemon.mjs (CODEX_TURN_START_PROOF_TIMEOUT_MS, CODEX_TURN_START_PROOF_POLL_MS), flagged in PR #17128 and deliberately left.
Avoided Traps
The naive de-sleep sweep, which is the obvious reading of this ticket and is wrong. Converting sleeps to polls without the cadence lever measured slower (6.4s → 12.2s), green the whole way. Anyone picking this up must confirm PR #17128 is merged first; without it this work is negative-value and the test suite will not tell you.
Treating 143.8s as the recoverable figure. It is nominal — the sum of declared durations, therefore an upper bound. Sleeps overlapping real work return less. A PR claiming ~143s recovered without measuring it is over-claiming.
Related
- #17123 — the cadence-quantization slice (PR #17128), whose AC-2 decomposition produced this ticket
- #17124 — the fixed-sleep lint that freezes this class; its baseline is what this burns down
- #17043 — wake-suite flake history, relevant when interpreting a red run here
Origin Session ID: d991f8f7-2ca6-4c60-86b9-52104dbcc8df
Retrieval Hint: query_raw_memories("wake daemon spec cadence quantization masked cost sleeps decomposition"); the evidence base is the re-measure comment on PR #17128 (https://github.com/neomjs/neo/pull/17128#issuecomment-5297359129) and that PR's body decomposition table.
Context
PR #17128 landed the poll-cadence lever for
test/playwright/unit/ai/daemons/wake/daemon.spec.mjsand took the file off CI's slow-file list (dev baseline flagged it at 5.9m as the only slow file; the branch run carries no annotation, and the unit suite moved 15.7m → 14.0m at an identical 13316 passes).That work deliberately did not touch the fixed sleeps, and #17123's own AC-2 required a decomposition receipt naming where the remaining wall clock lives before any conversion was attempted. This ticket is that remainder, and it exists because the decomposition inverted the earlier conclusion.
Live latest-open sweep: checked the latest 20 open issues at 2026-08-14T19:45Z; no equivalent found. A2A in-flight claim sweep over the last 30 messages: no competing claim; the only adjacent claim is
#17124's lint lane (see Interaction below), forked to me by @neo-opus-grace at 15:09 and already resolved as complementary.The Problem
I told @neo-opus-vega during #17123 intake that "it is not the sleeps," and I had measurements behind it: deleting a sleep made the spec twice as slow (6.4s → 12.2s), with every assertion still green.
That was true only at the shipped 3000ms cadence. The cadence quantized the file's wall clock — a wait shortened anywhere else landed before the same poll boundary and recovered nothing, and removing a boot sleep pushed DB injection before the daemon read its watermark, costing an extra full cycle.
With the cadence injectable at 50ms, that masking is gone and the sleeps are no longer hiding behind a tick. A source census of the current file:
setTimeout(resolve, N)withN >= 1000N < 1000(100 / 250 / 500)Largest groups: 63 × 1000ms, 8 × 4000ms, 3 × 5500ms, 2 × 5000ms, 2 × 4500ms, 2 × 3500ms.
Against the ~270s the file still costs locally after #17128, the fixed sleeps are ~53% of what remains — the single largest term. Vega's original #17123 premise was right about magnitude and wrong only about ordering: the cadence had to land first for de-sleeping to pay anything at all.
The generalizable finding, which is why this is worth a ticket rather than a line item: a masked cost is not a refuted cost. A measurement that shows "removing X does not help" is only valid at the configuration it was taken in; when a quantizing term is removed, X can become dominant. The first measurement did not falsify the sleeps, it measured them through a filter.
The Architectural Reality
test/playwright/unit/ai/daemons/wake/daemon.spec.mjs— the whole surface. 68 tests, 39 daemon spawns, 85 fixed sleeps.waitForDaemonReady()(added by PR #17128) polls the daemon's own readiness line[Wake Daemon] Started. Tail-syncing from GraphLog ID: N, emitted immediately afterlastSyncId = getLastSyncId(...)and immediately beforepollLoop()— the exact instant the watermark exists, which is the ordering the boot sleeps were protecting. It measured ~90ms against a 1000ms sleep.waitForConditionpattern inTextEmbeddingService.retry.spec.mjsis the established sibling precedent for non-boot synchronization.AiConfig.orchestrator.wakeDispatch.pollIntervalMs(PR #17128) is what makes the conversion pay; specs pin it to 50ms per spawn. One test stays pinned at the shipped 3000ms because its 4s gap must straddle exactly one poll boundary — that one is genuinely wall-clock-under-test and must not be converted.The Fix
Convert each fixed sleep whose purpose is state synchronization into a bounded condition poll or an event the daemon already exposes. Where elapsed wall-time IS the property under test, the sleep stays and carries the justification comment
#17124's lint requires.Work is confined to the one spec file; no production file changes, no new modules, no relocation.
Interaction with #17124 (sequencing, not duplication)
#17124AC-3 adds a mechanical lint failing any unit spec with a bare fixedsetTimeout(resolve, N),N >= 1000, lacking an explicit justification comment. @neo-opus-grace baselined this file's sites per-site as a burndown precisely so her gate could not block this work (her fork message, 2026-08-14 15:09; her census read 83 sites against my 82 — same class, counts taken at different heads).The two tickets are complementary and ordered:
#17124freezes the class, this ticket burns down the baseline. Landing this will require editing that baseline down in the same PR — an expected, already-agreed edit, not a gate violation.Acceptance Criteria
daemon.spec.mjswhose purpose is state synchronization becomes a bounded condition poll (the in-filewaitForDaemonReadyshape, or the 5mswaitForConditionpattern) or an event/promise the daemon already exposes.#17124's lint without a blanket exemption.#17124's per-site burndown baseline is reduced in the same PR to exactly the surviving sites; no site is silently exempted.test/playwright/unit/ai/daemons/wake/), not the single file — a single-file run is structurally blind to the cross-file worker state that produced the leak PR #17128 had to fix.Out of Scope
ai/daemons/wake/daemon.mjsbehaviour. The cadence leaf landed in PR #17128; nothing here changes the daemon.daemon.mjs(CODEX_TURN_START_PROOF_TIMEOUT_MS,CODEX_TURN_START_PROOF_POLL_MS), flagged in PR #17128 and deliberately left.Avoided Traps
The naive de-sleep sweep, which is the obvious reading of this ticket and is wrong. Converting sleeps to polls without the cadence lever measured slower (6.4s → 12.2s), green the whole way. Anyone picking this up must confirm PR #17128 is merged first; without it this work is negative-value and the test suite will not tell you.
Treating 143.8s as the recoverable figure. It is nominal — the sum of declared durations, therefore an upper bound. Sleeps overlapping real work return less. A PR claiming ~143s recovered without measuring it is over-claiming.
Related
Origin Session ID: d991f8f7-2ca6-4c60-86b9-52104dbcc8df
Retrieval Hint:
query_raw_memories("wake daemon spec cadence quantization masked cost sleeps decomposition"); the evidence base is the re-measure comment on PR #17128 (https://github.com/neomjs/neo/pull/17128#issuecomment-5297359129) and that PR's body decomposition table.