fleetd #425: fleet_profiles' default and worktree provisioning must read live placement #430
Closed
agent
wants to merge 1 commits from
worker/425-default-profile-live-f55534-8 into main
pull from: worker/425-default-profile-live-f55534-8
merge into: fleet:main
fleet:main
fleet:worker/fleetd-612-unita-87807e-1
fleet:worker/612-b3-mcpwirings-da2b58-3
fleet:worker/612-b2-cb185-176d3a-2
fleet:worker/612-b1-completion-457459-1
fleet:worker/612-agaps-73a926-2
fleet:worker/608-sleeps-3a64ff-3
fleet:worker/621-b4520b-1
fleet:worker/618-b83894-2
fleet:worker/fleetd-615-e05481-5
fleet:worker/lead-autocompact-5f1ab2-3
fleet:worker/fleetd-613-f85deb-3
fleet:worker/fleetd-608-flaky-nudge-test-d0c2d1-3
fleet:worker/lead-context-gauge-ad404f-1
fleet:worker/gauge-wiring-9158c1-4
fleet:worker/redeploy-slowstart-ead0e5-5
fleet:worker/charter-bytes-13668c-6
fleet:worker/rollover-outcome-291483-2
fleet:worker/589-f64303-2
fleet:worker/593-1a8025-5
fleet:worker/589-fcd2aa-1
fleet:worker/568-9fdaa2-3
fleet:worker/571-attempted-outcome-5739f7-2
fleet:worker/581-completionresolver-cas-sites-0542b7-6
fleet:worker/562-loop-health-wiring-test-99611c-5
fleet:worker/562-surface-loop-health-7df5cc-4
fleet:worker/575-waiter-cleanup-sites-62ad80-1
fleet:worker/572-answer-lock-release-46a9ae-5
fleet:worker/567-probe-channel-leak-a38fc5-6
fleet:worker/551-record-before-send-7cbf56-1
fleet:worker/561-listener-fanout-survives-a-throw-61d538-2
fleet:worker/555-redeploy-main-flow-seam-65c2f5-2
fleet:worker/556-injector-owns-registration-e027a5-1
fleet:worker/552-post-restart-mktemp-abort-bc2672-4
fleet:worker/553-onstatus-completion-leak-0da881-2
fleet:worker/550-shasum-linux-196132-1
fleet:worker/538-loop-dies-on-error-4a5eeb-6
fleet:worker/426-health-coverage-ef1fd4-4
fleet:worker/504-failed-reported-clean-3cfd66-3
fleet:worker/537-capturedlog-close-e4c437-2
fleet:worker/459-broken-link-targets-cadc17-5
fleet:worker/535-appender-leak-fe74c1-1
fleet:worker/512-part2-shutdown-detection-434701-9
fleet:worker/529-logger-level-sweep-2a5533-8
fleet:worker/528-drain-gate-call-site-5de83d-7
fleet:charter/forge-mcp-vs-token
fleet:worker/521-swap-guard-unpinned-28e931-5
fleet:worker/519-probe-test-harness-d25ab8-4
fleet:worker/525-logger-level-leak-1b4eb0-6
fleet:worker/518-fleetmcp-resolver-wiring-8ef96c-1
fleet:worker/512-drain-complete-line-7edd71-3
fleet:worker/517-abort-branch-and-jar-id-41b641-2
fleet:worker/500-9e52c9-3
fleet:worker/509-4912f4-2
fleet:worker/511-9a4b23-1
fleet:worker/493-479f45-2
fleet:worker/505-03f8b2-1
fleet:worker/492-followup-detect-unclear
fleet:worker/501-a31fa0-7
fleet:worker/498-451d1c-5
fleet:worker/494-1015ce-2
fleet:worker/492-209647-1
fleet:worker/489-001902-2
fleet:worker/480-relative-handover-path-906323-1
fleet:worker/480-b-handover-skill-45bf1f-5
fleet:worker/474-followup-source-pin-f54a55-17
fleet:worker/474-charter-check-on-reload-f54a55-17
fleet:worker/466-quarantine-repeatcount-report
fleet:worker/393-opencode-skill-seeding-71854b-13
fleet:worker/469-canonical-tool-names-2a472a-16
fleet:worker/466-quarantine-escalation-5ae9c1-15
fleet:worker/446-hot-exhausted-pattern-0af580-6
fleet:worker/464-charter-tool-name-guard-a85635-12
fleet:worker/463-listfleet-default-fails-open-f1c76c-11
fleet:worker/458-invariant-5-by-purpose-862f9a-10
fleet:worker/439-coordinator-row-gate-bc032a-8
fleet:worker/449-herdr-protocol-576015-4
fleet:worker/450-abstract-spawn-599e1c-5
fleet:worker/437-ack-refuses-177d91-1
fleet:worker/444-placement-window-feb56a-2
fleet:worker/440-helddurable-derived-d462d7-13
fleet:worker/425-rework-placement-resolve-c58ba1-9
fleet:worker/421-lead-peek-held-msgs-cdbad2-10
fleet:worker/435-fixed-policy-cap-fe11de-12
fleet:worker/422-gate-state-observability-9e79d6-11
fleet:worker/431-memberregistry-live-readers-cdbad2-10
fleet:worker/424-architect-slot-hot-038b41-7
fleet:worker/422-model-gate-spawn-c29f48-6
fleet:worker/415-coverage-wording-2cbf9c-5
fleet:worker/416-3ad1da-1
fleet:worker/418-588283-3
fleet:worker/deterministic-stamp-race-409-3cb7b6-10
fleet:worker/armed-reads-live-config-404-ed931f-9
fleet:worker/reply-peer-refusal-391-5a34bd-7
fleet:worker/models-allowlist-aa9e9b-3
fleet:worker/ttl-stamp-race-399-f1122f-8
fleet:worker/scrub-receipt-400-316b3e-5
fleet:worker/exhaustion-detection-395-105105-6
fleet:worker/scrub-abort-394-316b3e-5
fleet:fix/scrub-uid-abort
fleet:worker/task-scrub-517574-2
fleet:worker/t386-clock-bd5b78-4
fleet:worker/t384-scrub-813790-5
fleet:worker/t381-cc-748314-2
fleet:worker/t373-336973-2
fleet:worker/t365-3920c5-3
fleet:worker/t358-6e989b-1
fleet:worker/t355-8b321c-1
fleet:worker/fleetd-369-hermetic-git-tests-e8b19a-3
fleet:worker/fleetd-368-stale-lead-binding-f5682e-2
fleet:worker/fleetd-360-deploy-units-0d3793-1
fleet:worker/359-dead-lead-tabs-f1253b-4
fleet:worker/362-worktree-skills-c03e51-3
fleet:worker/361-coord-visibility-655144-1
fleet:362-plugin-visibility-and-drift
fleet:worker/errscan-bed2ca-2
fleet:worker/amqp-log-identity-bed2ca-2
fleet:worker/withdefaults-guard-561704
fleet:worker/sleepguard-82076d-1
fleet:worker/fd334-9ee1b6-5
fleet:worker/fd348-f1ab27-4
fleet:worker/fd335-a71c35-1
fleet:worker/fd342-174a17-2
fleet:worker/fd345-490d0f-3
fleet:worker/fleetd-337-5ec7d4-21
fleet:worker/fleetd-341-af5a6b-24
fleet:worker/fleetd-339-5ca0a2-23
fleet:worker/fleetd-338-83a4a1-22
fleet:worker/fleetd-333-281f46-18
fleet:worker/fleetd-329-11bdbb-16
fleet:worker/fleetd-330-2770fb-17
fleet:worker/fix-326-50506e-15
fleet:worker/fix-324-3e9bbf-14
fleet:worker/fix-323-b8287d-13
fleet:worker/fix-316b-bd0860-11
fleet:worker/fix-318-76ca36-9
fleet:worker/fix-317-486aec-8
fleet:worker/fix-315-ce47c5-6
fleet:worker/fix-307-275890-6
fleet:worker/fix-308-b4f664-7
fleet:worker/fix-309-ec3939-8
fleet:worker/fix-310-7a3974-9
fleet:worker/fix-302-52ad0e-9
fleet:worker/fix-298-ce1acb-8
fleet:worker/fix-297-66bd11-7
fleet:worker/fix-296-104622-6
fleet:worker/fix-293-bare-closetab-eb22b5-3
fleet:worker/fix-280-gone-ask-lapse-bca98e-2
fleet:worker/fix-290-reapidle-guard-coverage-9b0dd1-1
fleet:worker/fix-285-trust-seed-8f3565-10
fleet:worker/fix-284-backend-error-seat-85912c-11
fleet:worker/fix-282-chained-ask-e6d0bb-8
fleet:worker/fix-283-teardown-leaks-f40dfa-9
fleet:worker/fix-281-pin-handler-actions-4921ac-7
fleet:worker/audit-rendezvous-lifecycle-d072ae-2
fleet:worker/audit-health-placement-1a2476-6
fleet:worker/audit-teardown-exits-e207a5-3
fleet:worker/audit-launcher-asymmetry-27e370-4
fleet:worker/audit-rest-authz-6ca53c-5
fleet:worker/investigate-275-abandon-asking-fdef52-8
fleet:worker/fix-274-worktree-leak-b0095d-7
fleet:worker/fix-273-exhausted-pattern-9665b5-6
fleet:worker/fleetd-267-model-check-bd8068-1
fleet:worker/fleetd-131-archunit-18b834-7
fleet:worker/fleetd-266-sshagent-rename-a014ff-6
fleet:worker/fleetd-184-uid-claim-8e1f31-4
fleet:worker/fleetd-184-warn-b381ee-10
fleet:worker/fleetd-184-docs-be1d12-9
fleet:worker/fleetd-257-9bf010-7
fleet:worker/fleetd-103-23a113-6
fleet:worker/fleetd-247-342356-5
fleet:worker/fleetd-116-04dea8-4
fleet:worker/fleetd-252-a830e0-3
fleet:worker/fleetd-111-7e8673-9
fleet:worker/fleetd-155c-f8ef4b-8
fleet:worker/fleetd-176-b928ca-3
fleet:worker/fleetd-249-7a7878-2
fleet:worker/cb248-composition-root-b-9acdf7-15
fleet:worker/cb148-envrc-default-fa6c82-12
fleet:worker/cb201-unit5-wiring-6c12e6-8
fleet:worker/cb241-fallback-echo-1175e9-11
fleet:worker/cb149-trust-dialog-2392a5-9
fleet:worker/cb134-148-overlay-visible-c9b986-10
fleet:worker/cb234-session-id-keyed-04e1fc-1
fleet:worker/cb201-unit3-nudge-abdf5c-6
fleet:worker/cb201-unit2-policy-c1102c-5
fleet:worker/cb201-unit4-outcome-a13bfa-7
fleet:worker/cb201-unit1-classifier-91b9b1-4
fleet:worker/cb201-227-refine-831980-3
fleet:worker/cb175-model-readback-0f085f-1
fleet:worker/cb222-charter-tmpdir-17f013-1
fleet:worker/cb226-architect-slot-race-cd3aa8-3
fleet:worker/cb224-worktree-root-group-024523-2
fleet:worker/cb-123-role-demotion-c600f7-2
fleet:worker/cb-219-opencode-roots-1f677e-1
fleet:worker/cb214-claude-session-id-b9eab4-4
fleet:worker/cb213-zdotdir-wrong-process-dd6de4-3
fleet:worker/cb211-exhaustion-classification-9546e0-2
fleet:worker/cb137-ambiguous-task-4df3d8-4
fleet:worker/cb209-agentsessionid-4dfdb6-2
fleet:worker/cb185-hostenvnames-2692b5-3
fleet:worker/cb206-opencode-sqlite-128718-2
fleet:worker/cb185-worktree-group-fc0c99-1
fleet:worker/cb-137-ask-ticket-e7760c-2
fleet:worker/cb-172-broker-uri-d36ae4-4
fleet:worker/cb-175-model-readback-76ead6-3
fleet:worker/cb-161-pane-ancestry-293510-1
fleet:worker/cb-164-rebase-885863-8
fleet:worker/cb-164-empty-scrape-false-success-1a80af-3
fleet:fix/cb-197-ticket-ttl-from-completion
fleet:worker/cb-189-remote-url-coverage-4692f3-1
fleet:worker/cb-185-blockers-027756-4
fleet:worker/cb-192-gap-log-11b631-2
fleet:worker/cb-633-fix-5f4396-3
fleet:worker/cb185-router-d6436d-3
fleet:worker/cb185-router-routing-gaps-9e9d33-3
fleet:worker/cb185-paneids-992586-2
fleet:worker/cb-633-allow-list-union-ed374b-1
fleet:worker/cb-157-credential-in-remote-url-496e44-2
fleet:worker/cb-641-health-herdr-evidence-8f1f54-6
fleet:worker/cb-640-health-msg-evidence-99c9cd-1
fleet:worker/cb-642-fleets-status-skill-bbbc40-5
fleet:cb-634-ide-mcp
fleet:worker/lead-comms-wiring-c014b9-7
fleet:worker/lead-mailbox-c19577-6
fleet:worker/autocompact-window-82bc2f-5
fleet:worker/cb-634-probe-18056f-4
fleet:worker/cb635-broker-urienv
fleet:worker/cb-632-config-retry-8e0efa-7
fleet:lead/cb-622e-claude-md
fleet:lead/cb-622-followup
fleet:worker/cb-622a-165dff-1
fleet:lead/cb-622d-opencode-mount
fleet:worker/cb-622b-717c67-2
fleet:worker/cb-622c-ab7759-3
fleet:worker/cb-617b2-20ca4b-3
fleet:worker/cb-617a-5c2f4a-1
fleet:worker/cb596-4e49ef-3
fleet:worker/cb586-10500c-1
fleet:worker/cb-606-b9343a-25
fleet:worker/cb604-1445f8-24
fleet:worker/cb582-477374-21
fleet:worker/cb584-8c2281-22
fleet:worker/cb600-e6b9a9-20
fleet:worker/cb602-ce257f-19
fleet:worker/cb601-b42837-18
fleet:worker/cb598-6c7ba7-17
fleet:worker/cb599-740fe4-16
fleet:worker/cb597-282224-15
fleet:worker/cb590fix-185e9a-10
fleet:worker/cb528-recovery-race
fleet:worker/cb594-96bead-8
fleet:worker/cb590-916766-2
fleet:worker/cb527-997d99-3
fleet:worker/cb592-env-leak-3cbf9c-1
fleet:worker/cb588-async-ticket-nudge-3218f7-5
fleet:worker/cb578b-9dcb13-6
fleet:worker/cb581-d24826-5
fleet:worker/m2-u5-ef8c42-15
fleet:worker/cb578a-516499-2
fleet:worker/cb576-01a04b-17
fleet:worker/cb579-lead-tab-acba06-20
fleet:worker/cb580-terminal-health-ed6058-21
fleet:worker/cb577-f36fdc-18
fleet:worker/cb573b-3db06f-16
fleet:worker/cb568c-f36fdc-18
fleet:worker/cb568-drop-cause-c3ac1c
fleet:worker/cb575-cancelled-notification-c3ac1c
fleet:worker/m4-sol-a2cbec-3
fleet:worker/cb574-async-ask-c3ac1c
fleet:worker/cb573-health-model-8ca857-14
fleet:worker/cb572-unknown-target-7f2e35-13
fleet:worker/u4-700706-9
fleet:worker/u3-b9fcb6-6
fleet:worker/u2-ef5b68-4
fleet:worker/u1-469dce-1-clean
fleet:worker/u1-469dce-1
fleet:worker/cb-564-health-events-70cf7e-2
fleet:worker/cb-565-recycle-drops-role-98e58f-3
fleet:worker/cb-563-missing-reply-df2866-1
fleet:worker/cb-562-readiness-gate-silent-6c23c9-3
fleet:worker/cb-560-architect-presence-da8155-1
fleet:worker/cb-561-architect-silent-off-a71cab-2
fleet:worker/cb-548-bind-architect-slot-fe1b8c-1
fleet:worker/parity-overlay-settings-5fb711-1
fleet:secrets-central-store
fleet:cb-559-hot-key-correction
fleet:cb-557-fleet-role-pools
fleet:worker/cb-553-maxload-explicit-spawn-305ee3-6
fleet:worker/cb-551-idle-lead-heartbeat-f1633c-1
fleet:worker/cb-544-drain-preserves-worktree-925fad-3
fleet:worker/cb-552-docs-sync-1cb9cf-4
fleet:worker/cb-548-rendezvous-guard-rebased
fleet:worker/cb-548-rendezvous-guard-116b53-10
fleet:worker/cb-548-authz-v2-586df6-8
fleet:worker/cb-548-authz-264363-5
fleet:salvage/cb-528b-codex-home
fleet:salvage/cb-528a-codex-launcher
fleet:CB-518-primary-flow
fleet:feature/peer-launcher-spi
fleet:cb-103-injector
No Reviewers
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#430
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 "worker/425-default-profile-live-f55534-8"
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 changed
fleetd #425:
fleet_profiles'"default"was frozen at daemon boot (CompositePeerLauncher.defaultProfile, captured once fromcfg.effectiveDefaultProfile()), while an unqualifiedfleet_spawnresolves the dev pool LIVE, on every call, viadefaultProfileFor(MemberRole.DEV)/poolFor. Reorderingfleet.developersand reloading changed where a spawn landed without ever changing whatfleet_profilesreported.Fix 1 (reporting):
CompositePeerLauncher.defaultProfile()now delegates todefaultProfileFor(MemberRole.DEV)-- the same live, reload-aware pool read placement already uses -- falling back to the frozen field only when no profiles are configured at all. Per the lead's ruling (not re-opened here):fleet_profiles'"default"reports the live DEV default, not a per-role map, because an unqualifiedfleet_spawn's role defaults todev.PeerLaunchergains adefaultmethoddefaultProfileFor(MemberRole)so a genericPeerLauncherreference (e.g. inSessionManager) can ask for a role's live default without depending onCompositePeerLauncherdirectly. Its default implementation delegates todefaultProfile(), for launchers with no pool concept of their own (HerdrPeerLauncheralone, never reached this way in production).Fix 2 (worktree provisioning), chosen option -- resolve once, spawn with the same name:
SessionManager.acquireWithWorktreeused to resolve a profile vialauncher.defaultProfile()(DEV-only) to provisionrepoRoot/parityOverlay, then spawn using the ORIGINAL (possibly blank)profileparameter, which independently re-resolves via placement. For any non-DEV role, or across a config reload between the two reads, the two resolutions could disagree, provisioning a worktree (repo root, parity-overlay files) for a profile the member never actually runs on.I took Option 1: resolve
preResolvedProfileonce vialauncher.defaultProfileFor(memberRole)(the caller's actual role, not always DEV), and reuse that exact name forrepoRoot,parityOverlay, AND the spawn call itself. I chose this over "provision the overlay after placement has chosen" because provisioning-after-spawn would mean the worktree doesn't exist yet when the member's process starts, which is a bigger restructure of the spawn lifecycle than this ticket's scope: the fix here does not change when the worktree exists relative to spawn, only which profile name every step agrees on.Trade-off, stated honestly: the worktree-provisioned unqualified spawn is now a single explicit-profile spawn, so it loses
CompositePeerLauncher's cross-candidate retry onPeerUnreachableException(an explicit profile bypasses placement's retry-across-pool). I judged this acceptable: a worktree provisioned for the wrong backend (the #425 hazard) is worse than a spawn that fails cleanly and can be retried by the caller.Caveat: weighted/round-robin placement
defaultProfileFor/defaultProfileis exact only under thefixedplacement policy (the default), which readsroleDefaultas its first preferred candidate.weighted/round-robinpolicies can pick a different candidate from the pool even on the very first spawn; this fix does not simulate that choice -- it matches what the olddefaultProfile:-derived reporting always did (report the configured default, not a placement simulation). Not treated as a defect in this ticket; noting it for review.Mutation-proof results (mandatory)
One mutation at a time, applied and reverted:
A -- put the frozen field back in the reporting accessor (
defaultProfile()reverted to return the frozendefaultProfilefield instead ofdefaultProfileFor(MemberRole.DEV)):CompositePeerLauncherTest.defaultProfileTracksALiveDevPoolReorderAfterReloadfailed:(the reported default did not follow the reorder).
FleetProfilesLiveDefaultTest.fleetProfilesDefaultTracksALiveDevPoolReorderAfterReloadfailed the same way at thefleet_profiles' "default"assertion. Reverted.B -- make the accessor always return the live pool's first entry with no empty-pool fallback (
defaultProfileForchanged topoolFor(role).getFirst()unconditionally, dropping thepool.isEmpty() ? defaultProfile : ...guard):CompositePeerLauncherTest.defaultProfileFallsBackToTheFrozenFieldWhenNothingIsConfiguredAtAllfailed with aNoSuchElementExceptionfromList.getFirst()on the empty pool, instead of returning"opus". Reverted.C -- revert the worktree fix (
preResolvedProfilecomputed vialauncher.defaultProfile()instead oflauncher.defaultProfileFor(memberRole), and the spawn call passed the originalprofileinstead ofpreResolvedProfile):SessionManagerTest.acquireWithWorktreeForANonDevRoleUsesThatRolesPoolNotTheDevPoolfailed:Reverted. Note: an earlier reorder-only test for criterion 3 (
acquireWithWorktreeProvisionsTheOverlayForTheProfileActuallySpawned) did NOT catch this mutation, because for a DEV-role requestlauncher.defaultProfile()(post-fix-1) already agrees withdefaultProfileFor(DEV)live -- no divergence is observable without a genuine role mismatch. Kept both tests: the reorder test for general hot-reload-through-worktree regression coverage, and the role-mismatch test as the actual load-bearing mutation-C-catching test.Build
mvn clean installrun unpiped from thefleetd/directory:Out of scope (not touched, per brief)
HerdrPeerLauncher's own per-adapter frozendefaultProfilefield. My view: I believe it IS dead in production as the ticket states, on the evidence I read --CompositePeerLauncheris the only production caller I found that reaches aHerdrPeerLauncher, and it always callsspawn/effectiveCwd/parityOverlaywith an explicit profile it resolved itself (viapoolFor/defaultProfileFor), never relying on the adapter's owndefaultProfile(). The one gap I could not fully close by reading alone: I did not find every test double or future caller that might construct a bareHerdrPeerLauncherand calldefaultProfile()on it directly (some test files do, e.g. wiring tests), so "unreachable in production" is a narrower and safer claim than "unreachable, full stop" -- I'd remove it only after a search confirms zero non-test callers, which I did not do since it's out of scope here.MemberRegistryarchitect-slot freeze (#424) -- untouched.ConfigRef'sfleet:split-key reporting -- untouched.FleetHealthMonitor.coveragewording andCompletionResolver-- untouched.On the lead's DEV-default ruling
I agree with it and did not re-open it. The reasoning holds:
SpawnRequest's role defaults toMemberRole.DEV, so "the dev pool's live first entry" is exactly what an unqualifiedfleet_spawnlands on, and keepingfleet_profiles'"default"field a single string (not a per-role map) matches its existing shape/type with no breaking change to consumers.Not merging this yet. The reporting half is right and I want it. The
SessionManagerhalf introduces a regression that is bigger than the one the commit message owns up to.Your build claim checks out — I ran it myself:
Tests run: 1514, Failures: 0, Errors: 0, Skipped: 0, 0 compile errors, BUILD SUCCESS.The regression
The commit message says the cost is losing "
CompositePeerLauncher's cross-candidate retry onPeerUnreachableException". That is one of four things lost, and the smallest.CompositePeerLauncher.spawnhas two branches. The explicit-profile branch throws:The placement branch routes around the same conditions, by building
quarantined/coolingOff/modelOffsets and letting the policy skip them.acquireWithWorktreeused to pass a blank profile, so it got the routing branch. It now passesdefaultProfileFor(memberRole), so it gets the throwing branch. AnddefaultProfileForis blind to every one of those conditions — it returnspool.getFirst().Proof
A probe in your own worktree, on the default
fixed()policy, withsolquarantined andbfree:Same spawn, same config. Before: it lands on
b. After: it fails.Note
FixedPlacementPolicy's fast path already checksquarantined,coolingOff,unreachableand weight-0, so this is not aweighted-only concern — it hits the default policy.Why this matters more as of today
#429 merged a few minutes ago and added
enforceModelEnabledto that explicit branch. The whole point of themodels.allowon/off switch is that the operator flips a model off and the fleet keeps working on the profiles that are still on. With this PR as written, an unqualified worktree spawn whose pool-first profile names an off model gets a hardPlacementExceptioninstead of routing to an enabled profile — the gate stops being a switch and becomes an outage.Scope, stated honestly: this only bites spawns that name no profile. A
fleet_spawn{profile:"sonnet", worktree:true}is unaffected, becausepreResolvedProfileis then just the caller's own profile.What I think the fix is
The bug you found is real — provisioning
repoRoot/parityOverlayfor one profile and spawning on another is a genuine defect, and resolving once is the right shape. The mistake is resolving throughdefaultProfileFor, which answers a different question: "what is first in the pool", not "where would this spawn actually land".So the launcher needs a way to run placement without spawning — the same candidate list, the same
quarantined/coolingOff/modelOff/unreachablesets, the same policy — and return the chosen profile.acquireWithWorktreethen uses that one answer forrepoRoot,parityOverlayand the spawn. One resolution, and the exclusions survive.Please keep the
defaultProfile()/defaultProfileFor()/PeerLauncherhalf exactly as it is; that part I verified and want. Rework onlyacquireWithWorktree.Acceptance, so it does not come back with the same hole:
CompositePeerLauncherlevel: an unqualified worktree-shaped spawn whose pool-first profile is quarantined lands on the next candidate, underPlacementPolicies.fixed()— notweighted().fixed().repoRoot/parityOverlayand the spawn all name that same routed profile — the defect you originally found, now proven on the routed path rather than the first-in-pool path.Closing this in favour of PR #433, which reworks the same ticket.
Recap of why this one was rejected, so it is not re-tried: resolving
acquireWithWorktree's profile throughlauncher.defaultProfileFor(memberRole)moved the caller from the routing branch ofCompositePeerLauncher.spawnto the throwing branch. A blank profile goes to placement, which buildsquarantined/coolingOff/modelOffand routes around them; a named profile hitsenforceNotQuarantined/enforceNotCoolingOff/enforceMaxLoad/enforceModelEnabled, each of which throws. So pre-resolving the name turned a quarantined pool-first profile from "routed around" into a hard spawn failure. Proved with the PR's own wiring: blank profile →b;defaultProfileFor(DEV)→PlacementException: worker profile 'sol' is quarantined.The reporting half of this PR —
fleet_profiles'defaultreading live placement — was right and is carried over into #433.The general lesson, for anyone reading this later: when a fix computes a value earlier so that two consumers agree on it, ask which branch that value now takes. Resolving an input earlier is not a no-op; here it changed which validation ran.
#433 is not merged either yet — it closed quarantine, cool-off and model-off but left
maxLoadon the throwing side, which is the same shape one filter over. See my comment there and ticket #435.Pull request closed