fleetd #799: observer SEND header carries the space/tab label #800
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user