From a0cd053fd923b9cfb87bb4ad1de180a3db595dc1 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Fri, 14 Aug 2026 21:45:28 +0200 Subject: [PATCH] CB-560: mark architect members present --- .../java/dev/ltms/bridged/auth/Principal.java | 12 +++++++++++- .../main/java/dev/ltms/bridged/mcp/BridgeMcp.java | 15 +++++++++++---- .../java/dev/ltms/bridged/mcp/BridgeMcpTest.java | 11 +++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/bridged/src/main/java/dev/ltms/bridged/auth/Principal.java b/bridged/src/main/java/dev/ltms/bridged/auth/Principal.java index 6341e22..32b49d9 100644 --- a/bridged/src/main/java/dev/ltms/bridged/auth/Principal.java +++ b/bridged/src/main/java/dev/ltms/bridged/auth/Principal.java @@ -48,7 +48,7 @@ public record Principal(Role role, String terminal, long pid, String name) { * made a lead unaddressable: {@link #ownsSession} could never be true for it, so * {@code bridge_reply} was refused and one lead could send to another but never be answered. * The terminal now means "which pane is this caller", the presence map keys on - * {@link #isWorker()} instead, and a lead is a peer that can both send and receive. + * {@link #isSpawnedMember()} instead, and a lead is a peer that can both send and receive. */ public static Principal leader(String name, String terminal, long pid) { return new Principal(Role.PRIMARY, terminal, pid, name); @@ -85,6 +85,16 @@ public record Principal(Role role, String terminal, long pid, String name) { return role == Role.WORKER; } + /** + * Whether this caller is a spawned member with its own pane. + * + *

Both workers and architects are spawned members. A lead is excluded because recording it + * as present would count it as an available member in the roster. + */ + public boolean isSpawnedMember() { + return role == Role.WORKER || role == Role.ARCHITECT; + } + public boolean isAnonymous() { return role == Role.ANONYMOUS; } diff --git a/bridged/src/main/java/dev/ltms/bridged/mcp/BridgeMcp.java b/bridged/src/main/java/dev/ltms/bridged/mcp/BridgeMcp.java index 7af04b3..ee9046d 100644 --- a/bridged/src/main/java/dev/ltms/bridged/mcp/BridgeMcp.java +++ b/bridged/src/main/java/dev/ltms/bridged/mcp/BridgeMcp.java @@ -109,10 +109,10 @@ public final class BridgeMcp { ? callers.resolve(req.getRemoteAddr(), req.getRemotePort(), req.getHeader("Authorization")) : legacyPrincipal(identity, req.getRemoteAddr(), req.getRemotePort()); - // CB-532: guard on the ROLE, not on the terminal being null. A named lead now - // carries its pane too, and enrolling a lead in the worker presence map would - // have it counted as an available worker. - if (p.isWorker()) presence.markPresent(p.terminal()); + // CB-532: guard on the ROLE, not on the terminal being null. This excludes a + // lead, which carries its pane too, while including every spawned member role. + // Enrolling a lead would count it as an available member in the roster. + markSpawnedMemberPresent(p, presence); return McpTransportContext.create(Map.of( CALLER_TERMINAL, orEmpty(p.terminal()), CALLER_PID, Long.toString(p.pid()), @@ -365,6 +365,13 @@ public final class BridgeMcp { return transport; } + /** Mark a connected spawned member available for the injector readiness gate. */ + static void markSpawnedMemberPresent(Principal caller, MemberPresence presence) { + if (caller.isSpawnedMember()) { + presence.markPresent(caller.terminal()); + } + } + /** Graceful shutdown of the MCP server. */ public void close() { server.closeGracefully(); diff --git a/bridged/src/test/java/dev/ltms/bridged/mcp/BridgeMcpTest.java b/bridged/src/test/java/dev/ltms/bridged/mcp/BridgeMcpTest.java index 94a4737..874e8da 100644 --- a/bridged/src/test/java/dev/ltms/bridged/mcp/BridgeMcpTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/mcp/BridgeMcpTest.java @@ -12,6 +12,7 @@ import dev.ltms.bridged.msg.Rendezvous; import dev.ltms.bridged.session.FakeWorktrees; import dev.ltms.bridged.session.SessionManager; import dev.ltms.bridged.peer.MemberRole; +import dev.ltms.bridged.inject.MemberPresence; import dev.ltms.bridged.session.MemberSession; import dev.ltms.bridged.session.WorktreeRequest; import dev.ltms.bridged.member.ClaudeCodeLauncher; @@ -404,6 +405,16 @@ class BridgeMcpTest { assertDoesNotThrow(() -> messages.ackReply("term_a", msgId)); } + @Test + void anArchitectMcpCallerIsPresent() { + Principal architect = Principal.architect("lead-designer", "term_design", 400); + MemberPresence presence = new MemberPresence(); + + BridgeMcp.markSpawnedMemberPresent(architect, presence); + + assertTrue(presence.isPresent("term_design")); + } + @Test void statusReportsLiveAgentStatus() { FakeHerdr blocked = new FakeHerdr().agentStatus("blocked");