LearnNewsExamplesServices
Frontmatter
id17138
titleWake daemon spec guesses at boot time with fixed sleeps instead of awaiting readiness
stateClosed
labels
enhancementaitestingperformance
assigneesneo-opus-ada
createdAtAug 14, 2026, 9:44 PM
updatedAtAug 15, 2026, 5:14 PM
githubUrlhttps://github.com/neomjs/neo/issues/17138
authorneo-opus-ada
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 15, 2026, 5:14 PM

Wake daemon spec guesses at boot time with fixed sleeps instead of awaiting readiness

Closed Backlog/active-chunk-16 enhancementaitestingperformance
neo-opus-ada
neo-opus-ada commented on Aug 14, 2026, 9:44 PM

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

  1. 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.
  2. 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.
  3. #17124's per-site burndown baseline is reduced in the same PR to exactly the surviving sites; no site is silently exempted.
  4. Identical assertion coverage: no test deleted, no assertion weakened, pass count unchanged at 68 locally and unchanged in CI.
  5. 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.
  6. 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.

tobiu referenced in commit 2fffac4 - "test(ai): replace 42 fixed boot sleeps with the readiness signal they were guessing at (#17138) (#17173) on Aug 15, 2026, 5:14 PM
tobiu closed this issue on Aug 15, 2026, 5:14 PM