LeadRollover's failure logs print the CONFIGURED budget, not the measured wait — the exact thing that hid #489 #494
Closed
opened 2026-09-12 04:00:31 +02:00 by ltms
·
2 comments
No Branch/Tag Specified
main
worker/fleetd-612-unita-87807e-1
worker/612-b3-mcpwirings-da2b58-3
worker/612-b2-cb185-176d3a-2
worker/612-b1-completion-457459-1
worker/612-agaps-73a926-2
worker/608-sleeps-3a64ff-3
worker/621-b4520b-1
worker/618-b83894-2
worker/fleetd-615-e05481-5
worker/lead-autocompact-5f1ab2-3
worker/fleetd-613-f85deb-3
worker/fleetd-608-flaky-nudge-test-d0c2d1-3
worker/lead-context-gauge-ad404f-1
worker/gauge-wiring-9158c1-4
worker/redeploy-slowstart-ead0e5-5
worker/charter-bytes-13668c-6
worker/rollover-outcome-291483-2
worker/589-f64303-2
worker/593-1a8025-5
worker/589-fcd2aa-1
worker/568-9fdaa2-3
worker/571-attempted-outcome-5739f7-2
worker/581-completionresolver-cas-sites-0542b7-6
worker/562-loop-health-wiring-test-99611c-5
worker/562-surface-loop-health-7df5cc-4
worker/575-waiter-cleanup-sites-62ad80-1
worker/572-answer-lock-release-46a9ae-5
worker/567-probe-channel-leak-a38fc5-6
worker/551-record-before-send-7cbf56-1
worker/561-listener-fanout-survives-a-throw-61d538-2
worker/555-redeploy-main-flow-seam-65c2f5-2
worker/556-injector-owns-registration-e027a5-1
worker/552-post-restart-mktemp-abort-bc2672-4
worker/553-onstatus-completion-leak-0da881-2
worker/550-shasum-linux-196132-1
worker/538-loop-dies-on-error-4a5eeb-6
worker/426-health-coverage-ef1fd4-4
worker/504-failed-reported-clean-3cfd66-3
worker/537-capturedlog-close-e4c437-2
worker/459-broken-link-targets-cadc17-5
worker/535-appender-leak-fe74c1-1
worker/512-part2-shutdown-detection-434701-9
worker/529-logger-level-sweep-2a5533-8
worker/528-drain-gate-call-site-5de83d-7
charter/forge-mcp-vs-token
worker/521-swap-guard-unpinned-28e931-5
worker/519-probe-test-harness-d25ab8-4
worker/525-logger-level-leak-1b4eb0-6
worker/518-fleetmcp-resolver-wiring-8ef96c-1
worker/512-drain-complete-line-7edd71-3
worker/517-abort-branch-and-jar-id-41b641-2
worker/500-9e52c9-3
worker/509-4912f4-2
worker/511-9a4b23-1
worker/493-479f45-2
worker/505-03f8b2-1
worker/492-followup-detect-unclear
worker/501-a31fa0-7
worker/498-451d1c-5
worker/494-1015ce-2
worker/492-209647-1
worker/489-001902-2
worker/480-relative-handover-path-906323-1
worker/480-b-handover-skill-45bf1f-5
worker/474-followup-source-pin-f54a55-17
worker/474-charter-check-on-reload-f54a55-17
worker/466-quarantine-repeatcount-report
worker/393-opencode-skill-seeding-71854b-13
worker/469-canonical-tool-names-2a472a-16
worker/466-quarantine-escalation-5ae9c1-15
worker/446-hot-exhausted-pattern-0af580-6
worker/464-charter-tool-name-guard-a85635-12
worker/463-listfleet-default-fails-open-f1c76c-11
worker/458-invariant-5-by-purpose-862f9a-10
worker/439-coordinator-row-gate-bc032a-8
worker/449-herdr-protocol-576015-4
worker/450-abstract-spawn-599e1c-5
worker/437-ack-refuses-177d91-1
worker/444-placement-window-feb56a-2
worker/440-helddurable-derived-d462d7-13
worker/425-rework-placement-resolve-c58ba1-9
worker/421-lead-peek-held-msgs-cdbad2-10
worker/435-fixed-policy-cap-fe11de-12
worker/422-gate-state-observability-9e79d6-11
worker/431-memberregistry-live-readers-cdbad2-10
worker/424-architect-slot-hot-038b41-7
worker/422-model-gate-spawn-c29f48-6
worker/425-default-profile-live-f55534-8
worker/415-coverage-wording-2cbf9c-5
worker/416-3ad1da-1
worker/418-588283-3
worker/deterministic-stamp-race-409-3cb7b6-10
worker/armed-reads-live-config-404-ed931f-9
worker/reply-peer-refusal-391-5a34bd-7
worker/models-allowlist-aa9e9b-3
worker/ttl-stamp-race-399-f1122f-8
worker/scrub-receipt-400-316b3e-5
worker/exhaustion-detection-395-105105-6
worker/scrub-abort-394-316b3e-5
fix/scrub-uid-abort
worker/task-scrub-517574-2
worker/t386-clock-bd5b78-4
worker/t384-scrub-813790-5
worker/t381-cc-748314-2
worker/t373-336973-2
worker/t365-3920c5-3
worker/t358-6e989b-1
worker/t355-8b321c-1
worker/fleetd-369-hermetic-git-tests-e8b19a-3
worker/fleetd-368-stale-lead-binding-f5682e-2
worker/fleetd-360-deploy-units-0d3793-1
worker/359-dead-lead-tabs-f1253b-4
worker/362-worktree-skills-c03e51-3
worker/361-coord-visibility-655144-1
362-plugin-visibility-and-drift
worker/errscan-bed2ca-2
worker/amqp-log-identity-bed2ca-2
worker/withdefaults-guard-561704
worker/sleepguard-82076d-1
worker/fd334-9ee1b6-5
worker/fd348-f1ab27-4
worker/fd335-a71c35-1
worker/fd342-174a17-2
worker/fd345-490d0f-3
worker/fleetd-337-5ec7d4-21
worker/fleetd-341-af5a6b-24
worker/fleetd-339-5ca0a2-23
worker/fleetd-338-83a4a1-22
worker/fleetd-333-281f46-18
worker/fleetd-329-11bdbb-16
worker/fleetd-330-2770fb-17
worker/fix-326-50506e-15
worker/fix-324-3e9bbf-14
worker/fix-323-b8287d-13
worker/fix-316b-bd0860-11
worker/fix-318-76ca36-9
worker/fix-317-486aec-8
worker/fix-315-ce47c5-6
worker/fix-307-275890-6
worker/fix-308-b4f664-7
worker/fix-309-ec3939-8
worker/fix-310-7a3974-9
worker/fix-302-52ad0e-9
worker/fix-298-ce1acb-8
worker/fix-297-66bd11-7
worker/fix-296-104622-6
worker/fix-293-bare-closetab-eb22b5-3
worker/fix-280-gone-ask-lapse-bca98e-2
worker/fix-290-reapidle-guard-coverage-9b0dd1-1
worker/fix-285-trust-seed-8f3565-10
worker/fix-284-backend-error-seat-85912c-11
worker/fix-282-chained-ask-e6d0bb-8
worker/fix-283-teardown-leaks-f40dfa-9
worker/fix-281-pin-handler-actions-4921ac-7
worker/audit-rendezvous-lifecycle-d072ae-2
worker/audit-health-placement-1a2476-6
worker/audit-teardown-exits-e207a5-3
worker/audit-launcher-asymmetry-27e370-4
worker/audit-rest-authz-6ca53c-5
worker/investigate-275-abandon-asking-fdef52-8
worker/fix-274-worktree-leak-b0095d-7
worker/fix-273-exhausted-pattern-9665b5-6
worker/fleetd-267-model-check-bd8068-1
worker/fleetd-131-archunit-18b834-7
worker/fleetd-266-sshagent-rename-a014ff-6
worker/fleetd-184-uid-claim-8e1f31-4
worker/fleetd-184-warn-b381ee-10
worker/fleetd-184-docs-be1d12-9
worker/fleetd-257-9bf010-7
worker/fleetd-103-23a113-6
worker/fleetd-247-342356-5
worker/fleetd-116-04dea8-4
worker/fleetd-252-a830e0-3
worker/fleetd-111-7e8673-9
worker/fleetd-155c-f8ef4b-8
worker/fleetd-176-b928ca-3
worker/fleetd-249-7a7878-2
worker/cb248-composition-root-b-9acdf7-15
worker/cb148-envrc-default-fa6c82-12
worker/cb201-unit5-wiring-6c12e6-8
worker/cb241-fallback-echo-1175e9-11
worker/cb149-trust-dialog-2392a5-9
worker/cb134-148-overlay-visible-c9b986-10
worker/cb234-session-id-keyed-04e1fc-1
worker/cb201-unit3-nudge-abdf5c-6
worker/cb201-unit2-policy-c1102c-5
worker/cb201-unit4-outcome-a13bfa-7
worker/cb201-unit1-classifier-91b9b1-4
worker/cb201-227-refine-831980-3
worker/cb175-model-readback-0f085f-1
worker/cb222-charter-tmpdir-17f013-1
worker/cb226-architect-slot-race-cd3aa8-3
worker/cb224-worktree-root-group-024523-2
worker/cb-123-role-demotion-c600f7-2
worker/cb-219-opencode-roots-1f677e-1
worker/cb214-claude-session-id-b9eab4-4
worker/cb213-zdotdir-wrong-process-dd6de4-3
worker/cb211-exhaustion-classification-9546e0-2
worker/cb137-ambiguous-task-4df3d8-4
worker/cb209-agentsessionid-4dfdb6-2
worker/cb185-hostenvnames-2692b5-3
worker/cb206-opencode-sqlite-128718-2
worker/cb185-worktree-group-fc0c99-1
worker/cb-137-ask-ticket-e7760c-2
worker/cb-172-broker-uri-d36ae4-4
worker/cb-175-model-readback-76ead6-3
worker/cb-161-pane-ancestry-293510-1
worker/cb-164-rebase-885863-8
worker/cb-164-empty-scrape-false-success-1a80af-3
fix/cb-197-ticket-ttl-from-completion
worker/cb-189-remote-url-coverage-4692f3-1
worker/cb-185-blockers-027756-4
worker/cb-192-gap-log-11b631-2
worker/cb-633-fix-5f4396-3
worker/cb185-router-d6436d-3
worker/cb185-router-routing-gaps-9e9d33-3
worker/cb185-paneids-992586-2
worker/cb-633-allow-list-union-ed374b-1
worker/cb-157-credential-in-remote-url-496e44-2
worker/cb-641-health-herdr-evidence-8f1f54-6
worker/cb-640-health-msg-evidence-99c9cd-1
worker/cb-642-fleets-status-skill-bbbc40-5
cb-634-ide-mcp
worker/lead-comms-wiring-c014b9-7
worker/lead-mailbox-c19577-6
worker/autocompact-window-82bc2f-5
worker/cb-634-probe-18056f-4
worker/cb635-broker-urienv
worker/cb-632-config-retry-8e0efa-7
lead/cb-622e-claude-md
lead/cb-622-followup
worker/cb-622a-165dff-1
lead/cb-622d-opencode-mount
worker/cb-622b-717c67-2
worker/cb-622c-ab7759-3
worker/cb-617b2-20ca4b-3
worker/cb-617a-5c2f4a-1
worker/cb596-4e49ef-3
worker/cb586-10500c-1
worker/cb-606-b9343a-25
worker/cb604-1445f8-24
worker/cb582-477374-21
worker/cb584-8c2281-22
worker/cb600-e6b9a9-20
worker/cb602-ce257f-19
worker/cb601-b42837-18
worker/cb598-6c7ba7-17
worker/cb599-740fe4-16
worker/cb597-282224-15
worker/cb590fix-185e9a-10
worker/cb528-recovery-race
worker/cb594-96bead-8
worker/cb590-916766-2
worker/cb527-997d99-3
worker/cb592-env-leak-3cbf9c-1
worker/cb588-async-ticket-nudge-3218f7-5
worker/cb578b-9dcb13-6
worker/cb581-d24826-5
worker/m2-u5-ef8c42-15
worker/cb578a-516499-2
worker/cb576-01a04b-17
worker/cb579-lead-tab-acba06-20
worker/cb580-terminal-health-ed6058-21
worker/cb577-f36fdc-18
worker/cb573b-3db06f-16
worker/cb568c-f36fdc-18
worker/cb568-drop-cause-c3ac1c
worker/cb575-cancelled-notification-c3ac1c
worker/m4-sol-a2cbec-3
worker/cb574-async-ask-c3ac1c
worker/cb573-health-model-8ca857-14
worker/cb572-unknown-target-7f2e35-13
worker/u4-700706-9
worker/u3-b9fcb6-6
worker/u2-ef5b68-4
worker/u1-469dce-1-clean
worker/u1-469dce-1
worker/cb-564-health-events-70cf7e-2
worker/cb-565-recycle-drops-role-98e58f-3
worker/cb-563-missing-reply-df2866-1
worker/cb-562-readiness-gate-silent-6c23c9-3
worker/cb-560-architect-presence-da8155-1
worker/cb-561-architect-silent-off-a71cab-2
worker/cb-548-bind-architect-slot-fe1b8c-1
worker/parity-overlay-settings-5fb711-1
secrets-central-store
cb-559-hot-key-correction
cb-557-fleet-role-pools
worker/cb-553-maxload-explicit-spawn-305ee3-6
worker/cb-551-idle-lead-heartbeat-f1633c-1
worker/cb-544-drain-preserves-worktree-925fad-3
worker/cb-552-docs-sync-1cb9cf-4
worker/cb-548-rendezvous-guard-rebased
worker/cb-548-rendezvous-guard-116b53-10
worker/cb-548-authz-v2-586df6-8
worker/cb-548-authz-264363-5
salvage/cb-528b-codex-home
salvage/cb-528a-codex-launcher
CB-518-primary-flow
feature/peer-launcher-spi
cb-103-injector
v1.1.0
v1.0.0
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#494
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by the fleet01 lead, reviewing #489/#490 read-only. Their framing: "the nudge loop needs a bound and a named failure message, not a happy path … if the pickup never comes, the useful outcome is a failure that says which pane, how many nudges, and how long — not a 20-second budget quietly elapsing the way the 438 ms one quietly did not."
They are right, and the fix in #490 does not close it. #490 made the wait real. It did not make the wait observable.
The three ways the roll can end, and what an operator sees
LeadRollover.java, measured onmainat the deployed commit::400warncfg.clearSettleSeconds(), token:569infobootstrapTextanyway:406infolead-rollover: rolled token=… lead=…Defect 1 —
:400prints a number it did not measurecfg.clearSettleSeconds()is the configured budget. It is not how long the wait ran. An operator reads "within 20s" and concludes the daemon waited 20 seconds.This is not theoretical. It is the precise shape that hid #489 for a day. The live roll on 2026-09-12 used 438 ms of a 20-second budget and reported success:
Had the wait failed instead of passing, this line would have said
within 20swhile the truth was 0.438s. The log would have been confidently wrong, not merely silent. Same class as thea-status-field-must-read-the-source-the-behaviour-readslesson: print the value the behaviour actually produced, never the one that configured it.The elapsed time is already available —
nowMillisis injected and the method computesdeadlinefrom it. Nothing needs new plumbing.Defect 2 — the grace-limit release is an
info, and it is a success:569fires when/clearwas never observed asWORKINGafter 8 consecutiveIDLE/DONEpolls. It thenreturn true, sorunRolloverproceeds to sendbootstrapTextinto a pane where/clearmay never have run. That is the release-rather-than-wedge trade, and I still think release is right — a wedged lead is worse than a messy one.But it is logged at
infoand followed bylead-rollover: rolled. An operator grepping forWARNsees a clean roll. The one case where the roll is most likely to have produced/clearFresh-shaped garbage is the case that reports success most loudly.Asked for
:400— log the measured elapsed milliseconds, and the nudge count. Keep the configured budget too, but label both, so the two can be compared. A line that showsconfigured=20s elapsed=438msdiagnoses #489 on sight.:569— raise towarn, add measured elapsed and the target pane. Keep the release behaviour; only make it visible.:406— the success line should carry the elapsed time as well. A roll that "succeeds" in 438 ms is the signal, and right now nothing prints it.What is already proven, so nobody re-does it
I ran two mutations today against the deployed code, with a control after each. Baseline
LeadRolloverTest= 29 tests, 0 failures.waitUntilAtTurnBoundary): 1 failure,clearPickupIsNudgedBeforeBootstrapTextWhenPaneStaysIdle. So a test does exist that catches the original bug — the fleet01 lead's first question is answered yes.agents.submit(target); return true;— satisfies "a nudge happened, ordered between/clearandbootstrapText" and nothing else): 4 failures + 1 error. So the suite is not merely pinning the nudge; three tests assertbootstrapTextis not sent when the wait should not release.shasumbyte-identical to the pristine copy both times.So #490's behaviour is pinned. Its observability is not pinned at all, and that is what this ticket is for.
Related: #486 (the same loop hangs a suite instead of failing it, because the injected clock never advances while
settleSleeperis a no-op — note that fixing this ticket by measuring elapsed time will read that frozen clock, so the tests must set it deliberately), #489, #490, #480.Three corrections and one generalisation, all from the fleet01 lead's second read
1. The release-and-send trade: use this reason, not the one in the ticket
I justified sending
bootstrapTextafter the grace-limit release with "a wedged lead is worse than a messy one". That is a preference, and someone will reopen it. The fleet01 lead supplied the argument that closes it, and it is better:At that point the code cannot tell "the
/clearlanded but the pickup went undetected" apart from "the/clearnever landed". Those two states need opposite responses, and only one branch is non-fatal in both:/clearlanded/clearnever landedbootstrapTextSo releasing and sending is not a taste for mess over wedging. It is the only branch with no fatal cell. This also disposes of the obvious third option — "abort the roll and leave the pane alone" — which is strictly worse, because leaving an already-cleared pane alone is the fatal cell.
Do not change the behaviour. This replaces the reason, not the code.
2. The generalisation. This defect has now appeared three times in three subsystems.
The fleet01 lead names two earlier instances alongside this one:
LeadRollover:400printscfg.clearSettleSeconds()where a reader takes it as the measured wait,backend-error classification: offline,heldDurableliteral found on #438.All three are a field that reads as derived but is actually a constant, printed in a channel an operator trusts, wrong in the direction that says everything is fine. Not silent — confidently wrong, which is worse, because a wrong answer terminates the search where a missing one continues it.
Three instances across three subsystems is enough to state the rule:
That rule is what this ticket should be reviewed against, and it is broader than the three lines listed above. The "find what else has this shape" instruction in the worker's brief covers the same ground.
3. Correction to the mutation numbers I published
I reported mutation 2 as "4 failures + 1 error". The error should be counted as nothing, and the peer spotted why before I checked.
submitThatThrowsDoesNotAbortTheRoll:629errored rather than failed. Reading the stack, the exception escaped from my stub, not from an assertion catching the stub:My stub was
agents.submit(target); return true;with notry/catch. The real method wraps that call. So the mutant broke the test; the test did not catch the mutant. That is the mutant removing atry/catchincidentally — a different property from the one mutation 2 was built to probe.Corrected count: mutation 2 = 4 failures. The conclusion is unchanged and rests entirely on those four, three of which assert the negative (
bootstrapTextis not sent when the wait should not release). Those are assertions firing, not collateral.The general rule, worth applying to every future battery: when a mutation produces errors as well as failures, count only the failures. An error can be the mutant breaking the test rather than the test catching the mutant — the same family as an equivalent mutant, where the result looks like signal and is not.
Done — PR #496 merged as
8f59019Three commits:
3fb3311,c87cc25,e966cba.Verified by me, not taken from the worker's report:
Mutation M2 (
PICKUP_GRACE_POLLS8 → 5), run by me in a detached worktree: 2 failures,clearGraceReleaseLogIsWarnWithMeasuredElapsedandsuccessLogPrintsMeasuredElapsedForTheWholeRoll. The line the mutant emitted readafter 5 consecutive IDLE/DONE polls (4 of those were nudged), so both numbers now follow the loop rather than the constant. Restored,shasum -a 256byte-identical, control 33/0 green.The four log paths this ticket was about
within {}s= the configured budgetconfigured={}s elapsed={}ms/clear-settle timeoutwithin {}s= the configured budgetconfigured={}s elapsed={}ms nudges={}INFO, poll count and nudges both constantsWARN, both counters, pluselapsed={}mselapsedMs={}for the whole rollThe
/clear-settle timeout line is the one that matters most:nudgesthere is a genuine measurement that varies with the run, and it is covered by a test that can tell it from a constant.Two things this ticket did NOT achieve, stated plainly
1. The grace-release line cannot be pinned by any test.
idlePollsAwaitingPickupandnudgeseach have exactly one write site, both on the same branch of the same loop, so no input can make them differ fromPICKUP_GRACE_POLLSandPICKUP_GRACE_POLLS - 1. The change there buys one source of truth instead of two — so a later change to the loop cannot leave the message reporting a number the loop no longer produces — and nothing more. That limit is written into the code comment, and the worker reported the equivalent mutant honestly instead of manufacturing a failure count.2. The first follow-up's test did not pin its own fix, and I only found that by mutating it.
c87cc25added an assertion that built its expected value fromLeadRollover.PICKUP_GRACE_POLLS - 1— the exact expression the fix had just removed from the production code. Reverting the fix left all 33 tests green. A harness-proof cell (breaking the asserted substring) gave 1 failure naming the test, so that zero was real.The general rule, which is the most reusable thing this ticket produced: a test that derives its expected value the same way the code derives the real one is vacuous. Both sides move together and the assertion proves nothing.
e966cbareplaces it with plain literals and says in a comment that the literals are deliberate.Spun out
Injector.java:383-387printsREADINESS_GRACE_POLLS * POLL_INTERVAL_MILLIS / 1000as if it were how long the wait took. That is this ticket's original defect, verbatim, in the delivery loop, on the path that fails queued messages. Found by the worker's same-shape sweep, confirmed by me. It is a worse instance than this one, because nothing there measures wall-clock time at all and the two quantities are not equal by construction.Closing this. #501 carries the remaining work.