the platform keeps its own transcript-id mapping, because the sidecar's is invisible to it
The commander block still was not arriving. Not the delivery mechanism this time — the LOOKUP. Building the block needs the transcript id, the platform had only officer's session key, and it asked the sidecar's `state.ts` for the mapping. That map lives in the sidecar's memory; the platform process has its own empty copy of the module, and re-reading the state file from here does not find a live session either. Verified both ways before changing anything. So the platform keeps its own map, filled from the `session:claude` message it already handles — the one moment the mapping is announced. It is not derivable from anything: it is stated once, mid-turn, and whoever needs it either records it as it passes or does without. The delete route had the identical problem for the same reason and now asks the platform's map first, keeping the sidecar lookup as a fallback for a session that predates this process. Third distinct bug today from these two ids, and they are all the same shape: a lookup keyed on one id space against data held in the other, failing silently because both are UUIDs and neither side can tell. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,6 @@ import { resolveHomeDir } from '@@/user-home';
|
||||
import { renameClaudeSession, type ChatIdentity } from './claude-sessions';
|
||||
import { chatIdentity } from './chat';
|
||||
import { commanderBlock } from '../command-hq/commander-block';
|
||||
import { getClaudeSession } from '../../sidecar/claude/state';
|
||||
import { claudeLoginState } from '@@/os-user-claude';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { logger } from './logger';
|
||||
@@ -561,6 +560,8 @@ function createMessageHandler(
|
||||
// written on its very first turn rather than on its second.
|
||||
if (msg.type === 'session:claude' && msg.claudeSessionId) {
|
||||
transcriptId = msg.claudeSessionId;
|
||||
// The platform's own copy of the mapping — see `transcriptIdBySessionKey`.
|
||||
transcriptIdBySessionKey.set(sessionId, msg.claudeSessionId);
|
||||
record();
|
||||
fileAsCommander();
|
||||
}
|
||||
@@ -904,6 +905,24 @@ export type ClaudeTurnOutcome = { ok: true; transcriptId: string } | { ok: false
|
||||
* generic timeout: the reasons below (member not signed in, Claude not installed, spawn failure) are the
|
||||
* same reasons a websocket caller already sees via `sendToClient`, just mirrored to a second listener.
|
||||
*/
|
||||
/**
|
||||
* Officer's session key → Claude's transcript id, for the platform's own use.
|
||||
*
|
||||
* The sidecar keeps this mapping and the platform cannot read it: `state.ts` holds it in the sidecar's
|
||||
* memory, and re-loading its file from here does not find a live session. So the platform keeps its own,
|
||||
* filled from the `session:claude` message it already handles.
|
||||
*
|
||||
* This is the third distinct place the two ids have caused a bug today. The mapping is not derivable — it is
|
||||
* announced, once, mid-turn — so anything needing it either records it when it passes or does without.
|
||||
*/
|
||||
const transcriptIdBySessionKey = new Map<string, string>();
|
||||
|
||||
/** The mapping read backwards, for callers holding a transcript id — the delete route, mainly. */
|
||||
export function sessionKeyForTranscript(transcriptId: string): string | undefined {
|
||||
for (const [key, id] of transcriptIdBySessionKey) if (id === transcriptId) return key;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sessions already told what they are and where to record it.
|
||||
*
|
||||
@@ -960,7 +979,7 @@ export async function startClaudeTurn(params: {
|
||||
if (!briefedSessions.has(sessionId)) {
|
||||
const block = await commanderBlock({
|
||||
userId,
|
||||
sessionId: getClaudeSession(sessionId, userId) ?? msg.resumeSessionId ?? null,
|
||||
sessionId: transcriptIdBySessionKey.get(sessionId) ?? msg.resumeSessionId ?? null,
|
||||
});
|
||||
if (block) {
|
||||
briefedSessions.add(sessionId);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getCommandersDir, toShellUsername } from '@@/data-path';
|
||||
import { forgetChatSessions, getChatEventsSince, ingestSpawnedChildren } from 'officerdb';
|
||||
import { killClaudeSession } from '@@/sidecar/claude/claude-manager';
|
||||
import { findSessionKeyByClaudeSession } from '@@/sidecar/claude/state';
|
||||
import { sessionKeyForTranscript } from '../chat/websocket';
|
||||
import {
|
||||
clearSpawnedRecord,
|
||||
listSpawnedSessions,
|
||||
@@ -556,7 +557,9 @@ commandHqRouter.delete('/commanders/:sessionId', async (ctx) => {
|
||||
holds only the uuid. Ending the session takes its children with it, and the pid-based stop below then
|
||||
catches anything that outlived its parent.
|
||||
*/
|
||||
const sessionKey = findSessionKeyByClaudeSession(sessionId, user.id);
|
||||
// Same mapping problem as the briefing: the sidecar's map is not visible from here, so the platform's own
|
||||
// is asked first and the sidecar's is kept as a fallback for a session that predates this process.
|
||||
const sessionKey = sessionKeyForTranscript(sessionId) ?? findSessionKeyByClaudeSession(sessionId, user.id);
|
||||
const sessionKilled = sessionKey ? killClaudeSession(sessionKey, user.id) : false;
|
||||
const stopped = stopCommanderProcesses(who.email, sessionId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user