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:
2026-08-23 00:28:56 +00:00
co-authored by Claude Opus 5
parent 5c3ffa7a06
commit a51a969cd0
4 changed files with 50 additions and 4 deletions
+14
View File
@@ -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));
}
+13 -2
View File
@@ -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.