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));
}