Skip to content

fix(daemon): root-cause seven load-only daemon test failures - #4144

Merged
ScriptedAlchemy merged 7 commits into
masterfrom
fix/4135-daemon-load-flakes
Oct 16, 2026
Merged

ScriptedAlchemy merged 7 commits into
masterfrom
fix/4135-daemon-load-flakes

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Fixes #4135

Summary

Seven daemon tests failed only under CPU load. Two of them exposed production defects: the shared Codex discovery hub served a stale empty scan, and the hook orchestrator held its permit past settlement. The other five relied on test-side timing or thread identity. This PR fixes each at its cause. It adds no sleeps and widens no timeouts. Where a wall-clock bound only stood in for a typed outcome, the bound is gone and the outcome carries the assertion.

Load recipe (the #4124 recipe): bazel test //crates/tracedecay:unit_test --runs_per_test=30 --nocache_test_results --local_test_jobs=20 --test_timeout=1800 --run_under=<exec taskset -c 0-7 "$@">. Targeted runs restrict with --test_arg=<filter>.

Root causes

idle_history_reads_journey_test::streamed_codex_message_reads_only_its_rollout (production fix in tracedecay-sessions)

Failing before (2 of 30 at baseline, 6 of 30 and 8 of 20 in later runs):

history converged on the wrong message count (expected 66): {... "store":{... "messages":65 ...},
"projection":{"convergence":{"converged_at_unix_micros":1792154522065528,"epoch":2,"state":"converged"}, ...
"worker":{"backlog":0, ... "last_progress_at_unix_micros":1792154460306021 ...}}}

The project scope and the profile scope are two consumers of one shared Codex discovery hub. The hub queues every shared scan's pass for every consumer. A trace of the hub's deliveries in failing runs shows this order:

t=...542788 profile site=shared_scan paths=0   <- profile idle scan, queued for project
t=...543958 CODEXTEST append                    <- the test streams a message into the live rollout
t=...598766 project site=queued paths=0         <- project's next recheck pops that empty pass

The project's idle recheck came after the append. It popped the empty pass of a profile scan that began before the append, converged on it as epoch 2, and slept for another minute. In passing runs the profile's scan ran after the append, and the project received a pass with the rollout.

Fix: the hub numbers shared scans. When a consumer polls and finds an empty, frontier-neutral queued pass from a scan that began before the poll, it drops that pass and scans again. Passes that carry paths or move the frontier are still delivered in order. a_poll_after_growth_is_not_answered_by_an_earlier_empty_scan reproduces the defect without load and fails before the fix (the grown rollout must be rediscovered: []). The journey test itself is unchanged.

invocation_tests::types_tests::hook_orchestration_runs_feedback_work_without_scout_lifecycle

Failing before (1 of 30 at baseline; 58 of 300 in a targeted run):

hook admission must return before daemon-owned provider or model work begins

The hook work runs on a native worker (NativeTask::spawn). It can run before the admitting test thread reaches the assertion, so a shared flag read a background run as a synchronous one. This is the #4124 class: the instrumentation assumed the work ran relative to the test thread.

Fix: a thread-local marks the admitting thread for the duration of the admission call. Only work that runs inside that call counts as synchronous.

invocation_executor::controlled_invocation_tests::in_process_effect_cancellation_requests_daemon_cancel_and_awaits_settlement

Failing before (1 of 30):

authoritative settlement is joined: Elapsed(())

The settlement deadline was 1 s and the outer timeout was also 1 s. The chain from caller cancellation to daemon cancel to native completion crosses three threads, and under load it took longer than 1 s. A 1 s deadline also meant the deadline path could stand in for the cancellation path. The test now passes a 24 h deadline, so only the caller's cancellation can request the daemon cancel. The outer timeout is gone: settlement bounds itself, and a missed daemon cancel ends in ResetRequired with an undetached work flag. The deadline sibling (in_process_effect_deadline_requests_daemon_cancel_and_awaits_settlement) had the same 1 s outer bound and gets the same change.

invocation_executor::controlled_invocation_tests::in_process_read_observes_admitted_outer_cancellation_after_start

Failing before (7 of 30):

admitted cancellation settles the nested read: Elapsed(())

The nested read held its project lease until settlement aborted it after the 1 s read grace. That grace spent half of quiescence's 2 s production drain budget (DAEMON_TASK_ABORT_DEADLINE) and half of the test's 2 s outer timeout, leaving 1 s for scheduling. The read now releases its lease when it observes the admitted cancellation and then never returns. Settlement still takes its admitted-cancellation branch and aborts the read. The Cancelled outcome proves the admitted cancellation settled the read: without it the read would settle TimedOut at its 10 s deadline. A dropped channel proves the abort.

invocation_tests::types_tests::replayed_admission_after_retryable_failure_completes_a_fresh_cycle (production fix in tracedecay-daemon-service)

Forced before. A 20 ms pause was injected between settle_operation removing the in-flight entries and firing completions. It failed 5 of 5:

assertion `left == right` failed
  left: Backpressured
 right: Enqueued

A finished hook cycle left the in-flight maps and fired its completions while it still held its permit. An admission in that gap found no cycle to join and no free permit, so a replay of a settled cycle was backpressured. A completion also fired before the next admission could run. BoundedHookOrchestratorV1 now releases the permit before it settles the operation. The same 20 ms pause with the fix passes 5 of 5 across all of types_tests.

authentication_refusal_test::rejected_token_reads_typed_refusal_and_daemon_keeps_serving (daemon_suite)

Failing in the local merge gate (twice):

authentication_refusal_test.rs:78:10:
write handshake: Os { code: 32, kind: BrokenPipe, message: "Broken pipe" }

The daemon refuses on the preface alone. It writes the refusal frame, drains pipelined input until a line read sits idle for 100 ms, and closes. The raw client wrote preface, handshake and request as separate writes, and each expected success. A client stalled between writes under load hit a closed socket. A 300 ms pause after the preface reproduces this 3 of 3. The refusal frame is queued before the close, so the client now stops writing on a broken pipe or reset and still reads it.

Not changed

  • lcm_preserved_profile_journey_test::preserved_profile_lcm_discovery_converges_without_blocking_retrieval passed 90 of 90 at the issue's load. In one overloaded run (17 of 30 suites failing), it failed 5 times on production bounds: the 2 min tool_dispatch_deadline_exceeded on tracedecay_lcm_status and tracedecay_sessions_for, a timed_out message search, and one worker_stopped refresh worker. None of these is a test race, so this PR does not change the test.
  • project_capacity_reuse_test::projects_past_the_server_cache_open_and_serve failed in the gate at code_index_journey.rs:186:5, which is tracedecay init failed. The gate batch log kept only the first line, so the stderr is lost. It passed 100 of 100 dedicated loaded runs and 20 of 20 loaded full daemon_suite runs here. This PR does not change it.

Verification

  • Load, all fixes, 30 runs: every test in scope passed 30 of 30. That covers both Codex journeys, the hook test, both effect tests, the read test, the replay test and the preserved-profile journey. The run's other failures predate this PR: cancel_candidate_journey (10, whose flaky: cancelled_large_candidate_search_stops_before_the_next_batch fails under load #4124 fix is not on this base) and scheduler_config::concurrent_reenable_creates_one_live_scheduler_owner (1).
  • Targeted load, 300 runs of types_tests and controlled_invocation_tests: 300 of 300 pass. Master fails 58 of 300, all on the hook test.
  • authentication_refusal_test under load: 100 of 100.
  • Each repaired test still detects its defect, checked by breaking production temporarily:
    • Hook: run the work inline in admit_with_cancellation. The test fails with "hook admission must return before daemon-owned provider or model work begins".
    • Effect tests: drop the request_cancellations.cancel call in settle_in_process_invocation. Both fail with "work was not detached" after the 30 s grace.
    • Read test: ignore the admitted cancellation. The test fails on the Cancelled match after the 10 s deadline. Skip the abort: the test hangs on the dropped-channel await until the Bazel timeout.
    • Auth test: skip the refusal frame write. The test fails on "the refusal line must parse as the typed refusal frame".
    • Codex hub: the new hub test fails without the hub fix.
  • bazel test //crates/tracedecay:unit_test //crates/tracedecay-daemon-service:unit_test //crates/tracedecay-sessions:unit_test; bazel build --config=clippy for //crates/tracedecay:all, //crates/tracedecay-daemon-service:all, //crates/tracedecay-sessions:all; rustfmt_check_2024 for all three crates.

Devin Review

The hook work runs on a native worker and can finish before the
admitting test thread resumes, so checking a shared flag right after
admit read a background run as a synchronous one. Mark the admitting
thread for the duration of the call and count only work that ran
inside it.

Refs #4135
The cancellation test gave settlement a one second deadline and a one
second outer timeout, so a settlement chain that crossed three threads
under load hit the timeout. Put the deadline past any run so only the
caller's cancellation can request the daemon cancel. Settlement itself
bounds both tests: a missed daemon cancel ends in ResetRequired and an
undetached work flag, so the outer timeouts are dropped.

Refs #4135
A finished hook cycle left the in-flight maps and fired its
completions while still holding its permit. An admission in that gap
found no cycle to join and no permit, so a replay of a settled cycle
was backpressured. Release the permit before settling, so a completion
only fires once the next admission can run.

Refs #4135
The nested read held its project lease until settlement aborted it
after the one second read grace, so quiescence's two second drain
spent half its budget on that grace, and a two second outer timeout
bounded a settlement that needs one second plus scheduling. Under load
the timeout fired. The read now releases its lease when it observes
the admitted cancellation and never returns, so settlement still takes
its admitted-cancellation branch and aborts it. The Cancelled outcome,
not a timer, proves the admitted cancellation settled the read, and a
dropped channel proves the abort.

Refs #4135
The raw client wrote its preface, handshake and request as separate
writes and expected each to succeed. The daemon refuses on the preface
and closes once its drain of pipelined input sits idle for 100 ms, so
a client stalled between writes under load hit a broken pipe before
reading anything. The refusal frame is queued before that close, so
the client now stops writing on a broken pipe or reset and reads it.

Refs #4135
An idle scan one Codex consumer runs is queued for every other
consumer. A consumer that polls after a rollout grew is answered from
that queue with the empty pass of a scan that began before the growth,
so it converges without the new bytes. This fails until the hub stops
serving such stale empty passes.

Refs #4135
The shared Codex discovery hub queues each scan's pass for every
consumer. A consumer polling after a rollout grew popped the empty
pass of a peer's idle scan that began before the growth and converged
on it, so the streamed message waited for the next idle recheck a
minute later. Under load the journey test read that convergence as a
dropped message.

Number shared scans and drop a queued empty pass whose scan began
before the poll, so the consumer scans again. A pass with paths or a
moved frontier is still delivered in order.

Refs #4135
@changeset-bot

changeset-bot Bot commented Oct 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 186fa75

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review

Comment on lines +673 to +676
if scan <= scans_before_poll
&& queued.pass.report.paths.is_empty()
&& queued.pass.selected_sources.is_empty()
&& queued.pass.next_frontier == queued.base

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 New Codex rollouts missed after shared scan

When a rollout appears after a shared scan with existing paths, discover_blocking delivers that stale nonempty pass to another consumer. The consumer marks discovery complete without the new rollout and waits for its next idle recheck.

Learn more

Shared Codex scans are queued for every registered consumer. The new staleness check skips only passes with no paths, no selected sources, and an unchanged frontier. A completed scan containing existing rollouts remains queued when another consumer polls later; if a new rollout was created in between, that queued pass lacks it but still advances the second consumer to the completed frontier. The project and profile consumers use this delivery through run_codex and the profile ingestion path, so the missing rollout is not ingested on that recheck.

Example: Profile scans one existing rollout and queues a complete pass for Project. A second rollout is created before Project polls. Project receives the queued pass containing only the first rollout, commits completion, and does not see the second until its later idle recheck.

Recommended fix: Treat queued passes produced before the current poll as potentially stale even when they contain paths or advance the frontier. Preserve each consumer's frontier and ordered delivery while refreshing or validating stale passes against the current corpus; add a two-consumer test that creates a second rollout after a nonempty completed scan.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

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.

flaky: daemon unit tests fail under pinned 20x load

1 participant