CB-519: make PeerHandle.id() a host-unique opaque UUID, decoupled from the herdr pane id

This commit is contained in:
Dai Ha
2026-08-04 18:11:15 +02:00
parent 54b314ace5
commit 7ace184fe6
11 changed files with 156 additions and 41 deletions
@@ -575,15 +575,19 @@ public final class BridgeMcp {
"default", workers.defaultProfile() == null ? "" : workers.defaultProfile())));
}
/** {@code bridge_list}: bridge-owned roster merged with live herdr status by paneId. */
/**
* {@code bridge_list}: bridge-owned roster merged with live herdr status. CB-519 decoupled the
* registry key (a host-unique id) from the herdr pane coordinate, so the join is on the
* terminal id, which both the session and the live agent carry.
*/
static McpSchema.CallToolResult listWorkers(PeerLauncher workers, SessionManager sessions) {
try {
Map<String, Agent> live = workers.list().stream()
.map(Agent.class::cast)
.filter(a -> a.paneId() != null)
.collect(Collectors.toMap(Agent::paneId, Function.identity(), (_, b) -> b));
.filter(a -> a.terminalId() != null)
.collect(Collectors.toMap(Agent::terminalId, Function.identity(), (_, b) -> b));
List<Map<String, Object>> out = sessions.roster().stream()
.map(s -> SessionManager.rosterView(s, live.get(s.paneId())))
.map(s -> SessionManager.rosterView(s, live.get(s.terminalId())))
.toList();
return text(json(Map.of("workers", out)));
} catch (HerdrException e) {
@@ -12,9 +12,13 @@ package dev.ltms.bridged.peer;
public interface PeerHandle {
/**
* The registry/routing key — an opaque, launcher-assigned identifier. For the herdr-backed
* launcher this is the herdr pane id; for other launchers it is whatever their transport
* uses. Guaranteed to be non-null and unique among live peers within a single daemon process.
* The registry/routing key — an opaque, launcher-assigned identifier (CB-519). Multiple
* daemon processes may run on one host, so the contract is <em>host-unique</em>, not merely
* process-unique: the herdr-backed launcher mints a fresh UUID per spawn, and a non-herdr
* launcher is likewise expected to return an identifier that cannot collide across processes
* on the same host. This id is the routing key and is deliberately decoupled from any launcher
* transport coordinate (e.g. a herdr pane id), which stays launcher-private. Guaranteed to be
* non-null and unique among live peers on the host.
*/
String id();
@@ -225,12 +225,13 @@ public final class BridgedApp {
if (!allow(ctx, Authz.Action.READ, null)) {
return;
}
// CB-519: the registry key is a host-unique id, not the pane coordinate — join on terminal.
Map<String, Agent> live = workers.list().stream()
.map(Agent.class::cast)
.filter(a -> a.paneId() != null)
.collect(Collectors.toMap(Agent::paneId, Function.identity(), (_, b) -> b));
.filter(a -> a.terminalId() != null)
.collect(Collectors.toMap(Agent::terminalId, Function.identity(), (_, b) -> b));
List<Map<String, Object>> out = sessions.roster().stream()
.map(s -> SessionManager.rosterView(s, live.get(s.paneId())))
.map(s -> SessionManager.rosterView(s, live.get(s.terminalId())))
.toList();
ctx.status(200).json(Map.of("workers", out));
}
@@ -5,7 +5,10 @@ package dev.ltms.bridged.session;
* process spawned. Immutable; state transitions are performed by replacing the record in
* {@link SessionManager}'s registry.
*
* @param paneId herdr pane handle — the registry key and the argument to teardown
* @param paneId the host-unique opaque id (CB-519) — the registry key and the argument to
* teardown. Despite the historical name this is the {@link
* dev.ltms.bridged.peer.PeerHandle#id()}, a UUID, and is distinct from the
* launcher-private herdr pane coordinate.
* @param terminalId herdr terminal handle — the {@code target} for send/read/status
* @param profile the worker profile name that spawned this session
* @param cwd the resolved working directory the worker started in
@@ -21,6 +21,9 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.function.LongSupplier;
@@ -81,6 +84,13 @@ public abstract class HerdrPeerLauncher implements PeerLauncher {
// collide with same-profile peers that outlived a restart. See startUniquelyNamed.
private final String nameNonce = String.format("%06x", new SecureRandom().nextInt(1 << 24));
// CB-519: PeerHandle.id() is a host-unique opaque UUID, decoupled from the herdr pane id. The
// routing/registry key is the UUID; the herdr pane id is a launcher-private placement/teardown
// coordinate. This map bridges the two so stop(id) can resolve a host-unique key back to the
// exact pane it must tear down. The pane id is launcher-private (never the routing key) — see
// PeerHandle.id().
private final ConcurrentMap<String, String> paneByAgentId = new ConcurrentHashMap<>();
/**
* @param namePrefix label prefix for this peer kind (drives naming and reap)
* @param agents herdr agent control (start, status, close)
@@ -189,10 +199,13 @@ public abstract class HerdrPeerLauncher implements PeerLauncher {
* {@inheritDoc}
*
* <p>Delegates to {@link #spawnInternal} and wraps the resulting herdr {@link Agent} in a
* {@link WorkerHandle} whose {@link PeerHandle#id()} equals the agent's paneId. When
* {@code spawnReadyTimeoutMs > 0}, blocks until the peer's herdr status is injectable or the
* timeout elapses; on timeout the pane is closed (no orphan) and a
* {@link PeerUnreachableException} is thrown.
* {@link WorkerHandle} whose {@link PeerHandle#id()} is a fresh <em>host-unique</em> opaque
* UUID (CB-519), deliberately decoupled from the herdr pane id: the id is the registry/routing
* key and must never collide across daemon processes on the same host, while the herdr pane id
* stays a launcher-private placement/teardown coordinate, remembered here so {@link #stop}
* can resolve the host-unique key back to its pane. When {@code spawnReadyTimeoutMs > 0},
* blocks until the peer's herdr status is injectable or the timeout elapses; on timeout the
* pane is closed (no orphan) and a {@link PeerUnreachableException} is thrown.
*/
@Override
public PeerHandle spawn(SpawnRequest req) {
@@ -201,7 +214,9 @@ public abstract class HerdrPeerLauncher implements PeerLauncher {
if (spawnReadyTimeoutMs > 0) {
waitUntilInjectableOrThrow(paneId);
}
return new WorkerHandle(paneId, agent.terminalId());
String id = UUID.randomUUID().toString();
paneByAgentId.put(id, paneId);
return new WorkerHandle(id, agent.terminalId());
}
@Override
@@ -426,21 +441,31 @@ public abstract class HerdrPeerLauncher implements PeerLauncher {
// --- teardown ------------------------------------------------------------------------------
/**
* Tear a peer down by pane id: close the pane, and close its tab <em>only</em> when the peer is
* that tab's sole occupant. The single-pane check is what makes this safe regardless of how the
* peer was placed (or a placement-config change across a restart): a pane-placement peer sitting
* in one of the user's shared tabs has siblings, so its tab is never closed — we only ever
* remove a tab we created to hold one peer.
* Tear a peer down: close the pane, and close its tab <em>only</em> when the peer is that tab's
* sole occupant. The single-pane check is what makes this safe regardless of how the peer was
* placed (or a placement-config change across a restart): a pane-placement peer sitting in one
* of the user's shared tabs has siblings, so its tab is never closed — we only ever remove a
* tab we created to hold one peer.
*
* <p>{@code idOrPane} is the {@link PeerHandle#id()} of a peer this launcher spawned (CB-519's
* host-unique opaque UUID), resolved through {@link #paneByAgentId} to the pane it must tear
* down. An argument that is not one of our ids is treated as a raw herdr pane id — the
* {@link #reapOrphanWorkers() orphan-reap} and spawn-gate-timeout paths, plus any caller that
* passes a pane directly, keep working without an owning id.
*
* <p>Resolves the tab from the pane <em>before</em> closing it. An already-gone pane/tab
* (repeated DELETE, crashed peer) is treated as success; any other failure propagates so a
* genuinely failed teardown is not reported as done.
*/
@Override
public void stop(String paneId) {
// Teardown knows only the paneId, not which profile spawned it. Attempt tab cleanup when any
public void stop(String idOrPane) {
// Teardown knows only the pane, not which profile spawned it. Attempt tab cleanup when any
// profile uses tab placement (so the bridge may have created a dedicated peer tab); the
// single-occupant check below is what actually protects the user's shared tabs.
String paneId = paneByAgentId.remove(idOrPane);
if (paneId == null) {
paneId = idOrPane; // raw-pane fallback (reap, gate timeout, pane-addressed callers)
}
WorkspaceControl.PaneLocation loc = usesTabPlacement() ? spaces.locatePane(paneId) : null;
try {
agents.close(paneId);
@@ -31,6 +31,9 @@ public final class FakeHerdr implements HerdrClient {
private String agentSendErrorCode = null;
private volatile String agentStatus = "idle"; // steady-state agent.get status
private volatile String readText = "worker transcript tail"; // canned agent.read output
private int pinnedStarts = 0; // how many upcoming agent.start calls report a fixed pane
private String pinnedStartTerminal;
private String pinnedStartPane;
public FakeHerdr healthy(boolean h) {
this.healthy = h;
@@ -79,6 +82,19 @@ public final class FakeHerdr implements HerdrClient {
return this;
}
/**
* Force the next {@code n} {@code agent.start} calls to report this terminal/pane coordinate,
* instead of the fake's usual incrementing {@code term_new_n}/{@code w9:pW_n}. Lets a test make
* two spawns report the <em>same</em> herdr pane, to prove the host-unique id (CB-519) never
* collides on that coordinate.
*/
public FakeHerdr pinNextStarts(int n, String terminalId, String paneId) {
this.pinnedStarts = n;
this.pinnedStartTerminal = terminalId;
this.pinnedStartPane = paneId;
return this;
}
/**
* Seed a named agent into {@code agent.list} (e.g. an orphaned worker for CB-117 reaper tests).
@@ -175,12 +191,20 @@ public final class FakeHerdr implements HerdrClient {
"agent_name_taken", null);
}
long n = busyAdjusted - agentNameTakenFor;
// The agent starts INTO the requested pane, so its pane_id echoes the param.
boolean pinned = pinnedStarts > 0;
if (pinned) {
pinnedStarts--;
}
// Protocol 19: the agent starts INTO the requested pane, so its pane_id normally
// echoes the param. A pin overrides both coordinates, which is the only way to
// make two spawns report one pane — what CB-519's collision test needs.
String terminal = pinned ? pinnedStartTerminal : ("term_new_" + n);
Object pane = pinned ? pinnedStartPane : p.get("pane_id");
yield mapper.readTree(("""
{"type":"agent_started","agent":{
"terminal_id":"term_new_%d","name":"claude","agent_status":"unknown",
"terminal_id":"%s","name":"claude","agent_status":"unknown",
"workspace_id":"w9","tab_id":"w9:t2","pane_id":"%s"}}""")
.formatted(n, p.get("pane_id")));
.formatted(terminal, pane));
}
case "pane.split" -> mapper.readTree("""
{"type":"pane_info","pane":{"pane_id":"w1:pSplit","workspace_id":"w1",
@@ -216,12 +216,15 @@ class BridgeMcpTest {
@Test
void spawnReturnsTheNewWorkersSessionAndPane() {
FakeHerdr h = new FakeHerdr();
McpSchema.CallToolResult res = BridgeMcp.spawn(
sessionManager(h, "http://gx00.gw:8000", Set.of("gx00.gw")), null);
SessionManager sm = sessionManager(h, "http://gx00.gw:8000", Set.of("gx00.gw"));
McpSchema.CallToolResult res = BridgeMcp.spawn(sm, null);
assertNotEquals(Boolean.TRUE, res.isError());
String out = textOf(res);
assertTrue(out.contains("\"sessionId\":\"term_new_1\""), out);
assertTrue(out.contains("\"paneId\":\"w9:pRoot_1\""), out);
// CB-519: the "paneId" wire field now carries the host-unique opaque id, not the herdr pane.
WorkerSession s = sm.roster().getFirst();
assertTrue(out.contains("\"paneId\":\"" + s.paneId() + "\""), out);
assertNotEquals("w9:pRoot_1", s.paneId(), "the id is decoupled from the herdr pane coordinate");
assertTrue(out.contains("\"status\":\"spawning\""), out);
}
@@ -28,6 +28,7 @@ import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
@@ -156,7 +157,10 @@ class BridgedAppTest {
HttpResponse<String> res = req(port, "POST", "/workers");
assertEquals(201, res.statusCode());
JsonNode body = mapper.readTree(res.body());
assertEquals("w9:pRoot_1", body.get("paneId").asText());
// CB-519: the responded paneId is a host-unique opaque UUID, not the herdr pane coordinate.
String id = body.get("paneId").asText();
assertNotEquals("w9:pRoot_1", id, "paneId is the host-unique id, not the herdr pane");
assertDoesNotThrow(() -> UUID.fromString(id), "paneId must be a UUID: " + id);
assertEquals("spawning", body.get("state").asText());
// Subscription boundary (protocol 19): tab.create carried base_url + token in its env
@@ -172,9 +172,11 @@ class SessionManagerTest {
assertEquals(1, sessions.roster().size(), "only the fresh session remains");
assertEquals(fresh.paneId(), sessions.roster().getFirst().paneId());
// The old session was the first spawn → pane w9:pW_1 (CB-519: the registry key is the
// uuid id, so teardown is asserted on the real pane coordinate).
long paneCloseCount = herdr.calls.stream()
.filter(c -> "pane.close".equals(c.method()))
.filter(c -> oldPane.equals(((Map<?, ?>) c.params()).get("pane_id")))
.filter(c -> "w9:pW_1".equals(((Map<?, ?>) c.params()).get("pane_id")))
.count();
assertEquals(1, paneCloseCount, "the old worker was torn down");
}
@@ -307,7 +309,7 @@ class SessionManagerTest {
WorkerSession updated = sessions.get(session.paneId()).orElseThrow();
assertEquals(WorkerSession.State.DONE, updated.state(), "session finishes second turn");
assertEquals(2, updated.turnCount(), "turn count tracks both deliveries");
long releaseCloseCount = paneCloseCallsFor(herdr, session.paneId());
long releaseCloseCount = paneCloseCallsFor(herdr, "w9:pW_1"); // the real pane coordinate
assertEquals(0, releaseCloseCount, "cap disabled — no forced release of the worker pane");
}
@@ -330,7 +332,7 @@ class SessionManagerTest {
assertTrue(sessions.get(session.paneId()).isEmpty(), "session released after cap reached");
assertTrue(sessions.roster().isEmpty(), "released session leaves roster");
assertEquals(1, paneCloseCallsFor(herdr, session.paneId()),
assertEquals(1, paneCloseCallsFor(herdr, "w9:pW_1"),
"forced release tears the worker pane down exactly once");
}
@@ -351,9 +353,10 @@ class SessionManagerTest {
assertTrue(sessions.roster().isEmpty(), "drain clears the roster");
assertTrue(sessions.get(ready.paneId()).isEmpty(), "ready session is released");
assertTrue(sessions.get(busy.paneId()).isEmpty(), "busy session is released after timeout");
assertEquals(1, paneCloseCallsFor(herdr, ready.paneId()),
// ready is the first spawn → pane w9:pW_1, busy the second → w9:pW_2 (FakeHerdr order).
assertEquals(1, paneCloseCallsFor(herdr, "w9:pW_1"),
"ready worker pane is torn down");
assertEquals(1, paneCloseCallsFor(herdr, busy.paneId()),
assertEquals(1, paneCloseCallsFor(herdr, "w9:pW_2"),
"busy worker pane is torn down");
}
@@ -14,6 +14,7 @@ import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import static org.junit.jupiter.api.Assertions.*;
@@ -258,14 +259,18 @@ class ClaudeCodeLauncherTest {
// --- PeerHandle indirection ----------------------------------------------------------------
@Test
void spawnReturnsPeerHandleWithIdEqualToPaneId() {
void spawnReturnsPeerHandleWithHostUniqueOpaqueId() {
FakeHerdr herdr = new FakeHerdr();
ClaudeCodeLauncher svc = service(herdr, List.of("ccs", "ltms-local"), null);
PeerHandle handle = svc.spawn(new SpawnRequest(null, null, null));
assertNotNull(handle, "spawn must return a non-null handle");
assertEquals("w9:pRoot_1", handle.id(), "handle.id() must equal the agent's paneId");
// CB-519: id() is a host-unique opaque UUID, decoupled from the herdr pane coordinate.
assertNotEquals("w9:pRoot_1", handle.id(),
"handle.id() must NOT be the herdr pane id");
assertDoesNotThrow(() -> UUID.fromString(handle.id()),
"handle.id() must be a UUID: " + handle.id());
}
@Test
@@ -351,6 +356,40 @@ class ClaudeCodeLauncherTest {
assertTrue(herdr.called("pane.close"), "stop via handle.id() must close the pane");
}
// --- CB-519: host-unique id, decoupled from the pane coordinate ------------------------------
@Test
void twoSpawnsOnTheSamePaneNeverCollideOnHostUniqueId() {
// Two spawns may be placed on the same herdr pane coordinate (e.g. a pane that was reused
// or re-reported after a restart); the host-unique id must not collide even then.
FakeHerdr herdr = new FakeHerdr().pinNextStarts(2, "term_shared", "w9:pShared");
ClaudeCodeLauncher svc = service(herdr, List.of("ccs", "ltms-local"), null);
PeerHandle a = svc.spawn(new SpawnRequest(null, null, null));
PeerHandle b = svc.spawn(new SpawnRequest(null, null, null));
assertNotEquals(a.id(), b.id(),
"two spawns on the same pane coordinate get distinct host-unique ids");
assertNotEquals("w9:pShared", a.id(), "id is not the pane coordinate");
assertNotEquals("w9:pShared", b.id(), "id is not the pane coordinate");
}
@Test
void stopResolvesTheHostUniqueIdToThePaneThatSpawnedIt() {
// CB-519: id() != paneId, so stop(id) must tear down the exact pane the id names — and no
// other live peer's pane.
FakeHerdr herdr = new FakeHerdr(); // deterministic panes w9:pW_1, w9:pW_2 per spawn
ClaudeCodeLauncher svc = service(herdr, List.of("ccs", "ltms-local"), null);
PeerHandle a = svc.spawn(new SpawnRequest(null, null, null));
PeerHandle b = svc.spawn(new SpawnRequest(null, null, null));
svc.stop(b.id());
assertEquals(1, paneCloseCount(herdr, "w9:pW_2"), "stop(b.id()) closes only b's pane");
assertEquals(0, paneCloseCount(herdr, "w9:pW_1"), "a's pane is untouched");
}
// --- CB-306 spawn-readiness gate -----------------------------------------------------------
private static Map<String, BridgedConfig.Worker> workerConfigMap(String profile, String mcpUrl) {
@@ -386,7 +425,8 @@ class ClaudeCodeLauncherTest {
PeerHandle handle = svc.spawn(new SpawnRequest(null, null, null));
assertNotNull(handle, "spawn returns a handle when worker becomes injectable");
assertEquals("w9:pRoot_1", handle.id(), "handle id matches the started pane");
assertNotEquals("w9:pRoot_1", handle.id(),
"handle id is a host-unique opaque id, not the started pane");
assertEquals(0, paneCloseCount(herdr, "w9:pRoot_1"),
"no pane.close when worker becomes injectable before timeout");
}
@@ -445,8 +485,9 @@ class ClaudeCodeLauncherTest {
PeerHandle handle = svc.spawn(new SpawnRequest(null, null, null));
assertNotNull(handle, "spawn still succeeds with zero timeout");
assertEquals(0, paneCloseCount(herdr, handle.id()),
assertEquals(0, paneCloseCount(herdr, "w9:pW_1"),
"no orphan pane close from the gate path");
assertDoesNotThrow(() -> UUID.fromString(handle.id()));
}
// --- CB-511: worker environment seeding -----------------------------------------------------
@@ -127,15 +127,18 @@ class CompositePeerLauncherTest {
@Test
void stopTearsDownAPaneSpawnedThroughTheComposite() {
// CB-519: handle.id() is a host-unique opaque UUID, not the herdr pane — stop(id) must
// resolve it through the owning adapter down to the actual pane coordinate it spawned.
FakeHerdr herdr = new FakeHerdr();
PeerLauncher composite = composite(herdr);
PeerHandle handle = composite.spawn(new SpawnRequest("gemini", null, null));
assertNotEquals("w9:pW_1", handle.id(), "the id is decoupled from the pane coordinate");
composite.stop(handle.id());
assertTrue(herdr.calls.stream()
.anyMatch(c -> c.method().equals("pane.close")
&& handle.id().equals(((Map<?, ?>) c.params()).get("pane_id"))),
"stop routes to the spawning adapter and closes that worker's pane");
&& "w9:pW_1".equals(((Map<?, ?>) c.params()).get("pane_id"))),
"stop routes to the spawning adapter and closes exactly that worker's pane");
}
@Test