A config key's hot/deferred class is a claim about every consumer, and nothing checks it that way #427
Open
opened 2026-09-10 06:54:03 +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#427
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?
The structural gap behind #404, #416, #400, #424 and #425. Filing it separately because fixing any one of those does nothing about the next one.
The pattern, five times
exhaustionDetectionArmedread the live config; detection read the startup snapshotCapacitySourceread the live profilekeySet(); the spawn gate reads the frozen setMemberRegistryfreezesfleet.architects, so revoking an architect slot does not revoke itfleet_profilesreports a boot-timedefaultwhile placement reads the live poolEvery one was found by a person reading code. Not one was found by a test.
Why the existing checkers cannot see it
ConfigRefTopLevelCoverageTest,ConfigRefTopLevelReportingCoverageTestandConfigRefProfileCoverageTestare good tests and they all check the same thing: thatConfigRef'sCOLD_KEYS/SPLIT_KEYS/DEFERRED_KEYSsets are internally consistent withFleetConfig's record shape, and that each set member has a reporting branch.None of them looks at a consumer. So they catch a key nobody triaged, and they cannot catch a consumer that disagrees with the triage. But the triage is a claim about consumers — "hot" means every site re-reads it.
ConfigRef.java:29-37states that claim explicitly for role pools, and #424 proves it false for one consumer while true for another.fleet.architectsturns out to be a split key inside the already-splitfleet:key: read live for placement, frozen for identity. That was recorded nowhere, and the reload tells the operator the wrong half out loud (ConfigRef.java:543-551).Why it is easy to write and impossible to see
In
Fleetd.mainthese two sit three lines apart and look equally innocent:#416was exactly one lambda that mixed both: the livekeySet()beside the livemaxLoad(), where onlymaxLoadshould have been live. Reviewing that line, "it reads the config" is true and useless. Nothing in the type system, the naming, or the tests distinguishes the two.The frozen read is also invisible to search. Every one of us has hunted this with
grep 'config\.get()', which by construction can only find the live half. The frozen half is a constructor argument or a field, and it does not match any idiom you would think to grep for. That is why #424 and #425 sat unfound while three live-read instances were fixed.The goal
Make a frozen read of a config value an explicit, named, greppable act, so that "this site is frozen" is a decision somebody wrote down rather than the default that happens when you pass a value.
That is the invariant. Everything below is a candidate, not a requirement — pick your own mechanism, and if you find a better one, say so in your report.
Candidate A — forbid holding config by value
An ArchUnit rule: no class outside the
configpackage may declare a field whose type isFleetConfigor a nestedFleetConfig.*record. A long-lived collaborator must hold aSupplier/Functioninstead, so the live read is the path of least resistance.Frozen is then opt-in: a site that genuinely needs the boot snapshot declares it and appears in an explicit exception list, each entry naming why.
coordinator.peersatFleetd.java:684-688is the model of a correct frozen read — it reads the same snapshot the mailbox itself was opened from — and it would be the first, well-documented entry.This is the strongest candidate because it inverts the default. It is also the most invasive, so measure the blast radius before committing: count the fields that would violate it today and report that number before you change anything. If it is large, say so — that is a result, and it may push toward candidate B.
Candidate B — name the snapshot
Rename
Fleetd.main'scfglocal to something that says what it is (bootSnapshot, say). Thengrep -n 'bootSnapshot\.' Fleetd.javaenumerates every frozen read in the file in one command, and a reviewer can check each againstDEFERRED_KEYSby eye.Cheap, no new test, no new infrastructure — and it makes the invisible half greppable, which is the specific thing that let #424 and #425 hide. Weaker than A: it is a convention, and nothing enforces it. But a convention that makes a defect findable in one command is worth more than it looks, and it can ship the same day.
A and B are not exclusive. B is the cheap half of A and can land first.
What this ticket does not ask for
Do not build a checker that hand-maintains a second list of "which consumers read which key". That list would drift from the code exactly the way
DEFERRED_KEYS' old test-side copy did before #337 promoted it intoConfigRef— and the reason given there applies with full force here: a second, hand-maintained copy of a set is what silently drifts from the thing it describes. A checker that needs a human to keep it truthful has moved the problem, not solved it.The test must derive what it checks from the code.
Acceptance
mvn clean installgreen.Notes for whoever takes this
ConfigRef's class javadoc end to end first. It is long and it is the actual specification of hot/deferred/cold/split. The four definitions are the contract your rule has to encode.A live instance of this ticket's shape, found today. It is not in the code — it is in the config file's own comment, which is the surface an operator actually reads.
What I measured
fleetd/fleetd.yaml, in the comment block directly above themodels:key:That is false. The authority is the code:
modelsis inFleetConfig.KNOWN_TOP_LEVEL_KEYS(:1723-1728) and in neitherConfigRef.COLD_KEYS(:244, five keys:bind,herdrSocket,memberHerdrSocket,broker,auth) norConfigRef.DEFERRED_KEYS(:270-273, thirteen keys,modelsabsent). So it is hot.ConfigRef.java:169says so in words as well: "models:was added as deferred, and again forfleetd #422, which moved
models:" into the hot class.Control, so a zero match cannot read as a clean pass: the same grep pattern over
ConfigRef.javareturns 16 quoted key names, and
grep -c hotreturns 28 lines. The files were read.The shipped warning text agrees with the code, not with the config comment —
Fleetd.java:943tells the operator(models: is hot, no restart needed).Why this belongs on this ticket rather than its own
This ticket says a key's hot/deferred class is a claim about every consumer and nothing checks it
that way. The
models:comment is such a claim, made in the one place an operator is most likelyto read it, and it went stale the moment #422 moved the key. Nothing could have caught it, because
the check this ticket asks for does not exist and would not cover a comment anyway.
The direction of the error is what makes it expensive. The comment tells the operator to
restart when a reload is enough. During a subscription outage — the exact moment someone edits
models:to turn a model off — that sends them toscripts/redeploy-fleetd.sh, which drops everyin-flight ticket and rendezvous. The fix is cheaper than the workaround the comment recommends,
and the operator has no way to know.
This is the "false comment is worse than absent" case in its strongest form: a missing comment
makes the reader investigate, while a wrong one makes them stop investigating with a false
conclusion.
What I did not do
I did not fix it.
fleetd.yamlis gitignored, it is the live config, and a write to it wasrefused by the operator's command classifier in an earlier session. I did not route around that
refusal. Replacement text for the operator to paste, if they want it:
What this suggests for this ticket's scope
Whatever check this ticket produces, consider whether it can also cover
fleetd.example.yaml—the committed description of the schema, and the only copy a test can read. If the example file
carries the same hot/deferred annotations as the live file, a test can compare each annotation
against
COLD_KEYS/DEFERRED_KEYS/SPLIT_KEYSand fail when they drift. That would not havecaught this exact instance, because the stale text is in the gitignored file. It would stop the
next one from shipping, and it would give the operator a correct copy to compare against.
I am not widening the scope by decision — flagging it, because a checker that reads only Java
would have declared this clean.