diff --git a/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java b/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java
index 0e55579..d37773e 100644
--- a/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java
+++ b/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java
@@ -278,18 +278,22 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher {
/**
* Add the Claude-specific session-identity flags to {@code argv} and return the peer's OWN
- * session id — the resume handle. A resume request passes the prior id via {@code -r} and
- * returns that id; a fresh named session mints a new UUID, passes it via {@code --session-id},
- * and returns the mint. The bridge's logical name rides along as {@code -n} when present. When
- * no identity is requested (sessionName and resumeSessionId both blank) this adds
- * nothing and returns {@code null}, keeping the legacy no-identity launch byte-identical.
+ * session id — the resume handle. A resume passes the prior id via {@code -r} and returns
+ * that id; every other spawn mints a new UUID, passes it via {@code --session-id}, and
+ * returns the mint. The bridge's logical name rides along as {@code -n} when present.
+ *
+ *
fleetd #214: the mint is unconditional. A plain {@code fleet_spawn} passes neither
+ * sessionName nor resumeSessionId, yet the member must still be resumable, and this id is
+ * the only resume handle a claude-code member has — unlike opencode, nothing resolves it
+ * after the launch. Checked against the real binary (claude 2.1.252): the flag is safe on
+ * every spawn. The binary takes only a valid UUID — it refuses any other value at argument
+ * parsing ("Invalid session ID. Must be a valid UUID.") — so the {@code UUID.randomUUID()}
+ * mint is required, not incidental. The only flag interaction the binary documents is with
+ * {@code -r} (both claim the session id), and the resume branch above never combines the two.
*/
private static String applySessionIdentity(List argv, String sessionName, String resumeSessionId) {
boolean resuming = resumeSessionId != null && !resumeSessionId.isBlank();
boolean named = sessionName != null && !sessionName.isBlank();
- if (!resuming && !named) {
- return null; // no identity requested — keep the legacy launch byte-identical
- }
if (named) {
argv.add("-n");
argv.add(sessionName);
diff --git a/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java b/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java
index 6b6be77..0fdeefd 100644
--- a/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java
+++ b/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java
@@ -262,10 +262,16 @@ class ClaudeCodeLauncherTest {
Map, ?> start = (Map, ?>) herdr.lastCall("agent.start").params();
assertEquals("claude", start.get("kind"), "herdr launches the canonical executable by kind");
- // CB-533: the shared fixture pins model "coder", so the model flag is the whole args list.
// What this test guards is that argv[0] is NOT repeated — herdr supplies it from `kind`.
- assertEquals(List.of("--model", "coder"), start.get("args"),
- "the configured executable is not repeated in args");
+ // fleetd #214: a plain spawn now also carries fleetd's own minted session id.
+ List args = spawnedArgs(herdr);
+ assertFalse(args.contains("claude"), "the configured executable is not repeated in args");
+ int flag = args.indexOf("--session-id");
+ assertTrue(flag >= 0, "a plain spawn mints a session id: " + args);
+ assertDoesNotThrow(() -> UUID.fromString(args.get(flag + 1)), "the minted id is a valid UUID");
+ assertEquals(List.of("--model", "coder"),
+ List.of(args.get(args.size() - 2), args.get(args.size() - 1)),
+ "the CB-533 model flag still trails the launch flags: " + args);
}
@Test
@@ -278,7 +284,15 @@ class ClaudeCodeLauncherTest {
assertFalse(args.contains("--append-system-prompt"), "no reply charter without mcpUrl");
// CB-533: the model flag is independent of the MCP mount — pinning the model is not part of
// "mount the bridge", so an unmounted worker still runs the model its profile names.
- assertEquals(List.of("--verbose", "--model", "coder"), args,
+ // fleetd #214: a plain spawn now carries fleetd's own minted session id; drop the two
+ // mint elements when checking the rest of the argv.
+ int flag = args.indexOf("--session-id");
+ assertTrue(flag >= 0, "a plain spawn mints a session id even without a bridge mount: " + args);
+ assertDoesNotThrow(() -> UUID.fromString(args.get(flag + 1)), "the minted id is a valid UUID");
+ List rest = new java.util.ArrayList<>(args);
+ rest.remove(flag + 1);
+ rest.remove(flag);
+ assertEquals(List.of("--verbose", "--model", "coder"), rest,
"the operator's own args are preserved, in order, ahead of the model flag");
}
@@ -649,7 +663,7 @@ class ClaudeCodeLauncherTest {
assertEquals("/work/proj", cwd, "effectiveCwd via SpawnRequest must match the three-arg resolution");
}
- // --- CB-547a: durable session identity (mint / resume / no-identity legacy) -----------------
+ // --- CB-547a / fleetd #214: durable session identity (always mint / resume) -----------------
@Test
void freshSpawnMintsASessionIdAndPassesTheName() {
@@ -684,18 +698,28 @@ class ClaudeCodeLauncherTest {
}
@Test
- void noIdentitySpawnKeepsTheLegacyArgvAndCarriesNoSessionHandle() {
+ void plainSpawnMintsASessionIdSoEveryMemberIsResumable() {
+ // fleetd #214: a plain spawn passes no sessionName and no resumeSessionId, yet the member
+ // must still be resumable — the id is minted unconditionally, and it is the ONLY resume
+ // handle a claude-code member has (unlike opencode, nothing resolves it after the launch).
+ // The binary requires a valid UUID (checked against claude 2.1.252: a non-UUID is refused
+ // at argument parsing with "Invalid session ID. Must be a valid UUID.").
FakeHerdr herdr = new FakeHerdr();
ClaudeCodeLauncher svc = service(herdr, List.of("ccs", "ltms-local"), null);
PeerHandle handle = svc.spawn(new SpawnRequest("ltms-local", null, null));
List args = spawnedArgs(herdr);
- assertFalse(args.contains("--session-id"), "no identity → no --session-id");
- assertFalse(args.contains("-n"), "no identity → no -n");
- assertFalse(args.contains("-r"), "no identity → no -r");
- assertNull(handle.agentSessionId(), "no identity → no resume handle");
- assertNull(handle.sessionName(), "no identity → no logical name");
+ int flag = args.indexOf("--session-id");
+ assertTrue(flag >= 0 && flag + 1 < args.size(),
+ "--session-id is minted even when no identity is requested: " + args);
+ String minted = args.get(flag + 1);
+ assertDoesNotThrow(() -> UUID.fromString(minted), "--session-id is a valid UUID: " + minted);
+ assertEquals(minted, handle.agentSessionId(),
+ "the resume handle is the minted id, so every member is resumable from fleet_list");
+ assertFalse(args.contains("-n"), "no sessionName was requested → no -n");
+ assertFalse(args.contains("-r"), "no resume was requested → no -r");
+ assertNull(handle.sessionName(), "no sessionName was requested → no logical name");
}
@Test
diff --git a/fleetd/src/test/java/dev/ltms/fleet/session/SessionManagerTest.java b/fleetd/src/test/java/dev/ltms/fleet/session/SessionManagerTest.java
index 9fc0d37..9c3a56c 100644
--- a/fleetd/src/test/java/dev/ltms/fleet/session/SessionManagerTest.java
+++ b/fleetd/src/test/java/dev/ltms/fleet/session/SessionManagerTest.java
@@ -887,15 +887,18 @@ class SessionManagerTest {
}
@Test
- void acquireWithNeitherSessionFieldLeavesAgentSessionIdNull() {
+ void acquireWithNeitherSessionFieldStillMintsAnAgentSessionId() {
+ // fleetd #214: the claude-code launcher mints a session id for EVERY spawn, so a member is
+ // resumable even when the spawn asked for no session identity.
FakeHerdr herdr = new FakeHerdr();
SessionManager sessions = sessionManager(herdr);
MemberSession s = sessions.acquire("ltms-local", null, null, null);
- assertNull(s.agentSessionId(), "no identity requested — unchanged from before CB-584");
- assertFalse(SessionManager.rosterView(s, null).containsKey("agentSessionId"),
- "a null id is omitted from the roster, like charterSha256 for a receipt-less session");
+ assertNotNull(s.agentSessionId(),
+ "fleetd #214: a plain spawn mints a session id, so every member is resumable");
+ assertTrue(SessionManager.rosterView(s, null).containsKey("agentSessionId"),
+ "the minted id is in the roster, so fleet_list advertises every member's resume handle");
}
@Test