health: and coordinator: are split keys — add a fourth reload class that says so, then make the top-level list prove its own coverage #330
Closed
opened 2026-09-04 10:02:12 +02:00 by ltms
·
1 comment
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#330
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?
Closes the question #326 left open. I have made the decision; this ticket implements it. Do not re-open the options — implement what is below, and push back only if you find a fact that breaks it.
The facts, which I re-measured today
Every reader of either key, outside
FleetConfigitself:So each key is read both ways, at different sites. Neither is hot, neither is deferred, neither is cold. That is a fact about the code, not an opinion about the taxonomy.
The decision
Add a fourth class:
split. A reload that changes a split key reports it by name and says which part is live and which needs a restart.Why not the alternatives — so nobody re-litigates this:
splitreport does not have to answer, because it names both halves every time.coordinatorit means changingselfIdunder a running daemon, andselfIdnames this daemon's own AMQP inbox queue (lead.<selfId>.inbox). A peer lead that learned the old coord-id would not discover the new one. That is a distributed-identity change, not a reconnect, and nothing handles it.The asymmetry that decided it. Over-claiming a restart costs an unnecessary restart, which the operator sees and can recover from. Under-claiming — today's behaviour — tells the operator a change applied when it did not, and
ConfigRef's own doc calls that "the worst thing a reload can do to an operator debugging one". Both option 1 andsplitstop the under-claim. Onlysplitis true.Unit 1 — the
splitclassGoal: a reload that changes
health:orcoordinator:reports that key by name, and the message says what is live and what needs a restart.Invariants:
Outcome.applied()staystruefor a split change, for the same reason.MemberEnvAllowList. If you find a hole there, report it; do not fix it here.ConfigRefProfileCoverageTest's exclusion-set assertion stays untouched.Candidate mechanism, as a candidate only: a
SPLIT_KEYSset besideCOLD_KEYS, and a separate list onOutcomeso a caller can telldeferredfromsplit. Decide it yourself and justify it — in particular, decide whethersplitdeserves its own field onOutcomeor belongs indeferredwith a different message. Say which and why. A tested deviation you report is a good outcome.What the message must contain, whatever shape you pick — the operator has to be able to act on it:
health:— the monitor itself (enabled, interval,workingSuspectAfter) is frozen at startup and needs a restart; the coverage stringfleet_profilesreports is read live and already applied.coordinator:— theLeadMailboxconnection (uri,uriEnv,selfId,prefetch) is opened once and needs a restart; the broker URI env-var name used to keep that variable out of a member's environment is re-read on every spawn and already applied.Also update
ConfigRef's class doc. It currently describes three classes and says so explicitly. It also carries a denominator note I added, listinghealthandcoordinatoras undecided — that note is now stale and must saysplitinstead.Unit 2 — the top-level coverage checker
Only after unit 1. #326 held this back because
healthandcoordinatorhad no bucket to go in.splitgives them one, so the checker is now well-defined.Goal: a new top-level
FleetConfigcomponent cannot be added without someone triaging it.What it must do: enumerate
FleetConfig.class.getRecordComponents()and require every one to be accounted for — named inCOLD_KEYS, compared inchangedDeferredKeys, inSPLIT_KEYS, or in an explicit hot-exclusion set with a citation. Print its own denominator on every run, the wayConfigRefProfileCoverageTestdoes.Pin the escape hatch. The hot-exclusion set is this checker's own way to be silenced — assert its exact contents, with a message saying a key belongs there only if it is read live off the config supplier, never because adding it makes the build pass. That is not optional: in #323 the identical hatch let two keys be silently re-broken with the suite green, and I only found it by mutating the checker itself.
Say plainly what it cannot do. A checker over the record's shape cannot verify that a citation is true — "read live off
config.get()" is a fact aboutFleetd.javaandHerdrPeerLauncher.java, invisible from here. Write that limitation into the test's javadoc rather than letting the next reader assume the check is stronger than it is.Known-good starting values, measured by me today, so you are not deriving them from scratch — but verify each one rather than copying it:
bind,herdrSocket,memberHerdrSocket,broker,auth.changedDeferredKeys):guard,leadHeartbeat,lifecycle,quarantineCooldownSeconds,spawnReadyTimeoutMs/spawnReadyPollMs,worktreeGroup,worktreeRoot,primary,configReload.health,coordinator.profiles,placement,fleet,memberCredentials,memberLoginShell.If that adds up differently from 22 when you count it, trust your count and say so — I would rather be corrected than confirmed.
Rules
cd fleetd && mvn clean install, unpiped — and quote the realTests run:andBUILDlines. Never read$?after a pipe.git stash— the stash is shared across every worktree here.git worktree removeorgit worktree prune— other workers are live in those directories.git add -A. Never merge.fleetd/fleetd.yamlis gitignored and absent from your worktree. Do not report on its contents.fleet_reply.Already established, do not re-derive
memberCredentialsandmemberLoginShellare hot and correctly absent fromConfigRef— liveconfig.get()reads atFleetd.java:198, 205, 729andHerdrPeerLauncher#configuredMemberLoginShell.coordinator.uriEnvchanging in a reload can expose the old variable name to a later spawn only when that name is separately allow-listed for an unrelated reason (a profile'senv:map, ormemberCredentials.allow:), because the exclusion only ever removes a name from an allow set and never adds one. Checked in #326. Do not re-analyse it, and do not changeMemberEnvAllowList.Merged as
7b918c5(PR #331). Verified by me.Full build after the merge, unpiped:
The deviation was right
Moving
profilesfrom my "hot" list into deferred is correct and I should have had it that way.changedDeferredKeysdemonstrably compares it, twice over, and citing "read live" for the whole key would be false — onlyweight/maxLoad/credentialIdare live, andConfigRefProfileCoverageTestalready covers those at the sub-field level. My tally was wrong by one key; the worker's is right.Giving
splitits own field onOutcomerather than folding it intodeferredis also the right call, and for the reason stated: a caller that branches on the list needs to tell "the whole change waits" from "half already applied" without re-parsing prose.Mutation S — mine, different from the worker's
I dropped the
coordinatorblock fromchangedSplitKeyswhile leaving"coordinator"inSPLIT_KEYS:Three tests catch it, so both current split keys are behaviourally pinned. But note what did NOT fire: neither the new coverage test nor the
assertinchangedSplitKeys. Both are one-way. That is finding 2 below.Two findings — new ticket, not blockers
1.
fleet:is a split key and is filed as hot. This one is live. I checked it myself:LeadLauncherholds aFleetConfig, not aSupplier<FleetConfig>. Sofleet.leadersis frozen at startup — while the role pools,chartersandtabLabelare read live. That is the same shape ashealthandcoordinator, exactly. Today, adding, removing or re-tab-ing a lead underfleet.leadersneeds a restart and a reload reports nothing about it. That is the third instance of the bug #323 and #326 each fixed once.The worker spotted it, flagged it as a caveat, and did not expand scope — which is what I asked for. The brief is what got it wrong. I wrote
fleetinto the ticket's "known-good starting values" under Hot, and I scoped split to exactlyhealth/coordinator. The worker corrected me onprofilesand obeyed me onfleet. So a genuinely split key is now sitting in the checker's own escape hatch, blessed by the checker, on the checker's first commit. My list, my scope, my defect.2. Membership in
SPLIT_KEYSis not wired tochangedSplitKeys. The coverage test treats a name inSPLIT_KEYSas proof the key is triaged. It cannot see whetherchangedSplitKeysactually has a branch for it. So the cheapest way to pass this checker for a future split key is to add one string to aSetand write no reporting code at all — and mutation S shows nothing but the hand-written behavioural tests stands in the way.The same one-way shape is pre-existing on the cold side:
assert COLD_KEYS.containsAll(changed)catches "reported but not listed" and never "listed but not reported", andDEFERRED_TOP_LEVEL_KEYSin the test is a second hand-written copy of whatchangedDeferredKeysdoes. The worker mirrored the existing pattern faithfully; the pattern is what is one-way.Credit where it is due: the test's own javadoc already says it "CANNOT prove that any of the citations are true" and names what does prove them. That is the right disclosure, and it is what let me find both of these quickly instead of trusting a green run.
Both go to a follow-up ticket. Closing this one.