a file with no turns in it is not a conversation
Four "(untitled) · 0 msgs" rows accumulated in the sessions list from one evening of commander testing. They are transcripts containing only harness metadata — last-prompt, ai-title, atis-latch — written after a session was deleted or before it ever sent anything. Filtered at the list, not deleted on disk: the files belong to the harness, and a cleanup racing it would be the same class of bug that produced them. Which is also the cause of two of the four. The delete path resolves officer's session key from two in-memory maps, and both answer only for a session younger than the process. After a restart neither does, so no kill is sent, nothing is awaited, and the transcript is removed while the harness is still writing — which writes it back. Same failure as the events cleanup, same durable answer: the event rows carry both ids, so ask them when the maps cannot say. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -999,9 +999,23 @@ function scanGroup(who: ChatIdentity, cwd: string): TranscriptSummary[] {
|
||||
* It is applied by the CALLER, because that requires a database read and this function is synchronous over a
|
||||
* directory scan.
|
||||
*/
|
||||
/**
|
||||
* A transcript with no human or assistant turn in it is not a conversation.
|
||||
*
|
||||
* The harness writes metadata records — `last-prompt`, `ai-title`, `atis-latch` — into a transcript file
|
||||
* independently of the conversation, and it will happily create the file to hold them. So a session that was
|
||||
* deleted, or one that never sent anything, leaves a file containing only those three lines, which listed as
|
||||
* "(untitled) · 0 msgs" and accumulated: four of them from one evening of testing.
|
||||
*
|
||||
* Filtered at the list rather than deleted on disk. The files are the harness's, it is entitled to write
|
||||
* them, and a cleanup that raced it would be the same class of bug as the one that produced them.
|
||||
*/
|
||||
const hasConversation = (s: ClaudeSessionSummary): boolean => s.messageCount > 0;
|
||||
|
||||
export function listClaudeSessions(who: ChatIdentity, cwd: string): ClaudeSessionSummary[] {
|
||||
return mergeChains(scanGroup(who, cwd))
|
||||
.map(publish)
|
||||
.filter(hasConversation)
|
||||
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { checkBudget } from '../sub-agents/budget';
|
||||
import * as errors from '@@/custom-errors';
|
||||
import { resolveHomeDir } from '@@/user-home';
|
||||
import { getCommandersDir, toShellUsername } from '@@/data-path';
|
||||
import { forgetChatSessions, getChatEventsSince, ingestSpawnedChildren } from 'officerdb';
|
||||
import { forgetChatSessions, getChatEventsSince, ingestSpawnedChildren, sessionKeyForTranscriptId } from 'officerdb';
|
||||
import * as sidecar from '@@/sidecar-registry';
|
||||
import { findSessionKeyByClaudeSession } from '@@/sidecar/claude/state';
|
||||
import { sessionKeyForTranscript } from '../chat/websocket';
|
||||
@@ -559,7 +559,18 @@ commandHqRouter.delete('/commanders/:sessionId', async (ctx) => {
|
||||
*/
|
||||
// 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);
|
||||
/*
|
||||
Third fallback, and the only one that survives a restart.
|
||||
|
||||
Both maps above are in-memory and answer only for a session younger than this process. After a
|
||||
restart they both return undefined, no kill is sent, and the transcript is deleted while the harness
|
||||
is still writing — so it writes it back and the deleted commander returns as an untitled stub. Seen
|
||||
twice tonight, after the fix that was meant to prevent exactly this.
|
||||
*/
|
||||
const sessionKey =
|
||||
sessionKeyForTranscript(sessionId) ??
|
||||
findSessionKeyByClaudeSession(sessionId, user.id) ??
|
||||
(await sessionKeyForTranscriptId(sessionId).catch(() => undefined));
|
||||
/*
|
||||
Across the socket, and AWAITED — both halves matter.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user