From d98fb38b43234c9a0c77b926b411521731158d43 Mon Sep 17 00:00:00 2001 From: Martin Date: Sat, 24 Oct 2026 18:10:52 +0200 Subject: [PATCH 1/2] fix(ai, history): a Claude session is titled by the name Claude Code gave it (its ai-title line, shown in /resume and the terminal tab) instead of its first prompt, so the widget lists "Kalendora hotfix research" under that name; a /rename still wins --- .../agent-sessions/readers/claude.test.ts | 38 +++++++++++++++++++ src/utils/agent-sessions/readers/claude.ts | 18 +++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/utils/agent-sessions/readers/claude.test.ts b/src/utils/agent-sessions/readers/claude.test.ts index 2ac869489f..05d6d2c53b 100644 --- a/src/utils/agent-sessions/readers/claude.test.ts +++ b/src/utils/agent-sessions/readers/claude.test.ts @@ -305,6 +305,44 @@ test("large metadata reads preserve head and tail fields with a bounded search c expect(result.metadata?.summary).toBe("Latest tail summary"); }); +test("Claude Code's own session name (ai-title) is the title until a /rename, which still wins", async () => { + const { path, source } = fixture(); + const prompt = { + type: "user", + sessionId: MAIN_ID, + timestamp: "2026-09-01T11:00:00.000Z", + message: { content: "pls research whether we are affected" }, + }; + writeFileSync( + path, + jsonl( + prompt, + { type: "ai-title", aiTitle: "Early guess", sessionId: MAIN_ID }, + { type: "ai-title", aiTitle: "Hotfix research", sessionId: MAIN_ID } + ) + ); + + const named = await readClaudeMetadata(source); + + expect(named.metadata?.summary).toBe("Hotfix research"); + expect(named.metadata?.customTitle).toBeNull(); + expect(named.metadata?.firstPrompt).toBe("pls research whether we are affected"); + + writeFileSync( + path, + jsonl( + prompt, + { type: "ai-title", aiTitle: "Hotfix research", sessionId: MAIN_ID }, + { type: "custom-title", customTitle: "My own name", sessionId: MAIN_ID } + ) + ); + + const renamed = await readClaudeMetadata(source); + + expect(renamed.metadata?.customTitle).toBe("My own name"); + expect(renamed.metadata?.summary).toBe("Hotfix research"); +}); + test("compact operations factory binds metadata, scan, and selected-record reads", async () => { const { path, source } = fixture(); writeFileSync( diff --git a/src/utils/agent-sessions/readers/claude.ts b/src/utils/agent-sessions/readers/claude.ts index c42af7c93a..3f465b5f6d 100644 --- a/src/utils/agent-sessions/readers/claude.ts +++ b/src/utils/agent-sessions/readers/claude.ts @@ -218,6 +218,16 @@ function contentEntries(options: { return entries; } +/** + * The name Claude Code gave the session itself: an `ai-title` line (the name its /resume list and the terminal tab + * show), or the `summary` line older versions wrote. Both are stored as the summary, the newest line winning; a + * `/rename` (`custom-title`) still comes first. Without `ai-title` the widget listed "Kalendora hotfix research" under + * its first prompt, and Martin could not find it (2026-10-24). + */ +function autoTitleOf(row: JsonRecord): string { + return row.type === "ai-title" ? asText(row.aiTitle) : row.type === "summary" ? asText(row.summary) : ""; +} + function claudeRecordContext(row: JsonRecord): Pick { const timestamp = asText(row.timestamp) || undefined; const role = @@ -228,7 +238,7 @@ function claudeRecordContext(row: JsonRecord): Pick, "parse export function createClaudeHistoryOperations(): ClaudeHistoryOperations { return { - parserVersion: "5", + parserVersion: "6", readMetadata: readClaudeMetadata, scan: scanClaudeRecords, readRecords: readClaudeRecords, From 0cb096754847c4c1a2bd0a97f86c088d7e74b575 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 26 Oct 2026 01:47:34 +0100 Subject: [PATCH 2/2] fix(ai, history): #588 followup - a session's own Claude Code title is a searchable system record --- .../agent-sessions/readers/claude.test.ts | 54 +++++++++++++++++++ src/utils/agent-sessions/readers/claude.ts | 18 +++++-- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/utils/agent-sessions/readers/claude.test.ts b/src/utils/agent-sessions/readers/claude.test.ts index 05d6d2c53b..9f0f6920db 100644 --- a/src/utils/agent-sessions/readers/claude.test.ts +++ b/src/utils/agent-sessions/readers/claude.test.ts @@ -3,6 +3,8 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { SafeJSON } from "@genesiscz/utils/json"; +import { claudeHistoryReader } from "../compact-readers"; +import { searchHistorySource } from "../search-source"; import type { NativeSessionSource } from "../types"; import { createClaudeHistoryOperations, readClaudeMetadata, readClaudeRecords, scanClaudeRecords } from "./claude"; @@ -343,6 +345,58 @@ test("Claude Code's own session name (ai-title) is the title until a /rename, wh expect(renamed.metadata?.summary).toBe("Hotfix research"); }); +test("ai-title is searchable as a system record when its text is absent from the prompt", async () => { + const { path, source } = fixture(); + const title = { type: "ai-title", aiTitle: "Hotfix research", sessionId: MAIN_ID }; + writeFileSync( + path, + jsonl( + { + type: "user", + sessionId: MAIN_ID, + timestamp: "2026-09-01T11:00:00.000Z", + message: { content: "Investigate the deployment" }, + }, + title + ) + ); + const { metadata } = await readClaudeMetadata(source); + + if (!metadata) { + throw new Error("Fixture metadata missing"); + } + + const result = await searchHistorySource({ + source, + reader: claudeHistoryReader, + metadata: { + ...metadata, + providerId: "claude", + sourceKey: "fixture-source", + parentNativeId: null, + projectDirectory: metadata.projectDirectory ?? null, + lastTimestamp: metadata.lastTimestamp ?? null, + archived: false, + resumeMode: "native", + identityStatus: "resolved", + boundedFields: metadata.boundedFields ?? [], + }, + filters: { query: "Hotfix research" }, + }); + + expect(result?.matchedLocators).toEqual(["jsonl:2"]); + expect(result?.matchedText).toBe("Hotfix research"); + expect(result?.firstPrompt).toBe("Investigate the deployment"); + const selected = await readClaudeRecords(source, { locators: result?.matchedLocators ?? [] }); + expect(selected.complete).toBe(true); + expect(selected.records[0]).toMatchObject({ + role: "system", + metadataChanges: { summary: "Hotfix research" }, + entries: [{ role: "system", text: "Hotfix research", line: 2 }], + original: SafeJSON.stringify(title, { strict: true }), + }); +}); + test("compact operations factory binds metadata, scan, and selected-record reads", async () => { const { path, source } = fixture(); writeFileSync( diff --git a/src/utils/agent-sessions/readers/claude.ts b/src/utils/agent-sessions/readers/claude.ts index 3f465b5f6d..f2075b0738 100644 --- a/src/utils/agent-sessions/readers/claude.ts +++ b/src/utils/agent-sessions/readers/claude.ts @@ -233,7 +233,10 @@ function claudeRecordContext(row: JsonRecord): Pick, "parse export function createClaudeHistoryOperations(): ClaudeHistoryOperations { return { - parserVersion: "6", + parserVersion: "7", readMetadata: readClaudeMetadata, scan: scanClaudeRecords, readRecords: readClaudeRecords,