From cc1df11f69ba01bcacffb78fa4e4744a11e1114c Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sun, 16 Aug 2026 18:25:24 +0200 Subject: [PATCH] CB-584: carry agentSessionId on a failed ticket's ReleaseDetail Closes the one piece issue #65 deliberately left out of 5d5b3bd: a released member's agentSessionId now rides alongside worktree, branch and snapshotRef on ReleaseDetail, and Bridged's onRelease handler names it in the abandon reason, so a lead can resume the member's conversation instead of only re-dispatching a fresh one onto the same files. Also updates the CLAUDE.md bridge_spawn/bridge_list table row, which 5d5b3bd shipped the sessionName/resumeSessionId/agentSessionId surface for but never updated. --- CLAUDE.md | 4 ++-- .../main/java/dev/ltms/bridged/Bridged.java | 5 +++++ .../ltms/bridged/session/SessionManager.java | 7 ++++-- .../session/WorktreeSessionManagerTest.java | 22 +++++++++++++++++++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 69f77b4..6ea95c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,8 +105,8 @@ the merge — and merging on a reviewer's word is delegating it by proxy. |---|---| | Confirm your own role | `bridge_whoami` | | See backends available | `bridge_profiles` | -| Start a member | `bridge_spawn{role?, profile?, cwd?, worktree?, ticket?}` → `sessionId` + `paneId` | -| See the fleet | `bridge_list` → `leads` (your peers) + `members` · one peer's state: `bridge_status{sessionId}` | +| Start a member | `bridge_spawn{role?, profile?, cwd?, worktree?, ticket?, sessionName?, resumeSessionId?}` → `sessionId` + `paneId` | +| See the fleet | `bridge_list` → `leads` (your peers) + `members` (each carries `agentSessionId` when its backend knows one) · one peer's state: `bridge_status{sessionId}` | | Delegate (blocking) | `bridge_send{sessionId, content}` | | Delegate (long task) | `bridge_send{sessionId, content, wait:false}` → ticket → `bridge_poll{ticket}` | | Answer a member's `bridge_ask` | `bridge_send{turnId, content}` — **not** `sessionId` | diff --git a/bridged/src/main/java/dev/ltms/bridged/Bridged.java b/bridged/src/main/java/dev/ltms/bridged/Bridged.java index c65eac1..3543eb8 100644 --- a/bridged/src/main/java/dev/ltms/bridged/Bridged.java +++ b/bridged/src/main/java/dev/ltms/bridged/Bridged.java @@ -441,6 +441,11 @@ public final class Bridged { reason += "; worktree=" + detail.worktreePath() + " branch=" + detail.branch() + " snapshot=" + (detail.snapshotRef() != null ? detail.snapshotRef() : "none"); } + // CB-584 (issue #65 criterion 5): also name the agent session, so a lead can resume the + // member's conversation instead of only re-dispatching a fresh one onto the same files. + if (detail.agentSessionId() != null) { + reason += " agentSessionId=" + detail.agentSessionId(); + } messages.abandon(detail.terminalId(), reason); replyInbox.release(detail.terminalId()); primaryRegistry.forgetDelegation(detail.terminalId()); // CB-532: don't leak the lead binding diff --git a/bridged/src/main/java/dev/ltms/bridged/session/SessionManager.java b/bridged/src/main/java/dev/ltms/bridged/session/SessionManager.java index 2b417ed..60012bd 100644 --- a/bridged/src/main/java/dev/ltms/bridged/session/SessionManager.java +++ b/bridged/src/main/java/dev/ltms/bridged/session/SessionManager.java @@ -293,8 +293,10 @@ public final class SessionManager implements TurnListener { // blocked caller fails fast with a real reason instead of sitting on a rendezvous // nothing will ever resolve. CB-578 stage C: carry the worktree/branch/snapshot ref // too, so a failed ticket's detail can point a lead at the same tree to re-dispatch. + // CB-584 (issue #65 criterion 5): carry agentSessionId alongside them, so a lead can + // also resume the member's conversation, not just re-dispatch onto its files. notifyReleased(new ReleaseDetail(removed.terminalId(), removed.worktree(), - removed.branch(), snapshotRef)); + removed.branch(), snapshotRef, removed.agentSessionId())); } } // CB-581: the pane must always stop, even if the dirty check above threw. A session removed @@ -348,7 +350,8 @@ public final class SessionManager implements TurnListener { * are {@code null} for a shared-tree session; {@code snapshotRef} is {@code null} unless this * release snapshotted a dirty worktree into {@code refs/wip/}. */ - public record ReleaseDetail(String terminalId, String worktreePath, String branch, String snapshotRef) { + public record ReleaseDetail(String terminalId, String worktreePath, String branch, String snapshotRef, + String agentSessionId) { } /** diff --git a/bridged/src/test/java/dev/ltms/bridged/session/WorktreeSessionManagerTest.java b/bridged/src/test/java/dev/ltms/bridged/session/WorktreeSessionManagerTest.java index e3c008d..aa1cf28 100644 --- a/bridged/src/test/java/dev/ltms/bridged/session/WorktreeSessionManagerTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/session/WorktreeSessionManagerTest.java @@ -422,6 +422,28 @@ class WorktreeSessionManagerTest { "acceptance criterion 6: a failed ticket's detail must carry the snapshot ref"); } + @Test + void releaseNotifiesTheListenerWithTheAgentSessionId() { + // CB-584 (issue #65 criterion 5): a failed ticket's detail must also name the agent + // session, alongside worktree/branch/snapshot, so a lead can resume the conversation + // rather than only re-dispatch a fresh member onto the same files. + FakeHerdr herdr = new FakeHerdr(); + FakeWorktrees worktrees = new FakeWorktrees().withRepoRoot("/repo").withPrefix("/wt") + .withDirty(true); + SessionManager sessions = new SessionManager(workerService(herdr), worktrees); + java.util.List released = new java.util.concurrent.CopyOnWriteArrayList<>(); + sessions.onRelease(released::add); + MemberSession s = sessions.acquire("ltms-local", MemberRole.DEV, null, "/caller/proj", null, + new WorktreeRequest("cb-584-e", null), "cb-584-session", null); + + assertNotNull(s.agentSessionId(), "a named session must mint an agent session id to assert on"); + sessions.release(s.paneId()); + + assertEquals(1, released.size()); + SessionManager.ReleaseDetail detail = released.getFirst(); + assertEquals(s.agentSessionId(), detail.agentSessionId()); + } + @Test void aFailingSnapshotStillPreservesTheWorktreeStopsThePaneAndNotifies() { FakeHerdr herdr = new FakeHerdr();