fleet manager: run and drive several fleets on ONE host from a single session #160
Open
opened 2026-08-23 14:26:11 +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#160
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?
Design ticket. This is not #6. #6 (CB-308) is multi-host federation: one fleet spread over many machines. This is the opposite and much nearer: many fleets on one machine, driven from one Claude or OpenCode session, behind a fleet-manager MCP.
The motivating case is already real. One fleet works on
fleetd; another should work onkb. A lead must be started in the project directory its fleet works on, so one fleet per project is the natural unit. Today an operator can only talk to one.What a "fleet" is here
One
fleetdprocess, plus the herdr session it drives, plus the project it works in. So a second fleet on this host means a secondfleetdwith its own port, its own herdr session, its own worktree root and its own broker vhost.The good news: the config is already almost entirely per-instance
I read
FleetConfiglooking for things that would stop two daemons sharing a host. There are very few.bind.port, configurable.8765appears once in the codebase, as the defaultherdrSocket, configurable.~/.config/herdr/herdr.sockis only the fallbackworktreeRoot, configurablebroker.uriEnvnames an env var, so each daemon can point at its own vhostProfile.mcpUrl, per profile, so a member can be pointed at its own daemonfleet01already proves the herdr half: it runs two herdr sessions side by side, andfleetdis pinned to the named one.So "run two fleetds on one host" is mostly a packaging and convention problem, not a code problem. That is the cheap part.
Blocker 1: a fleet has no name
There is no
fleetName, no instance id, nothing. I grepped for every spelling.A manager cannot address what has no name. Every tool the manager offers needs a
fleetargument, every roster row needs to say which fleet it came from, and every log line an operator reads needs to say which daemon wrote it. This is the first unit of work and everything else depends on it.Blocker 2: the authz model breaks when one session talks to several fleets
This is the real problem, and it is not obvious.
CallerResolver.resolvedecides who you are from the connection: remote address and port → peer pid → herdr terminal. Then:fleet.leaders.*.tab, you are a lead;primary.An operator's session normally runs inside a herdr pane — that is why
fleet.leaders.*.tabexists at all. So if that session mounts a second daemon whose config does not name its tab, it is not a lead there. It falls to the worker branch, and every orchestration call is refused.This means the zero-code option does not work. "Just put two fleetd servers in
.mcp.json" looks like it should be enough — Claude Code mounts many MCP servers happily, and each would namespace its tools. But the second daemon would resolve the operator as a worker and refuse to spawn, send or stop.There are three ways out, and choosing between them is the main design decision here:
fleetargument. Because the proxy is not in a herdr pane, each daemon resolves it asprimary, so the demotion problem disappears by construction.I lean to 2, with 1 run first as a one-hour experiment to find the unknown unknowns.
If we build the proxy, one rule is not negotiable
Members must never mount the manager. A member's identity works precisely because its connection comes from its own pane. If a member called through the proxy, the proxy's connection would be resolved instead, and every member would be seen as
primary. That is a full privilege escalation.So: members keep pointing at their own daemon via
Profile.mcpUrl; the manager is an operator-side front door only. Whatever is built has to make that hard to get wrong, not merely documented.Blocker 3: nothing starts, stops or supervises a fleet
Today a fleet is started by hand — the Mac has a launchd plist,
fleet01has a shell script and no supervisor at all (#103, and #156 §8). A manager that can list fleets but cannot start or restart one is a viewer, not a manager.This is where this ticket and the ops gaps meet. "Start fleet
kb" has to mean something.What is worth deciding before any code
agent.<target>.inboxwith no fleet namespace, so two fleets sharing a vhost would collide. One vhost per fleet avoids it and matches what we just did for the two hosts.fleetargument on the existing tools is the obvious answer, but it changes every call site and every brief.Suggested first units
name:in config, in/healthz, in the startup log, and in every roster row. Small, unblocks everything, useful even with one fleet.Related
fleet01, including the config and lifecycle facts this ticket relies on.Not verified
I reasoned blocker 2 from
CallerResolver.resolveand thefleet.leaders.*.tabmechanism. I did not stand up a second daemon and watch a session get demoted to worker. That is exactly what suggested unit 2 would prove, and it should be proven before any manager design is committed to.Experiment run — the "not verified" parts are now measured
I ran the two-daemon experiment from the bottom of this ticket. Two fleetds on one host, daemon 2 on port 8766, sharing the one herdr session, with a config that does not name my pane as a lead. Everything below is measured, not reasoned.
1. The proxy mechanism works (confirmed)
A process that is not in a herdr pane is resolved as
primaryby every fleetd on the host. I calledfleet_whoamifrom a plain Python process against both daemons — both answeredprimary. And a lead-onlyfleet_stopfrom that non-pane process succeeded against both the experimental daemon and the live one. So a proxy manager MCP, which is not in a pane, is seen asprimaryby each fleet it fronts. Blocker 2's proposed fix (option 2) holds up.2. Two daemons must NOT share one herdr session — they kill each other (new, important)
The moment daemon 2 booted, it ran its startup orphan-reap and stopped daemon 1's live member:
The reap keys on a random per-process
nameNonce(HerdrPeerLauncher), so every daemon treats every other daemon's members as its own leaked orphans and tears them down. And daemon 1 never noticed:fleet_liststill showed the killed member asstate: readywith its capacity slot counted; onlyliveStatusflipped tounknown, andfleet_statuson it returnedagent_not_found.Consequence for the design: each fleet needs its own herdr session (fleet01 already runs two side by side, so the mechanism exists). Co-tenancy on one herdr session is not just untidy, it is destructive. This is a hard requirement, not a preference.
3. Starting a second daemon silently spawns a second subscription lead (new)
Daemon 2's
LeadLauncherfound no pane matching its tab and launched one — a freshopus(subscription) Claude Code lead. So naively starting a second fleet burns a second subscription lead with no prompt. A manager that starts fleets must own lead launch, or make it opt-in.4. Identity is per-connecting-process, which is a single-fleet escalation risk (new, NOT fully verified)
Identity comes from the pid that opens the connection, matched against herdr's
shell_pidand foreground pids only (PaneLocator.paneOwnsPid). A grandchild process of a pane (e.g.python3a worker shells out to) matches neither, so it falls through to loopback-trust and is resolved asprimary. My non-pane probe demonstrates the mechanism.I tried to confirm it from inside a worker pane — I asked a real worker to run the lead-only probe — and could not: the worker's own local safety classifier blocked the script before it ran. So the in-pane escalation is plausible from the code and unproven in practice. It should be pinned down separately, because if it holds, a worker can escalate to primary on its own fleet just by shelling out, independent of any manager. Filing that as its own security ticket.
Direction from the operator (2026-08-23)
Status tool — working prototype
I built it as a standalone probe that reads each fleet's REST API and touches no fleetd Java — already honoring "don't mix with current modules". It reads a small
fleets.jsonregistry (name → REST url → project), and for each fleet reports health, roster, and a per-member verdict.The valuable part is telling working from stalled, because
liveStatusalone lies — a member reads "working" for hours doing nothing. So the tool does not trust the flag: for any member claiming to be busy it samples the worktree's newest file mtime twice, a few seconds apart. Files changed ⇒ WORKING; unchanged ⇒ STALLED, with the age of the newest file.Proven against the live fleet, all three states real, not mocked:
WORKING (files changed)idle, correctly not called workingremote (not probed), stated honestly rather than guessedKnown limit, written into the tool: a member that is legitimately thinking without writing files reads as stalled. And it sees only this host's fleets, because fleetd's REST binds to loopback — cross-host status needs the broker or ssh, which is the CB-308 boundary, not this one.
I have the prototype and its output ready to show. The one open decision is packaging: a separate git repo vs a top-level non-Maven directory in this repo. I lean to a separate repo, matching how
fleet-agent-kitwas planned, so the manager and the coming dashboard get their own lifecycle and CI and never entangle with the Java build.