CB-558: launch a declared lead at startup when none is live

`fleet.leaders.<name>.instances` was descriptive. Now the daemon reads it: a
lead that names a `profile:` is started when fewer than `instances` are running.
A lead with only a `terminal:` stays recognise-only, as before.

A lead is not a member, and LeadLauncher exists to keep it that way. Every other
spawn path goes through HerdrPeerLauncher, which does three things a lead must
never get: it appends the worker reply charter ("you are an off-subscription
worker … end every turn with bridge_reply" — the opposite of an orchestrator);
it registers the session with SessionManager, whose idle reaper would kill a
lead for being idle, which is a lead's normal state; and it can move a peer off
the subscription. So this launcher talks to AgentControl/WorkspaceControl
directly. The duplicated argv/env assembly is the cheaper half of that trade.

Not double-spawning is the safety property, so liveness needs two pieces of
evidence. A running agent in a tab labelled `lead: <name>` finds an
auto-launched lead. A running agent on a pinned `terminal:` finds one the
operator opened by hand — without it, a pinned lead whose tab carries no
matching label would be relaunched on every boot. Member workspaces are
excluded, so a member in a matching tab is never counted. If herdr cannot be
reached, nothing is started: a second orchestrator is worse than none.

Liveness deliberately requires the AGENT, not just the label. LeadTabScanner
used to promise that bridged never writes a lead label, so there was no
round-trip from the daemon's own rename back into its next decision. That is no
longer true, and its javadoc now says so. The trust direction is unaffected — a
label is a name, not a capability — but staleness becomes real: a label left by
a crashed session would otherwise read as a live lead forever and disable
auto-launch permanently.

Two new knobs. `workspace:` (default "leads") is where a launched lead's tab
goes; it must not be a member workspace, because those are excluded from the
scan and a lead placed in one would never be found again. `cwd:` defaults to
bridged's own working directory.

Also: WorkspaceControl.listTabs, and a FakeHerdr tab seeder that leaves the
canned response byte-identical when no tab is seeded.

617 tests pass (16 new), IDE-clean.
This commit is contained in:
Dai Ha
2026-08-14 16:47:57 +02:00
parent 61944fc045
commit 57f8fa257a
8 changed files with 666 additions and 13 deletions
@@ -4,7 +4,9 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Recording fake {@link HerdrClient} for unit/acceptance tests. Returns canned frames
@@ -24,6 +26,8 @@ public final class FakeHerdr implements HerdrClient {
private boolean healthy = true;
private final List<String> extraWorkspaces = new ArrayList<>();
private final List<String> extraAgents = new ArrayList<>();
/** workspaceId → extra tabs that {@code tab.list} reports for it (CB-558 lead scans). */
private final Map<String, List<String>> extraTabs = new LinkedHashMap<>();
private int agentNameTakenFor = 0;
private int agentPaneBusyFor = 0;
private int workerTabPaneCount = 1;
@@ -109,6 +113,18 @@ public final class FakeHerdr implements HerdrClient {
return this;
}
/**
* Seed a labelled tab into {@code tab.list} for one workspace (e.g. an existing {@code lead: x}
* tab). Pair it with {@link #withAgent} on the same {@code tabId} to make the lead <em>live</em>;
* seeding the tab alone models the stale-label case.
*/
public FakeHerdr withTab(String workspaceId, String tabId, String label) {
extraTabs.computeIfAbsent(workspaceId, _ -> new ArrayList<>())
.add(("{\"tab_id\":\"%s\",\"workspace_id\":\"%s\",\"label\":\"%s\",\"pane_count\":1}")
.formatted(tabId, workspaceId, label));
return this;
}
/** Seed an additional workspace into {@code workspace.list} (e.g. a pre-existing worker space). */
public FakeHerdr withWorkspace(String id, String label) {
extraWorkspaces.add(("{\"workspace_id\":\"%s\",\"label\":\"%s\",\"focused\":false,"
@@ -228,11 +244,19 @@ public final class FakeHerdr implements HerdrClient {
case "tab.rename" -> mapper.readTree("""
{"type":"tab_info","tab":{"tab_id":"w9:t2","workspace_id":"w9",
"label":"worker: ltms-local","pane_count":1}}""");
case "tab.list" -> mapper.readTree(("""
case "tab.list" -> {
// The base pair is returned for every workspace, exactly as before. Seeded tabs
// are appended only for the workspace they were registered against, so a test
// that seeds none sees the historical response byte for byte.
Object wsId = params instanceof java.util.Map<?, ?> m ? m.get("workspace_id") : null;
List<String> seeded = extraTabs.getOrDefault(String.valueOf(wsId), List.of());
yield mapper.readTree(("""
{"type":"tab_list","tabs":[
{"tab_id":"w9:t1","workspace_id":"w9","label":"1","pane_count":1},
{"tab_id":"w9:t2","workspace_id":"w9","label":"worker: ltms-local","pane_count":%d}]}""")
.formatted(workerTabPaneCount));
{"tab_id":"w9:t2","workspace_id":"w9","label":"worker: ltms-local","pane_count":%d}%s]}""")
.formatted(workerTabPaneCount,
seeded.isEmpty() ? "" : "," + String.join(",", seeded)));
}
case "tab.close" -> mapper.readTree("{\"type\":\"ok\"}");
case "pane.get" -> mapper.readTree("""
{"type":"pane_info","pane":{"pane_id":"w9:pW","workspace_id":"w9",
@@ -0,0 +1,255 @@
package dev.ltms.bridged.lead;
import dev.ltms.bridged.config.BridgedConfig;
import dev.ltms.bridged.herdr.AgentControl;
import dev.ltms.bridged.herdr.FakeHerdr;
import dev.ltms.bridged.herdr.WorkspaceControl;
import org.junit.jupiter.api.Test;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* CB-558 — the daemon starts a declared lead when none is running.
*
* <p>Two properties carry the whole feature. It must not double-spawn (a second orchestrator is
* worse than none), and what it starts must be a <em>lead</em> and not a member: no reply charter,
* no off-subscription env, and never registered with the session lifecycle.
*/
class LeadLauncherTest {
/** The lead's backend: a subscription profile with the bridge mounted, as `opus` really is. */
private static BridgedConfig.Profile opusProfile() {
return new BridgedConfig.Profile(
"opus", null, "claude-opus-5", null, "BRIDGED_WORKER_TOKEN",
List.of("ccs", "ltms"), "tab", "bridged-workers", null,
"http://127.0.0.1:8765/mcp", null, null,
null, null, null,
Map.of("CLAUDE_CODE_AUTO_COMPACT_WINDOW", "300000"), null, null, true);
}
private static BridgedConfig configWith(BridgedConfig.Leader lead) {
Map<String, BridgedConfig.Leader> leaders = new LinkedHashMap<>();
leaders.put("opus", lead);
BridgedConfig.Fleet fleet =
new BridgedConfig.Fleet(leaders, Map.of(), Map.of(), Map.of(), null);
return new BridgedConfig(
null, null, Map.of("opus", opusProfile()), null, null, null, null, null,
null, null, fleet, null, "fixed", null).withDefaults();
}
private static BridgedConfig.Leader lead(String profile, String terminal, int instances) {
return new BridgedConfig.Leader(profile, terminal, instances, "lead:", 10, null, null,
"leads", "/repo");
}
private static LeadLauncher launcher(FakeHerdr herdr, BridgedConfig cfg) {
return new LeadLauncher(new AgentControl(herdr), new WorkspaceControl(herdr), cfg);
}
@SuppressWarnings("unchecked")
private static List<String> startedArgs(FakeHerdr herdr) {
return (List<String>) ((Map<String, Object>) herdr.lastCall("agent.start").params()).get("args");
}
private static String startedName(FakeHerdr herdr) {
return (String) ((Map<?, ?>) herdr.lastCall("agent.start").params()).get("name");
}
@SuppressWarnings("unchecked")
private static Map<String, String> tabEnv(FakeHerdr herdr) {
return (Map<String, String>) ((Map<String, Object>) herdr.lastCall("tab.create").params()).get("env");
}
// ── it starts a lead when none is live ────────────────────────────────────────────────────
@Test
void startsTheDeclaredLeadWhenNoneIsRunning() {
FakeHerdr herdr = new FakeHerdr();
assertEquals(1, launcher(herdr, configWith(lead("opus", null, 1))).ensureLeads());
assertTrue(herdr.called("agent.start"), "a lead must actually be started");
assertEquals("lead-opus", startedName(herdr));
}
/** The tab is labelled so the scanner finds the lead on the next resolve. */
@Test
void labelsTheTabWithThePrefixTheScannerReadsBack() {
FakeHerdr herdr = new FakeHerdr();
launcher(herdr, configWith(lead("opus", null, 1))).ensureLeads();
assertEquals("lead: opus",
((Map<?, ?>) herdr.lastCall("tab.rename").params()).get("label"));
}
/** `instances: 2` with none live means two starts, not one. */
@Test
void startsAsManyInstancesAsAreDeclared() {
FakeHerdr herdr = new FakeHerdr();
assertEquals(2, launcher(herdr, configWith(lead("opus", null, 2))).ensureLeads());
assertEquals(2, herdr.calls.stream().filter(c -> c.method().equals("agent.start")).count());
}
// ── it must not double-spawn ──────────────────────────────────────────────────────────────
/** A labelled tab WITH a running agent in it is a live lead — leave it alone. */
@Test
void doesNotStartASecondLeadWhenOneIsAlreadyRunning() {
FakeHerdr herdr = new FakeHerdr()
.withWorkspace("wL", "leads")
.withTab("wL", "wL:t1", "lead: opus")
.withAgent("lead-opus", "term_lead", "wL:p1", "wL:t1");
assertEquals(0, launcher(herdr, configWith(lead("opus", null, 1))).ensureLeads());
assertFalse(herdr.called("agent.start"), "the live lead must not be duplicated");
}
/**
* The reason liveness is not "does the label exist". A tab left labelled by a session that has
* since died must not block the relaunch, or one crash disables auto-launch permanently.
*/
@Test
void aLabelledTabWithNoRunningAgentIsNotALiveLead() {
FakeHerdr herdr = new FakeHerdr()
.withWorkspace("wL", "leads")
.withTab("wL", "wL:t1", "lead: opus"); // label only — nothing running in it
assertEquals(1, launcher(herdr, configWith(lead("opus", null, 1))).ensureLeads(),
"a stale label is not a lead; the lead must be relaunched");
}
/**
* A lead the operator opened by hand and pinned with `terminal:` is live even though its tab
* carries no matching label. Counting labels alone would relaunch it on every boot.
*/
@Test
void aPinnedTerminalWithARunningAgentCountsAsLive() {
FakeHerdr herdr = new FakeHerdr()
.withAgent("hand-opened", "term_pinned", "wX:p1", "wX:t1");
assertEquals(0, launcher(herdr, configWith(lead("opus", "term_pinned", 1))).ensureLeads());
assertFalse(herdr.called("agent.start"));
}
/** A member sitting in a matching tab must never be counted — or spawn a lead — as one. */
@Test
void aMemberWorkspaceIsNeverScannedForLeads() {
FakeHerdr herdr = new FakeHerdr()
.withWorkspace("wM", "bridged-workers") // a configured member space
.withTab("wM", "wM:t1", "lead: opus") // a member tab that looks like a lead
.withAgent("claude-opus-x", "term_m", "wM:p1", "wM:t1");
assertEquals(1, launcher(herdr, configWith(lead("opus", null, 1))).ensureLeads(),
"a member in a lead-labelled tab is not a lead, so the real lead is still missing");
}
/** If herdr cannot be counted, start nothing: guessing risks a second orchestrator. */
@Test
void anUncountableHerdrStartsNothing() {
FakeHerdr herdr = new FakeHerdr().healthy(false);
assertEquals(0, launcher(herdr, configWith(lead("opus", null, 1))).ensureLeads());
assertFalse(herdr.called("agent.start"));
}
// ── what it starts is a LEAD, not a member ────────────────────────────────────────────────
/**
* The single most important assertion here. The worker charter tells its reader it is an
* off-subscription worker that must end every turn with bridge_reply — the opposite of what an
* orchestrator is. A lead must never receive it.
*/
@Test
void theLeadNeverReceivesTheWorkerReplyCharter() {
FakeHerdr herdr = new FakeHerdr();
launcher(herdr, configWith(lead("opus", null, 1))).ensureLeads();
List<String> args = startedArgs(herdr);
assertFalse(args.contains("--append-system-prompt"),
"the reply charter is a worker contract and must not be injected into a lead");
assertTrue(args.stream().noneMatch(a -> a.contains("bridge_reply")), args.toString());
}
/** It still mounts the bridge — a lead that cannot orchestrate is pointless. */
@Test
void theLeadMountsTheBridgeMcpAndPinsItsModel() {
FakeHerdr herdr = new FakeHerdr();
launcher(herdr, configWith(lead("opus", null, 1))).ensureLeads();
List<String> args = startedArgs(herdr);
assertTrue(args.contains("--mcp-config"));
assertTrue(args.stream().anyMatch(a -> a.contains("http://127.0.0.1:8765/mcp")), args.toString());
assertEquals("claude-opus-5", args.get(args.indexOf("--model") + 1));
assertTrue(args.indexOf("--model") > args.indexOf("--mcp-config"),
"--model is appended last so it outranks the ccs wrapper (CB-533)");
}
/** A lead runs on the operator's subscription. Nothing may move it off. */
@Test
void theLeadEnvCarriesNoAnthropicBinding() {
FakeHerdr herdr = new FakeHerdr();
launcher(herdr, configWith(lead("opus", null, 1))).ensureLeads();
Map<String, String> env = tabEnv(herdr);
assertNull(env.get("ANTHROPIC_BASE_URL"));
assertNull(env.get("ANTHROPIC_AUTH_TOKEN"));
assertEquals("300000", env.get("CLAUDE_CODE_AUTO_COMPACT_WINDOW"),
"the profile's own env: still applies");
}
/** The lead's tab goes in its own workspace, never a member one — the scanner skips those. */
@Test
void theLeadTabIsCreatedOutsideEveryMemberWorkspace() {
FakeHerdr herdr = new FakeHerdr();
launcher(herdr, configWith(lead("opus", null, 1))).ensureLeads();
String label = (String) ((Map<?, ?>) herdr.lastCall("workspace.create").params()).get("label");
assertEquals("leads", label);
assertNotEquals("bridged-workers", label);
}
// ── recognise-only and misconfiguration ───────────────────────────────────────────────────
/** A lead with a pin but no profile is recognise-only by design — not an error, not a launch. */
@Test
void aLeadThatNamesNoProfileIsRecognisedButNeverLaunched() {
FakeHerdr herdr = new FakeHerdr();
assertEquals(0, launcher(herdr, configWith(lead(null, "term_dead", 1))).ensureLeads());
assertFalse(herdr.called("agent.start"));
}
/** `instances: 0` is a deliberate off switch. */
@Test
void zeroInstancesLaunchesNothing() {
FakeHerdr herdr = new FakeHerdr();
assertEquals(0, launcher(herdr, configWith(lead("opus", null, 0))).ensureLeads());
assertFalse(herdr.called("agent.start"));
}
/** A profile name with no matching profile is logged and skipped, never a daemon crash. */
@Test
void anUnknownProfileIsSkippedRatherThanThrown() {
FakeHerdr herdr = new FakeHerdr();
assertEquals(0, launcher(herdr, configWith(lead("nope", null, 1))).ensureLeads());
assertFalse(herdr.called("agent.start"));
}
/** No leads declared at all: not a herdr call in sight. */
@Test
void noLeadersConfiguredTouchesHerdrNotAtAll() {
FakeHerdr herdr = new FakeHerdr();
BridgedConfig cfg = new BridgedConfig(
null, null, Map.of("opus", opusProfile()), null, null, null, null, null,
null, null, null, null, "fixed", null).withDefaults();
assertEquals(0, launcher(herdr, cfg).ensureLeads());
assertTrue(herdr.calls.isEmpty(), "nothing declared ⇒ nothing scanned");
}
}