turnSettleSeconds=20 is shorter than the goodbye turn the handover skill tells the lead to write, so a correct roll refuses itself — measured 2026-10-02 #651
Closed
opened 2026-10-03 14:37:56 +02:00 by ltms
·
3 comments
No Branch/Tag Specified
main
worker/702-4f5c7f-2
worker/715-5c43fc-1
worker/721-70f9ea-5
worker/718-99362b-2
worker/task-15-af0d10-12
worker/task-16-50a702-13
worker/task-12-4d0479-9
worker/task-13-823ce2-10
worker/705-ticket-owner-af9928-8
worker/703-list-collaborators-9c06c2-7
worker/669-example-truth-0b303d-6
worker/669-collab-deliverability-9ba859-3
worker/669-collab-reload-report-2a21bd-4
worker/669-7e80a6-1
worker/669-unit-d-efbbd7-1
worker/669-1b786a-1
worker/669-1d1d9f-1
worker/692-4afb9d-2
worker/689-02fced-13
worker/693-cf23fa-14
worker/677-fix-lead-collision-f69073-12
worker/638-fix-overmask-dbb1bf-11
worker/675-5b7478-4
worker/669-unit-a-70cc8f-3
worker/677-8cdaaf-5
worker/638-a7b391-1
worker/683-4536d6-2
worker/651-a75bbe-8
worker/680-20607d-7
worker/664-c12e95-3
worker/668-08534d-4
worker/672-0f2469-2
worker/670-7d1022-1
worker/661-ac7c28-2
worker/664-37fb9b-3
worker/663-remove-3arg-read-3f6783-1
worker/659-remove-dead-backcompat-ba5e6f-1
worker/637-revision-60a488-23
worker/656-redact-regression-tests-892903-19
worker/637-context-gauge-threshold-466eb5-16
worker/639-redact-line-numbers-de4ac4-17
worker/641-set-reformat-guard-6f96a4-18
worker/642-herdr-guard-scope-5de0e4-15
worker/650-javadoc-scope-95f3b3-14
worker/612-01e9f7-13
worker/612-a-r4-quarantine-outage-7ab0e8-5
worker/612-a-r9-r11-capacity-coverage-peers-cfcc79-7
worker/612-a-r10-loophealth-ccc872-8
worker/612-a-r12-turnregistrar-9e3bb7-9
worker/612-a-r5-leadconfigdir-9e70cf-6
lead/config-edit-redact-anchor-wording
worker/config-edit-seam-ca8dc1-1
worker/612-r67-630-lifecycle-290b8d-3
worker/629-625-ports-seams-da7d5d-4
worker/612-r12-exhaustion-f37cd7-1
worker/612-r38-amqp-24b083-2
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#651
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?
What happened
A lead ran the full
fleet_handoverprocedure correctly and the roll did not happen. The twolog lines, from
fleetd/fleetd.out:rolledstayed at 8 across the attempt. Control:grep -c "lead-rollover:" fleetd/fleetd.outwent32 → 35, so three lines were written and the pattern is sound.
The refusal itself is the correct branch — clearing a live turn would destroy context. The
defect is that a lead following the documented procedure lands in that branch.
The conflict
turnSettleSecondsdefaults to 20 (config/FleetConfig.java:1406,:1428) and is not setin this host's
fleetd.yaml:So the deferred roll waits 20 seconds for the calling pane to reach IDLE or DONE, then gives up and
writes
TURN_NEVER_SETTLED(lead/LeadRollover.java:550,:560).Meanwhile the
handoverskill tells the lead, aboutconfirmreturningaccepted:A lead that obeys that instruction writes its closing message inside the same turn. Here that turn
ran past 20 seconds (
elapsed=20394ms) and the roll refused. The instruction and the timeoutcontradict each other. The longer and more useful the goodbye, the more likely the roll fails.
I have not measured the distribution of lead closing-turn lengths — this is one observation. What I
can say is that 20s is below what one ordinary closing message took, and nothing in the skill warns
the lead to keep it short.
Why it is easy to miss
confirmalready returnedaccepted, so the caller believes it succeeded. The skill documentsthat a late refusal is logged only, with no caller left to tell. That is honest, but it means this
failure is invisible unless somebody greps the daemon log afterwards. The lead carries on with a
full context believing it has been replaced.
Options (not a decision — recording them)
turnSettleSecondsinfleetd.yaml. Cheapest. Needs a number with a reason behind it,not a guess, and the key is read at boot so it needs a redeploy.
/clearwait(
clearSettleSeconds) has a real reason to time out: the pane may never pick the command up. Thecalling turn will end on its own, so waiting longer costs nothing but a parked virtual thread.
A much larger bound, or one derived from the turn rather than a constant, may be the right shape.
timeout. This is the option that needs no code, and the one that makes the feature worse to use.
Related: #486 (LeadRollover's settle poll has no bound of its own), #489 and #480 (the rollover
feature), #491.
Decision (lead). I read the code myself rather than ruling off the options list. Two of the ticket's premises turned out to be wrong, and both make this cheaper than it looked.
Premise 1 that was wrong: this needs a redeploy
leadRolloveris hot, not boot-read.ConfigRef.java:188-196names it explicitly:And
LeadRollover.confirm()does read it live:So option 1 costs a config reload, not a redeploy. That was the main thing making it look expensive.
(One detail:
cfgis captured atconfirm()and passed into the deferred continuation, so a reload mid-roll does not change a roll already scheduled. The next one picks it up. That is correct behaviour, not a bug.)Premise 2 that was wrong: the failure is invisible
The ticket says the refusal "is invisible unless somebody greps the daemon log". It is not.
fleet_handover{action:"status", token}already exists —FleetMcp.java:1359dispatches it toLeadRollover.status(String token)at:619, which reads the sameoutcomesmap the refusal writesTURN_NEVER_SETTLEDinto at:560, carrying the measuredelapsed.The real gap is that nobody is told to look. And there is a clean signal that costs nothing: if the roll worked, the lead has been cleared and is not there to wonder. So a lead that is still alive after its goodbye turn already knows the roll did not happen. That is a reliable self-check, and no code is needed for it.
The ruling
Not option 3. Telling the lead to keep its goodbye short makes the feature worse at the one moment it matters, and the deadline would still be invisible while the lead writes.
1. Raise
turnSettleSecondsto 300, in both the live config and the code default.The number is not a guess, and it is not derived from the single 20394ms observation — one data point cannot set a bound. It comes from the only other constant in this codebase that answers "how long may a lead legitimately be mid-turn?",
leadHeartbeat.idleAfterSeconds, default 300, whose own javadoc gives the reason:The same judgement applies here, and the two should not disagree by a factor of fifteen.
turnSettleSeconds=20is far below the only existing estimate of a lead's natural rhythm.2. Keep it bounded. I considered the ticket's option 2 (do not bound this wait at all) and rejected it. The argument for it is good — the first wait has sent nothing, so waiting costs only a parked thread, unlike
clearSettleSecondswhere/clearis already out. But a lead whose turn genuinely never ends would park a continuation and hold a pending token forever, and that failure is harder to see than a logged refusal. 300s is far above any real goodbye and still terminates.3. Fix a documentation defect I found while reading this. The config javadoc describes both waits as waiting for the pane "to report an injectable state again" (
FleetConfig.java:1403,:1426). The code does not do that:injectable()also acceptsBLOCKED, which is a paused live turn — exactly the state where sending/clearwould destroy context. The code is right and the doc is wrong, and the doc is the dangerous half: it invites a future reader to "simplify" the check toinjectable()and reintroduce the bug the strict check prevents.4. Add the survival check to the
handoverskill. After the goodbye turn, a lead that is still running must treat that as evidence the roll refused: readfleet_handover{action:"status", token}, and if it saysTURN_NEVER_SETTLED, open a fresh request and retry. This replaces the "keep it short" warning with something that costs the lead nothing and cannot be forgotten at the wrong moment.Acceptance criteria
turnSettleSecondsunset, the resolved value is 300. With it set to a positive number, that number is used. With it set to 0 or negative, it falls back to 300 — the existing<= 0guard atFleetConfig.java:1447already does this; assert it still holds./clear. A roll whose pane staysWORKINGpast the budget still writesTURN_NEVER_SETTLEDand sends no/clear. Both directions — the refusal branch is correct and must not be removed.BLOCKEDis not treated as settled. Assert this directly; it is the case the javadoc currently mis-describes.FleetConfigorLeadRolloverstill describes either wait as waiting for an "injectable" state when the code requires IDLE or DONE.Note on the live config
I will set
turnSettleSeconds: 300in this host'sfleetd.yamlmyself — it is gitignored and lead-only, so a worker cannot see or change it. Whoever takes the code half should not expect to find it in the repo. Flagging it because a config-dependent change that workers cannot see has shipped green and inert here before.Live config half done, and it confirms the hot-reload claim by measurement rather than by reading the javadoc.
Set in this host's
fleetd.yaml(gitignored, lead-only) at 21:27:56 CEST. The daemon picked it up on its own:Four seconds apart, and it is the last line in the log. No restart was needed and none happened — the daemon is still pid 42543, up since 20:05:53.
The important detail is what that line does not say. When a reload touches a key the running daemon cannot apply,
ConfigRefnames it. An earlier line in this same log shows the other shape:My reload carries no such clause, so
turnSettleSeconds: 300is live now. That is the empirical version of theConfigRef.java:188-196claim in my decision above, and it settles the ticket's "needs a redeploy" premise: it does not.I verified the edit before letting the watcher see it:
diffagainst a backup showed exactly one seven-line addition and nothing else, and a YAML parse returnedturnSettleSeconds=300as an Integer. I also checked the parser rejects a deliberately broken copy, so "it parsed" is not a broken check reporting success.The config comment records the date, the measurement and how to re-measure (roll a lead, then read
fleet_handover{action:"status", token};TURN_NEVER_SETTLEDmeans 300 is still too low), because a config file is a notebook. The code half must carry none of that — the reasoning belongs in its commit message.Remaining for this ticket: the code default (delegated, task-8) and change 4, the
handoverskill's survival check. The skill is primary-side, so that one is mine.Closed — all four changes are in
Merged locally as
209e123(PR #682, closed by hand as usual).git ls-remote origin main→209e1231eafe1fdad20a71e32a96de6ff9f9c3ff, matching local HEAD.turnSettleSeconds: 300fleetd/fleetd.yaml(lead, hot-reloaded)FleetConfig.java:1450FleetConfig.java:1402,:1427,:1430.claude/skills/handover/SKILL.md(31b3c24)Two of this ticket's premises were wrong, and the record should say so
"the key is read at boot so it needs a redeploy" — it does not.
ConfigRef.java:188-196says all fiveleadRolloverkeys are read live offconfig.get(), taken at the nextopen()/confirm(). Measured on the live daemon: edit written 21:27:56,config reloadedlogged 21:28:00.593, no "needs a restart" clause in the line, and the daemon never restarted (still pid 42543). So change 1 was live four seconds after it was saved."this failure is invisible unless somebody greps the daemon log" — also not so.
fleet_handover{action: "status", token}returnsTURN_NEVER_SETTLEDto the lead itself. The real gap was that nothing told the lead to look, which is why change 4 exists: a roll that works clears you, so surviving your own goodbye is itself the signal that it refused.Why 300 and not something derived from the turn
Option 2 (unbounded, or derived from the turn) was rejected. The bound is a safety property, not a performance one:
LeadRollovercannot distinguish "turn still running" from "pane wedged", so removing the bound means a wedged pane parks a roll forever with no outcome recorded. 300 is not fitted to the singleelapsed=20394msobservation — one observation cannot set a timeout. It is taken fromleadHeartbeat.idleAfterSeconds, which is already 300 in this codebase with the written reason "5 minutes absorbs normal pauses without stalling". The same reasoning applies to a closing turn, so the two now agree instead of contradicting each other.clearSettleSecondsstays at 20 on purpose. That wait has a genuine reason to expire — the pane may never pick/clearup — which is the distinction the ticket's option 2 drew, and it holds.A documentation defect found on the way
The javadoc said both waits are for the pane to "report an injectable state again".
AgentStatus.java:37definesinjectable()asIDLE || BLOCKED || DONE, butLeadRollover.waitUntilAtTurnBoundary(:724) requiresIDLE || DONE.BLOCKEDis a live turn merely paused — the exact state where/clearwould destroy context. The wording named the wrong predicate, so a reader checking the code against the docs would have concluded the code was buggy. Fixed in change 3.Lead verification (not the worker's run)
Three-dot diff against current
origin/main: 2 files, +49/−9, nothing outside the two declared paths, nowiki/, no.mcp.json, nofleetd.yaml. Trial merge in a throwaway worktree: clean, andscripts/redeploy-fleetd.sh'sJAR="$MODULE/run/fleetd.jar"from the three newermaincommits survived it.Full build in that worktree:
Tests run: 1932, Failures: 0, Errors: 0, Skipped: 0, 172 surefire files afterrm -rf target/surefire-reports. Positive control — all three new test names appear inTEST-…FleetConfigTest.xml, so they really ran.Three mutations, run by me, line-anchored:
turnSettleSecondsUsesAnExplicitPositiveValuestayed green. The selectivity is the point: the tests pin 300, not merely "a default".turnSettleSeconds = 300;unconditionally (my own complement, which the worker did not run) —turnSettleSecondsUsesAnExplicitPositiveValuewent red, so that test is not vacuous and an explicit value really is honoured.:724also acceptBLOCKED—LeadRolloverTest.blockedTheWholeTurnSettleWindowSendsNoClearAtAllfailed, 45 run / 1 failed, no compilation error in the log. This one first looked like a bare exit 1 with no named test, because my own grep anchored on(and that test takes no parameters. A zero match is not a finding; the log said otherwise.git diffempty after all three reverts.Left open on purpose
LeadRollover.java's class javadoc (~:32-44) narrates the history of an earlier #480 fix — "An earlier version of this class…", "That is wrong, because…". That is history in a code comment, against the project rule. It is pre-existing, it describes code that no longer exists rather than mischaracterising current behaviour, and it is outside this ticket's four changes. The worker flagged it instead of fixing it, which was the right call. Not filed as its own ticket; it is a comment-hygiene sweep, not a defect.Closing.