The reload classifier says "compares every component the launcher reads at spawn" and misses four keys, so those reloads report success and do nothing #323
Closed
opened 2026-09-04 09:15:04 +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#323
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 a delegated hunter over
config/andrest/. I confirmed both instances by reading the code myself, and I corrected one overstated consequence.What is wrong
ConfigRefclassifies a reload into hot, deferred and cold keys. A deferred key is reported to the operator so they know a restart is needed.ConfigRef.sameLaunchSettingsdecides that for a profile, and its javadoc claims:It does not. A key that is baked in at startup but missing from that list produces the outcome the surrounding code already names as the worst one:
Outcome.appliedistrue,deferredis empty, the summary is a bareconfig reloaded— and the running daemon keeps the old value.Instance 1 — three profile keys,
ConfigRef.java:267-302sameLaunchSettingscomparesbaseUrl,model,configDir,tokenEnv,argv,placement,workspace,tabLabel,mcpUrl,ideMcpUrl,cwd,parityOverlay,gitTokenEnv,gitHostEnv,kind,env,subscription,exhaustedPattern,errorPattern.It never compares
ideProjectDir,ideOpenCommandorautoCompactWindow.I checked that all three really are read at spawn off a
Profilethat is frozen at daemon startup:And that
cfgcannot be live.HerdrPeerLauncherholds both a frozen map and a live supplier, so this needed checking rather than assuming:The live
Supplier<FleetConfig> configis used at lines 1454, 1488, 1505, 1530 and 1716 — environment allow-lists and broker env names. It never supplies theProfile. So the spawn path is frozen and the finding holds.Instance 2 —
worktreeGroup,ConfigRef.java:210-265GitWorktreesis built once, from the startup config:changedDeferredKeyschecks the sibling key and not this one:worktreeGroupappears nowhere inConfigRef.java— not in the check, and not in the class doc's hot/deferred/cold lists either, thoughworktreeRootis listed as deferred.Correcting the hunter on the consequence
The hunter wrote that an operator turning
worktreeGroupoff is "revoking a different-OS-user member's write access" and silently failing to. That overstates it, and I want the overstatement out of the record before someone acts on it.shareWithGrouprunschgrp/chmodon each worktree when that worktree is provisioned. Those permissions are already on disk. Turning the key off does not take them away from existing worktrees even after a full restart — it only stops new worktrees being shared. So config alone was never a revocation mechanism, and this defect does not break one.The real harm is narrower and still worth fixing: newly provisioned worktrees keep the old sharing behaviour, and the operator is told the change is live. Both directions are silent.
The shape
This is the deferred-key list as a hand-maintained second copy of "what the launcher reads at spawn". It drifts, exactly the way the tool catalogue in
docs/MCP-Contract.mddrifted for a month (CB-609 / #114). Adding four fields fixes today's four and leaves the mechanism that produced them.What I want
Goal: it must not be possible to add a field that is baked in at startup and have the reload classifier silently ignore it.
Invariants:
weight,maxLoadandcredentialIdare excluded on purpose and the javadoc says why. Do not turn those into deferred keys.autoCompactWindowtake effect live is a different, larger change and is not wanted here.ConfigRefTestalready pinsmodel,exhaustedPatternanderrorPatternas deferred. Those stay green.Candidate mechanism, as a candidate only: add a test that enumerates every record component of
FleetConfig.Profileby reflection and asserts each one is either compared bysameLaunchSettingsor named on an explicit, commented exclusion list. A new field then fails the build until someone classifies it. Decide it yourself and justify it. If reflection over a record is awkward here, say so and propose what you would do instead — a checker that cannot enumerate its own denominator is the thing we are trying to stop building.Whatever you choose, the check must print or assert its denominator: how many components exist, how many are compared, how many are excluded. A checker that silently checks nothing is the failure mode this repo keeps hitting.
Then fix the four instances: the three profile keys, and
worktreeGroupinchangedDeferredKeysplus the class doc's deferred list.Two things to weigh, and I do not know the answers:
worktreeGroupdeferred or cold?worktreeRootis listed as deferred.GitWorktreesis never rebuilt, so a change needs a restart either way. Say which list it belongs on and why.changedDeferredKeyscompares a hand-written set of top-levelFleetConfigfields too. Check whether that list has the same gap, and report what you find. Fix it only if it is the same one-line shape; if it is bigger, report it and leave it.Rules
ideProjectDir(and one forworktreeGroup) must report the key as deferred.sameLaunchSettingscurrently states an invariant the code does not keep. Correct it, and make it say what the new mechanism guarantees rather than repeating a claim nobody can check.fleetd/fleetd.yamlis gitignored and absent from your worktree. Do not report on its contents.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.cd fleetd && mvn clean installunpiped; quote the realTests run:andBUILDlines. Never read$?after a pipe.fleet_reply.Also noted, not part of this ticket
The hunter flagged that
FleetConfig'scoordinator:javadoc (around line 81) says "nothing here wires it into a liveLeadMailbox; that is a separate ticket", whileFleetd.java:502callsopenLeadMailbox(cfg.coordinator(), ...)at startup. That looks like another stale comment. I have not verified it. Do not fix it here.Merged as the merge commit above, with one follow-up commit
e545c08. Pushed tomain.Build on
main:Tests run: 1339, Failures: 0, Errors: 0, Skipped: 0—BUILD SUCCESS, unpiped. The coverage test prints its denominator on every run:The mechanism found a fifth gap by itself
The
profilecomponent — a profile's own identity string — was neither compared nor excluded. Nobody had noticed. That is the mechanism doing exactly what it was built for on its first run, and it is the best argument for building it rather than adding four fields.The answers are right
worktreeGroupis deferred, not cold. The reasoning holds: cold is for a key whose new value would leave the daemon inconsistent with an already-open resource — a bound socket, an open broker, decided auth.worktreeGrouphas none of that. It lands safely in the snapshot and simply does not re-chgrpworktrees that already exist. Its siblingworktreeRoot, baked into the same constructor call, is already deferred.The top-level drift is real and correctly scoped out.
healthandcoordinatorare the interesting ones and they are worse than a missing check: the same key is read from the startup snapshot in one place and offconfig.get()in another.healthatFleetd.java:556-563versusFleetd.java:648;coordinatoratFleetd.java:502versusHerdrPeerLauncher.java:1530. A key that is half hot and half deferred cannot be classified correctly by any list, so it needs a decision rather than a patch. The worker was right to report and not touch it. I will file that separately.My own mutation, and what it found
The worker reverted the four field comparisons. I went after the mechanism instead, because a checker is only as good as the thing that stops it being switched off.
N — excuse
ideProjectDirinto the exclusion set and drop its comparison. This is the lazy move a failing coverage test invites. Caught, but not by the coverage test:The coverage test itself passed happily —
22 compared, 4 excluded, denominator balanced.O — excuse
autoCompactWindowandideOpenCommand, the two of the four fields that got no behavioural test.Green. Two of the four fields this ticket fixed could be silently re-broken, and the checker printed the change while asserting nothing about it.
So the mechanism caught a new field and not an excused existing one. That is the same one-way gate shape this ticket was raised about, reproduced inside the gate built to stop it. It is not a criticism of the design — the escape hatch has to exist, because
weight,maxLoadandcredentialIdgenuinely belong in it. It just was not pinned.Fixed in
e545c08: the coverage test now assertsLAUNCH_SETTINGS_EXCLUDEDequals exactly{weight, maxLoad, credentialId}. Growing it now means editing that assertion too — a visible, deliberate diff instead of a quiet one. Re-running mutation O:The general rule, for the next one of these. When a ticket asks for a checker instead of a fix, the checker needs its own mutation pass, and the question is always the same: what is the cheapest way to make this checker pass without doing the work? Here it was one line in a
Set.of.Also good
The worker caught itself mid-mutation: it noted that
git checkout --would have reverted to the pre-fixHEADrather than restoring an uncommitted fix, and used a saved patch instead. Saying that out loud is worth more than a clean report that hides it.Closing.