fleetd #799: observer SEND header carries the space/tab label #800

Open
agent wants to merge 1 commits from worker/799-a76336-9 into main
6 changed files with 171 additions and 9 deletions
@@ -192,6 +192,31 @@ public final class PaneLocator {
return out;
}
/**
* The tab and workspace id of the herdr pane carrying {@code terminal}, from {@code pane.list}
* across every searched daemon — the same call {@link #terminalForPid} scans. Either field is
* {@code null} when no pane matches {@code terminal}, or when the matching pane itself carries
* no tab or workspace id.
*/
public record PaneLocation(String tabId, String workspaceId) {
static final PaneLocation NONE = new PaneLocation(null, null);
}
/** Resolve {@code terminal} to the tab/workspace id of the pane it occupies. See {@link PaneLocation}. */
public PaneLocation locate(String terminal) {
if (terminal == null) {
return PaneLocation.NONE;
}
for (HerdrClient herdr : herdrs) {
for (JsonNode pane : herdr.call("pane.list", Map.of()).path("panes")) {
if (terminal.equals(pane.path("terminal_id").asText(null))) {
return new PaneLocation(pane.path("tab_id").asText(null), pane.path("workspace_id").asText(null));
}
}
}
return PaneLocation.NONE;
}
/** Whether a pane owns one of the scanned pid's ancestors, or the check of it failed outright. */
private enum Ownership { OWNS, DOES_NOT_OWN, UNKNOWN }
@@ -9,6 +9,7 @@ import dev.ltms.fleet.auth.Role;
import dev.ltms.fleet.guard.GuardException;
import dev.ltms.fleet.herdr.Agent;
import dev.ltms.fleet.herdr.AgentStatus;
import dev.ltms.fleet.herdr.PaneLocator;
import dev.ltms.fleet.metrics.FleetMetrics;
import dev.ltms.fleet.metrics.Metrics;
import dev.ltms.fleet.inject.MemberPresence;
@@ -503,7 +504,7 @@ public final class FleetMcp {
// An observer's SEND reaches a pane that cannot otherwise distinguish this
// from a human paste (see attributeIfObserver); every other caller's content
// passes through unchanged.
String content = attributeIfObserver(caller, str(a, "content"));
String content = attributeIfObserver(caller, str(a, "content"), identity.panes());
String turnId = str(a, "turnId");
String coordId = str(a, "coordId");
if (coordId != null && !coordId.isBlank()) {
@@ -987,12 +988,49 @@ public final class FleetMcp {
/**
* The text an observer's {@code SEND} actually delivers: prefixed with the sender's own
* connection-resolved terminal, which the receiving pane cannot otherwise tell apart from a
* human paste. Every other caller's content passes through unchanged. Shared with {@code
* FleetApp}'s REST entry path so both surfaces attribute identically.
* human paste, plus its tab and workspace display label in parentheses when herdr can supply
* either. The terminal id is the only thing in the header a reply can target — a label is
* display-only, is never unique, and is never substituted for it. Every other caller's content
* passes through unchanged. Shared with {@code FleetApp}'s REST entry path so both surfaces
* attribute identically.
*/
public static String attributeIfObserver(Principal caller, String content) {
return caller != null && caller.isObserver()
? "[fleet_send from observer " + caller.terminal() + "]\n" + content : content;
public static String attributeIfObserver(Principal caller, String content, PaneLocator panes) {
if (caller == null || !caller.isObserver()) {
return content;
}
return "[fleet_send from observer " + observerHeader(caller.terminal(), panes) + "]\n" + content;
}
/**
* {@code terminal}, plus {@code (space "…", tab "…")} for whichever of its workspace/tab
* labels herdr reports — omitted entirely when neither is known, so a degraded lookup still
* reads as the bare id and never as {@code null} or an empty parenthetical.
*/
private static String observerHeader(String terminal, PaneLocator panes) {
String tabLabel = null;
String workspaceLabel = null;
try {
PaneLocator.PaneLocation location = panes.locate(terminal);
tabLabel = location.tabId() == null ? null : panes.tabLabelsByTabId().get(location.tabId());
workspaceLabel = location.workspaceId() == null ? null
: panes.workspaceLabelsByWorkspaceId().get(location.workspaceId());
} catch (HerdrException e) {
// degrade to the id alone
}
if (tabLabel == null && workspaceLabel == null) {
return terminal;
}
StringBuilder header = new StringBuilder(terminal).append(" (");
if (workspaceLabel != null) {
header.append("space \"").append(workspaceLabel).append('"');
}
if (tabLabel != null && workspaceLabel != null) {
header.append(", ");
}
if (tabLabel != null) {
header.append("tab \"").append(tabLabel).append('"');
}
return header.append(')').toString();
}
/**
@@ -714,7 +714,7 @@ public final class FleetApp {
// An observer's SEND reaches a pane that cannot otherwise distinguish this from a human
// paste (see FleetMcp#attributeIfObserver, the same rule on the MCP entry path); every
// other caller's content passes through unchanged.
content = FleetMcp.attributeIfObserver(caller, content);
content = FleetMcp.attributeIfObserver(caller, content, new PaneLocator(herdr, memberHerdr));
timeout = Math.clamp(timeout, 1, MAX_MESSAGE_TIMEOUT_MS);
// Answering a worker's fleet_ask (CB-205): always blocks, and derives the worker from turnId.
@@ -0,0 +1,93 @@
package dev.ltms.fleet.mcp;
import dev.ltms.fleet.auth.Principal;
import dev.ltms.fleet.herdr.FakeHerdr;
import dev.ltms.fleet.herdr.PaneLocator;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* {@link FleetMcp#attributeIfObserver}: the terminal id in an observer's header is always
* present, a tab/workspace label is display-only and additive, and a missing label degrades to
* the id alone rather than rendering {@code null} or an empty parenthetical.
*/
class FleetMcpAttributeIfObserverTest {
@Test
void aNonObserverPassesContentThroughUnchangedRegardlessOfPanes() {
Principal worker = Principal.worker("term_worker", 1);
assertEquals("hello", FleetMcp.attributeIfObserver(worker, "hello", null));
}
@Test
void theTerminalIdIsAlwaysPresentAndNeverReplacedByALabel() {
FakeHerdr herdr = new FakeHerdr()
.withTab("w2", "w2:t8", "shell-tab");
PaneLocator panes = new PaneLocator(herdr);
Principal observer = Principal.observer("term_shell", 2);
String result = FleetMcp.attributeIfObserver(observer, "hi", panes);
assertEquals("[fleet_send from observer term_shell (space \"ltms\", tab \"shell-tab\")]\nhi", result);
}
/**
* Two tabs sharing one label must still read as two different senders. The label is the same
* on both headers; the id is what differs, and is the only part of either header a reply can
* target.
*/
@Test
void aSharedLabelStillYieldsDistinctHeadersByTerminalId() {
FakeHerdr herdr = new FakeHerdr()
.withTab("w2", "w2:t7", "lead")
.withTab("w2", "w2:t8", "lead");
PaneLocator panes = new PaneLocator(herdr);
String fromA = FleetMcp.attributeIfObserver(Principal.observer("term_a", 1), "msg", panes);
String fromShell = FleetMcp.attributeIfObserver(Principal.observer("term_shell", 2), "msg", panes);
assertEquals("[fleet_send from observer term_a (space \"ltms\", tab \"lead\")]\nmsg", fromA);
assertEquals("[fleet_send from observer term_shell (space \"ltms\", tab \"lead\")]\nmsg", fromShell);
assertNotEquals(fromA, fromShell, "identical labels must not collapse two senders into one header");
}
@Test
void anUnknownPaneDegradesToTheIdAloneWithNoLabelAndNoEmptyParens() {
PaneLocator panes = new PaneLocator(new FakeHerdr().withNoPanes());
Principal observer = Principal.observer("term_ghost", 3);
String result = FleetMcp.attributeIfObserver(observer, "msg", panes);
assertEquals("[fleet_send from observer term_ghost]\nmsg", result);
assertFalse(result.contains("null"), "a missing label must never render as the literal \"null\"");
assertFalse(result.contains("()"), "a missing label must never leave an empty parenthetical");
}
@Test
void aHerdrFailureDuringTheLabelLookupDegradesToTheIdAlone() {
FakeHerdr herdr = new FakeHerdr().workspaceListFailsWith("unavailable");
PaneLocator panes = new PaneLocator(herdr);
Principal observer = Principal.observer("term_shell", 4);
String result = FleetMcp.attributeIfObserver(observer, "msg", panes);
assertEquals("[fleet_send from observer term_shell]\nmsg", result);
}
@Test
void onlyTheKnownLabelAppearsWhenTheOtherIsMissing() {
// term_shell's pane sits in workspace "w2" ("ltms"); its tab "w2:t8" carries no seeded
// label, so only the space label appears.
PaneLocator panes = new PaneLocator(new FakeHerdr());
Principal observer = Principal.observer("term_shell", 5);
String result = FleetMcp.attributeIfObserver(observer, "msg", panes);
assertEquals("[fleet_send from observer term_shell (space \"ltms\")]\nmsg", result);
assertTrue(result.contains("term_shell"), "the id must still be present");
}
}
@@ -119,7 +119,10 @@ class FleetMcpObserverSendDeliveryTest {
@SuppressWarnings("unchecked")
Map<String, Object> params = (Map<String, Object>) herdr.lastCall("agent.prompt").params();
assertEquals("[fleet_send from observer term_shell]\nhi there", params.get("text"),
// term_shell's pane sits in workspace "w2", which FakeHerdr's workspace.list labels
// "ltms"; its tab "w2:t8" carries no label in FakeHerdr's tab.list, so only the space
// label appears.
assertEquals("[fleet_send from observer term_shell (space \"ltms\")]\nhi there", params.get("text"),
"the receiving pane must see the sender's own daemon-resolved terminal, never a raw "
+ "echo of the content and never a client-supplied name");
}
@@ -140,7 +140,10 @@ class FleetMcpObserverSendToLeadDeliveryTest {
@SuppressWarnings("unchecked")
Map<String, Object> params = (Map<String, Object>) herdr.lastCall("agent.prompt").params();
assertEquals("[fleet_send from observer term_shell]\ncan we split the review?",
// term_shell's pane sits in workspace "w2", which FakeHerdr's workspace.list labels
// "ltms"; its tab "w2:t8" carries no label in FakeHerdr's tab.list, so only the space
// label appears.
assertEquals("[fleet_send from observer term_shell (space \"ltms\")]\ncan we split the review?",
params.get("text"),
"a lead must see the sender's own daemon-resolved terminal, never a raw echo of "
+ "the content and never a client-supplied name");