diff --git a/src/servers/api/chat/chat.ts b/src/servers/api/chat/chat.ts index 77b81878..7a86f154 100644 --- a/src/servers/api/chat/chat.ts +++ b/src/servers/api/chat/chat.ts @@ -101,14 +101,17 @@ chatRouter.get('/sessions/:id', async (ctx) => { // // `pendingTasks` is background work started but not yet notified — with `isGenerating` it is what the // agent's own idle GC consults, so a caller can tell "busy" from "merely open" the same way it does. -// Titles are resolved here rather than in the client: the agent reports session keys and nothing else, -// and the client can only name the sessions in the group it happens to be browsing — which is how the -// list ended up showing raw ids for anything running elsewhere. +// Titles are resolved here rather than in the client, which can only name the sessions in the group it +// happens to be browsing — which is how the list ended up showing raw ids for anything running elsewhere. chatRouter.get('/live', async (ctx) => { const email = ctx.get('user').email; const live = await sidecar.listLiveClaudeSessions(); const sessions = live.map((session) => { - const resolved = isOpenCodeSessionId(session.sessionKey) ? null : liveSessionTitle(email, session.sessionKey); + // Resolve by Claude's id, never by the session key — the key is officer's handle and the transcript + // is named after Claude's. Null until the first turn reports one, which is a conversation that has + // genuinely not been written yet. + const transcriptId = session.claudeSessionId; + const resolved = transcriptId && !isOpenCodeSessionId(transcriptId) ? liveSessionTitle(email, transcriptId) : null; return { ...session, title: resolved?.title ?? null, cwd: resolved?.cwd ?? null }; }); return ctx.json({ sessions }); diff --git a/src/servers/sidecar/claude/claude-manager.ts b/src/servers/sidecar/claude/claude-manager.ts index 15b067a3..3930ca96 100644 --- a/src/servers/sidecar/claude/claude-manager.ts +++ b/src/servers/sidecar/claude/claude-manager.ts @@ -511,6 +511,9 @@ export function clearSession(sessionKey: string): void { export function listSessions(): LiveClaudeSession[] { return Array.from(sessions.values()).map((session) => ({ sessionKey: session.sessionKey, + // The only place this mapping exists. Without it a caller cannot find the transcript, because the + // key is officer's handle and the filename is Claude's id. + claudeSessionId: getClaudeSession(session.sessionKey) ?? null, isGenerating: session.isGenerating, pendingTasks: session.pendingTasks.size, })); diff --git a/src/servers/sidecar/protocol.ts b/src/servers/sidecar/protocol.ts index bc9d7d07..09bf2237 100644 --- a/src/servers/sidecar/protocol.ts +++ b/src/servers/sidecar/protocol.ts @@ -13,6 +13,14 @@ export type SidecarMessage = SidecarCommand | SidecarEvent; */ export type LiveClaudeSession = { sessionKey: string; + /** + * Claude's own session id — the transcript's filename — or null before the first turn reports one. + * + * NOT the same as `sessionKey`, which is officer's handle for the conversation. Only the agent holds + * the mapping (`setClaudeSession`/`getClaudeSession`), so anything that wants to find the transcript + * has to be told it here. Assuming the two were equal made every live row unnameable and unlinkable. + */ + claudeSessionId: string | null; isGenerating: boolean; pendingTasks: number; }; diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/LiveSessions.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/LiveSessions.tsx index d01b5ec7..c74f7f52 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/LiveSessions.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/LiveSessions.tsx @@ -40,16 +40,16 @@ export const LiveSessions = () => { return ( {live.map((session) => { - // No title means no transcript on disk yet — a conversation whose first turn has not landed. It - // is shown but not linked: a row that navigates to a session you cannot open is worse than one - // that plainly isn't a link. - const isDraft = session.title === null; + // The route opens Claude's id, not the agent's session key — they are different things and only + // the agent holds the mapping. No id yet means a conversation whose first turn has not landed: + // shown, but deliberately not a link, since there is nothing to open. + const transcriptId = session.claudeSessionId; return (