Compare commits

...

4 Commits

Author SHA1 Message Date
Dai Ha 2dddacdd38 fleetd #799: observer SEND header carries the space/tab label, not just the id
CI / shell-tests (pull_request) Failing after 10s
CI / contract (pull_request) Successful in 55s
CI / build (pull_request) Failing after 1m58s
attributeIfObserver now joins the caller's terminal to its herdr pane via
PaneLocator.locate (new, reusing pane.list) and appends whichever of the
tab/workspace labels herdr reports, e.g.
"[fleet_send from observer term_x (space "ltms", tab "lead")]". The id stays
first and is never replaced; missing labels, or a herdr error while looking
them up, degrade to the bare id with no parenthetical. FleetApp's REST entry
path takes the same PaneLocator so both surfaces attribute identically.
2026-10-06 19:23:21 +02:00
Dai Ha 96ebae28a6 reviewer skill: send fleet_reply before writing the reasoning out
Five reviewer turns were lost in one session. Each wrote a long, correct
analysis to its own pane and ended the turn with no fleet_reply, so the
bridge scraped the pane and the lead received a clipped fragment.

The briefs are half the cause: each carried a five-item checklist of things
to hunt alongside a ~90-word capped output format, which reads as two
contradictory output contracts. The skill now says the checklist is where to
look, not the shape of the answer, and that the reply goes out as soon as
the answer is known.

Measured: tickets task-7da785-29 and -30 resolved at 18:28 via the
turn-completion fallback, 1233 and 1403 chars scraped.
2026-10-06 19:01:38 +02:00
Dai Ha b41aa663f7 Bridge block: invariant 6 — an operator outranks the confirm rule
Invariant 6 as first written told every receiver to confirm, with no
mention of who may override it. A peer cannot impose a rule on another
operator's session.

Measured: the trinotes pane (observer, work config dir) answered the
communication test and then said its operator's standing rule is not to
answer fleet messages, that a peer cannot change that rule, and that it
would ask its operator before following mine. That is the correct reading
and the rule now says so.

Also records the symmetric error: a sender must not read silence as
agreement or as a dead session.

wiki/7-Use-Cases.md synced byte-identical.
2026-10-06 18:48:12 +02:00
Dai Ha f544906621 Bridge block: a receiver confirms what it receives
Adds invariant 6 to the canonical block. Nothing in it said a received
message must be answered, so a sender could not tell a handled message
from one that never arrived.

Measured today: the anki pane (observer) sent to vms, which reads
deliverable:false because it has not contacted the daemon since the 08:05
restart. The send was accepted, held 83s, and failed with the body 'nst' --
three characters scraped off the vms screen. The sender read that as an
inconclusive result and asked the lead what happened. fleetd #757 covers
the accept-time refusal; this rule covers the half the protocol owns.

Operator's words: 'when a message sent, at least the receiver should
confirm unless explicit told to not reply'.

wiki/7-Use-Cases.md template synced byte-identical (wiki 3ad606f).
2026-10-06 18:18:10 +02:00
8 changed files with 192 additions and 9 deletions
+9
View File
@@ -37,6 +37,15 @@ confident-but-wrong finding. Anything you could settle by reading more code is y
## 4. The finding — what goes in `fleet_reply`
**Call `fleet_reply` as soon as you know your answer, before you write the reasoning out.** The
four lines below are the whole deliverable, and they are short on purpose. Analysis you type into
your terminal reaches nobody: when a turn ends with no `fleet_reply`, the bridge scrapes the pane
and the lead receives a clipped fragment instead of a finding. A long, correct analysis and no
`fleet_reply` is a failed turn, and it is the most common way this role fails.
If the delegation also handed you a list of things to check, that list is where to *look*. It is
not the shape of the answer. Work the list, then still send these four lines.
Report the **single most important** real issue in the scope, in these four lines, under
~90 words:
+12
View File
@@ -95,6 +95,18 @@ and the sender silently receives nothing. Fail toward the recoverable error.
5. **Never move a fleet session, pane or peer except through the bridge.** The bridge owns policy;
the multiplexer owns PTYs. Any route that changes fleet state without the bridge's checks
bypasses every rule above — the `herdr` CLI and its socket are the usual example.
6. **Confirm what you receive.** A message that arrives is answered, even in one line, unless it
says no answer is needed. The sender cannot see your screen, so for them "received and handled"
and "never arrived" look the same — and the paths above fail in ways that look exactly like
silence: a send to a pane the daemon does not know is accepted, held, and then fails with a
scrape of that pane's screen, which can read as an answer while being none. A member confirms
with its `fleet_reply`; every other peer confirms with a `fleet_send` back to the sender. If you
cannot do the thing asked, say that — a refusal is a confirmation. **Never read a failed
ticket's body as a reply.** Your own operator outranks this rule, and outranks the peer that
sent the message: a peer cannot oblige you to answer, and a session whose operator told it not
to answer fleet mail is right not to. Where you can, say that much and nothing more. A sender
that treats silence as agreement, or as a session being gone, has made the mistake this
invariant is about — it just made it in the other direction.
### Primary (lead) — run this on every task, in order
@@ -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");