fleetd #743: make the pane-label scan best-effort, trim justification comments
CI / shell-tests (pull_request) Failing after 10s
CI / contract (pull_request) Successful in 51s
CI / build (pull_request) Failing after 1m56s

A workspace.list/tab.list failure in the label scan no longer costs the
caller the agent roster (GET /agents) or the leads/members/capacity/
coordinator rows (fleet_list) that never needed it. Both call sites now
fall back to an empty label map on HerdrException, so a pane row still
renders with label:null instead of the whole response failing.

Also cuts four comments down to the current contract, per the project's
comment rule: dropped the reviewer-facing justification from Fleetd's
deliverableTo javadoc, panesVisibleTo's javadoc, the fleet_list handler's
inline comment, and the panesVisible assembly-gate comment, and removed
the two fragments describing what a test must do.
This commit is contained in:
Dai Ha
2026-10-05 09:19:16 +02:00
parent 459a523e2c
commit b1d2cb48ac
6 changed files with 92 additions and 33 deletions
@@ -233,9 +233,6 @@ public final class Fleetd {
*
* <p>Both sets are read through their supplier on each call rather than snapshotted, so a lead or
* collaborator discovered by {@code leadScan} after startup becomes deliverable without a restart.
*
* <p>Public so {@code fleet_list}'s {@code panes} row can report the exact same gate the
* injector enforces, rather than a second, separately-derived guess at reachability.
*/
public static Predicate<String> deliverableTo(MemberPresence presence, Supplier<Map<String, String>> leads,
Supplier<Map<String, String>> collaborators) {
@@ -582,11 +582,8 @@ public final class FleetMcp {
(exchange, _) -> {
McpSchema.CallToolResult denied = deny(exchange, toolAction("fleet_list", Map.of()), null);
if (denied != null) return denied;
// The label lookup costs a herdr scan, so it is only a Supplier here —
// listFleet reads it (inside its own HerdrException handling) only once
// panesVisibleTo has already said this caller receives the row at all. The
// deliverable predicate is the exact gate the injector enforces, never a second,
// separately-derived guess (see Fleetd#deliverableTo's own javadoc).
// A Supplier: the label lookup costs a herdr scan, and must stay behind
// panesVisible so it only runs for a caller that receives the row at all.
PaneSource panes = new PaneSource(() -> identity.panes().tabLabelsByTabId(),
Fleetd.deliverableTo(presence, callers::leads, callers::collaborators));
return listFleet(workers, sessions, messages, capacity, healthCoverage, loopHealth, quarantine, outage,
@@ -823,15 +820,9 @@ public final class FleetMcp {
/**
* Who may see {@code fleet_list}'s {@code panes} array — every herdr-tracked agent pane on the
* host, labelled and addressable by {@code sessionId}, including a hand-opened tab this daemon
* never spawned and never configured as a lead or collaborator. {@code READ}'s own grant rests
* on "the roster carries no secrets" ({@code Authz.java}'s comment on its {@code READ} case) —
* a tab label and a member's cwd are not that, so this array gets the same narrower gate as
* {@link #leadsVisibleTo}/{@link #collaboratorsVisibleTo} rather than riding bare {@code READ}:
* exactly the roles that may {@link Authz.Action#SEND} to a named peer. A plain worker or an
* unconfigured observer pane can never {@code SEND} at all, so listing every other pane's label
* and working directory to one would expose host shape with no use to that caller — the same
* reasoning already applied to {@code collaborators}.
* host, carrying a tab label and a member's {@code cwd}. Visible to exactly the roles that may
* {@link Authz.Action#SEND} to a named peer; a plain worker or an observer holds {@code READ}
* but never {@code SEND}, so it does not see this array.
*/
static boolean panesVisibleTo(Principal caller) {
return caller.isPrimary() || caller.isArchitect() || caller.isCollaborator();
@@ -2009,10 +2000,8 @@ public final class FleetMcp {
}
/**
* As below, with no pane discovery — every wrapper overload above delegates here, so
* {@code panes} is omitted and {@code panesVisible} is {@code false}. A test that wants the
* {@code panes} row must call the canonical overload below with an explicit {@link PaneSource}
* and {@code panesVisible}.
* As below, with no pane discovery — {@code panes} is {@link PaneSource#none()} and
* {@code panesVisible} is {@code false}.
*/
static McpSchema.CallToolResult listFleet(PeerLauncher workers, SessionManager sessions, MessageService messages,
CapacitySource capacity, HealthCoverageSource healthCoverage,
@@ -2051,9 +2040,7 @@ public final class FleetMcp {
* {@code panes} row; {@link PaneSource#none()} for a caller that
* does not want the row
* @param panesVisible whether this caller may see the {@code panes} array (see
* {@link #panesVisibleTo}); every wrapper overload above passes
* {@code false}, so a test that wants the row must call this overload
* with an explicit {@code true}
* {@link #panesVisibleTo})
*/
static McpSchema.CallToolResult listFleet(PeerLauncher workers, SessionManager sessions, MessageService messages,
CapacitySource capacity, HealthCoverageSource healthCoverage,
@@ -2113,9 +2100,7 @@ public final class FleetMcp {
.map(e -> collaboratorRow(e.getKey(), e.getValue()))
.toList());
}
// A pane's label and a member's cwd are not roster facts every READ-gated caller may
// see (see panesVisibleTo) -- gate BEFORE assembling the row, same reason as every
// other array above.
// gate BEFORE assembling the row, so the key is absent rather than present-and-empty.
if (panesVisible) {
result.put("panes", paneRows(live, roster, leads, collaborators, panes));
}
@@ -2444,6 +2429,19 @@ public final class FleetMcp {
return m;
}
/**
* {@code panes.tabLabels()}'s herdr scan, or an empty map on a {@code HerdrException} — a
* missing label must not cost the {@code leads}/{@code members}/{@code capacity}/
* {@code coordinator} rows that share {@code listFleet}'s own {@code catch}.
*/
private static Map<String, String> tabLabelsOrEmpty(PaneSource panes) {
try {
return panes.tabLabels().get();
} catch (HerdrException e) {
return Map.of();
}
}
/**
* One row per herdr-tracked agent pane, sorted by terminal id for a stable order. {@code live}
* is the same terminal-keyed {@link Agent} map {@code leadView}/{@code memberCapacityView}
@@ -2452,7 +2450,7 @@ public final class FleetMcp {
*/
private static List<Map<String, Object>> paneRows(Map<String, Agent> live, List<MemberSession> roster,
Map<String, String> leads, Map<String, String> collaborators, PaneSource panes) {
Map<String, String> tabLabels = panes.tabLabels().get();
final Map<String, String> tabLabels = tabLabelsOrEmpty(panes);
Map<String, MemberSession> byTerminal = roster.stream()
.filter(s -> s.terminalId() != null)
.collect(Collectors.toMap(MemberSession::terminalId, Function.identity(), (_, b) -> b));
@@ -437,16 +437,25 @@ public final class FleetApp {
}
}
/**
* Tab id → its herdr display label, or an empty map on a {@code workspace.list}/{@code
* tab.list} failure — a missing label must not cost the agent roster.
*/
private Map<String, String> tabLabelsOrEmpty() {
try {
return new PaneLocator(herdr, memberHerdr).tabLabelsByTabId();
} catch (HerdrException e) {
return Map.of();
}
}
/** Discovery: every agent herdr tracks, keyed by its Claude session UUID. */
private void agents(Context ctx) {
if (!allow(ctx, routeAction("GET /agents"), null)) {
return;
}
final Map<String, String> tabLabels = tabLabelsOrEmpty();
try {
// A tab's label is the only human-usable address for a pane this daemon never spawned
// (a hand-opened observer tab); agent.list carries no label of its own, so it is merged
// in from the same herdr daemon(s) the agent roster itself is drawn from.
Map<String, String> tabLabels = new PaneLocator(herdr, memberHerdr).tabLabelsByTabId();
ctx.status(200).json(Map.of("agents",
workers.list().stream().map(Agent.class::cast).map(a -> view(a, tabLabels)).toList()));
} catch (HerdrException e) {
@@ -48,6 +48,7 @@ public final class FakeHerdr implements HerdrClient {
private final Map<String, String> processInfoErrorCodeFor = new ConcurrentHashMap<>();
private String tabCloseErrorCode = null;
private final Map<String, String> tabCloseErrorCodeFor = new ConcurrentHashMap<>();
private String workspaceListErrorCode = null;
private String agentSendErrorCode = null;
private boolean noPanes = false;
private volatile String agentStatus = "idle"; // steady-state agent.get status
@@ -142,6 +143,12 @@ public final class FakeHerdr implements HerdrClient {
return this;
}
/** Make {@code workspace.list} fail with this herdr error code; every other method still succeeds. */
public FakeHerdr workspaceListFailsWith(String code) {
this.workspaceListErrorCode = code;
return this;
}
/**
* Make {@code pane.list} report no panes at all — models a second herdr daemon (CB-185) that
* simply does not host the pane a {@link PaneLocator} is searching for.
@@ -302,11 +309,17 @@ public final class FakeHerdr implements HerdrClient {
case "ping" -> mapper.readTree(
("{\"type\":\"pong\",\"version\":\"%s\",\"protocol\":%d}")
.formatted(pingVersion, pingProtocol));
case "workspace.list" -> mapper.readTree(("""
case "workspace.list" -> {
if (workspaceListErrorCode != null) {
throw new HerdrException("herdr error [" + workspaceListErrorCode + "]: workspace.list failed",
workspaceListErrorCode, null);
}
yield mapper.readTree(("""
{"type":"workspace_list","workspaces":[
{"workspace_id":"w1","label":"dev-mgnl","focused":true,"pane_count":7,"agent_status":"unknown"},
{"workspace_id":"w2","label":"ltms","focused":false,"pane_count":5,"agent_status":"done"}%s]}""")
.formatted(extraWorkspaces.isEmpty() ? "" : "," + String.join(",", extraWorkspaces)));
}
case "agent.list" -> mapper.readTree(("""
{"type":"agent_list","agents":[
{"terminal_id":"term_a","agent":"claude","agent_status":"idle",
@@ -1179,6 +1179,30 @@ class FleetMcpTest {
assertFalse(out.contains("\"panes\""), out);
}
/**
* The tab-label scan behind {@code panes} shares no failure path with the rest of
* {@code listFleet} -- a {@code workspace.list}/{@code tab.list} failure costs only the
* labels in the {@code panes} row (each renders {@code null}), never the {@code leads}/
* {@code members} arrays, which never needed that scan at all.
*/
@Test
void listStillReportsEveryOtherArrayWhenTheLabelScanFails() {
FakeHerdr h = new FakeHerdr().workspaceListFailsWith("unavailable");
FleetMcp.PaneSource panes = new FleetMcp.PaneSource(
() -> new PaneLocator(h).tabLabelsByTabId(),
Fleetd.deliverableTo(new MemberPresence(), Map::of, Map::of));
McpSchema.CallToolResult res = listFleetWithPanes(h, panes, true);
assertNotEquals(Boolean.TRUE, res.isError(), textOf(res));
String out = textOf(res);
assertTrue(out.contains("\"panes\":["), out);
assertTrue(out.contains("\"sessionId\":\"term_a\""), out);
assertTrue(out.contains("\"label\":null"), out);
assertTrue(out.contains("\"leads\":[]"), "a label-scan failure must not cost the leads array: " + out);
assertTrue(out.contains("\"members\":[]"), "a label-scan failure must not cost the members array: " + out);
}
/**
* fleetd #421: {@code mailbox.pending} counts only broker-ready messages, so a blocked lead's
* normal, healthy state is {@code "pending": 0} next to a non-empty {@code held[]} — which
@@ -223,6 +223,24 @@ class FleetAppTest {
assertTrue(body.has("detail"), res.body());
}
/**
* The tab-label scan ({@code workspace.list}/{@code tab.list}) is decoration on top of
* {@code workers.list()}'s own agent roster, so its failure must not cost that roster: a row
* reports a {@code null} label instead, never the {@code herdr_error} envelope.
*/
@Test
void agentsStillReportsTheRosterWhenTheLabelScanFails() throws Exception {
FakeHerdr herdr = new FakeHerdr().workspaceListFailsWith("unavailable");
int port = start(herdr, "http://gx00.gw:8000", Set.of("gx00.gw"));
HttpResponse<String> res = req(port, "GET", "/agents");
assertEquals(200, res.statusCode(), res.body());
JsonNode agents = mapper.readTree(res.body()).get("agents");
assertEquals(1, agents.size());
assertEquals("sess-1111", agents.get(0).get("sessionId").asText());
assertTrue(agents.get(0).get("label").isNull(), "a failed label scan must report a null label, not fail the roster: " + res.body());
}
@Test
void spawnWorkerLandsInOwnTabInWorkerSpaceAndInjectsBaseUrl() throws Exception {
FakeHerdr herdr = new FakeHerdr();