@threadbase-sh/scanner 0.14.6 → 0.14.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/filters.ts","../src/git.ts","../src/logger.ts","../src/indexer.ts","../src/search-matches.ts","../src/parser.ts","../src/persistent/metadata-reducer.ts","../src/providers/provider.ts","../src/tags.ts","../src/persistent/conversation-reducer.ts","../src/persistent/sidecar.ts","../src/profiles.ts","../src/providers/codex-cli.ts","../src/canonical-path.ts","../src/discovery.ts","../src/providers/threadbase.ts","../src/scanner.ts","../src/cache.ts","../src/persistent/conversation-stream.ts","../src/persistent/paged-reader.ts","../src/persistent/jsonl-tail-reader.ts","../src/persistent/cursor.ts","../src/persistent/index-engine.ts","../src/providers/parse.ts","../src/search-document.ts","../src/tiers.ts","../src/persistent/db.ts","../src/persistent/schema.ts","../src/persistent/migrations.ts","../src/persistent/dir-watermark.ts","../src/persistent/repositories/checkpoints.repo.ts","../src/persistent/repositories/conversation-files.repo.ts","../src/persistent/repositories/conversations.repo.ts","../src/persistent/repositories/fts.repo.ts","../src/persistent/repositories/scanned-dirs.repo.ts","../src/watcher/file-watcher.ts","../src/watcher/index-queue.ts","../src/types.ts","../src/index.ts"],"sourcesContent":["import type { ConversationMeta, Include, SortOrder } from \"./types\";\n\nexport function applySort(metas: ConversationMeta[], order: SortOrder): ConversationMeta[] {\n const out = [...metas];\n // Tie-break by id (the file path) so the order is fully deterministic and does\n // not depend on input order. This matters because metas can arrive in\n // different orders (filesystem discovery vs. an SQLite query), and a tie on\n // the primary key would otherwise resolve differently between them.\n const tie = (a: ConversationMeta, b: ConversationMeta) => a.id.localeCompare(b.id);\n switch (order) {\n case \"recent\":\n out.sort((a, b) => b.timestamp.localeCompare(a.timestamp) || tie(a, b));\n break;\n case \"oldest\":\n out.sort((a, b) => a.timestamp.localeCompare(b.timestamp) || tie(a, b));\n break;\n case \"messages-desc\":\n out.sort((a, b) => b.messageCount - a.messageCount || tie(a, b));\n break;\n case \"messages-asc\":\n out.sort((a, b) => a.messageCount - b.messageCount || tie(a, b));\n break;\n case \"alpha\":\n out.sort((a, b) => {\n const cmp = a.projectName.localeCompare(b.projectName);\n if (cmp !== 0) return cmp;\n return a.preview.localeCompare(b.preview) || tie(a, b);\n });\n break;\n }\n return out;\n}\n\nexport function applySinceFilter(metas: ConversationMeta[], since: string): ConversationMeta[] {\n const cutoff = parseSinceCutoff(since);\n return metas.filter((m) => new Date(m.timestamp).getTime() >= cutoff.getTime());\n}\n\nexport function applyIncludeFilter(\n metas: ConversationMeta[],\n include: Include,\n): ConversationMeta[] {\n switch (include) {\n case \"all\":\n return metas;\n case \"conversations\":\n return metas.filter((m) => !m.isSubagent && !m.isTeammate);\n case \"subagents\":\n return metas.filter((m) => m.isSubagent);\n case \"teammates\":\n return metas.filter((m) => m.isTeammate);\n }\n}\n\nexport function applyProjectFilter(metas: ConversationMeta[], project: string): ConversationMeta[] {\n const lower = project.toLowerCase();\n return metas.filter(\n (m) =>\n m.projectPath.toLowerCase().includes(lower) || m.projectName.toLowerCase().includes(lower),\n );\n}\n\nexport function applyAccountFilter(metas: ConversationMeta[], account: string): ConversationMeta[] {\n return metas.filter((m) => m.account === account);\n}\n\nexport function applyPagination<T>(\n items: T[],\n limit: number,\n offset: number,\n): { items: T[]; total: number } {\n return {\n items: items.slice(offset, offset + limit),\n total: items.length,\n };\n}\n\nexport function parseSinceCutoff(value: string): Date {\n const s = value.trim();\n if (!s) throw new Error(\"Empty --since value\");\n\n // Try ISO date\n const isoMatch = s.match(/^\\d{4}-\\d{2}-\\d{2}$/);\n if (isoMatch) {\n const d = new Date(`${s}T00:00:00Z`);\n if (!Number.isNaN(d.getTime())) return d;\n }\n\n // Try duration: digits + unit\n const durationMatch = s.match(/^(\\d+)([hdw])$/);\n if (!durationMatch) {\n throw new Error(\n `Invalid --since value \"${s}\": expected duration like \"7d\", \"24h\", \"2w\" or ISO date \"2006-01-02\"`,\n );\n }\n\n const n = parseInt(durationMatch[1], 10);\n const unit = durationMatch[2];\n let ms: number;\n switch (unit) {\n case \"h\":\n ms = n * 60 * 60 * 1000;\n break;\n case \"d\":\n ms = n * 24 * 60 * 60 * 1000;\n break;\n case \"w\":\n ms = n * 7 * 24 * 60 * 60 * 1000;\n break;\n default:\n throw new Error(`Invalid unit \"${unit}\"`);\n }\n\n return new Date(Date.now() - ms);\n}\n","import { readFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport { getLogger } from \"./logger\";\n\nconst REF_PREFIX = \"ref: refs/heads/\";\nconst MAX_DEPTH = 6;\n\nexport function readGitBranch(projectPath: string): string | null {\n if (!projectPath) return null;\n const log = getLogger();\n\n let dir = projectPath;\n let depth = 0;\n\n while (depth < MAX_DEPTH) {\n const headPath = join(dir, \".git\", \"HEAD\");\n try {\n const content = readFileSync(headPath, \"utf-8\").trim();\n if (content.startsWith(REF_PREFIX)) {\n const branch = content.slice(REF_PREFIX.length);\n log.trace({ projectPath, dir, branch }, \"git: branch resolved\");\n return branch;\n }\n // Detached HEAD: raw commit SHA\n if (content.length >= 7) {\n log.trace({ projectPath, dir }, \"git: detached HEAD\");\n return \"(detached)\";\n }\n return null;\n } catch {\n // .git/HEAD not found at this level\n }\n\n const parent = dirname(dir);\n if (parent === dir) return null; // filesystem root\n dir = parent;\n depth++;\n }\n\n log.trace({ projectPath }, \"git: no .git found within depth\");\n return null;\n}\n","import pino, { type Logger, type LoggerOptions } from \"pino\";\n\nlet currentLogger: Logger = pino({ level: \"silent\" });\n\nexport function createLogger(options?: LoggerOptions | Logger): Logger {\n if (options && typeof (options as Logger).child === \"function\") {\n return options as Logger;\n }\n return pino((options as LoggerOptions) ?? { level: \"silent\" });\n}\n\nexport function setLogger(logger: Logger): void {\n currentLogger = logger;\n}\n\nexport function getLogger(): Logger {\n return currentLogger;\n}\n\nexport type { Logger, LoggerOptions };\n","import FlexSearchModule from \"flexsearch\";\n\n// FlexSearch has inconsistent default export across ESM/CJS\nconst FlexSearch = (FlexSearchModule as any).default ?? FlexSearchModule;\n\nimport { getLogger } from \"./logger\";\nimport { buildContentMatch, generateMatches } from \"./search-matches\";\nimport type { ConversationMeta, SearchMatch, SearchResult } from \"./types\";\n\nexport class SearchIndexer {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private index: any;\n private documents = new Map<string, ConversationMeta>();\n // The indexed body per conversation, kept so a hit can produce a real excerpt\n // instead of falling back to an unrelated preview. Sized by the caller (the\n // scanner tail-caps it to the content tier) — see the note on addDocument.\n private searchContents = new Map<string, string>();\n\n constructor() {\n this.index = this.createIndex();\n }\n\n private createIndex() {\n return new (FlexSearch as any).Document({\n document: {\n id: \"id\",\n index: [\n \"content\",\n \"projectName\",\n \"projectPath\",\n \"sessionId\",\n \"sessionName\",\n \"account\",\n \"model\",\n \"gitBranch\",\n \"toolNames\",\n ],\n store: [\"id\"],\n },\n tokenize: \"forward\",\n resolution: 9,\n cache: 100,\n });\n }\n\n // `searchContent` is the combined search document (text + thinking + tools),\n // NOT meta.contentSnippet.\n //\n // It arrives pre-capped. This index is `tokenize: \"forward\"` at resolution 9,\n // which stores every prefix of every token, so it is a resident-memory\n // structure whose cost is very different from the on-disk FTS index. Feeding\n // it the full ~128 KB budget across a few hundred conversations would be a\n // multi-hundred-MB-to-GB index. The scanner therefore caps this to the active\n // tier's snippetMax and accepts that `persistent: false` has lower recall than\n // SQLite — an honest, documented divergence rather than a silent one.\n addDocument(meta: ConversationMeta, searchContent = \"\"): void {\n this.documents.set(meta.id, meta);\n this.searchContents.set(meta.id, searchContent);\n this.index.add(toIndexDoc(meta, searchContent));\n }\n\n buildIndex(metas: ConversationMeta[], searchContents?: Map<string, string>): void {\n this.clear();\n for (const meta of metas) {\n this.addDocument(meta, searchContents?.get(meta.id) ?? \"\");\n }\n getLogger().debug({ docCount: metas.length }, \"indexer: built\");\n }\n\n search(query: string, options?: { fields?: string[]; limit?: number }): SearchResult[] {\n const limit = options?.limit ?? 50;\n\n if (!query.trim()) {\n return this.getRecent(limit);\n }\n\n const results = this.index.search(query, { limit: limit * 2, enrich: true });\n\n const seen = new Set<string>();\n const searchResults: SearchResult[] = [];\n\n for (const fieldResult of results) {\n if (!fieldResult.result) continue;\n for (const item of fieldResult.result) {\n const id = typeof item === \"object\" ? (item as { id: string }).id : String(item);\n if (seen.has(id)) continue;\n seen.add(id);\n\n const meta = this.documents.get(id);\n if (!meta) continue;\n\n searchResults.push({\n meta,\n score: 1,\n matches: this.matchesFor(meta, query),\n });\n if (searchResults.length >= limit) break;\n }\n if (searchResults.length >= limit) break;\n }\n\n return searchResults;\n }\n\n // Body context first (that is what explains why the result appeared), then any\n // metadata matches. Only when neither hits does generateMatches' preview\n // fallback stand in.\n private matchesFor(meta: ConversationMeta, query: string): SearchMatch[] {\n const contentMatch = buildContentMatch(this.searchContents.get(meta.id) ?? \"\", query);\n const metaMatches = generateMatches(meta, query);\n if (!contentMatch) return metaMatches;\n return [contentMatch, ...metaMatches.filter((m) => m.field !== \"preview\")];\n }\n\n private getRecent(limit: number): SearchResult[] {\n return Array.from(this.documents.values())\n .sort((a, b) => b.timestamp.localeCompare(a.timestamp))\n .slice(0, limit)\n .map((meta) => ({\n meta,\n score: 1,\n matches: [{ field: \"timestamp\", snippet: meta.preview }],\n }));\n }\n\n getDocumentCount(): number {\n return this.documents.size;\n }\n\n // Replace an already-indexed document in place. FlexSearch's `add` does not\n // overwrite an existing id, so a single-file refresh must go through\n // `update` to avoid stale matches lingering in the index.\n updateDocument(meta: ConversationMeta, searchContent = \"\"): void {\n this.documents.set(meta.id, meta);\n this.searchContents.set(meta.id, searchContent);\n this.index.update(toIndexDoc(meta, searchContent));\n }\n\n removeDocument(id: string): void {\n this.documents.delete(id);\n this.searchContents.delete(id);\n this.index.remove(id);\n }\n\n clear(): void {\n this.documents.clear();\n this.searchContents.clear();\n this.index = this.createIndex();\n getLogger().trace(\"indexer: cleared\");\n }\n}\n\nfunction toIndexDoc(meta: ConversationMeta, searchContent: string) {\n return {\n id: meta.id,\n content: searchContent,\n projectName: meta.projectName,\n projectPath: meta.projectPath,\n sessionId: meta.sessionId,\n sessionName: meta.sessionName,\n account: meta.account,\n model: meta.model || \"\",\n gitBranch: meta.gitBranch || \"\",\n toolNames: meta.toolNames.join(\" \"),\n };\n}\n","import type { ConversationMeta, SearchHighlight, SearchMatch } from \"./types\";\n\n// Delimiters handed to FTS5 snippet() to mark matched tokens, stripped again\n// before the excerpt reaches a caller. Control characters (STX/ETX) rather than\n// a textual sentinel like [[HIT]]: a transcript can legitimately contain wiki\n// link syntax, and a collision would silently shift every highlight offset.\nexport const FTS_HIT_OPEN = \"\\u0002\";\nexport const FTS_HIT_CLOSE = \"\\u0003\";\n\n// Tokens of context FTS5 puts around a hit. Clients decide final layout.\nexport const FTS_SNIPPET_TOKENS = 16;\n\nexport const FTS_ELLIPSIS = \"…\";\n\n// Field name used for any conversation-body hit, whichever body column matched.\n// Column identity (text/thinking/tools) is an indexing detail; consumers only\n// need to distinguish \"body\" from \"metadata\".\nexport const CONTENT_FIELD = \"content\";\n\n// Convert one raw FTS5 snippet() string into plain text plus highlight ranges.\n// Returns null when the column carried no marked hit — snippet() returns the\n// head of a column even when the match was in a different column, so marker\n// presence is the only reliable \"did THIS column match\" signal.\nexport function parseFtsSnippet(\n raw: string | null | undefined,\n): { snippet: string; highlights: SearchHighlight[] } | null {\n if (!raw?.includes(FTS_HIT_OPEN)) return null;\n\n const highlights: SearchHighlight[] = [];\n let snippet = \"\";\n let openAt = -1;\n\n for (const ch of raw) {\n if (ch === FTS_HIT_OPEN) {\n openAt = snippet.length;\n continue;\n }\n if (ch === FTS_HIT_CLOSE) {\n if (openAt >= 0 && snippet.length > openAt) {\n highlights.push({ start: openAt, end: snippet.length });\n }\n openAt = -1;\n continue;\n }\n snippet += ch;\n }\n\n return highlights.length > 0 ? { snippet, highlights } : null;\n}\n\n// Per-field match snippets for a METADATA hit. Body context no longer comes from\n// here: it comes from FTS5 snippet() (persistent) or buildContentMatch()\n// (in-memory), because meta.contentSnippet is a 5 KB head-biased preview, not\n// the search corpus.\n//\n// Field ids are camelCase, matching ConversationMeta — never the snake_case FTS\n// column names. Consumers switch on matches[].field, so the two vocabularies\n// must not mix. Order follows the documented precedence.\nexport function generateMatches(meta: ConversationMeta, query: string): SearchMatch[] {\n const matches: SearchMatch[] = [];\n const lowerQuery = query.toLowerCase();\n\n const fields: [string, string][] = [\n [\"sessionName\", meta.sessionName],\n [\"projectName\", meta.projectName],\n [\"gitBranch\", meta.gitBranch || \"\"],\n [\"toolNames\", meta.toolNames.join(\" \")],\n [\"model\", meta.model || \"\"],\n [\"account\", meta.account],\n [\"sessionId\", meta.sessionId],\n ];\n\n for (const [field, value] of fields) {\n const idx = value.toLowerCase().indexOf(lowerQuery);\n if (idx !== -1) {\n const start = Math.max(0, idx - 80);\n const end = Math.min(value.length, idx + query.length + 120);\n let snippet = value.slice(start, end);\n if (start > 0) snippet = `...${snippet}`;\n if (end < value.length) snippet = `${snippet}...`;\n matches.push({ field, snippet });\n }\n }\n\n return matches.length > 0 ? matches : [{ field: \"preview\", snippet: meta.preview }];\n}\n\n// Body context for the in-memory path, which has no FTS5 snippet(). Locates the\n// query literally in the combined search document and returns the same\n// {field, snippet, highlights} shape the persistent path produces.\nexport function buildContentMatch(searchContent: string, query: string): SearchMatch | null {\n if (!searchContent || !query.trim()) return null;\n const idx = searchContent.toLowerCase().indexOf(query.toLowerCase());\n if (idx === -1) return null;\n\n const start = Math.max(0, idx - 80);\n const end = Math.min(searchContent.length, idx + query.length + 120);\n const body = searchContent.slice(start, end).replace(/\\s+/g, \" \").trim();\n\n // Measure the hit AFTER the whitespace collapse, so the offsets describe the\n // string actually being returned.\n const hitAt = body.toLowerCase().indexOf(query.toLowerCase());\n const prefix = start > 0 ? FTS_ELLIPSIS : \"\";\n const suffix = end < searchContent.length ? FTS_ELLIPSIS : \"\";\n const snippet = `${prefix}${body}${suffix}`;\n\n const highlights: SearchHighlight[] =\n hitAt === -1\n ? []\n : [{ start: hitAt + prefix.length, end: hitAt + prefix.length + query.length }];\n\n return { field: CONTENT_FIELD, snippet, highlights };\n}\n","import { createReadStream } from \"fs\";\nimport { basename } from \"path\";\nimport { createInterface } from \"readline\";\nimport { getLogger } from \"./logger\";\nimport { applyTeamInfo, initialConvState, reduceConvLine } from \"./persistent/conversation-reducer\";\nimport {\n deriveSessionNameFromFirstMessage,\n finalizeMeta,\n initialReducerState,\n reduceLine,\n} from \"./persistent/metadata-reducer\";\nimport { cleanSystemTags } from \"./tags\";\nimport type {\n ContentTier,\n Conversation,\n ConversationMessage,\n ConversationMeta,\n TeamInfo,\n ToolResultBlock,\n ToolUseBlock,\n TurnDuration,\n} from \"./types\";\n\n// `onEntry` lets a caller fold a second accumulator (the search document) over\n// the same lines without re-reading the file.\nexport async function parseMeta(\n filePath: string,\n account: string,\n tier: ContentTier,\n onEntry?: (entry: Record<string, unknown>) => void,\n): Promise<ConversationMeta | null> {\n const log = getLogger();\n log.trace({ filePath, account, tier: tier.name }, \"parseMeta: start\");\n\n // Stream the whole file through the same per-line fold the incremental\n // indexer uses, so a full parse and an append-then-resume produce identical\n // metadata by construction.\n const state = initialReducerState();\n const fileStream = createReadStream(filePath);\n const rl = createInterface({ input: fileStream, crlfDelay: Infinity });\n\n try {\n for await (const line of rl) {\n if (!line.trim()) continue;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n state.badJsonLines++;\n continue;\n }\n reduceLine(state, entry, tier);\n onEntry?.(entry);\n }\n } catch (err) {\n log.warn({ filePath, err }, \"parseMeta: read failed\");\n return null;\n }\n\n if (state.badJsonLines > 0) {\n log.warn(\n { filePath, badJsonLines: state.badJsonLines },\n \"parseMeta: skipped malformed JSON lines\",\n );\n }\n\n const meta = finalizeMeta(state, filePath, account, tier);\n if (!meta) log.trace({ filePath }, \"parseMeta: no messages\");\n return meta;\n}\n\nexport async function parseConversation(\n filePath: string,\n account: string,\n): Promise<Conversation | null> {\n const log = getLogger();\n log.trace({ filePath, account }, \"parseConversation: start\");\n const messages: ConversationMessage[] = [];\n let badJsonLines = 0;\n const textParts: string[] = [];\n const turnDurations: TurnDuration[] = [];\n\n // The message line→message reduction lives in reduceConvLine (shared with the\n // bounded page reader). turn_duration collection is parseConversation-only\n // (pages don't need it), so it stays inline here.\n const state = initialConvState();\n\n const fileStream = createReadStream(filePath);\n const rl = createInterface({ input: fileStream, crlfDelay: Infinity });\n\n try {\n for await (const line of rl) {\n if (!line.trim()) continue;\n\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n badJsonLines++;\n continue;\n }\n\n if (\n entry.type === \"system\" &&\n entry.subtype === \"turn_duration\" &&\n typeof entry.durationMs === \"number\"\n ) {\n turnDurations.push({\n durationMs: entry.durationMs as number,\n messageCount: (entry.messageCount as number) || 0,\n uuid: entry.uuid as string | undefined,\n });\n continue;\n }\n\n const message = reduceConvLine(state, entry);\n if (message) {\n messages.push(message);\n if (message.text) textParts.push(message.text);\n }\n }\n } catch (err) {\n log.warn({ filePath, err }, \"parseConversation: read failed\");\n return null;\n }\n\n if (badJsonLines > 0) {\n log.warn({ filePath, badJsonLines }, \"parseConversation: skipped malformed JSON lines\");\n }\n\n if (messages.length === 0) {\n log.trace({ filePath }, \"parseConversation: no messages\");\n return null;\n }\n\n log.debug({ filePath, messageCount: messages.length }, \"parseConversation: complete\");\n applyTeamInfo(messages, state);\n\n return {\n id: filePath,\n filePath,\n projectPath: state.cwd,\n projectName: getShortProjectName(state.cwd),\n sessionId: state.sessionId || basename(filePath, \".jsonl\"),\n sessionName:\n state.sessionName ||\n deriveSessionNameFromFirstMessage(messages.find((m) => m.role === \"user\" && m.text) ?? null),\n messages,\n fullText: textParts.join(\" \"),\n timestamp: state.latestTimestamp || new Date().toISOString(),\n messageCount: messages.length,\n account,\n turnDurations: turnDurations.length > 0 ? turnDurations : undefined,\n lastPrompt: state.lastPrompt || undefined,\n };\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────\n\nexport function extractTextContent(content: unknown): string {\n if (!content) return \"\";\n if (typeof content === \"string\") return cleanSystemTags(content);\n if (Array.isArray(content)) {\n return content\n .map((item) => {\n if (typeof item === \"string\") return item;\n if (item?.type === \"text\" && item?.text) return item.text;\n if (item?.type === \"tool_result\" && typeof item?.content === \"string\") return item.content;\n return \"\";\n })\n .filter(Boolean)\n .map(cleanSystemTags)\n .join(\" \");\n }\n return \"\";\n}\n\nexport function extractToolUseNames(content: unknown): string[] {\n if (!Array.isArray(content)) return [];\n return content\n .filter((item) => item?.type === \"tool_use\" && item?.name)\n .map((item) => item.name as string);\n}\n\nexport function extractToolUseBlocks(content: unknown): ToolUseBlock[] {\n if (!Array.isArray(content)) return [];\n return content\n .filter((item) => item?.type === \"tool_use\" && item?.name && item?.id)\n .map((item) => ({\n id: item.id as string,\n name: item.name as string,\n input: (item.input as Record<string, unknown>) || {},\n }));\n}\n\nconst TOOL_NAME_TO_TYPE: Record<string, ToolResultBlock[\"type\"]> = {\n Edit: \"edit\",\n Write: \"write\",\n Read: \"read\",\n Bash: \"bash\",\n Grep: \"grep\",\n Glob: \"glob\",\n Agent: \"taskAgent\",\n TaskCreate: \"taskCreate\",\n TaskUpdate: \"taskUpdate\",\n};\n\nexport function extractToolResultBlocks(\n content: unknown,\n pendingToolUses: Map<string, ToolUseBlock>,\n): ToolResultBlock[] {\n if (!Array.isArray(content)) return [];\n return content\n .filter((item) => item?.type === \"tool_result\" && item?.tool_use_id)\n .map((item) => {\n const toolName = pendingToolUses.get(item.tool_use_id as string)?.name ?? \"\";\n return {\n toolUseId: item.tool_use_id as string,\n type: TOOL_NAME_TO_TYPE[toolName] ?? \"generic\",\n content:\n typeof item.content === \"string\"\n ? { text: item.content as string }\n : ((item.content as Record<string, unknown>) ?? {}),\n isError: typeof item.is_error === \"boolean\" ? item.is_error : undefined,\n };\n });\n}\n\nexport function collectToolNames(content: unknown, toolSet: Set<string>): void {\n if (!Array.isArray(content)) return;\n for (const item of content) {\n if (item?.type === \"tool_use\" && item?.name) {\n toolSet.add(item.name as string);\n }\n }\n}\n\nexport function isOnlyToolResultContent(content: unknown): boolean {\n if (!Array.isArray(content)) return false;\n return content.length > 0 && content.every((item) => item?.type === \"tool_result\");\n}\n\nexport function isTeammateContent(content: unknown): boolean {\n const raw =\n typeof content === \"string\"\n ? content\n : Array.isArray(content)\n ? content\n .map((item) =>\n typeof item === \"string\" ? item : item?.type === \"text\" ? (item.text ?? \"\") : \"\",\n )\n .join(\"\")\n : \"\";\n return raw.includes(\"<teammate-message\");\n}\n\nexport function extractThinking(content: unknown): { content: string; signature: string } {\n if (!Array.isArray(content)) return { content: \"\", signature: \"\" };\n const blocks = content.filter((item) => item?.type === \"thinking\");\n return {\n content: blocks\n .map((b) => b.thinking as string)\n .filter(Boolean)\n .join(\"\\n\\n\"),\n signature: blocks\n .map((b) => b.signature as string)\n .filter(Boolean)\n .join(\"\"),\n };\n}\n\nexport function hasImageBlocks(content: unknown): boolean {\n if (!Array.isArray(content)) return false;\n return content.some(\n (item) =>\n item?.type === \"image\" &&\n (item?.source?.type === \"base64\" || item?.file?.base64 !== undefined),\n );\n}\n\nexport function parseTeammateMessageTag(content: string): TeamInfo | null {\n const match = content.match(/<teammate-message\\s+([^>]*)>/);\n if (!match) return null;\n const attrs = match[1];\n const id = attrs.match(/teammate_id=\"([^\"]*)\"/)?.[1];\n if (!id) return null;\n const summary = attrs.match(/summary=\"([^\"]*)\"/)?.[1];\n const color = attrs.match(/color=\"([^\"]*)\"/)?.[1];\n return { teammateId: id, summary, color };\n}\n\nexport function getShortProjectName(fullPath: string): string {\n const parts = fullPath.split(\"/\").filter(Boolean);\n return parts.slice(-3).join(\"/\");\n}\n","import { basename, dirname, join } from \"path\";\nimport {\n collectToolNames,\n extractTextContent,\n extractThinking,\n extractToolUseBlocks,\n isOnlyToolResultContent,\n isTeammateContent,\n} from \"../parser\";\nimport { CLAUDE_CODE_PROVIDER } from \"../providers/provider\";\nimport type { ContentTier, ConversationMeta, MessageSender, MessageSnapshot } from \"../types\";\n\n// Serializable fold state for incremental metadata indexing. parseMeta()'s\n// per-line loop is expressed as reduceLine(state, entry); finalizeMeta(state)\n// produces the ConversationMeta. Because the state is plain JSON, it can be\n// persisted in conversation_files.reducer_state and resumed when a file grows,\n// so an append only needs to fold the newly-read lines.\nexport interface ReducerState {\n sessionId: string;\n sessionName: string;\n latestTimestamp: string;\n cwd: string;\n teamName: string;\n model: string | null;\n messageCount: number;\n // Count of messages parseConversation() would produce — broader than\n // messageCount (also counts tool_use-only and thinking-only lines). Used as\n // the page total for bounded reads, since metadata messageCount differs.\n pageMessageCount: number;\n lastMessageSender: MessageSender;\n isTeammate: boolean;\n firstUserSeen: boolean;\n firstMessage: MessageSnapshot | null;\n lastMessage: MessageSnapshot | null;\n lastPrompt: string;\n toolNames: string[];\n previewParts: string[];\n snippetParts: string[];\n previewLength: number;\n snippetLength: number;\n badJsonLines: number;\n}\n\nexport function initialReducerState(): ReducerState {\n return {\n sessionId: \"\",\n sessionName: \"\",\n latestTimestamp: \"\",\n cwd: \"\",\n teamName: \"\",\n model: null,\n messageCount: 0,\n lastMessageSender: \"user\",\n isTeammate: false,\n firstUserSeen: false,\n firstMessage: null,\n lastMessage: null,\n lastPrompt: \"\",\n pageMessageCount: 0,\n toolNames: [],\n previewParts: [],\n snippetParts: [],\n previewLength: 0,\n snippetLength: 0,\n badJsonLines: 0,\n };\n}\n\n// Fold one already-parsed JSONL entry into the state, mutating it in place.\n// Mirrors parseMeta()'s loop body exactly so a full reduce equals a streamed\n// parseMeta. `entry` is the JSON.parse() of one non-empty line.\nexport function reduceLine(\n state: ReducerState,\n entry: Record<string, unknown>,\n tier: ContentTier,\n): void {\n if (entry.cwd && !state.cwd) state.cwd = entry.cwd as string;\n if (entry.sessionId && !state.sessionId) state.sessionId = entry.sessionId as string;\n if (entry.slug && !state.sessionName) state.sessionName = entry.slug as string;\n if (entry.teamName && !state.teamName) state.teamName = entry.teamName as string;\n if (entry.timestamp) {\n const ts = entry.timestamp as string;\n if (!state.latestTimestamp || ts > state.latestTimestamp) state.latestTimestamp = ts;\n }\n\n const type = entry.type as string;\n\n if (type === \"last-prompt\") {\n if (entry.lastPrompt && !state.lastPrompt) state.lastPrompt = entry.lastPrompt as string;\n return;\n }\n\n // file-history-snapshot and other non-message entries are excluded.\n if (type !== \"user\" && type !== \"assistant\") return;\n if (entry.isMeta) return;\n\n const msg = entry.message as Record<string, unknown> | undefined;\n\n if (state.model === null && msg?.model) state.model = msg.model as string;\n\n if (type === \"user\" && !state.firstUserSeen) {\n state.firstUserSeen = true;\n if (isTeammateContent(msg?.content)) state.isTeammate = true;\n }\n\n const content = extractTextContent(msg?.content);\n const hasToolUseResult = type === \"user\" && entry.toolUseResult != null;\n const isOnlyToolResult = hasToolUseResult && isOnlyToolResultContent(msg?.content);\n\n const toolSet = new Set(state.toolNames);\n collectToolNames(msg?.content, toolSet);\n state.toolNames = Array.from(toolSet);\n\n // Count messages the same way parseConversation does, so bounded paging has a\n // correct total. (Broader than messageCount: includes tool_use-only and\n // thinking-only lines.)\n const toolUseBlocks = extractToolUseBlocks(msg?.content);\n const thinking = type === \"assistant\" ? extractThinking(msg?.content) : null;\n const hasThinking = !!(thinking?.content || thinking?.signature);\n if (content || isOnlyToolResult || toolUseBlocks.length > 0 || hasThinking) {\n state.pageMessageCount++;\n }\n\n if (content || isOnlyToolResult) {\n state.messageCount++;\n state.lastMessageSender = type as MessageSender;\n\n if (content) {\n const ts = (entry.timestamp as string) || \"\";\n if (!state.firstMessage) state.firstMessage = { text: content.slice(0, 200), timestamp: ts };\n state.lastMessage = { text: content.slice(0, 200), timestamp: ts };\n\n if (state.previewLength < tier.previewMax) {\n state.previewParts.push(content);\n state.previewLength += content.length;\n }\n if (state.snippetLength < tier.snippetMax) {\n const remaining = tier.snippetMax - state.snippetLength;\n const chunk = content.length > remaining ? content.slice(0, remaining) : content;\n state.snippetParts.push(chunk);\n state.snippetLength += chunk.length;\n }\n }\n }\n}\n\n// Build the ConversationMeta from accumulated state, or null when no messages\n// were seen — matching parseMeta()'s final shape exactly.\nexport function finalizeMeta(\n state: ReducerState,\n filePath: string,\n account: string,\n tier: ContentTier,\n): ConversationMeta | null {\n if (state.messageCount === 0) return null;\n\n const isSubagent = filePath.includes(\"/subagents/\");\n let parentSessionId: string | null = null;\n if (isSubagent) {\n const uuidDir = dirname(dirname(filePath));\n parentSessionId = join(dirname(uuidDir), `${basename(uuidDir)}.jsonl`);\n }\n\n const projectPath = state.cwd;\n return {\n id: filePath,\n filePath,\n provider: CLAUDE_CODE_PROVIDER,\n sessionId: state.sessionId || basename(filePath, \".jsonl\"),\n sessionName: state.sessionName || deriveSessionNameFromFirstMessage(state.firstMessage),\n projectPath,\n projectName: getShortProjectName(projectPath),\n account,\n timestamp: state.latestTimestamp || new Date().toISOString(),\n messageCount: state.messageCount,\n lastMessageSender: state.lastMessageSender,\n preview: state.previewParts.join(\" \").slice(0, tier.previewMax),\n contentSnippet: state.snippetParts.join(\" \"),\n gitBranch: null,\n model: state.model,\n isSubagent,\n parentSessionId,\n isTeammate: state.isTeammate,\n teamName: state.teamName || null,\n toolNames: state.toolNames,\n firstMessage: state.firstMessage,\n lastMessage: state.lastMessage,\n lastPrompt: state.lastPrompt || undefined,\n };\n}\n\n// Fallback session name for interactive Claude Code conversations, which carry\n// no `slug` (only SDK/agent sessions do). The first user message is the only\n// human-readable name source in a normal JSONL, so use its first line, trimmed\n// to a title-sized length. Empty when there is no message text.\nexport function deriveSessionNameFromFirstMessage(firstMessage: { text: string } | null): string {\n const firstLine = firstMessage?.text.split(\"\\n\", 1)[0]?.trim() ?? \"\";\n return firstLine.slice(0, 80);\n}\n\nfunction getShortProjectName(fullPath: string): string {\n const parts = fullPath.split(\"/\").filter(Boolean);\n return parts.slice(-3).join(\"/\");\n}\n","import type { ContentTier, ConversationMeta } from \"../types\";\n\n// A scanner provider knows how to discover and parse one local conversation-log\n// format into the shared ConversationMeta model. The reducer triplet\n// (createEmptyAccumulator / reduceEntry / finalize) is the same fold used by a\n// full streamed parse and — by design — a future offset-resumed incremental\n// parse, since the accumulator is plain serializable state. See\n// metadata-reducer.ts for the Threadbase implementation this mirrors.\nexport interface ScannerProvider<Acc = unknown> {\n name: ScannerProviderName;\n\n // Discover candidate files for this provider under the given roots. Roots are\n // already absolute. Returns one entry per file worth parsing.\n discover(roots: string[]): Promise<DiscoveredConversationFile[]>;\n\n // Cheap structural sniff: does this file look like this provider's format?\n // `sample` is the first few lines (newline-joined). Used so a single roots\n // list can be shared across providers without misclassifying files.\n canParse(filePath: string, sample: string): boolean;\n\n createEmptyAccumulator(): Acc;\n\n // Fold one already-JSON.parsed line into the accumulator, in place. Must never\n // throw on an unknown entry shape — ignore what it doesn't understand.\n reduceEntry(acc: Acc, entry: Record<string, unknown>, tier: ContentTier): void;\n\n // Build the ConversationMeta from the accumulator, or null if no messages.\n finalize(acc: Acc, filePath: string, account: string, tier: ContentTier): ConversationMeta | null;\n}\n\nexport type ScannerProviderName = \"claude-code\" | \"codex-cli\";\n\nexport const CLAUDE_CODE_PROVIDER = \"claude-code\" as const;\nexport const CODEX_CLI_PROVIDER = \"codex-cli\" as const;\n\nexport interface DiscoveredConversationFile {\n filePath: string;\n account: string;\n}\n","const SYSTEM_TAGS = [\n \"system-reminder\",\n \"command-name\",\n \"command-message\",\n \"command-args\",\n \"ide_selection\",\n \"ide_opened_file\",\n \"local-command-stdout\",\n \"local-command-caveat\",\n \"retrieval_status\",\n \"task_id\",\n \"task_type\",\n \"task-id\",\n \"task-notification\",\n \"fast_mode_info\",\n \"persisted-output\",\n \"tool_use_error\",\n \"user-prompt-submit-hook\",\n \"thinking\",\n \"ask_user\",\n \"teammate-message\",\n];\n\nconst SYSTEM_TAG_RE = new RegExp(`<(${SYSTEM_TAGS.join(\"|\")})[^>]*>[\\\\s\\\\S]*?<\\\\/\\\\1>`, \"g\");\n\nexport function cleanSystemTags(text: string): string {\n return text\n .replace(SYSTEM_TAG_RE, \"\")\n .replace(/[^\\S\\n]+/g, \" \")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n}\n","import {\n extractTextContent,\n extractThinking,\n extractToolResultBlocks,\n extractToolUseBlocks,\n extractToolUseNames,\n hasImageBlocks,\n isOnlyToolResultContent,\n parseTeammateMessageTag,\n} from \"../parser\";\nimport type {\n AttachmentSidecar,\n ConversationMessage,\n MessageMetadata,\n MessageSender,\n TeamInfo,\n ToolUseBlock,\n} from \"../types\";\n\n// Resumable, serializable state for the conversation line→message reduction.\n// Mirrors parseConversation()'s loop so a full reduce equals a streamed parse,\n// and so a bounded page read can resume an *equivalent* parse from a checkpoint\n// by restoring the cross-line state (pending tool_use blocks + team info).\nexport interface ConvReducerState {\n cwd: string;\n sessionId: string;\n sessionName: string;\n latestTimestamp: string;\n lastPrompt: string;\n // id -> tool_use block, so a later tool_result line resolves its tool type.\n pendingToolUses: Record<string, ToolUseBlock>;\n // teamName -> info, collected as lines are read and applied to messages.\n teamInfo: Record<string, TeamInfo>;\n}\n\nexport function initialConvState(): ConvReducerState {\n return {\n cwd: \"\",\n sessionId: \"\",\n sessionName: \"\",\n latestTimestamp: \"\",\n lastPrompt: \"\",\n pendingToolUses: {},\n teamInfo: {},\n };\n}\n\n// Fold one JSONL entry. Returns the produced ConversationMessage (its message\n// index is the count of messages emitted so far), or null if the line produced\n// no message. Mutates `state` (header fields + cross-line maps).\nexport function reduceConvLine(\n state: ConvReducerState,\n entry: Record<string, unknown>,\n): ConversationMessage | null {\n if (entry.cwd && !state.cwd) state.cwd = entry.cwd as string;\n if (entry.sessionId && !state.sessionId) state.sessionId = entry.sessionId as string;\n if (entry.slug && !state.sessionName) state.sessionName = entry.slug as string;\n if (entry.timestamp) {\n const ts = entry.timestamp as string;\n if (!state.latestTimestamp || ts > state.latestTimestamp) state.latestTimestamp = ts;\n }\n\n const type = entry.type as string;\n\n if (type === \"last-prompt\") {\n if (entry.lastPrompt && !state.lastPrompt) state.lastPrompt = entry.lastPrompt as string;\n return null;\n }\n // system (incl. turn_duration), file-history-snapshot, etc. produce no message.\n if (type !== \"user\" && type !== \"assistant\") return null;\n if (entry.isMeta) return null;\n\n const msg = entry.message as Record<string, unknown> | undefined;\n\n const toolUseBlocks = extractToolUseBlocks(msg?.content);\n for (const block of toolUseBlocks) state.pendingToolUses[block.id] = block;\n\n const hasToolUseResult = type === \"user\" && entry.toolUseResult != null;\n const isToolResultOnly = hasToolUseResult && isOnlyToolResultContent(msg?.content);\n const content = extractTextContent(msg?.content);\n const thinking = type === \"assistant\" ? extractThinking(msg?.content) : null;\n const hasThinking = !!(thinking?.content || thinking?.signature);\n\n if (!(content || isToolResultOnly || toolUseBlocks.length > 0 || hasThinking)) return null;\n\n const metadata: MessageMetadata = {};\n if (msg?.model) metadata.model = msg.model as string;\n if (msg?.stop_reason !== undefined) metadata.stopReason = msg.stop_reason as string | null;\n if (entry.gitBranch) metadata.gitBranch = entry.gitBranch as string;\n if (entry.version) metadata.version = entry.version as string;\n\n const usage = msg?.usage as Record<string, number> | undefined;\n if (usage) {\n if (usage.input_tokens) metadata.inputTokens = usage.input_tokens;\n if (usage.output_tokens) metadata.outputTokens = usage.output_tokens;\n if (usage.cache_read_input_tokens) metadata.cacheReadTokens = usage.cache_read_input_tokens;\n if (usage.cache_creation_input_tokens)\n metadata.cacheCreationTokens = usage.cache_creation_input_tokens;\n }\n\n const toolUseNames = extractToolUseNames(msg?.content);\n if (toolUseNames.length > 0) metadata.toolUses = toolUseNames;\n if (toolUseBlocks.length > 0) metadata.toolUseBlocks = toolUseBlocks;\n\n if (isToolResultOnly) {\n const pending = new Map(Object.entries(state.pendingToolUses));\n const toolResultBlocks = extractToolResultBlocks(msg?.content, pending);\n if (toolResultBlocks.length > 0) {\n metadata.toolResults = toolResultBlocks;\n // A result consumes its tool_use: prune it so the cross-line map stays\n // bounded by IN-FLIGHT tool calls instead of growing with the file.\n // Unpruned, every checkpoint snapshot/serialization of this state costs\n // O(file) — the structuredClone stall on large live conversations.\n for (const block of toolResultBlocks) delete state.pendingToolUses[block.toolUseId];\n }\n }\n\n if (entry.teamName) {\n metadata.teamName = entry.teamName as string;\n if (!state.teamInfo[metadata.teamName] && content) {\n const info = parseTeammateMessageTag(content);\n if (info) state.teamInfo[metadata.teamName] = info;\n }\n }\n\n const thinkingContent = thinking?.content || undefined;\n const thinkingSignature = thinking?.signature || undefined;\n const hasMetadata = Object.keys(metadata).length > 0;\n\n return {\n role: type as MessageSender,\n text: content || \"\",\n timestamp: (entry.timestamp as string) || \"\",\n uuid: (entry.uuid as string) || undefined,\n metadata: hasMetadata ? metadata : undefined,\n isToolResult: isToolResultOnly || undefined,\n isThinking: thinkingContent || thinkingSignature ? true : undefined,\n thinkingContent,\n thinkingSignature,\n parentUuid: entry.parentUuid !== undefined ? (entry.parentUuid as string | null) : undefined,\n requestId: type === \"assistant\" ? (entry.requestId as string | undefined) : undefined,\n promptId: type === \"user\" ? (entry.promptId as string | undefined) : undefined,\n isSidechain: typeof entry.isSidechain === \"boolean\" ? entry.isSidechain : undefined,\n permissionMode: type === \"user\" ? (entry.permissionMode as string | undefined) : undefined,\n hasImages: hasImageBlocks(msg?.content) || undefined,\n attachment:\n entry.attachment !== undefined ? (entry.attachment as AttachmentSidecar) : undefined,\n };\n}\n\n// Parse/classify one raw JSONL line — the exact line→message mapping the\n// scanner uses internally, exported for downstream indexers (e.g. a watcher\n// tailing appended lines) so their semantics cannot drift from the scanner's.\n// Returns the produced ConversationMessage, or null when the line yields none\n// (system/summary/sidecar records, blank or malformed lines). Whether a line\n// produces a message — and its core fields (role, uuid, timestamp) — is\n// state-independent, so calls without `state` classify correctly on their own;\n// pass one shared `state` across successive lines of a file to also resolve\n// cross-line references (tool_result → tool_use types, team info) identically\n// to a full parse.\nexport function parseJsonlLine(\n line: string,\n state: ConvReducerState = initialConvState(),\n): ConversationMessage | null {\n const text = line.trimEnd();\n if (text.trim().length === 0) return null;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(text);\n } catch {\n return null;\n }\n return reduceConvLine(state, entry);\n}\n\n// Back-apply collected team info to a set of messages (the post-pass\n// parseConversation does). Safe to call on a window: teamInfo for a given name\n// is stable once first seen, so a window restored from a checkpoint that\n// carries earlier teamInfo gets the same result.\nexport function applyTeamInfo(messages: ConversationMessage[], state: ConvReducerState): void {\n if (Object.keys(state.teamInfo).length === 0) return;\n for (const m of messages) {\n const name = m.metadata?.teamName;\n if (name && state.teamInfo[name] && m.metadata) m.metadata.teamInfo = state.teamInfo[name];\n }\n}\n","import { readFileSync, writeFileSync } from \"fs\";\nimport { getLogger } from \"../logger\";\nimport type { ConversationMeta } from \"../types\";\n\nexport const SIDECAR_VERSION = 1;\n\n// Portable, human-readable index summary written next to a JSONL file as\n// `<file>.idx.json`. Not the canonical index (SQLite is) — useful for\n// debugging, portability, and rebuilding the DB if it's lost. Off by default.\nexport interface Sidecar {\n version: number;\n sourcePath: string;\n sizeBytes: number;\n mtimeMs: number;\n lastIndexedOffset: number;\n lastIndexedLine: number;\n messageCount: number;\n projectPath: string;\n projectName: string;\n branch: string | null;\n firstSentAt: string | null;\n firstSentText: string | null;\n lastSentAt: string | null;\n lastSentText: string | null;\n updatedAt: string;\n}\n\nexport function sidecarPath(jsonlPath: string): string {\n return `${jsonlPath}.idx.json`;\n}\n\nexport function buildSidecar(\n meta: ConversationMeta,\n cursor: { sizeBytes: number; mtimeMs: number; offset: number; line: number },\n updatedAt: string,\n): Sidecar {\n return {\n version: SIDECAR_VERSION,\n sourcePath: meta.filePath,\n sizeBytes: cursor.sizeBytes,\n mtimeMs: cursor.mtimeMs,\n lastIndexedOffset: cursor.offset,\n lastIndexedLine: cursor.line,\n messageCount: meta.messageCount,\n projectPath: meta.projectPath,\n projectName: meta.projectName,\n branch: meta.gitBranch,\n firstSentAt: meta.firstMessage?.timestamp ?? null,\n firstSentText: meta.firstMessage?.text ?? null,\n lastSentAt: meta.lastMessage?.timestamp ?? null,\n lastSentText: meta.lastMessage?.text ?? null,\n updatedAt,\n };\n}\n\n// Write the sidecar; never throw into the indexing path — a sidecar is a\n// best-effort artifact, so a write failure (read-only dir, etc.) is logged and\n// swallowed.\nexport function writeSidecar(jsonlPath: string, sidecar: Sidecar): void {\n try {\n writeFileSync(sidecarPath(jsonlPath), JSON.stringify(sidecar, null, 2));\n } catch (err) {\n getLogger().warn({ jsonlPath, err }, \"sidecar: write failed\");\n }\n}\n\nexport function readSidecar(jsonlPath: string): Sidecar | null {\n try {\n return JSON.parse(readFileSync(sidecarPath(jsonlPath), \"utf-8\")) as Sidecar;\n } catch {\n return null;\n }\n}\n","import { mkdir, readFile, writeFile } from \"fs/promises\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { getLogger } from \"./logger\";\nimport type { Profile } from \"./types\";\n\nconst PROFILES_FILE = \"profiles.json\";\n\nexport function resolveConfigDir(configDir: string): string {\n return configDir.replace(/^~/, homedir());\n}\n\nexport function getProjectsDir(profile: Profile): string {\n return join(resolveConfigDir(profile.configDir), \"projects\");\n}\n\nexport async function detectDefaultProfile(): Promise<Profile> {\n return {\n id: \"default\",\n label: \"Default\",\n configDir: join(homedir(), \".claude\"),\n enabled: true,\n emoji: \"🤖\",\n };\n}\n\nexport async function loadProfiles(configPath: string): Promise<Profile[]> {\n const log = getLogger();\n try {\n const resolved = resolveConfigDir(configPath);\n const data = await readFile(join(resolved, PROFILES_FILE), \"utf-8\");\n const profiles = JSON.parse(data) as Profile[];\n log.debug({ configPath, count: profiles.length }, \"profiles: loaded\");\n return profiles;\n } catch (err) {\n log.debug({ configPath, err }, \"profiles: load failed, using default\");\n const defaultProfile = await detectDefaultProfile();\n return [defaultProfile];\n }\n}\n\nexport async function saveProfiles(profiles: Profile[], configPath: string): Promise<void> {\n const resolved = resolveConfigDir(configPath);\n await mkdir(resolved, { recursive: true });\n await writeFile(join(resolved, PROFILES_FILE), JSON.stringify(profiles, null, 2));\n getLogger().debug({ configPath, count: profiles.length }, \"profiles: saved\");\n}\n","import fg from \"fast-glob\";\nimport { createReadStream } from \"fs\";\nimport { stat } from \"fs/promises\";\nimport { basename } from \"path\";\nimport { createInterface } from \"readline\";\nimport { canonicalPath } from \"../canonical-path\";\nimport { getLogger } from \"../logger\";\nimport { cleanSystemTags } from \"../tags\";\nimport type {\n ContentTier,\n Conversation,\n ConversationMessage,\n ConversationMeta,\n MessageSender,\n MessageSnapshot,\n} from \"../types\";\nimport {\n CODEX_CLI_PROVIDER,\n type DiscoveredConversationFile,\n type ScannerProvider,\n} from \"./provider\";\n\n// Serializable fold state for a Codex CLI rollout session. Plain JSON so a\n// future persistent engine can resume it from a byte offset (same contract as\n// the Threadbase ReducerState).\nexport interface CodexAccumulator {\n sessionId: string;\n cwd: string;\n gitBranch: string | null;\n model: string | null;\n latestTimestamp: string;\n messageCount: number;\n lastMessageSender: MessageSender;\n firstUser: MessageSnapshot | null;\n lastUser: MessageSnapshot | null;\n lastAssistant: MessageSnapshot | null;\n toolNames: string[];\n previewParts: string[];\n previewLength: number;\n snippetParts: string[];\n snippetLength: number;\n}\n\n// Codex CLI rollout sessions live as rollout-*.jsonl under a date-partitioned\n// tree. The Codex provider is opt-in: the scanner only discovers under roots the\n// caller passes explicitly (no default home scan).\nexport class CodexCliProvider implements ScannerProvider<CodexAccumulator> {\n readonly name = CODEX_CLI_PROVIDER;\n\n async discover(roots: string[]): Promise<DiscoveredConversationFile[]> {\n const log = getLogger();\n const results: DiscoveredConversationFile[] = [];\n for (const root of roots) {\n let paths: string[];\n try {\n paths = await fg([\"**/rollout-*.jsonl\", \"**/*.jsonl\"], {\n cwd: root,\n absolute: true,\n dot: false,\n unique: true,\n });\n } catch (err) {\n log.warn({ root, err }, \"codex discovery: glob failed\");\n continue;\n }\n for (const filePath of paths) {\n try {\n const s = await stat(filePath);\n // Canonical form — fast-glob emits forward slashes even on Windows,\n // but every by-path lookup keys off the canonical spelling.\n if (s.size > 0) results.push({ filePath: canonicalPath(filePath), account: \"codex\" });\n } catch (err) {\n log.warn({ filePath, err }, \"codex discovery: stat failed\");\n }\n }\n }\n return results;\n }\n\n // Codex rollout lines carry distinctive top-level types.\n canParse(_filePath: string, sample: string): boolean {\n for (const line of sample.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const e = JSON.parse(line) as Record<string, unknown>;\n if (e.type === \"session_meta\" || e.type === \"response_item\" || e.type === \"event_msg\") {\n return true;\n }\n if (e.type === \"user\" || e.type === \"assistant\") return false;\n } catch {}\n }\n return false;\n }\n\n createEmptyAccumulator(): CodexAccumulator {\n return {\n sessionId: \"\",\n cwd: \"\",\n gitBranch: null,\n model: null,\n latestTimestamp: \"\",\n messageCount: 0,\n lastMessageSender: \"user\",\n firstUser: null,\n lastUser: null,\n lastAssistant: null,\n toolNames: [],\n previewParts: [],\n previewLength: 0,\n snippetParts: [],\n snippetLength: 0,\n };\n }\n\n reduceEntry(acc: CodexAccumulator, entry: Record<string, unknown>, tier: ContentTier): void {\n reduceCodexEntry(acc, entry, tier);\n }\n\n finalize(\n acc: CodexAccumulator,\n filePath: string,\n account: string,\n tier: ContentTier,\n ): ConversationMeta | null {\n return finalizeCodexMeta(acc, filePath, account, tier);\n }\n}\n\n// ─── Reducer (exported for tests / future incremental engine) ───────────\n\nconst asString = (v: unknown): string => (typeof v === \"string\" ? v : \"\");\n\n// Pull display text out of a Codex content array of {type,text} blocks.\nfunction extractCodexText(content: unknown): string {\n if (typeof content === \"string\") return cleanSystemTags(content);\n if (!Array.isArray(content)) return \"\";\n return content\n .map((item) => {\n if (typeof item === \"string\") return item;\n const t = item?.type;\n if ((t === \"input_text\" || t === \"output_text\" || t === \"text\") && item?.text) {\n return item.text as string;\n }\n return \"\";\n })\n .filter(Boolean)\n .map(cleanSystemTags)\n .join(\" \");\n}\n\n// Fold one Codex rollout line into the accumulator. Tolerant by construction:\n// any line it doesn't recognise is ignored, never thrown on.\nexport function reduceCodexEntry(\n acc: CodexAccumulator,\n entry: Record<string, unknown>,\n tier: ContentTier,\n): void {\n const ts = asString(entry.timestamp);\n if (ts && (!acc.latestTimestamp || ts > acc.latestTimestamp)) acc.latestTimestamp = ts;\n\n const payload = entry.payload as Record<string, unknown> | undefined;\n if (!payload || typeof payload !== \"object\") return;\n\n const type = entry.type as string;\n\n if (type === \"session_meta\") {\n if (!acc.sessionId) acc.sessionId = asString(payload.id);\n if (!acc.cwd) acc.cwd = asString(payload.cwd);\n const git = payload.git as Record<string, unknown> | undefined;\n if (acc.gitBranch === null && git?.branch) acc.gitBranch = asString(git.branch) || null;\n return;\n }\n\n // Model lives on turn_context records (and occasionally elsewhere).\n if (acc.model === null && payload.model) acc.model = asString(payload.model) || null;\n\n if (type !== \"response_item\") return;\n\n const ptype = payload.type as string;\n\n // Tool calls — collect the tool name.\n if (ptype === \"function_call\" || ptype === \"custom_tool_call\") {\n const name = asString(payload.name);\n if (name && !acc.toolNames.includes(name)) acc.toolNames.push(name);\n return;\n }\n\n if (ptype !== \"message\") return;\n\n const role = payload.role;\n // developer/system/tool roles carry sandbox boilerplate — not user-visible turns.\n if (role !== \"user\" && role !== \"assistant\") return;\n\n const text = extractCodexText(payload.content);\n if (!text) return;\n\n const sender = role as MessageSender;\n acc.messageCount++;\n acc.lastMessageSender = sender;\n\n const snapshot: MessageSnapshot = { text: text.slice(0, 200), timestamp: ts };\n if (sender === \"user\") {\n if (!acc.firstUser) acc.firstUser = snapshot;\n acc.lastUser = snapshot;\n } else {\n acc.lastAssistant = snapshot;\n }\n\n if (acc.previewLength < tier.previewMax) {\n acc.previewParts.push(text);\n acc.previewLength += text.length;\n }\n if (acc.snippetLength < tier.snippetMax) {\n const remaining = tier.snippetMax - acc.snippetLength;\n const chunk = text.length > remaining ? text.slice(0, remaining) : text;\n acc.snippetParts.push(chunk);\n acc.snippetLength += chunk.length;\n }\n}\n\nexport function finalizeCodexMeta(\n acc: CodexAccumulator,\n filePath: string,\n account: string,\n tier: ContentTier,\n): ConversationMeta | null {\n if (acc.messageCount === 0) return null;\n\n const sessionId = acc.sessionId || basename(filePath, \".jsonl\");\n const projectPath = acc.cwd;\n // Treat a session with tool calls but no plain back-and-forth as task-shaped.\n const kind: \"conversation\" | \"task\" =\n acc.lastAssistant === null && acc.toolNames.length > 0 ? \"task\" : \"conversation\";\n\n return {\n id: filePath,\n filePath,\n provider: CODEX_CLI_PROVIDER,\n kind,\n externalSessionId: acc.sessionId || undefined,\n sessionId,\n sessionName: \"\",\n projectPath,\n projectName: getShortProjectName(projectPath),\n account,\n timestamp: acc.latestTimestamp || new Date().toISOString(),\n messageCount: acc.messageCount,\n lastMessageSender: acc.lastMessageSender,\n preview: acc.previewParts.join(\" \").slice(0, tier.previewMax),\n contentSnippet: acc.snippetParts.join(\" \"),\n gitBranch: acc.gitBranch,\n model: acc.model,\n isSubagent: false,\n parentSessionId: null,\n isTeammate: false,\n teamName: null,\n toolNames: acc.toolNames,\n firstMessage: acc.firstUser,\n lastMessage: acc.lastAssistant ?? acc.lastUser,\n lastPrompt: acc.lastUser?.text || undefined,\n };\n}\n\nfunction getShortProjectName(fullPath: string): string {\n return fullPath.split(\"/\").filter(Boolean).slice(-3).join(\"/\");\n}\n\n// Full conversation parse for a Codex rollout session — the Codex analogue of\n// parseConversation(). Streams the file, emitting one ConversationMessage per\n// user/assistant message item. Used by getConversation()/getConversationPage()\n// when the resolved meta is a codex-cli conversation.\nexport async function parseCodexConversation(\n filePath: string,\n account: string,\n): Promise<Conversation | null> {\n const log = getLogger();\n const messages: ConversationMessage[] = [];\n const textParts: string[] = [];\n let sessionId = \"\";\n let cwd = \"\";\n let latestTimestamp = \"\";\n let lastUserText = \"\";\n\n const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });\n try {\n for await (const line of rl) {\n if (!line.trim()) continue;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n const ts = asString(entry.timestamp);\n if (ts && (!latestTimestamp || ts > latestTimestamp)) latestTimestamp = ts;\n const payload = entry.payload as Record<string, unknown> | undefined;\n if (!payload || typeof payload !== \"object\") continue;\n\n if (entry.type === \"session_meta\") {\n if (!sessionId) sessionId = asString(payload.id);\n if (!cwd) cwd = asString(payload.cwd);\n continue;\n }\n if (entry.type !== \"response_item\" || payload.type !== \"message\") continue;\n const role = payload.role;\n if (role !== \"user\" && role !== \"assistant\") continue;\n const text = extractCodexText(payload.content);\n if (!text) continue;\n messages.push({ role: role as MessageSender, text, timestamp: ts });\n textParts.push(text);\n if (role === \"user\") lastUserText = text;\n }\n } catch (err) {\n log.warn({ filePath, err }, \"parseCodexConversation: read failed\");\n return null;\n }\n\n if (messages.length === 0) return null;\n\n return {\n id: filePath,\n filePath,\n projectPath: cwd,\n projectName: getShortProjectName(cwd),\n sessionId: sessionId || basename(filePath, \".jsonl\"),\n sessionName: \"\",\n messages,\n fullText: textParts.join(\" \"),\n timestamp: latestTimestamp || new Date().toISOString(),\n messageCount: messages.length,\n account,\n lastPrompt: lastUserText || undefined,\n };\n}\n","import { sep } from \"path\";\n\n// Canonical absolute-path form used as the identity key for a conversation file\n// across the whole index (SQLite rows, the in-memory metadata cache, the\n// dir-mtime watermarks, and every by-path lookup).\n//\n// The problem this solves: fast-glob (discovery) emits forward-slash paths even\n// on Windows, while the file watcher (chokidar), path.join(), and every caller\n// that builds a path with the platform API emit native (back)slashes. The same\n// real file could therefore be stored/looked-up under two different string keys\n// on Windows, so by-path APIs miss rows depending on the caller's separator\n// style. Normalizing collapses both forms onto one key.\n//\n// The native separator is the canonical form, not fast-glob's forward slashes:\n// a path handed back to a caller (ConversationMeta.filePath/id, a change event,\n// a metadata-cache key) is compared against platform-built paths by every\n// consumer, so the native spelling is the one the public surface has to speak.\n// The cost is a one-time reindex for an index.db written before this change\n// (its forward-slash rows miss, get reinserted in native form, and the same\n// pass's deletion-reconcile retires the old ones) — bounded and self-healing,\n// unlike the recurrent duplicate-row churn this fixes.\n//\n// On POSIX this is a no-op: the separator is already \"/\", and a backslash is a\n// legal filename character there, so it must be left untouched.\nexport function canonicalPath(p: string): string {\n return sep === \"\\\\\" ? p.replace(/\\//g, \"\\\\\") : p;\n}\n","import fg from \"fast-glob\";\nimport { stat } from \"fs/promises\";\nimport { canonicalPath } from \"./canonical-path\";\nimport { getLogger } from \"./logger\";\n\nexport interface DiscoveredFile {\n filePath: string;\n account: string;\n}\n\nconst EXCLUDED_SEGMENTS = [\"/memory/\", \"/tool-results/\"];\n\n// stat() is cheap and IO-bound, so a large history scans much faster when the\n// calls run concurrently rather than one-at-a-time. Cap concurrency well under\n// typical fd limits.\nconst STAT_CONCURRENCY = 32;\n\nexport async function discoverJsonlFiles(\n dirs: { projectsDir: string; account: string }[],\n onProgress?: (found: number) => void,\n): Promise<DiscoveredFile[]> {\n const log = getLogger();\n const results: DiscoveredFile[] = [];\n\n for (const { projectsDir, account } of dirs) {\n let filePaths: string[];\n try {\n filePaths = await fg(\"**/*.jsonl\", {\n cwd: projectsDir,\n absolute: true,\n dot: false,\n });\n } catch (err) {\n log.warn({ projectsDir, account, err }, \"discovery: glob failed\");\n continue;\n }\n\n // Filter out excluded directory segments\n const filtered = filePaths.filter((fp) => !EXCLUDED_SEGMENTS.some((seg) => fp.includes(seg)));\n\n // Filter out empty files. Stat concurrently in bounded chunks; chunk order\n // is preserved and intra-chunk order follows the input, so discovery order\n // is stable (downstream sorts anyway).\n let kept = 0;\n let skippedEmpty = 0;\n let skippedInaccessible = 0;\n for (let i = 0; i < filtered.length; i += STAT_CONCURRENCY) {\n const chunk = filtered.slice(i, i + STAT_CONCURRENCY);\n const statted = await Promise.all(\n chunk.map(async (filePath) => {\n try {\n const s = await stat(filePath);\n return { filePath, size: s.size };\n } catch (err) {\n log.warn({ filePath, err }, \"discovery: stat failed\");\n return { filePath, size: -1 };\n }\n }),\n );\n for (const { filePath, size } of statted) {\n if (size < 0) {\n skippedInaccessible++;\n } else if (size > 0) {\n // fast-glob emits forward slashes even on Windows. Emit the canonical\n // form instead, so a discovered path is the same string as the one a\n // watcher/path.join caller produces for that file — discovery output\n // keys the in-memory metadata cache and the has_nested comparison in\n // dir-watermark, neither of which goes through a repository.\n results.push({ filePath: canonicalPath(filePath), account });\n kept++;\n } else {\n skippedEmpty++;\n }\n }\n }\n\n log.debug(\n {\n projectsDir,\n account,\n globMatches: filePaths.length,\n afterExclusions: filtered.length,\n kept,\n skippedEmpty,\n skippedInaccessible,\n },\n \"discovery: directory scanned\",\n );\n\n onProgress?.(results.length);\n }\n\n log.debug({ totalFiles: results.length, dirs: dirs.length }, \"discovery: complete\");\n return results;\n}\n","import { discoverJsonlFiles } from \"../discovery\";\nimport {\n finalizeMeta,\n initialReducerState,\n type ReducerState,\n reduceLine,\n} from \"../persistent/metadata-reducer\";\nimport type { ContentTier, ConversationMeta } from \"../types\";\nimport {\n CLAUDE_CODE_PROVIDER,\n type DiscoveredConversationFile,\n type ScannerProvider,\n} from \"./provider\";\n\n// The existing Claude/Threadbase format, expressed as a provider. All behavior\n// is the already-shared reducer (metadata-reducer.ts) — no logic is duplicated\n// here. Discovery is delegated to discoverJsonlFiles; the scanner passes the\n// profiles' projects dirs as roots paired with their account ids.\nexport class ThreadbaseProvider implements ScannerProvider<ReducerState> {\n readonly name = CLAUDE_CODE_PROVIDER;\n\n // Roots are passed as \"<projectsDir>\\0<account>\" so the scanner can carry the\n // per-root account through the shared interface. The scanner builds these.\n async discover(roots: string[]): Promise<DiscoveredConversationFile[]> {\n const dirs = roots.map((r) => {\n const [projectsDir, account = \"default\"] = r.split(\"\\0\");\n return { projectsDir, account };\n });\n return discoverJsonlFiles(dirs);\n }\n\n // Threadbase JSONL has top-level type \"user\"/\"assistant\" with a cwd/sessionId.\n canParse(_filePath: string, sample: string): boolean {\n for (const line of sample.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const e = JSON.parse(line) as Record<string, unknown>;\n if (e.type === \"user\" || e.type === \"assistant\") return true;\n // Codex's distinctive top-level types — definitively not us.\n if (e.type === \"session_meta\" || e.type === \"response_item\") return false;\n } catch {}\n }\n return false;\n }\n\n createEmptyAccumulator(): ReducerState {\n return initialReducerState();\n }\n\n reduceEntry(acc: ReducerState, entry: Record<string, unknown>, tier: ContentTier): void {\n reduceLine(acc, entry, tier);\n }\n\n finalize(\n acc: ReducerState,\n filePath: string,\n account: string,\n tier: ContentTier,\n ): ConversationMeta | null {\n // finalizeMeta already tags provider: \"claude-code\".\n return finalizeMeta(acc, filePath, account, tier);\n }\n}\n","import { EventEmitter } from \"events\";\nimport { closeSync, openSync, readSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { LRUCache } from \"./cache\";\nimport { canonicalPath } from \"./canonical-path\";\nimport { discoverJsonlFiles } from \"./discovery\";\nimport {\n applyAccountFilter,\n applyIncludeFilter,\n applyPagination,\n applyProjectFilter,\n applySinceFilter,\n applySort,\n parseSinceCutoff,\n} from \"./filters\";\nimport { readGitBranch } from \"./git\";\nimport { SearchIndexer } from \"./indexer\";\nimport { getLogger } from \"./logger\";\nimport { parseConversation, parseMeta } from \"./parser\";\nimport {\n type CachedConversation,\n extendConversation,\n parseConversationResumable,\n} from \"./persistent/conversation-stream\";\nimport { classify } from \"./persistent/cursor\";\nimport { PersistentEngine } from \"./persistent/index-engine\";\nimport { getProjectsDir, loadProfiles } from \"./profiles\";\nimport { CodexCliProvider, parseCodexConversation } from \"./providers/codex-cli\";\nimport { parseMetaWithProvider } from \"./providers/parse\";\nimport {\n CLAUDE_CODE_PROVIDER,\n CODEX_CLI_PROVIDER,\n type ScannerProvider,\n} from \"./providers/provider\";\nimport { ThreadbaseProvider } from \"./providers/threadbase\";\nimport {\n appendSearchDelta,\n combineSearchContent,\n emptySearchDocument,\n extractSearchDelta,\n type SearchDocument,\n} from \"./search-document\";\nimport { CONTENT_FIELD, generateMatches } from \"./search-matches\";\nimport { resolveTier } from \"./tiers\";\nimport type {\n ContentTier,\n Conversation,\n ConversationMeta,\n ConversationPage,\n GetConversationOptions,\n GetConversationPageOptions,\n GroupedConversations,\n Profile,\n ScanOptions,\n ScanResult,\n SearchOptions,\n SearchResult,\n SingleFilePage,\n TreeConversation,\n} from \"./types\";\nimport { FileWatcher } from \"./watcher/file-watcher\";\nimport { type IndexJob, IndexQueue } from \"./watcher/index-queue\";\n\nconst BATCH_SIZE = 12;\nconst DEFAULT_CONFIG_PATH = \"~/.config/threadbase-scanner\";\n\n// A test process that reaches the default path writes its fixtures into the\n// developer's real index. Measured on one live machine: 579 of 1,144 rows were\n// test fixtures, 357 of them from tb-streamer's suite, which constructs\n// scanners with no dbPath and has no TB_SCANNER_DB of its own. Those rows are\n// indistinguishable from real conversations in every coverage query, so they\n// silently corrupt any measurement taken of the index — including the\n// completeness metrics a durable archive would report.\n//\n// The env var already existed for this; it just failed open. Failing loudly is\n// what makes the omission visible at the moment it is introduced rather than a\n// year later, and it belongs here rather than in each consumer's setup because\n// the scanner is the thing that knows which path is the real one.\nfunction isTestProcess(): boolean {\n return (\n Boolean(process.env.VITEST) ||\n Boolean(process.env.JEST_WORKER_ID) ||\n process.env.NODE_ENV === \"test\"\n );\n}\n\n// Default persistent-index location. Overridable via TB_SCANNER_DB (required\n// under a test runner, and handy for pointing at an alternate DB in ops).\nfunction defaultDbPath(): string {\n const override = process.env.TB_SCANNER_DB;\n if (override) return override;\n if (isTestProcess()) {\n throw new Error(\n \"TB_SCANNER_DB must be set under a test runner. Refusing to open the default index at \" +\n \"~/.config/threadbase-scanner/index.db, because test fixtures written there are \" +\n \"indistinguishable from real conversations and corrupt every measurement of the index. \" +\n \"Point it at a temp directory in your test setup, or pass persistent.dbPath explicitly.\",\n );\n }\n return join(homedir(), \".config\", \"threadbase-scanner\", \"index.db\");\n}\n\n// Flatten a search document for the in-memory index, tail-capped to the content\n// tier. Tail rather than head for the same reason the buckets themselves are\n// tail-biased: people search for what they did recently.\n//\n// This is where `persistent: false` deliberately diverges from SQLite. The\n// FlexSearch index is `tokenize: \"forward\"` (every prefix of every token, held\n// in RAM), so handing it the full ~128 KB budget per conversation would cost\n// hundreds of MB to GB. Extraction RULES are shared with FTS5; indexed VOLUME\n// is not. Raise this only against a measurement, never on speculation.\nfunction capForMemory(doc: SearchDocument, max: number): string {\n const combined = combineSearchContent(doc);\n return combined.length > max ? combined.slice(-max) : combined;\n}\n\nexport interface PersistentConfig {\n dbPath?: string;\n // Write a portable <file>.idx.json sidecar next to each indexed JSONL for\n // debugging/portability/recovery. Off by default. SQLite stays canonical.\n sidecar?: boolean;\n}\n\nexport interface ConversationScannerOptions {\n metadataCacheSize?: number;\n conversationCacheSize?: number;\n // SQLite-backed persistent index. Enabled by default (at DEFAULT_DB_PATH).\n // Pass `false` for the legacy in-memory path (no native dependency, no DB\n // file). Pass `{ dbPath }` to override the database location.\n persistent?: false | PersistentConfig;\n}\n\nexport interface WatchOptions {\n profiles?: Profile[];\n // Watcher debounce window (ms). Default 400.\n debounceMs?: number;\n // Periodic full rescan as a correctness fallback for missed FS events.\n // Default 60_000ms; set 0 to disable.\n periodicMs?: number;\n}\n\n// Emitted after the index changes for one file. `meta` is the fresh metadata,\n// or null when the file was removed/emptied (deleted from the index).\nexport interface ScannerChangeEvent {\n filePath: string;\n account: string;\n meta: ConversationMeta | null;\n reason: IndexJob[\"reason\"];\n}\n\nexport class ConversationScanner {\n private metadataCache: Map<string, ConversationMeta> = new Map();\n // Parsed conversations plus (persistent claude-code entries only) the resume\n // point that lets refreshFile extend them in place when the file grows.\n private conversationLRU: LRUCache<string, CachedConversation>;\n // session_id is NOT unique, so this maps a sessionId to every active meta that\n // carries it. Resolution picks deterministically (newest timestamp, then path\n // ascending) so dropping one file never hides another with the same id.\n private sessionIdIndex: Map<string, ConversationMeta[]> = new Map();\n private projects: Set<string> = new Set();\n private indexer: SearchIndexer = new SearchIndexer();\n // Search body per conversation for the in-memory path, already tail-capped to\n // the content tier. Survives scan() so a statCache hit — which skips the parse\n // entirely — can still index a body rather than an empty string.\n private searchContents = new Map<string, string>();\n // Tier the most recent scan() ran with, so refreshFile() re-parses a single\n // file at the same content depth. Defaults to the standard tier.\n private lastTier: ContentTier = resolveTier(\"standard\");\n\n // null when persistent mode is disabled (legacy in-memory path). Lazily\n // opened on first use so merely constructing a scanner never touches disk.\n private readonly dbPath: string | null;\n private readonly sidecarEnabled: boolean;\n private engineInstance: PersistentEngine | null = null;\n // The most recent scan()'s promise while it is still running, or null when\n // idle. close() awaits it before closing the SQLite handle so a fire-and-\n // forget scan can't have its DB shut mid-indexAll (\"database connection is\n // not open\"). Tracks the latest scan only — concurrent scans on one instance\n // aren't a supported pattern (a single writer DB), and the latest resolving\n // implies earlier ones already settled in practice.\n private inFlightScan: Promise<unknown> | null = null;\n\n private emitter = new EventEmitter();\n private watcher: FileWatcher | null = null;\n private queue: IndexQueue | null = null;\n private periodicTimer: ReturnType<typeof setInterval> | null = null;\n // The in-flight periodicReconcile() job, if one is mid-run. unwatch() awaits\n // it before dropping the queue/engine so its post-await engine.files access\n // can't hit a closed DB (the watch-mode half of Bug #4).\n private inFlightReconcile: Promise<void> | null = null;\n\n constructor(options?: ConversationScannerOptions) {\n this.conversationLRU = new LRUCache<string, CachedConversation>(\n options?.conversationCacheSize ?? 5,\n );\n if (options?.persistent === false) {\n this.dbPath = null;\n this.sidecarEnabled = false;\n } else {\n this.dbPath = options?.persistent?.dbPath ?? defaultDbPath();\n this.sidecarEnabled = options?.persistent?.sidecar ?? false;\n }\n }\n\n private get persistent(): boolean {\n return this.dbPath !== null;\n }\n\n private engine(): PersistentEngine {\n if (!this.engineInstance) {\n this.engineInstance = new PersistentEngine(this.dbPath as string, {\n sidecar: this.sidecarEnabled,\n });\n }\n return this.engineInstance;\n }\n\n // Release the SQLite connection. No-op in legacy mode. Safe to call\n // repeatedly. Awaits watcher teardown AND any in-flight scan before closing\n // the DB handle, so a fire-and-forget scan() can never have its connection\n // shut mid-indexAll() (\"database connection is not open\"). This is why close()\n // is async — callers should await it during shutdown. (Scanner review Bug #4.)\n async close(): Promise<void> {\n if (this.watcher || this.queue || this.periodicTimer) await this.unwatch();\n // Let any running scan finish touching the DB first. Swallow its result/\n // rejection here — the caller that started the scan owns its outcome; close()\n // only needs it to stop using the handle before we close it.\n if (this.inFlightScan) {\n try {\n await this.inFlightScan;\n } catch {\n // scan failed on its own; close() proceeds to release the handle\n }\n }\n this.engineInstance?.close();\n this.engineInstance = null;\n }\n\n async scan(options: ScanOptions = {}): Promise<ScanResult> {\n // Track this scan so close() can await it before shutting the DB handle.\n // Cleared on settle — but only if it's still the current one (a newer scan\n // may have replaced it). Errors propagate to the caller unchanged.\n const promise = this.runScan(options);\n this.inFlightScan = promise;\n try {\n return await promise;\n } finally {\n if (this.inFlightScan === promise) this.inFlightScan = null;\n }\n }\n\n private async runScan(options: ScanOptions): Promise<ScanResult> {\n const profiles = await this.resolveProfiles(options.profiles);\n const activeProfiles = profiles.filter((p) => p.enabled && p.scanHistory !== false);\n this.lastTier = resolveTier(options.tier ?? \"standard\", options.tiers);\n\n if (this.persistent) {\n return this.scanPersistent(activeProfiles, options);\n }\n return this.scanInMemory(activeProfiles, options);\n }\n\n // SQLite-backed scan: (re)index changed files into the DB, then query all\n // active metas and run the identical filter/sort/view/paginate pipeline as\n // the in-memory path — guaranteeing an identical ScanResult shape.\n private async scanPersistent(\n activeProfiles: Profile[],\n options: ScanOptions,\n ): Promise<ScanResult> {\n const log = getLogger();\n const startedAt = Date.now();\n const engine = this.engine();\n\n const { scanned } = await engine.indexAll(activeProfiles, options);\n const allMetas = engine.allActive();\n\n const { conversations, total } = this.finalize(allMetas, options);\n log.info(\n { scanned, kept: allMetas.length, filteredTotal: total, elapsedMs: Date.now() - startedAt },\n \"scan: complete (persistent)\",\n );\n return { conversations, total, scanned };\n }\n\n // Apply include/project/account/since filters, sort, view transform, and\n // pagination. Shared by both backends so results never diverge.\n private finalize(\n allMetas: ConversationMeta[],\n options: ScanOptions,\n ): { conversations: ScanResult[\"conversations\"]; total: number } {\n let filtered = allMetas;\n if (options.include && options.include !== \"all\") {\n filtered = applyIncludeFilter(filtered, options.include);\n }\n if (options.project) filtered = applyProjectFilter(filtered, options.project);\n if (options.account) filtered = applyAccountFilter(filtered, options.account);\n if (options.since) filtered = applySinceFilter(filtered, options.since);\n\n filtered = applySort(filtered, options.sort ?? \"recent\");\n const total = filtered.length;\n const conversations = this.transformView(filtered, options);\n\n if (Array.isArray(conversations)) {\n const limit = options.limit ?? 50;\n const offset = options.offset ?? 0;\n return { conversations: applyPagination(conversations, limit, offset).items, total };\n }\n return { conversations, total };\n }\n\n private async scanInMemory(activeProfiles: Profile[], options: ScanOptions): Promise<ScanResult> {\n const log = getLogger();\n const startedAt = Date.now();\n const tier = this.lastTier;\n\n log.info(\n {\n activeProfiles: activeProfiles.length,\n tier: tier.name,\n sort: options.sort ?? \"recent\",\n include: options.include ?? \"all\",\n view: options.view ?? \"flat\",\n },\n \"scan: start\",\n );\n\n // Clear caches. searchContents is deliberately NOT cleared: it is keyed by\n // file path and only ever replaced by a fresh parse, so keeping it lets a\n // statCache hit (which skips parsing) still index a real body.\n this.metadataCache.clear();\n this.conversationLRU.clear();\n this.sessionIdIndex.clear();\n this.projects.clear();\n this.indexer.clear();\n\n const files = await this.discoverWithProviders(activeProfiles, options);\n const totalFiles = files.length;\n let scanned = 0;\n let parseFailures = 0;\n\n const allMetas: ConversationMeta[] = [];\n // Memoize git-branch lookups per project path for the duration of this\n // scan. readGitBranch walks the filesystem for .git/HEAD; without this it\n // runs once per conversation (thousands of times) even though there are\n // only a handful of distinct project roots. Scoped to one scan() call —\n // branches can change between scans, so a longer-lived memo would go stale.\n const gitBranchMemo = new Map<string, string | null>();\n const resolveGitBranch = (projectPath: string): string | null => {\n let branch = gitBranchMemo.get(projectPath);\n if (branch === undefined) {\n branch = readGitBranch(projectPath);\n gitBranchMemo.set(projectPath, branch);\n }\n return branch;\n };\n const { statCache } = options;\n for (let i = 0; i < files.length; i += BATCH_SIZE) {\n const batch = files.slice(i, i + BATCH_SIZE);\n const results = await Promise.all(\n batch.map(async ({ filePath, account, provider }) => {\n if (statCache) {\n const cached = statCache.get(filePath);\n if (cached) {\n try {\n const s = statSync(filePath);\n if (s.mtimeMs === cached.stat.mtimeMs && s.size === cached.stat.size) {\n // Unchanged file: whatever body we extracted last time is\n // still accurate, so reuse it instead of indexing nothing.\n return {\n meta: cached.meta,\n searchContent: this.searchContents.get(cached.meta.id),\n };\n }\n } catch {\n // file disappeared — fall through to parse which will return null\n }\n }\n }\n try {\n // Fold the search document over the same lines as the metadata\n // parse — one read of the file, not two.\n let doc = emptySearchDocument();\n const meta = await parseMetaWithProvider(provider, filePath, account, tier, (entry) => {\n doc = appendSearchDelta(doc, extractSearchDelta(entry));\n });\n // The provider may already know its branch (Codex reads it from\n // session_meta). Only walk the filesystem when it doesn't.\n if (meta && meta.gitBranch === null && meta.projectPath) {\n meta.gitBranch = resolveGitBranch(meta.projectPath);\n }\n return { meta, searchContent: capForMemory(doc, tier.snippetMax) };\n } catch (err) {\n parseFailures++;\n log.warn({ filePath, account, provider: provider.name, err }, \"scan: parse threw\");\n return { meta: null, searchContent: undefined };\n }\n }),\n );\n\n const batchMetas: ConversationMeta[] = [];\n for (const { meta, searchContent } of results) {\n if (meta && meta.messageCount > 0) {\n this.metadataCache.set(meta.id, meta);\n this.addToSessionIndex(meta);\n this.projects.add(meta.projectPath);\n allMetas.push(meta);\n batchMetas.push(meta);\n const content = searchContent ?? this.searchContents.get(meta.id) ?? \"\";\n this.searchContents.set(meta.id, content);\n this.indexer.addDocument(meta, content);\n }\n }\n\n if (batchMetas.length > 0) {\n options.onBatch?.(batchMetas);\n }\n\n scanned += batch.length;\n log.debug({ scanned, totalFiles, batchKept: batchMetas.length }, \"scan: batch complete\");\n options.onProgress?.(scanned, totalFiles);\n }\n\n const { conversations, total } = this.finalize(allMetas, options);\n\n const elapsedMs = Date.now() - startedAt;\n log.info(\n {\n totalFiles,\n scanned,\n kept: allMetas.length,\n filteredTotal: total,\n parseFailures,\n elapsedMs,\n },\n \"scan: complete\",\n );\n\n return { conversations, total, scanned };\n }\n\n async search(query: string, options: SearchOptions = {}): Promise<SearchResult[]> {\n const log = getLogger();\n log.debug({ query, indexSize: this.indexer.getDocumentCount() }, \"search: start\");\n\n let results: SearchResult[];\n if (this.persistent) {\n // SQLite FTS5 is the persistent search engine. Index once if the DB is\n // empty (mirroring the in-memory \"scan on first search\" contract), then\n // query FTS directly — no in-memory index to warm.\n const engine = this.engine();\n if (engine.conversations.count() === 0) {\n log.debug(\"search: persistent index empty, triggering scan\");\n const profiles = await this.resolveProfiles(options.profiles);\n const activeProfiles = profiles.filter((p) => p.enabled && p.scanHistory !== false);\n await engine.indexAll(activeProfiles, { ...options, limit: undefined, offset: undefined });\n }\n if (query.trim()) {\n // Filters go down into SQL so they run BEFORE the LIMIT. Applying them\n // to an already-truncated hit list would report \"no results\" whenever\n // the surviving conversations sit outside the unfiltered top-N — much\n // more likely now that the corpus is wide.\n const want = (options.limit ?? 50) + (options.offset ?? 0);\n results = engine\n .searchHits(query, want, {\n account: options.account,\n provider: options.provider,\n project: options.project,\n since: options.since ? parseSinceCutoff(options.since) : undefined,\n include: options.include,\n })\n .map(({ meta, body }) => ({\n meta,\n score: 1,\n matches: body\n ? [\n { field: CONTENT_FIELD, snippet: body.snippet, highlights: body.highlights },\n ...generateMatches(meta, query).filter((m) => m.field !== \"preview\"),\n ]\n : generateMatches(meta, query),\n }));\n } else {\n results = engine.recentMetas((options.limit ?? 50) + (options.offset ?? 0)).map((meta) => ({\n meta,\n score: 1,\n matches: [{ field: \"timestamp\", snippet: meta.preview }],\n }));\n }\n } else {\n if (this.indexer.getDocumentCount() === 0) {\n log.debug(\"search: index empty, triggering scan\");\n await this.scan({ ...options, limit: undefined, offset: undefined });\n }\n results = this.indexer.search(query, {\n fields: options.fields,\n limit: (options.limit ?? 50) * 2,\n });\n }\n\n if (options.include && options.include !== \"all\") {\n results = results.filter((r) => {\n switch (options.include) {\n case \"conversations\":\n return !r.meta.isSubagent && !r.meta.isTeammate;\n case \"subagents\":\n return r.meta.isSubagent;\n case \"teammates\":\n return r.meta.isTeammate;\n default:\n return true;\n }\n });\n }\n if (options.project) {\n const lower = options.project.toLowerCase();\n results = results.filter(\n (r) =>\n r.meta.projectPath.toLowerCase().includes(lower) ||\n r.meta.projectName.toLowerCase().includes(lower),\n );\n }\n if (options.account) {\n results = results.filter((r) => r.meta.account === options.account);\n }\n if (options.provider) {\n results = results.filter(\n (r) => (r.meta.provider ?? CLAUDE_CODE_PROVIDER) === options.provider,\n );\n }\n if (options.since) {\n const cutoff = parseSinceCutoff(options.since);\n results = results.filter((r) => new Date(r.meta.timestamp).getTime() >= cutoff.getTime());\n }\n\n const limit = options.limit ?? 50;\n const offset = options.offset ?? 0;\n const sliced = results.slice(offset, offset + limit);\n log.debug({ query, matched: results.length, returned: sliced.length }, \"search: complete\");\n return sliced;\n }\n\n async getConversation(\n id: string,\n _options?: GetConversationOptions,\n ): Promise<Conversation | null> {\n const log = getLogger();\n // Canonicalize the file-path form of the id so a native (join/watcher) and a\n // forward-slash (fast-glob) spelling of the same file resolve to one entry.\n // A sessionId (no separators) is unaffected.\n const cid = canonicalPath(id);\n const cached = this.conversationLRU.get(cid);\n if (cached) {\n log.debug({ id }, \"getConversation: cache hit\");\n return cached.conversation;\n }\n\n const meta = this.persistent\n ? this.engine().getByIdOrSession(cid)\n : (this.metadataCache.get(cid) ?? this.resolveSessionId(cid));\n if (!meta) {\n log.debug({ id }, \"getConversation: not found in metadata\");\n return null;\n }\n\n log.debug({ id, filePath: meta.filePath }, \"getConversation: cache miss, parsing\");\n try {\n // Persistent claude-code files parse through the resumable streaming fold\n // so the cached entry carries its resume point — refreshFile can then\n // extend it with only the appended bytes instead of evicting it. Codex\n // and legacy-mode parses stay on their existing parsers (no resume state;\n // a refresh evicts them as before).\n if (this.persistent && meta.provider !== CODEX_CLI_PROVIDER) {\n const parsed = await parseConversationResumable(meta.filePath, meta.account);\n if (parsed) this.conversationLRU.set(cid, parsed);\n return parsed?.conversation ?? null;\n }\n const conversation =\n meta.provider === CODEX_CLI_PROVIDER\n ? await parseCodexConversation(meta.filePath, meta.account)\n : await parseConversation(meta.filePath, meta.account);\n if (conversation) {\n this.conversationLRU.set(cid, { conversation });\n }\n return conversation;\n } catch (err) {\n log.warn({ id, filePath: meta.filePath, err }, \"getConversation: parse failed\");\n return null;\n }\n }\n\n // Return one bounded window of a conversation's messages plus the total\n // message count, so a caller can serve the last page and scroll back without\n // holding the whole conversation itself.\n //\n // The window is `[max(0, beforeIndex - limit), beforeIndex)` in chronological\n // order; `beforeIndex` defaults to `total` (the newest page). `fromIndex` is\n // the window's start index, so the caller can derive `has_more_older`\n // (fromIndex > 0). Returns null when the id can't be resolved/parsed — the\n // same contract as getConversation.\n //\n // Persistent mode: a true bounded read — the engine seeks from the nearest\n // checkpoint and parses only the requested window (checkpoints are built\n // lazily for large conversations). Windowed message indices are proven\n // identical to parseConversation().messages.slice(...) by the paged-reader\n // equivalence test.\n //\n // Legacy mode: parse-once-then-slice via getConversation (cached in the LRU),\n // which is identical by construction.\n async getConversationPage(\n id: string,\n options: GetConversationPageOptions,\n ): Promise<ConversationPage | null> {\n if (this.persistent) {\n return this.engine().getPage(canonicalPath(id), options);\n }\n\n const conversation = await this.getConversation(id);\n if (!conversation) return null;\n\n const { messages } = conversation;\n const total = messages.length;\n const { limit } = options;\n const beforeIndex = options.beforeIndex ?? total;\n\n const fromIndex = Math.max(0, beforeIndex - limit);\n const window = messages.slice(fromIndex, beforeIndex);\n\n return { messages: window, total, fromIndex };\n }\n\n // Parse one JSONL file directly and slice a page window — without any prior\n // scan() or metadata index. This is the cold-start fast path: a single\n // conversation can be served from one file parse (~ms) instead of waiting\n // for a full filesystem scan. The window is the same\n // `[max(0, beforeIndex - limit), beforeIndex)` slice as getConversationPage,\n // and the parsed Conversation is returned alongside so the caller can build\n // the response meta (projectPath, timestamp, messageCount, …) without a\n // second parse. Returns null when the file can't be parsed. `account` only\n // feeds the conversation's account field; \"default\" is the single-profile\n // fallback, matching refreshFile.\n async parseSingleFilePage(\n filePath: string,\n account: string | undefined,\n options: GetConversationPageOptions,\n ): Promise<SingleFilePage | null> {\n const conversation = await parseConversation(filePath, account ?? \"default\");\n if (!conversation) return null;\n\n const { messages } = conversation;\n const total = messages.length;\n const { limit } = options;\n const beforeIndex = options.beforeIndex ?? total;\n\n const fromIndex = Math.max(0, beforeIndex - limit);\n const window = messages.slice(fromIndex, beforeIndex);\n\n return { messages: window, total, fromIndex, conversation };\n }\n\n // Re-parse a single JSONL file and update every in-memory index in place —\n // metadata cache, sessionId index, project set, search index — and evict the\n // file's parsed conversation from the LRU so the next getConversation()\n // re-reads it. This lets a long-lived scanner stay current with a file that\n // grew after the initial scan() without paying for a full rescan.\n //\n // `account` defaults to the account already recorded for this file (the id\n // is the file path), falling back to \"default\" for a file the scanner has\n // not seen before. Returns the fresh ConversationMeta, or null when the file\n // no longer parses (missing/empty) — in which case any prior entry for it is\n // dropped from all indexes.\n //\n // Single-flighted per path: concurrent callers (stacked client retries, a\n // watcher tick racing a caller) await the one in-flight refresh instead of\n // each re-reading the file.\n private readonly refreshesInFlight = new Map<string, Promise<ConversationMeta | null>>();\n\n refreshFile(filePath: string, account?: string): Promise<ConversationMeta | null> {\n // Key every refresh off the canonical path so a fast-glob (forward slash)\n // and a watcher/join (native) spelling of the same file share one in-flight\n // refresh and hit the same stored row.\n const cpath = canonicalPath(filePath);\n const inFlight = this.refreshesInFlight.get(cpath);\n if (inFlight) return inFlight;\n const refresh = this.doRefreshFile(cpath, account).finally(() => {\n if (this.refreshesInFlight.get(cpath) === refresh) {\n this.refreshesInFlight.delete(cpath);\n }\n });\n this.refreshesInFlight.set(cpath, refresh);\n return refresh;\n }\n\n private async doRefreshFile(\n filePath: string,\n account?: string,\n ): Promise<ConversationMeta | null> {\n const log = getLogger();\n\n if (this.persistent) {\n const engine = this.engine();\n const previous = engine.getByIdOrSession(filePath);\n const resolvedAccount = account ?? previous?.account ?? \"default\";\n // Resolve the provider so a Codex file refreshes through the Codex reducer\n // rather than the Threadbase tail-read. Prefer stored metadata; fall back\n // to a structural sniff for a file the index hasn't seen yet.\n const provider = await this.resolveProviderForFile(filePath, previous);\n // force=false: let classify() decide. It already distinguishes\n // unchanged/appended/reindex/vanished correctly (including the\n // replace-with-larger-file case, cursor.ts), so the watcher's live-append\n // path resumes from the byte cursor instead of reparsing the whole file\n // on every debounced tick.\n const { meta, change } = await engine.indexFile(\n filePath,\n resolvedAccount,\n this.lastTier.name,\n undefined,\n readGitBranch,\n false,\n provider,\n );\n\n // Cached parses are keyed by whatever id getConversation() was asked for\n // — the file-path id or the sessionId — so collect every candidate key.\n const cacheKeys = new Set<string>();\n for (const m of [previous, meta]) {\n if (m) {\n cacheKeys.add(m.id);\n cacheKeys.add(m.sessionId);\n }\n }\n if (!meta || change === \"reindex\" || change === \"vanished\") {\n // Truncated/replaced/gone: the cached parse no longer matches the\n // bytes on disk — drop it so the next read re-parses.\n for (const key of cacheKeys) this.conversationLRU.delete(key);\n } else if (change === \"appended\") {\n // The file only grew: advance any cached parse by folding just the\n // appended bytes, leaving the LRU warm instead of evicting it.\n await this.extendCachedConversations(cacheKeys, filePath, meta.account);\n }\n // unchanged → the cached parse is still valid; leave it warm.\n\n log.debug({ filePath, change, kept: !!meta }, \"refreshFile: updated persistent index\");\n return meta;\n }\n\n const previous = this.metadataCache.get(filePath);\n const resolvedAccount = account ?? previous?.account ?? \"default\";\n\n let meta: ConversationMeta | null = null;\n let searchDoc = emptySearchDocument();\n try {\n meta = await parseMeta(filePath, resolvedAccount, this.lastTier, (entry) => {\n searchDoc = appendSearchDelta(searchDoc, extractSearchDelta(entry));\n });\n } catch (err) {\n log.warn({ filePath, err }, \"refreshFile: parseMeta threw\");\n meta = null;\n }\n\n // The LRU is keyed by the id used at getConversation() time, which can be\n // either the file-path id or the sessionId — evict both for the prior and\n // refreshed metas so no stale parse survives.\n const evict = (m: ConversationMeta | undefined | null) => {\n if (!m) return;\n this.conversationLRU.delete(m.id);\n this.conversationLRU.delete(m.sessionId);\n };\n evict(previous);\n evict(meta);\n\n if (!meta || meta.messageCount === 0) {\n if (previous) {\n this.metadataCache.delete(previous.id);\n this.removeFromSessionIndex(previous);\n this.indexer.removeDocument(previous.id);\n this.searchContents.delete(previous.id);\n }\n log.debug({ filePath }, \"refreshFile: dropped (no parseable messages)\");\n return null;\n }\n\n meta.gitBranch = readGitBranch(meta.projectPath);\n\n // A re-parse can change the sessionId mapping; clear the old entry first so\n // we don't leave this file listed under a stale sessionId.\n if (previous) this.removeFromSessionIndex(previous);\n this.metadataCache.set(meta.id, meta);\n this.addToSessionIndex(meta);\n this.projects.add(meta.projectPath);\n const searchContent = capForMemory(searchDoc, this.lastTier.snippetMax);\n this.searchContents.set(meta.id, searchContent);\n if (previous) {\n this.indexer.updateDocument(meta, searchContent);\n } else {\n this.indexer.addDocument(meta, searchContent);\n }\n\n log.debug(\n { filePath, messageCount: meta.messageCount },\n \"refreshFile: updated in-memory indexes\",\n );\n return meta;\n }\n\n // Advance every cached parse of an appended file by folding only the new\n // bytes through the conversation reducer — the in-memory analogue of the\n // persisted metadata fold. Entries without resume state (Codex) and entries\n // whose extension fails are evicted so the next read re-parses from scratch.\n private async extendCachedConversations(\n cacheKeys: Set<string>,\n filePath: string,\n account: string,\n ): Promise<void> {\n // Multiple keys can point at the same wrapper (path id + sessionId) —\n // group by wrapper so each cached parse is extended exactly once.\n const wrappers = new Map<CachedConversation, string[]>();\n for (const key of cacheKeys) {\n const wrapper = this.conversationLRU.get(key);\n if (!wrapper) continue;\n const keys = wrappers.get(wrapper) ?? [];\n keys.push(key);\n wrappers.set(wrapper, keys);\n }\n for (const [wrapper, keys] of wrappers) {\n if (!wrapper.resume) {\n for (const key of keys) this.conversationLRU.delete(key);\n continue;\n }\n try {\n const extended = await extendConversation(\n wrapper.conversation,\n wrapper.resume,\n filePath,\n account,\n );\n // Mutate the wrapper in place so every key holding it sees the update.\n wrapper.conversation = extended.conversation;\n wrapper.resume = extended.resume;\n } catch (err) {\n getLogger().warn({ filePath, err }, \"refreshFile: cache extension failed, evicting\");\n for (const key of keys) this.conversationLRU.delete(key);\n }\n }\n }\n\n getMetadataCache(): Map<string, ConversationMeta> {\n if (this.persistent) {\n const map = new Map<string, ConversationMeta>();\n for (const meta of this.engine().allActive()) map.set(meta.id, meta);\n return map;\n }\n return this.metadataCache;\n }\n\n // Collision-safe sessionId lookup. session_id is NOT unique (the parser falls\n // back to the file basename, and resumed/subagent sessions repeat ids), so\n // getConversation(sessionId) is a convenience that resolves to one match;\n // this returns every active conversation sharing the id, newest first.\n getConversationsBySessionId(sessionId: string): ConversationMeta[] {\n if (this.persistent) {\n return this.engine().getAllBySessionId(sessionId);\n }\n return this.sortBySessionPriority(this.sessionIdIndex.get(sessionId) ?? []);\n }\n\n getProjects(): string[] {\n const source = this.persistent ? this.engine().getProjects() : this.projects;\n const normalized = new Set<string>();\n for (const p of source) {\n normalized.add(p.replace(/\\/+$/, \"\"));\n }\n return Array.from(normalized).sort();\n }\n\n // ── File watching (persistent mode only) ────────────────────────────────\n\n // Subscribe to index changes. Events: \"change\" → ScannerChangeEvent,\n // \"error\" → Error. Returns this for chaining.\n on(event: \"change\", listener: (e: ScannerChangeEvent) => void): this;\n on(event: \"error\", listener: (err: Error) => void): this;\n on(event: string, listener: (...args: any[]) => void): this {\n this.emitter.on(event, listener);\n return this;\n }\n\n off(event: string, listener: (...args: any[]) => void): this {\n this.emitter.off(event, listener);\n return this;\n }\n\n // Start watching the active profiles' project dirs. A filesystem watcher\n // feeds debounced, path-deduplicated index jobs through a single-writer\n // queue; a periodic full rescan backstops any events the watcher misses\n // (sleep/wake, network FS, restarts). Emits \"change\" per indexed file.\n // Persistent mode only — throws in legacy mode (no durable index to update).\n async watch(options: WatchOptions = {}): Promise<void> {\n if (!this.persistent) {\n throw new Error(\"watch() requires persistent mode; construct with persistent enabled\");\n }\n if (this.watcher) return; // already watching\n\n const profiles = await this.resolveProfiles(options.profiles);\n const activeProfiles = profiles.filter((p) => p.enabled && p.scanHistory !== false);\n\n this.queue = new IndexQueue(async (job) => {\n try {\n // Create/change and delete both route through refreshFile: a removed\n // file stat-fails or parses empty, so refreshFile drops its row and\n // returns null. The index stays correct either way.\n const meta = await this.refreshFile(job.filePath, job.account);\n this.emitter.emit(\"change\", {\n filePath: job.filePath,\n account: job.account,\n meta,\n reason: job.reason,\n } satisfies ScannerChangeEvent);\n } catch (err) {\n this.emitter.emit(\"error\", err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n this.watcher = new FileWatcher(\n activeProfiles,\n (e) => {\n this.queue?.enqueue({\n filePath: e.filePath,\n account: e.account,\n reason: \"watcher\",\n });\n },\n { debounceMs: options.debounceMs },\n );\n await this.watcher.start();\n\n const periodicMs = options.periodicMs ?? 60_000;\n if (periodicMs > 0) {\n this.periodicTimer = setInterval(() => {\n // Track the job so unwatch() can await it — otherwise its post-await\n // engine.files access can land after the DB is closed.\n const job = this.periodicReconcile(activeProfiles).finally(() => {\n if (this.inFlightReconcile === job) this.inFlightReconcile = null;\n });\n this.inFlightReconcile = job;\n }, periodicMs);\n // Don't keep the process alive solely for the rescan timer.\n this.periodicTimer.unref?.();\n }\n\n getLogger().info({ profiles: activeProfiles.length, periodicMs }, \"watch: started\");\n }\n\n // Periodic correctness backstop: re-discover all files and enqueue them\n // through the same queue the watcher uses, so any add/change the watcher\n // missed still gets indexed and emits a \"change\" event. Vanished files\n // (active in the DB but no longer on disk) are enqueued too — refreshFile\n // drops them. Routing through the queue (not a direct scan) keeps event\n // emission and single-writer serialization unified.\n private async periodicReconcile(activeProfiles: Profile[]): Promise<void> {\n if (!this.queue) return;\n try {\n const engine = this.engine();\n const configDirs = activeProfiles.map((p) => ({\n projectsDir: getProjectsDir(p),\n account: p.id,\n }));\n const discovered = await discoverJsonlFiles(configDirs);\n const seen = new Set<string>();\n for (const { filePath, account } of discovered) {\n seen.add(filePath);\n // Only enqueue genuinely-changed files so an idle tick stays silent\n // (no spurious \"change\" events for thousands of unchanged files).\n const { change } = classify(filePath, engine.files.getByPath(filePath));\n if (change !== \"unchanged\") {\n this.queue.enqueue({ filePath, account, reason: \"periodic\" });\n }\n }\n // Files in the index but gone from disk → enqueue so refreshFile drops them.\n for (const path of engine.files.allActivePaths()) {\n if (!seen.has(path)) {\n this.queue.enqueue({ filePath: path, account: \"default\", reason: \"periodic\" });\n }\n }\n } catch (err) {\n this.emitter.emit(\"error\", err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n // Stop watching and drain any in-flight index jobs.\n async unwatch(): Promise<void> {\n if (this.periodicTimer) {\n clearInterval(this.periodicTimer);\n this.periodicTimer = null;\n }\n // Let a reconcile that already started finish touching the DB before we\n // drop the queue/engine below. Its own catch swallows errors; we only wait.\n if (this.inFlightReconcile) {\n await this.inFlightReconcile;\n }\n if (this.watcher) {\n await this.watcher.stop();\n this.watcher = null;\n }\n if (this.queue) {\n await this.queue.onIdle();\n this.queue = null;\n }\n }\n\n // ── Provider plumbing (in-memory path) ──────────────────────────────────\n\n // Build the flat parse worklist for the enabled providers. Threadbase\n // discovers under the active profiles' project dirs; Codex discovers under\n // the explicit codexRoots (opt-in — no default home scan).\n private async discoverWithProviders(\n activeProfiles: Profile[],\n options: ScanOptions,\n ): Promise<{ filePath: string; account: string; provider: ScannerProvider }[]> {\n const enabled = options.providers ?? [CLAUDE_CODE_PROVIDER];\n const work: { filePath: string; account: string; provider: ScannerProvider }[] = [];\n\n if (enabled.includes(CLAUDE_CODE_PROVIDER)) {\n const provider = new ThreadbaseProvider();\n const roots = activeProfiles.map((p) => `${getProjectsDir(p)}\\0${p.id}`);\n for (const f of await provider.discover(roots)) {\n work.push({ ...f, provider });\n }\n }\n if (enabled.includes(CODEX_CLI_PROVIDER) && (options.codexRoots?.length ?? 0) > 0) {\n const provider = new CodexCliProvider();\n for (const f of await provider.discover(options.codexRoots as string[])) {\n work.push({ ...f, provider });\n }\n }\n return work;\n }\n\n // Resolve which provider should (re)parse a file. Stored metadata wins; for a\n // file the index has not seen, sniff the first lines with each non-Threadbase\n // provider's canParse. Returns undefined for Threadbase (the engine's default\n // tail-read path).\n private async resolveProviderForFile(\n filePath: string,\n previous: ConversationMeta | null,\n ): Promise<ScannerProvider | undefined> {\n if (previous?.provider === CODEX_CLI_PROVIDER) return new CodexCliProvider();\n if (previous?.provider) return undefined; // known Threadbase\n let sample = \"\";\n try {\n const fd = openSync(filePath, \"r\");\n try {\n const buf = Buffer.alloc(8192);\n const n = readSync(fd, buf, 0, buf.length, 0);\n sample = buf.subarray(0, n).toString(\"utf8\");\n } finally {\n closeSync(fd);\n }\n } catch {\n return undefined;\n }\n const codex = new CodexCliProvider();\n if (codex.canParse(filePath, sample)) return codex;\n return undefined;\n }\n\n private addToSessionIndex(meta: ConversationMeta): void {\n const list = this.sessionIdIndex.get(meta.sessionId);\n if (list) {\n // Replace any existing entry for the same file (re-index), else append.\n const i = list.findIndex((m) => m.id === meta.id);\n if (i >= 0) list[i] = meta;\n else list.push(meta);\n } else {\n this.sessionIdIndex.set(meta.sessionId, [meta]);\n }\n }\n\n private removeFromSessionIndex(meta: ConversationMeta): void {\n const list = this.sessionIdIndex.get(meta.sessionId);\n if (!list) return;\n const next = list.filter((m) => m.id !== meta.id);\n if (next.length > 0) this.sessionIdIndex.set(meta.sessionId, next);\n else this.sessionIdIndex.delete(meta.sessionId);\n }\n\n // Deterministic single-result sessionId resolution: newest timestamp first,\n // tie-broken by absolute path ascending.\n private resolveSessionId(sessionId: string): ConversationMeta | undefined {\n return this.sortBySessionPriority(this.sessionIdIndex.get(sessionId) ?? [])[0];\n }\n\n private sortBySessionPriority(metas: ConversationMeta[]): ConversationMeta[] {\n return [...metas].sort((a, b) => {\n if (a.timestamp !== b.timestamp) return a.timestamp < b.timestamp ? 1 : -1;\n return a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0;\n });\n }\n\n private async resolveProfiles(profiles?: Profile[]): Promise<Profile[]> {\n // An explicit array (including an empty one) means \"use exactly these\" —\n // `[]` scans zero profiles. Only an absent argument falls back to defaults.\n // Treating `[]` as \"load defaults\" silently scanned the real ~/.claude\n // history, which looked like a hang for callers isolating from it.\n if (profiles) return profiles;\n return loadProfiles(DEFAULT_CONFIG_PATH);\n }\n\n private transformView(\n metas: ConversationMeta[],\n options: ScanOptions,\n ): ConversationMeta[] | TreeConversation[] | GroupedConversations {\n switch (options.view) {\n case \"tree\":\n return this.toTree(metas);\n case \"grouped\":\n return this.toGrouped(metas);\n default:\n return metas;\n }\n }\n\n private toTree(metas: ConversationMeta[]): TreeConversation[] {\n const parents: TreeConversation[] = [];\n const subagents: ConversationMeta[] = [];\n\n for (const meta of metas) {\n if (meta.isSubagent) {\n subagents.push(meta);\n } else {\n parents.push({ ...meta, subagents: [] });\n }\n }\n\n const parentById = new Map(parents.map((p) => [p.id, p]));\n for (const sub of subagents) {\n const parent = sub.parentSessionId ? parentById.get(sub.parentSessionId) : undefined;\n if (parent) {\n parent.subagents.push(sub);\n } else {\n parents.push({ ...sub, subagents: [] });\n }\n }\n\n return parents;\n }\n\n private toGrouped(metas: ConversationMeta[]): GroupedConversations {\n const groups: GroupedConversations = {};\n for (const meta of metas) {\n const key = meta.teamName || \"_default\";\n if (!groups[key]) groups[key] = [];\n groups[key].push(meta);\n }\n return groups;\n }\n}\n","export class LRUCache<K, V> {\n private map = new Map<K, V>();\n private readonly capacity: number;\n\n constructor(capacity: number) {\n this.capacity = capacity;\n }\n\n get(key: K): V | undefined {\n const value = this.map.get(key);\n if (value === undefined) return undefined;\n // Move to end (most recently used)\n this.map.delete(key);\n this.map.set(key, value);\n return value;\n }\n\n set(key: K, value: V): void {\n this.map.delete(key);\n this.map.set(key, value);\n if (this.map.size > this.capacity) {\n const oldest = this.map.keys().next();\n if (!oldest.done) this.map.delete(oldest.value);\n }\n }\n\n has(key: K): boolean {\n return this.map.has(key);\n }\n\n delete(key: K): boolean {\n return this.map.delete(key);\n }\n\n clear(): void {\n this.map.clear();\n }\n\n get size(): number {\n return this.map.size;\n }\n}\n","import { basename } from \"path\";\nimport { getShortProjectName } from \"../parser\";\nimport type { Conversation, ConversationMessage, TurnDuration } from \"../types\";\nimport { applyTeamInfo, type ConvReducerState, initialConvState } from \"./conversation-reducer\";\nimport { deriveSessionNameFromFirstMessage } from \"./metadata-reducer\";\nimport { streamMessages } from \"./paged-reader\";\n\n// A parsed conversation held in the scanner's LRU together with the resume\n// point needed to extend it in place when its file grows: the conversation\n// reducer state at EOF plus the byte/line offset just past the last complete\n// line. `resume` is absent for providers without a resumable reducer (Codex)\n// — a refresh evicts those entries instead of extending them.\nexport interface ConversationResume {\n state: ConvReducerState;\n offset: number;\n line: number;\n}\n\nexport interface CachedConversation {\n conversation: Conversation;\n resume?: ConversationResume;\n}\n\n// Fold [resume.offset, EOF) through the conversation reducer, collecting the\n// produced messages plus turn_duration entries. turn_duration lines are\n// consumed BEFORE the reducer — exactly as parseConversation does — so their\n// header fields (timestamps) never leak into the conversation state. Mutates\n// resume.state in place; on failure the caller must discard the cached entry.\nasync function foldTail(filePath: string, resume: ConversationResume) {\n const messages: ConversationMessage[] = [];\n const textParts: string[] = [];\n const turnDurations: TurnDuration[] = [];\n const end = await streamMessages(\n filePath,\n resume.offset,\n resume.line,\n resume.state,\n (message) => {\n messages.push(message);\n if (message.text) textParts.push(message.text);\n return false; // never stop early — fold to EOF\n },\n (entry) => {\n if (\n entry.type === \"system\" &&\n entry.subtype === \"turn_duration\" &&\n typeof entry.durationMs === \"number\"\n ) {\n turnDurations.push({\n durationMs: entry.durationMs as number,\n messageCount: (entry.messageCount as number) || 0,\n uuid: entry.uuid as string | undefined,\n });\n return true;\n }\n return false;\n },\n );\n return { messages, textParts, turnDurations, end };\n}\n\n// Assemble the Conversation shape exactly as parseConversation() does.\nfunction assemble(\n filePath: string,\n account: string,\n messages: ConversationMessage[],\n fullText: string,\n turnDurations: TurnDuration[],\n state: ConvReducerState,\n): Conversation {\n return {\n id: filePath,\n filePath,\n projectPath: state.cwd,\n projectName: getShortProjectName(state.cwd),\n sessionId: state.sessionId || basename(filePath, \".jsonl\"),\n sessionName:\n state.sessionName ||\n deriveSessionNameFromFirstMessage(messages.find((m) => m.role === \"user\" && m.text) ?? null),\n messages,\n fullText,\n timestamp: state.latestTimestamp || new Date().toISOString(),\n messageCount: messages.length,\n account,\n turnDurations: turnDurations.length > 0 ? turnDurations : undefined,\n lastPrompt: state.lastPrompt || undefined,\n };\n}\n\n// Full parse that also captures the resume point, so a later append can extend\n// the result in place instead of re-parsing the file. Identical to\n// parseConversation() by construction (same reducer, same assembly); the one\n// deliberate difference is torn-write discipline: a final line with no\n// trailing newline is treated as in-flight (not parsed yet) rather than\n// parsed, matching the persistent index's byte-cursor semantics.\nexport async function parseConversationResumable(\n filePath: string,\n account: string,\n): Promise<CachedConversation | null> {\n const resume: ConversationResume = { state: initialConvState(), offset: 0, line: 0 };\n const { messages, textParts, turnDurations, end } = await foldTail(filePath, resume);\n if (messages.length === 0) return null;\n applyTeamInfo(messages, resume.state);\n const conversation = assemble(\n filePath,\n account,\n messages,\n textParts.join(\" \"),\n turnDurations,\n resume.state,\n );\n return { conversation, resume: { state: resume.state, offset: end.offset, line: end.line } };\n}\n\n// Extend a cached conversation with only the bytes appended since its resume\n// point. Returns a NEW Conversation with a fresh messages array — the old\n// ARRAY is never mutated, but team info discovered in the delta enriches the\n// shared message objects in place (the same enrichment a full re-parse would\n// emit), so element-level immutability is not guaranteed.\nexport async function extendConversation(\n previous: Conversation,\n resume: ConversationResume,\n filePath: string,\n account: string,\n): Promise<CachedConversation> {\n const { messages: fresh, textParts, turnDurations, end } = await foldTail(filePath, resume);\n const messages = fresh.length > 0 ? previous.messages.concat(fresh) : previous.messages;\n // Team info discovered in the delta back-applies to earlier messages, the\n // same way a full parse applies it once at the end.\n applyTeamInfo(messages, resume.state);\n const fullText =\n textParts.length === 0\n ? previous.fullText\n : previous.fullText\n ? `${previous.fullText} ${textParts.join(\" \")}`\n : textParts.join(\" \");\n const allTurnDurations = (previous.turnDurations ?? []).concat(turnDurations);\n const conversation = assemble(\n filePath,\n account,\n messages,\n fullText,\n allTurnDurations,\n resume.state,\n );\n return { conversation, resume: { state: resume.state, offset: end.offset, line: end.line } };\n}\n","import { createReadStream } from \"fs\";\nimport { setImmediate as yieldToEventLoop } from \"timers/promises\";\nimport type { ConversationMessage, ConversationPage } from \"../types\";\nimport {\n applyTeamInfo,\n type ConvReducerState,\n initialConvState,\n reduceConvLine,\n} from \"./conversation-reducer\";\nimport { YIELD_EVERY_LINES } from \"./jsonl-tail-reader\";\nimport type { Checkpoint } from \"./repositories/checkpoints.repo\";\n\n// Snapshot the parser state every CHECKPOINT_INTERVAL messages.\nexport const CHECKPOINT_INTERVAL = 500;\n\n// Stream a JSONL file from `startOffset`, folding each complete line through the\n// conversation reducer. `onMessage` receives every produced message plus the\n// byte offset of the line that FOLLOWS it (a safe resume point) and the current\n// state. `onEntry` (optional) sees every parsed entry first — return true to\n// consume it before it reaches the reducer (parseConversation's turn_duration\n// handling). Returns the offset/line just past the last COMPLETE line consumed\n// — a trailing torn line is never included, so the result is a safe resume\n// point for a later incremental fold.\nexport async function streamMessages(\n filePath: string,\n startOffset: number,\n startLine: number,\n state: ConvReducerState,\n // Return true to stop streaming (window filled).\n onMessage: (message: ConversationMessage, nextByteOffset: number, nextLine: number) => boolean,\n onEntry?: (entry: Record<string, unknown>) => boolean,\n): Promise<{ offset: number; line: number }> {\n const stream = createReadStream(filePath, { start: startOffset, encoding: \"utf8\" });\n let buffer = \"\";\n let offset = startOffset;\n let line = startLine;\n let sinceYield = 0;\n\n for await (const chunk of stream) {\n buffer += chunk;\n let nl: number;\n // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic line scan\n while ((nl = buffer.indexOf(\"\\n\")) >= 0) {\n const lineWithNewline = buffer.slice(0, nl + 1);\n const text = lineWithNewline.trimEnd();\n buffer = buffer.slice(nl + 1);\n offset += Buffer.byteLength(lineWithNewline, \"utf8\");\n line += 1;\n\n // Same cooperative yield as the tail reader: without it, buffered chunks\n // drain through microtasks and a full-file walk blocks the event loop.\n if (++sinceYield >= YIELD_EVERY_LINES) {\n sinceYield = 0;\n await yieldToEventLoop();\n }\n\n if (text.length === 0) continue;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(text);\n } catch {\n continue;\n }\n if (onEntry?.(entry)) continue;\n const message = reduceConvLine(state, entry);\n if (message && onMessage(message, offset, line)) {\n stream.destroy();\n return { offset, line };\n }\n }\n }\n return { offset, line };\n}\n\n// Build checkpoints by streaming the file once. Each checkpoint captures the\n// byte offset + reducer state immediately AFTER message (k*interval - 1), i.e.\n// a clean resume point for message index k*interval. Pass `from` (the last\n// persisted checkpoint) to EXTEND an existing chain: the stream resumes from\n// its offset/state and only checkpoints past it are returned — an append costs\n// O(new bytes), never a re-walk of the immutable prefix.\nexport async function buildCheckpoints(\n filePath: string,\n interval = CHECKPOINT_INTERVAL,\n from: Checkpoint | null = null,\n): Promise<Checkpoint[]> {\n const checkpoints: Checkpoint[] = [];\n const state = from ? from.state : initialConvState();\n let index = from ? from.messageIndex : 0;\n\n await streamMessages(\n filePath,\n from?.byteOffset ?? 0,\n from?.lineNumber ?? 0,\n state,\n (_msg, nextOffset, nextLine) => {\n index += 1;\n // After consuming `index` messages, if the next message index is a multiple\n // of the interval, snapshot a resume point for it.\n if (index % interval === 0) {\n checkpoints.push({\n messageIndex: index,\n byteOffset: nextOffset,\n lineNumber: nextLine,\n state: structuredClone(state),\n });\n }\n return false; // never stop early — we want the whole file\n },\n );\n\n return checkpoints;\n}\n\n// Read a bounded message window [fromIndex, beforeIndex) plus the total message\n// count, seeking from the nearest checkpoint <= fromIndex. `total` requires\n// knowing the message count; callers pass it (from the indexed summary) so we\n// never scan the whole file just to count.\nexport async function readPage(\n filePath: string,\n total: number,\n options: { beforeIndex?: number; limit: number },\n floor: Checkpoint | null,\n): Promise<ConversationPage> {\n const beforeIndex = options.beforeIndex ?? total;\n const fromIndex = Math.max(0, beforeIndex - options.limit);\n\n const state = floor ? structuredClone(floor.state) : initialConvState();\n const startOffset = floor ? floor.byteOffset : 0;\n const startLine = floor ? floor.lineNumber : 0;\n let index = floor ? floor.messageIndex : 0;\n\n const window: ConversationMessage[] = [];\n await streamMessages(filePath, startOffset, startLine, state, (message) => {\n const current = index;\n index += 1;\n if (current >= fromIndex && current < beforeIndex) window.push(message);\n return index >= beforeIndex; // stop once the window is filled\n });\n\n applyTeamInfo(window, state);\n return { messages: window, total, fromIndex };\n}\n","import { createReadStream } from \"fs\";\nimport { setImmediate as yieldToEventLoop } from \"timers/promises\";\nimport type { ContentTier } from \"../types\";\nimport { type ReducerState, reduceLine } from \"./metadata-reducer\";\n\n// Cooperatively yield every N folded lines so a full parse of a large file\n// (cold index, truncation fallback) never blocks the event loop for its whole\n// duration — buffered stream chunks otherwise drain through microtasks without\n// ever reaching the macrotask queue.\nexport const YIELD_EVERY_LINES = 500;\n\nexport interface TailReadResult {\n // Byte offset of the first un-consumed byte (start of the trailing partial\n // line, or EOF). Safe to persist as the next cursor.\n newOffset: number;\n newLine: number;\n parsedLines: number;\n badJsonLines: number;\n}\n\n// Stream a JSONL file from `startOffset` to EOF, folding each COMPLETE line into\n// `state`. A trailing line with no newline (a writer mid-append) is left\n// un-consumed: the offset is not advanced past it, so the next pass re-reads it\n// once it's complete. Offsets are byte offsets (Buffer.byteLength), never string\n// .length, so multibyte content stays aligned. (Spec §7.2–7.4.)\n// `onEntry` rides the same line loop as the metadata fold, so search-document\n// extraction costs no extra file read — important when a cold index or a v5\n// migration re-reads the whole corpus.\nexport async function tailReduce(\n filePath: string,\n startOffset: number,\n startLine: number,\n state: ReducerState,\n tier: ContentTier,\n onEntry?: (entry: Record<string, unknown>) => void,\n): Promise<TailReadResult> {\n const stream = createReadStream(filePath, { start: startOffset, encoding: \"utf8\" });\n\n let buffer = \"\";\n let offset = startOffset;\n let line = startLine;\n let parsedLines = 0;\n let sinceYield = 0;\n\n for await (const chunk of stream) {\n buffer += chunk;\n let nl: number;\n // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic line scan\n while ((nl = buffer.indexOf(\"\\n\")) >= 0) {\n const lineWithNewline = buffer.slice(0, nl + 1);\n const text = lineWithNewline.trimEnd();\n buffer = buffer.slice(nl + 1);\n\n if (text.length > 0) {\n try {\n const entry = JSON.parse(text) as Record<string, unknown>;\n reduceLine(state, entry, tier);\n onEntry?.(entry);\n } catch {\n state.badJsonLines++;\n }\n parsedLines++;\n }\n\n offset += Buffer.byteLength(lineWithNewline, \"utf8\");\n line++;\n if (++sinceYield >= YIELD_EVERY_LINES) {\n sinceYield = 0;\n await yieldToEventLoop();\n }\n }\n }\n\n return { newOffset: offset, newLine: line, parsedLines, badJsonLines: state.badJsonLines };\n}\n","import { createHash } from \"crypto\";\nimport { closeSync, openSync, readSync, statSync } from \"fs\";\nimport type { FileRow } from \"./repositories/conversation-files.repo\";\n\nexport type FileChange = \"unchanged\" | \"appended\" | \"reindex\" | \"vanished\";\n\nexport interface FileStat {\n size: number;\n mtimeMs: number;\n}\n\n// Cheap content fingerprint: hash of the first + last 4KB plus the size. Lets\n// us detect a same-size in-place rewrite (e.g. a file replaced atomically)\n// without hashing an entire 200MB file. (Spec §9.4.)\nconst FP_BYTES = 4096;\n\nexport function fingerprint(filePath: string, size: number): string {\n const hash = createHash(\"sha1\");\n hash.update(String(size));\n const fd = openSync(filePath, \"r\");\n try {\n const head = Buffer.alloc(Math.min(FP_BYTES, size));\n if (head.length > 0) {\n readSync(fd, head, 0, head.length, 0);\n hash.update(head);\n }\n if (size > FP_BYTES) {\n const tailLen = Math.min(FP_BYTES, size);\n const tail = Buffer.alloc(tailLen);\n readSync(fd, tail, 0, tailLen, size - tailLen);\n hash.update(tail);\n }\n } finally {\n closeSync(fd);\n }\n return hash.digest(\"hex\");\n}\n\n// Classify how a file changed relative to its persisted cursor row. Drives\n// whether the indexer skips, tail-reads from the cursor, or reindexes from 0.\n// (Spec §9.)\nexport function classify(\n filePath: string,\n existing: FileRow | undefined,\n): { change: FileChange; stat?: FileStat } {\n let stat: FileStat;\n try {\n const s = statSync(filePath);\n stat = { size: s.size, mtimeMs: s.mtimeMs };\n } catch {\n return { change: \"vanished\" };\n }\n\n if (!existing || existing.status !== \"active\" || existing.last_indexed_offset === 0) {\n return { change: \"reindex\", stat };\n }\n\n // Smaller than where we left off → truncated/replaced. Reindex.\n if (stat.size < existing.last_indexed_offset) {\n return { change: \"reindex\", stat };\n }\n\n // Same size + same mtime → nothing to do.\n if (stat.size === existing.size_bytes && stat.mtimeMs === existing.mtime_ms) {\n return { change: \"unchanged\", stat };\n }\n\n // Same size but mtime moved → possible in-place rewrite. Fingerprint to be\n // sure; a changed fingerprint means the bytes we already indexed are stale.\n if (stat.size === existing.last_indexed_offset) {\n const fp = fingerprint(filePath, stat.size);\n if (existing.content_fingerprint && fp !== existing.content_fingerprint) {\n return { change: \"reindex\", stat };\n }\n return { change: \"unchanged\", stat };\n }\n\n // Grew past the cursor. Usually a genuine append — but an atomic replace with\n // a DIFFERENT, LONGER file also lands here (size > offset, mtime moved). If we\n // trusted \"appended\" blindly we'd resume the OLD reducer state and fold only\n // the new tail, blending two conversations. So re-check the region we already\n // folded before taking the fast path.\n //\n // The stored content_fingerprint was computed as fingerprint(_, size_bytes)\n // over the old file. It describes the same byte range as fingerprint(current,\n // last_indexed_offset) ONLY when size_bytes === last_indexed_offset (no\n // trailing partial line was pending at index time — the normal case). When\n // that holds and the fingerprints match, we treat it as a real append and\n // resume the cursor; a mismatch means the indexed region changed → reindex.\n // When it doesn't hold (a partial line was pending), we can't reconstruct the\n // stored window, so we reindex to be safe.\n //\n // ponytail: fingerprint() only covers the first 4KB + the 4KB ending at the\n // offset — NOT the whole [0, offset) prefix. So this catches every real atomic\n // replace (any rewrite changes line 1 → different head 4KB) but NOT a rewrite\n // that keeps both 8KB windows byte-identical and differs only in the middle.\n // That's the same bounded ceiling the existing same-size edge-fingerprint has,\n // accepted by design. Upgrade to a full-prefix hash (or an offset-keyed head\n // fingerprint column) only if a mid-file-preserving-envelope rewrite is ever\n // observed in practice — and that same column would also let the pending-\n // partial-then-grew case resume instead of reindexing.\n if (\n existing.content_fingerprint &&\n existing.size_bytes === existing.last_indexed_offset &&\n fingerprint(filePath, existing.last_indexed_offset) === existing.content_fingerprint\n ) {\n return { change: \"appended\", stat };\n }\n return { change: \"reindex\", stat };\n}\n","import { statSync } from \"fs\";\nimport { canonicalPath } from \"../canonical-path\";\nimport { readGitBranch } from \"../git\";\nimport { getLogger } from \"../logger\";\nimport { getProjectsDir } from \"../profiles\";\nimport { CodexCliProvider, parseCodexConversation } from \"../providers/codex-cli\";\nimport { parseMetaWithProvider } from \"../providers/parse\";\nimport {\n CLAUDE_CODE_PROVIDER,\n CODEX_CLI_PROVIDER,\n type ScannerProvider,\n} from \"../providers/provider\";\nimport { appendSearchDelta, emptySearchDocument, extractSearchDelta } from \"../search-document\";\nimport { resolveTier } from \"../tiers\";\nimport type {\n ConversationMeta,\n ConversationPage,\n GetConversationPageOptions,\n Profile,\n ScanOptions,\n SearchHighlight,\n} from \"../types\";\nimport { classify, type FileChange, fingerprint } from \"./cursor\";\nimport { type DB, openDatabase } from \"./db\";\nimport { discoverJsonlFilesGated, FULL_RECONCILE_EVERY_N_SCANS } from \"./dir-watermark\";\nimport { type TailReadResult, tailReduce } from \"./jsonl-tail-reader\";\nimport { finalizeMeta, initialReducerState, type ReducerState } from \"./metadata-reducer\";\nimport { buildCheckpoints, CHECKPOINT_INTERVAL, readPage } from \"./paged-reader\";\nimport { CheckpointsRepo } from \"./repositories/checkpoints.repo\";\nimport { ConversationFilesRepo } from \"./repositories/conversation-files.repo\";\nimport { ConversationsRepo } from \"./repositories/conversations.repo\";\nimport { FtsRepo, type FtsSearchFilters } from \"./repositories/fts.repo\";\nimport { ScannedDirsRepo } from \"./repositories/scanned-dirs.repo\";\nimport { buildSidecar, writeSidecar } from \"./sidecar\";\n\nconst BATCH_SIZE = 12;\n\nexport interface EngineSearchHit {\n meta: ConversationMeta;\n // Null when only metadata matched — the caller falls back to generateMatches.\n body: { snippet: string; highlights: SearchHighlight[] } | null;\n}\n\n// Persistent indexing engine. Owns the SQLite connection and the discover ->\n// classify -> tail-read -> upsert pipeline. Query helpers return ConversationMeta\n// straight from the DB so the scanner facade can run the existing\n// filter/sort/view pipeline over them, guaranteeing parity with the in-memory\n// path.\n//\n// Indexing is incremental: an appended file resumes the persisted reducer fold\n// from its byte cursor and reads only the new bytes (cursor.ts + the tail\n// reader); a truncated/replaced file reindexes from offset 0.\nexport class PersistentEngine {\n readonly db: DB;\n readonly files: ConversationFilesRepo;\n readonly conversations: ConversationsRepo;\n readonly fts: FtsRepo;\n readonly checkpoints: CheckpointsRepo;\n readonly scannedDirs: ScannedDirsRepo;\n // When true, write a portable <file>.idx.json sidecar next to each indexed\n // JSONL. Off by default.\n private readonly sidecar: boolean;\n // Counts indexAll() passes so the dir-mtime gate's full-reconcile backstop\n // (FULL_RECONCILE_EVERY_N_SCANS) can fire periodically. In-memory only: a\n // restart just means the first few post-restart scans don't force an early\n // backstop pass, which is harmless (watermarks themselves persist in the DB).\n private scanCount = 0;\n // In-flight checkpoint build/extension per file, so concurrent getPage\n // callers share one stream instead of each walking the file.\n private readonly checkpointBuilds = new Map<string, Promise<void>>();\n\n constructor(dbPath: string, options: { sidecar?: boolean } = {}) {\n this.db = openDatabase(dbPath);\n this.files = new ConversationFilesRepo(this.db);\n this.conversations = new ConversationsRepo(this.db);\n this.fts = new FtsRepo(this.db);\n this.checkpoints = new CheckpointsRepo(this.db);\n this.scannedDirs = new ScannedDirsRepo(this.db);\n this.sidecar = options.sidecar ?? false;\n }\n\n close(): void {\n this.db.close();\n }\n\n // Discover all JSONL files under the active profiles, (re)parse files that\n // changed since the last index, and upsert their metadata. Returns the number\n // of files seen on disk this pass (the scan \"scanned\" count).\n async indexAll(activeProfiles: Profile[], options: ScanOptions): Promise<{ scanned: number }> {\n const log = getLogger();\n const tier = resolveTier(options.tier ?? \"standard\", options.tiers);\n\n const enabled = options.providers ?? [CLAUDE_CODE_PROVIDER];\n\n // Each discovered file carries the provider that should parse it. claude-code\n // files fold through the byte-offset-resumable tail reader; Codex files\n // (opt-in, only under explicit codexRoots) reparse from offset 0.\n const discovered: { filePath: string; account: string; provider?: ScannerProvider }[] = [];\n\n if (enabled.includes(CLAUDE_CODE_PROVIDER)) {\n const configDirs = activeProfiles.map((p) => ({\n projectsDir: getProjectsDir(p),\n account: p.id,\n }));\n // Escape hatch: an explicit fullRescan bypasses the dir-mtime gate\n // entirely — an explicit refresh is precisely the \"don't trust the\n // gate, check for real\" signal. Otherwise every FULL_RECONCILE_EVERY_N_SCANS\n // passes also bypasses it, self-healing any add/remove a filesystem that\n // doesn't honor directory mtimes might have hidden from the gate.\n this.scanCount++;\n const forceFullGlob =\n options.fullRescan === true || this.scanCount % FULL_RECONCILE_EVERY_N_SCANS === 0;\n const gated = await discoverJsonlFilesGated(configDirs, this.files, this.scannedDirs, {\n fullRescan: forceFullGlob,\n });\n for (const f of gated) discovered.push(f);\n }\n\n const codex = new CodexCliProvider();\n if (enabled.includes(CODEX_CLI_PROVIDER) && (options.codexRoots?.length ?? 0) > 0) {\n for (const f of await codex.discover(options.codexRoots as string[])) {\n discovered.push({ ...f, provider: codex });\n }\n }\n let scanned = 0;\n\n const gitBranchMemo = new Map<string, string | null>();\n const resolveGitBranch = (projectPath: string): string | null => {\n if (gitBranchMemo.has(projectPath)) {\n return gitBranchMemo.get(projectPath) ?? null;\n }\n const branch = readGitBranch(projectPath);\n gitBranchMemo.set(projectPath, branch);\n return branch;\n };\n\n for (let i = 0; i < discovered.length; i += BATCH_SIZE) {\n const batch = discovered.slice(i, i + BATCH_SIZE);\n const results = await Promise.all(\n batch.map(async ({ filePath, account, provider }) => {\n const { meta } = await this.indexFile(\n filePath,\n account,\n tier.name,\n options.tiers,\n resolveGitBranch,\n false,\n provider,\n );\n return meta;\n }),\n );\n const kept = results.filter((m): m is ConversationMeta => m != null);\n if (kept.length > 0) options.onBatch?.(kept);\n scanned += batch.length;\n options.onProgress?.(scanned, discovered.length);\n }\n\n // Reconcile deletions: any previously-active file that wasn't discovered\n // this pass is gone from disk — mark it deleted. This is the correctness\n // backstop for unlink events the watcher may have missed (spec §9.5).\n //\n // Scope the reconcile to the accounts THIS scan actually covered. The index\n // is shared across profiles/accounts (one index.db), so a scan of account A\n // must not mark account B's files deleted just because B's files weren't in\n // A's discovered set — B is simply out of this scan's scope, not gone from\n // disk. Covered = the claude-code profile ids scanned (when that provider is\n // enabled) plus \"codex\" (when codex roots were scanned).\n const coveredAccounts = new Set<string>();\n if (enabled.includes(CLAUDE_CODE_PROVIDER)) {\n for (const p of activeProfiles) coveredAccounts.add(p.id);\n }\n if (enabled.includes(CODEX_CLI_PROVIDER) && (options.codexRoots?.length ?? 0) > 0) {\n coveredAccounts.add(\"codex\");\n }\n // Canonical form on both sides: the stored paths are canonical, and a\n // discovery source that emits native separators would otherwise leave every\n // one of its files out of the seen-set and get them marked deleted.\n const seen = new Set(discovered.map((d) => canonicalPath(d.filePath)));\n for (const path of this.files.activePathsByAccounts([...coveredAccounts])) {\n if (!seen.has(path)) this.markDeleted(path);\n }\n\n log.info({ scanned, indexed: this.conversations.count() }, \"persistent: indexAll complete\");\n return { scanned };\n }\n\n // (Re)index a single file. Classifies the change vs. the persisted cursor:\n // unchanged → return the stored summary; appended → resume the fold and read\n // only new bytes; reindex/force → fold from offset 0. Writes the summary +\n // cursor + reducer state in one transaction so a crash never leaves a\n // half-written row or an over-advanced cursor. Returns the classification\n // alongside the meta so callers (refreshFile) can keep, extend, or evict\n // their own per-file caches without re-stat'ing the file (racy) themselves.\n async indexFile(\n rawFilePath: string,\n account: string,\n tierName: string,\n customTiers: ScanOptions[\"tiers\"],\n resolveGitBranch: (projectPath: string) => string | null,\n force = false,\n provider?: ScannerProvider,\n ): Promise<{ meta: ConversationMeta | null; change: FileChange }> {\n const log = getLogger();\n const tier = resolveTier(tierName, customTiers);\n\n // Canonicalize once, here: everything downstream (the cursor row, the\n // conversation's source_path/id, the checkpoint + FTS keys, the sidecar, and\n // the meta handed back to the caller) then keys off the one canonical\n // spelling, whichever separator style the caller passed in.\n const filePath = canonicalPath(rawFilePath);\n const existing = this.files.getByPath(filePath);\n const { change, stat } = classify(filePath, existing);\n\n if (change === \"vanished\" || !stat) {\n // File gone between discovery and indexing — treat as deleted.\n this.markDeleted(filePath);\n return { meta: null, change: \"vanished\" };\n }\n // Unchanged: serve the persisted summary without re-reading (unless a\n // caller forces a re-parse, e.g. refreshFile after a same-size edit).\n if (change === \"unchanged\" && !force) {\n return { meta: this.conversations.getBySourcePath(filePath), change };\n }\n\n // ponytail: Codex reparses from offset 0 on every change rather than\n // resuming a byte cursor like claude-code. Codex rollout sessions are small,\n // so a full reparse is cheap; the resumable-fold path stays claude-code-only.\n // Upgrade path if Codex files ever get large: give CodexAccumulator the same\n // serialized-reducer-state treatment and route it through tailReduce.\n if (provider && provider.name !== CLAUDE_CODE_PROVIDER) {\n const meta = await this.indexFileWithProvider(\n provider,\n filePath,\n account,\n tier,\n stat,\n resolveGitBranch,\n );\n return { meta, change };\n }\n\n // appended → resume the persisted fold from the cursor; reindex/force →\n // start fresh from offset 0. This is the incremental win: an append only\n // reads the newly-written bytes.\n const resume = change === \"appended\" && !force && existing?.reducer_state;\n const state: ReducerState = resume\n ? (JSON.parse(existing.reducer_state as string) as ReducerState)\n : initialReducerState();\n const startOffset = resume ? existing.last_indexed_offset : 0;\n const startLine = resume ? existing.last_indexed_line : 0;\n\n // Search extraction is a sibling fold over the same lines as the metadata\n // reducer — no second read of the file, and an append only ever extracts\n // from the newly-read region.\n let searchDelta = emptySearchDocument();\n const collectSearch = (entry: Record<string, unknown>) => {\n searchDelta = appendSearchDelta(searchDelta, extractSearchDelta(entry));\n };\n\n let result: TailReadResult;\n try {\n result = await tailReduce(filePath, startOffset, startLine, state, tier, collectSearch);\n } catch (err) {\n log.warn({ filePath, err }, \"persistent: tail read failed\");\n return { meta: null, change };\n }\n\n const meta = finalizeMeta(state, filePath, account, tier);\n if (!meta) {\n this.markDeleted(filePath);\n return { meta: null, change };\n }\n meta.gitBranch = resolveGitBranch(meta.projectPath);\n\n const fp = stat.size > 0 ? fingerprint(filePath, stat.size) : null;\n const fileId = this.files.ensure(filePath, account);\n\n // A resumed fold extends the buckets already in FTS; a from-0 fold replaces\n // them. Tail-append means a full bucket still admits the new material and\n // drops its oldest instead of freezing.\n const base = resume\n ? (this.fts.readDocument(fileId) ?? emptySearchDocument())\n : emptySearchDocument();\n const searchDoc = appendSearchDelta(base, searchDelta);\n\n const upsert = this.db.transaction(() => {\n this.conversations.upsert(fileId, meta, state.pageMessageCount);\n // Re-tokenizing ~128 KB is the expensive part of an append; skip it when\n // the row (body + metadata) would come out identical.\n if (!this.fts.isCurrent(fileId, meta, searchDoc)) {\n this.fts.upsert(fileId, meta, searchDoc);\n }\n // Checkpoints cover the file's immutable prefix, so an append (a resumed\n // fold) leaves them valid — drop them only when the fold restarted from\n // offset 0 (truncate/replace/new). They're rebuilt lazily on page access.\n if (!resume) this.checkpoints.remove(filePath);\n this.files.updateCursor(fileId, {\n sizeBytes: stat.size,\n mtimeMs: stat.mtimeMs,\n // Advance only to the last fully-parsed line; a trailing partial line\n // is left for the next pass.\n offset: result.newOffset,\n line: result.newLine,\n reducerState: JSON.stringify(state),\n fingerprint: fp,\n status: \"active\",\n });\n });\n upsert();\n\n if (this.sidecar) {\n writeSidecar(\n filePath,\n buildSidecar(\n meta,\n {\n sizeBytes: stat.size,\n mtimeMs: stat.mtimeMs,\n offset: result.newOffset,\n line: result.newLine,\n },\n new Date().toISOString(),\n ),\n );\n }\n\n log.debug(\n { filePath, change, bytesRead: result.newOffset - startOffset, msgs: meta.messageCount },\n \"persistent: indexed file\",\n );\n return { meta, change };\n }\n\n // Index a non-Threadbase provider file: full reparse from offset 0 through the\n // provider's reducer/finalize, then the same upsert + FTS write + cursor bump\n // the Threadbase path uses. The cursor records size/mtime (and offset = size)\n // so the next pass classifies an unchanged file as \"unchanged\" and skips it;\n // any change reparses from 0 again. No reducer_state is persisted.\n private async indexFileWithProvider(\n provider: ScannerProvider,\n filePath: string,\n account: string,\n tier: ReturnType<typeof resolveTier>,\n stat: { size: number; mtimeMs: number },\n resolveGitBranch: (projectPath: string) => string | null,\n ): Promise<ConversationMeta | null> {\n const log = getLogger();\n\n // Codex always reparses from byte 0, so the search document is rebuilt whole\n // from the same pass rather than tail-extended.\n let searchDoc = emptySearchDocument();\n const meta = await parseMetaWithProvider(provider, filePath, account, tier, (entry) => {\n searchDoc = appendSearchDelta(searchDoc, extractSearchDelta(entry));\n });\n if (!meta) {\n this.markDeleted(filePath);\n return null;\n }\n // The provider may already know its branch (Codex reads it from\n // session_meta). Only walk the filesystem when it doesn't.\n if (meta.gitBranch === null && meta.projectPath) {\n meta.gitBranch = resolveGitBranch(meta.projectPath);\n }\n\n const fp = stat.size > 0 ? fingerprint(filePath, stat.size) : null;\n const fileId = this.files.ensure(filePath, account);\n const upsert = this.db.transaction(() => {\n this.conversations.upsert(fileId, meta, meta.messageCount);\n if (!this.fts.isCurrent(fileId, meta, searchDoc)) {\n this.fts.upsert(fileId, meta, searchDoc);\n }\n this.checkpoints.remove(filePath);\n this.files.updateCursor(fileId, {\n sizeBytes: stat.size,\n mtimeMs: stat.mtimeMs,\n // offset = size marks the file fully consumed (non-zero so the next pass\n // can classify it \"unchanged\"); no resumable reducer state is kept.\n offset: stat.size,\n line: 0,\n reducerState: null,\n fingerprint: fp,\n status: \"active\",\n });\n });\n upsert();\n\n log.debug(\n { filePath, provider: provider.name, msgs: meta.messageCount },\n \"persistent: indexed provider file\",\n );\n return meta;\n }\n\n private markDeleted(filePath: string): void {\n const existing = this.files.getByPath(filePath);\n if (!existing) return;\n const tx = this.db.transaction(() => {\n this.conversations.deleteByFileId(existing.id);\n this.fts.remove(existing.id);\n this.checkpoints.remove(filePath);\n this.files.setStatus(existing.id, \"deleted\");\n });\n tx();\n }\n\n // ── Query helpers (read straight from SQLite) ───────────────────────────\n\n allActive(): ConversationMeta[] {\n return this.conversations.allActive();\n }\n\n getByIdOrSession(id: string): ConversationMeta | null {\n return this.conversations.getByIdOrSession(id);\n }\n\n getAllBySessionId(sessionId: string): ConversationMeta[] {\n return this.conversations.getAllBySessionId(sessionId);\n }\n\n // Ranked hits matching the FTS query, best first, each already resolved to its\n // active conversation row and carrying the body excerpt when the match was in\n // the conversation body.\n //\n // Filters are passed down into SQL rather than applied to the result: with a\n // wide corpus, filtering an already-LIMITed list drops conversations that\n // would have matched.\n searchHits(query: string, limit: number, filters: FtsSearchFilters = {}): EngineSearchHit[] {\n const hits: EngineSearchHit[] = [];\n for (const hit of this.fts.search(query, limit, filters)) {\n const meta = this.conversations.getBySourcePath(hit.sourcePath);\n if (meta) hits.push({ meta, body: hit.body });\n }\n return hits;\n }\n\n // Empty-query listing, mirroring the in-memory indexer's behavior.\n recentMetas(limit: number): ConversationMeta[] {\n return this.conversations.recent(limit);\n }\n\n getProjects(): string[] {\n return this.conversations.distinctProjects();\n }\n\n // Bounded conversation page: read only the requested window from the file,\n // seeking from the nearest checkpoint. Returns null if the id can't be\n // resolved to an indexed conversation. For conversations large enough to span\n // a checkpoint interval, checkpoints are built lazily on first access and\n // reused thereafter. Smaller conversations read from the start (cheap).\n async getPage(id: string, options: GetConversationPageOptions): Promise<ConversationPage | null> {\n const meta = this.conversations.getByIdOrSession(id);\n if (!meta) return null;\n const filePath = meta.filePath;\n\n // Codex files don't fold through the conversation-reducer the bounded reader\n // (readPage) uses — that reducer only understands the claude-code line shape,\n // so readPage would return an empty window for a Codex file. Codex rollout\n // sessions are small (already reparsed from offset 0 on every change), so\n // parse the whole conversation and slice the window — identical math to the\n // claude-code path and to the legacy getConversationPage slice.\n if (meta.provider === CODEX_CLI_PROVIDER) {\n const conversation = await parseCodexConversation(filePath, meta.account);\n if (!conversation) return null;\n const { messages } = conversation;\n const total = messages.length;\n const beforeIndex = options.beforeIndex ?? total;\n const fromIndex = Math.max(0, beforeIndex - options.limit);\n return { messages: messages.slice(fromIndex, beforeIndex), total, fromIndex };\n }\n\n // Bounded paging uses the parseConversation message total, which differs\n // from meta.messageCount (the metadata count excludes tool_use-only and\n // thinking-only lines).\n const total = this.conversations.pageMessageCount(filePath);\n\n await this.ensureCheckpoints(filePath, total);\n\n const beforeIndex = options.beforeIndex ?? total;\n const fromIndex = Math.max(0, beforeIndex - options.limit);\n let floor = this.checkpoints.floor(filePath, fromIndex);\n // A checkpoint past the current EOF is stale — the file was truncated or\n // replaced on disk and no refresh has landed yet. Seeking there would read\n // nothing (or garbage), so fall back to byte 0: correct, just slower, and\n // the next refresh drops the stale chain. A failed stat is left to\n // readPage, which surfaces the read error exactly as before.\n if (floor) {\n try {\n if (floor.byteOffset > statSync(filePath).size) floor = null;\n } catch {\n // fall through to readPage's own error handling\n }\n }\n return readPage(filePath, total, options, floor);\n }\n\n // Build or extend the checkpoint chain so it covers `total` messages. Cold\n // file → full build; a file that grew → extend from the last persisted\n // checkpoint (reads only past its offset, never the prefix). Single-flighted\n // per path: concurrent getPage callers await the same build instead of\n // streaming the file in parallel.\n private ensureCheckpoints(filePath: string, total: number): Promise<void> {\n if (total <= CHECKPOINT_INTERVAL) return Promise.resolve();\n const inFlight = this.checkpointBuilds.get(filePath);\n if (inFlight) return inFlight;\n\n const build = (async () => {\n const last = this.checkpoints.last(filePath);\n // Chain already covers every checkpointable index — nothing to do.\n if (last && total < last.messageIndex + CHECKPOINT_INTERVAL) return;\n const fresh = await buildCheckpoints(filePath, CHECKPOINT_INTERVAL, last);\n if (fresh.length === 0) return;\n if (last) this.checkpoints.append(filePath, fresh);\n else this.checkpoints.replaceAll(filePath, fresh);\n })().finally(() => {\n if (this.checkpointBuilds.get(filePath) === build) this.checkpointBuilds.delete(filePath);\n });\n this.checkpointBuilds.set(filePath, build);\n return build;\n }\n}\n","import { createReadStream } from \"fs\";\nimport { createInterface } from \"readline\";\nimport { getLogger } from \"../logger\";\nimport type { ContentTier, ConversationMeta } from \"../types\";\nimport type { ScannerProvider } from \"./provider\";\n\n// Stream a file through a provider's reducer triplet. This is the same fold a\n// future offset-resumed incremental parse would run (createEmptyAccumulator →\n// reduceEntry per line → finalize), just starting from offset 0. Bad JSON lines\n// and unknown entry shapes are skipped, never fatal.\n// `onEntry` lets a caller fold a second, independent accumulator (the search\n// document) over the same lines without re-reading the file. It never affects\n// the provider's own reduce.\nexport async function parseMetaWithProvider(\n provider: ScannerProvider,\n filePath: string,\n account: string,\n tier: ContentTier,\n onEntry?: (entry: Record<string, unknown>) => void,\n): Promise<ConversationMeta | null> {\n const log = getLogger();\n const acc = provider.createEmptyAccumulator();\n const rl = createInterface({\n input: createReadStream(filePath),\n crlfDelay: Infinity,\n });\n\n try {\n for await (const line of rl) {\n if (!line.trim()) continue;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n try {\n provider.reduceEntry(acc, entry, tier);\n onEntry?.(entry);\n } catch (err) {\n // A provider reducer should never throw, but one bad line must not kill\n // the whole file.\n log.warn({ filePath, provider: provider.name, err }, \"provider reduce threw; line skipped\");\n }\n }\n } catch (err) {\n log.warn({ filePath, provider: provider.name, err }, \"provider parse: read failed\");\n return null;\n }\n\n return provider.finalize(acc, filePath, account, tier);\n}\n","import { extractThinking } from \"./parser\";\nimport { cleanSystemTags } from \"./tags\";\n\n// The derived search corpus: one document per conversation, split into three\n// independently budgeted buckets so a later user message always lands in `text`\n// rather than after previously-indexed tool output. Assembled into FTS body\n// columns (persistent) or one combined string (FlexSearch).\n//\n// Budgets are JavaScript string lengths (UTF-16 code units), NOT bytes. ASCII\n// makes the two coincide; Hebrew/CJK/emoji cost 2-3 UTF-8 bytes per unit, so a\n// 64K `textMax` can hold ~128-192 KB on disk. Size index-growth estimates from\n// that, and don't describe these as byte caps.\nexport const SEARCH_BUDGET = {\n textMax: 64 * 1024,\n thinkingMax: 32 * 1024,\n toolsMax: 32 * 1024,\n\n toolPayloadMax: 4 * 1024,\n} as const;\n\nexport interface SearchDocument {\n text: string;\n thinking: string;\n tools: string;\n}\n\n// Same shape as SearchDocument, but holding only the material extracted from\n// newly-read lines — what gets tail-appended onto the durable buckets.\nexport type SearchDocumentDelta = SearchDocument;\n\nconst SEP = \"\\n\\n\";\n\nexport function emptySearchDocument(): SearchDocument {\n return { text: \"\", thinking: \"\", tools: \"\" };\n}\n\nexport function isEmptyDelta(delta: SearchDocumentDelta): boolean {\n return !delta.text && !delta.thinking && !delta.tools;\n}\n\nexport function searchDocumentsEqual(a: SearchDocument, b: SearchDocument): boolean {\n return a.text === b.text && a.thinking === b.thinking && a.tools === b.tools;\n}\n\n// Tail-biased append: each bucket is a sliding window over the NEWEST material.\n// Head-biased fill (stop once full) would freeze a conversation the moment it\n// crossed the budget — the same bug as the old 5 KB contentSnippet, just larger,\n// and it would miss exactly the actively-growing conversations search exists for.\nexport function appendSearchDelta(doc: SearchDocument, delta: SearchDocumentDelta): SearchDocument {\n return {\n text: tailAppend(doc.text, delta.text, SEARCH_BUDGET.textMax),\n thinking: tailAppend(doc.thinking, delta.thinking, SEARCH_BUDGET.thinkingMax),\n tools: tailAppend(doc.tools, delta.tools, SEARCH_BUDGET.toolsMax),\n };\n}\n\n// ponytail: slice(-max) can cut mid-token; the truncated head token simply\n// doesn't match. Grapheme/token-aware windowing isn't worth it for v1.\nfunction tailAppend(current: string, incoming: string, max: number): string {\n if (!incoming) return current;\n const joined = current ? current + SEP + incoming : incoming;\n return joined.length <= max ? joined : joined.slice(-max);\n}\n\n// Flatten to one string for the in-memory FlexSearch index, which has a single\n// body field. The persistent path keeps the buckets as separate FTS columns\n// instead (concatenating there would double-weight body hits).\nexport function combineSearchContent(doc: SearchDocument): string {\n return [doc.text, doc.thinking, doc.tools].filter(Boolean).join(SEP);\n}\n\n// Truncate one tool payload before it joins the tools bucket, so a single huge\n// Read/Bash/compiler dump can't consume the whole window.\nexport function capToolPayload(value: unknown): string {\n const raw = stringifyPayload(value);\n if (!raw) return \"\";\n return raw.length > SEARCH_BUDGET.toolPayloadMax\n ? raw.slice(0, SEARCH_BUDGET.toolPayloadMax)\n : raw;\n}\n\nfunction stringifyPayload(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"string\") return value;\n try {\n return JSON.stringify(value) ?? \"\";\n } catch {\n return \"\";\n }\n}\n\n// Extract the search-document delta from one already-parsed JSONL entry.\n// Handles both Claude/Threadbase and Codex CLI line shapes so every indexing\n// path — persistent, in-memory, both providers — shares one corpus definition.\nexport function extractSearchDelta(entry: Record<string, unknown>): SearchDocumentDelta {\n if (entry.type === \"response_item\" || entry.type === \"session_meta\") {\n return extractCodexDelta(entry);\n }\n return extractClaudeDelta(entry);\n}\n\nfunction extractClaudeDelta(entry: Record<string, unknown>): SearchDocumentDelta {\n const type = entry.type;\n if (type !== \"user\" && type !== \"assistant\") return emptySearchDocument();\n if (entry.isMeta) return emptySearchDocument();\n\n const msg = entry.message as Record<string, unknown> | undefined;\n const content = msg?.content;\n\n const tools = [\n extractClaudeToolContent(content),\n // Claude stores the rich/structured tool result at the JSONL entry's top\n // level, not inside message.content — indexing only message.content would\n // miss most real tool output (file reads, command stdout).\n capToolPayload(entry.toolUseResult),\n ]\n .filter(Boolean)\n .join(SEP);\n\n return {\n text: extractClaudeText(content),\n thinking: type === \"assistant\" ? extractThinking(content).content : \"\",\n tools,\n };\n}\n\n// Human/assistant prose only. Deliberately NOT parser.extractTextContent():\n// that helper folds `tool_result` strings into the same string as user text,\n// which would let a large tool result eat the high-priority text window.\n//\n// It DOES keep extractTextContent's cleanSystemTags step, and that is\n// mandatory: without it every user turn's <system-reminder> (CLAUDE.md dump,\n// skill listing, memory block) lands in `text`, exhausting the window in a\n// couple of turns and making every conversation on the machine a hit for tokens\n// that only ever appear in those wrappers.\nfunction extractClaudeText(content: unknown): string {\n if (typeof content === \"string\") return cleanSystemTags(content);\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const item of content) {\n if (typeof item === \"string\") {\n const cleaned = cleanSystemTags(item);\n if (cleaned) parts.push(cleaned);\n } else if (item?.type === \"text\" && typeof item.text === \"string\") {\n const cleaned = cleanSystemTags(item.text);\n if (cleaned) parts.push(cleaned);\n }\n }\n return parts.join(SEP);\n}\n\n// Tool I/O is the payload, so it is NOT tag-cleaned — stripping it as if it\n// were a prompt wrapper would delete the content being indexed. The per-payload\n// cap is the safety valve instead.\nfunction extractClaudeToolContent(content: unknown): string {\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const item of content) {\n if (item?.type === \"tool_use\") {\n const capped = capToolPayload(item.input);\n if (capped) parts.push(capped);\n } else if (item?.type === \"tool_result\") {\n const capped = capToolPayload(item.content);\n if (capped) parts.push(capped);\n }\n }\n return parts.join(SEP);\n}\n\nfunction extractCodexDelta(entry: Record<string, unknown>): SearchDocumentDelta {\n const payload = entry.payload as Record<string, unknown> | undefined;\n if (!payload || typeof payload !== \"object\") return emptySearchDocument();\n\n const ptype = payload.type as string;\n\n if (ptype === \"function_call\" || ptype === \"custom_tool_call\") {\n return { text: \"\", thinking: \"\", tools: capToolPayload(payload.arguments) };\n }\n if (ptype === \"function_call_output\" || ptype === \"custom_tool_call_output\") {\n return { text: \"\", thinking: \"\", tools: capToolPayload(payload.output) };\n }\n if (ptype === \"reasoning\") {\n return { text: \"\", thinking: extractCodexReasoning(payload), tools: \"\" };\n }\n if (ptype === \"message\") {\n const role = payload.role;\n if (role !== \"user\" && role !== \"assistant\") return emptySearchDocument();\n return { text: extractCodexText(payload.content), thinking: \"\", tools: \"\" };\n }\n return emptySearchDocument();\n}\n\n// Codex reasoning carries its text under `summary` (and sometimes `content`),\n// each a list of {type, text} blocks. Never a signature — there isn't one here,\n// and on the Claude side the thinking signature is a base64 crypto blob that is\n// pure noise in a search index.\nfunction extractCodexReasoning(payload: Record<string, unknown>): string {\n const parts: string[] = [];\n for (const key of [\"summary\", \"content\"] as const) {\n const blocks = payload[key];\n if (!Array.isArray(blocks)) continue;\n for (const block of blocks) {\n if (typeof block === \"string\") parts.push(block);\n else if (typeof block?.text === \"string\") parts.push(block.text);\n }\n }\n return parts.filter(Boolean).join(SEP);\n}\n\nfunction extractCodexText(content: unknown): string {\n if (typeof content === \"string\") return cleanSystemTags(content);\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const item of content) {\n if (typeof item === \"string\") {\n const cleaned = cleanSystemTags(item);\n if (cleaned) parts.push(cleaned);\n continue;\n }\n const t = item?.type;\n if (\n (t === \"input_text\" || t === \"output_text\" || t === \"text\") &&\n typeof item.text === \"string\"\n ) {\n const cleaned = cleanSystemTags(item.text);\n if (cleaned) parts.push(cleaned);\n }\n }\n return parts.join(SEP);\n}\n","import type { ContentTier } from \"./types\";\n\nexport const DEFAULT_TIERS: Record<string, ContentTier> = {\n standard: { name: \"standard\", previewMax: 200, snippetMax: 5_000 },\n full: { name: \"full\", previewMax: 1_200, snippetMax: 50_000 },\n};\n\nexport function resolveTier(\n tierName: string,\n customTiers?: Record<string, ContentTier>,\n): ContentTier {\n const tier = customTiers?.[tierName] ?? DEFAULT_TIERS[tierName];\n if (!tier) {\n throw new Error(\n `Unknown tier \"${tierName}\". Available: ${Object.keys({ ...DEFAULT_TIERS, ...customTiers }).join(\", \")}`,\n );\n }\n return tier;\n}\n","import Database from \"better-sqlite3\";\nimport { mkdirSync } from \"fs\";\nimport { dirname } from \"path\";\nimport { getLogger } from \"../logger\";\nimport { runMigrations } from \"./migrations\";\n\nexport type DB = Database.Database;\n\n// Open (creating if needed) the SQLite index at dbPath, apply WAL pragmas, and\n// run migrations. \":memory:\" is supported for tests.\nexport function openDatabase(dbPath: string): DB {\n if (dbPath !== \":memory:\") {\n mkdirSync(dirname(dbPath), { recursive: true });\n }\n\n let db: DB;\n try {\n db = new Database(dbPath);\n } catch (err) {\n if (isNativeBindingFailure(err)) throw nativeBindingError(err, dbPath);\n throw err;\n }\n\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"synchronous = NORMAL\");\n db.pragma(\"temp_store = MEMORY\");\n db.pragma(\"foreign_keys = ON\");\n // Let a reader briefly wait out a concurrent indexing write instead of failing\n // immediately with SQLITE_BUSY. Operational resilience only — search recall is\n // unaffected.\n db.pragma(\"busy_timeout = 5000\");\n\n runMigrations(db);\n\n getLogger().debug({ dbPath }, \"db: opened\");\n return db;\n}\n\n// better-sqlite3 is a native addon, and `require()` of it succeeds even when its\n// binary is absent — the JS entry point loads fine and the addon is only touched\n// when a Database is constructed. So a broken install stays invisible until this\n// exact line, then fails with an opaque list of candidate .node paths that reads\n// like an ABI mismatch and sends people chasing Node versions.\n//\n// It usually isn't an ABI mismatch: the path in the message is normally correct\n// for the running Node, and the file simply was never produced.\nfunction isNativeBindingFailure(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n return /could not locate the bindings file|NODE_MODULE_VERSION|was compiled against|\\.node['\"\\s]/i.test(\n message,\n );\n}\n\nfunction nativeBindingError(err: unknown, dbPath: string): Error {\n const detail = err instanceof Error ? err.message : String(err);\n return new Error(\n [\n `Could not load better-sqlite3's native binary, so the persistent index at ${dbPath} cannot be opened.`,\n \"This usually means the binary was never built or downloaded — not that your Node version is wrong.\",\n \"\",\n \"Try, in order:\",\n \" 1. npm rebuild better-sqlite3\",\n \" 2. rm -rf node_modules && npm install\",\n \" (npm 12 blocks package install scripts by default; approve better-sqlite3 if asked)\",\n \" 3. Run without SQLite entirely: new ConversationScanner({ persistent: false })\",\n \" or pass --no-persist on the CLI. Search falls back to the in-memory index.\",\n \"\",\n `Original error: ${detail}`,\n ].join(\"\\n\"),\n );\n}\n","// Persistent index schema for @threadbase-sh/scanner.\n//\n// Embedded as a string (not a sibling .sql file) so it survives tsup bundling\n// into dist/ without runtime file I/O.\n//\n// One JSONL file == one conversation, so conversation_files.id maps 1:1 to\n// conversations.file_id.\n//\n// NOTE: session_id is intentionally NOT unique. parseMeta() falls back to the\n// file's basename when a line carries no sessionId, and resumed/subagent\n// sessions can repeat a sessionId across files. The stable unique key is the\n// file's absolute_path (and conversations.file_id).\n\n// Bumped whenever DDL below changes; drives migrations.ts via PRAGMA user_version.\n// v4: added the scanned_dirs table (dir-mtime discovery gate). runMigrations()\n// early-returns when the stored user_version >= SCHEMA_VERSION, so a new table\n// in SCHEMA_SQL is only reached on an existing DB if this number is bumped —\n// otherwise every DB already at v3 skips the migration and never gets the table.\n// v5: FTS body split from one `content` column into text/thinking/tools, fed by\n// the tail-biased search document instead of meta.contentSnippet.\nexport const SCHEMA_VERSION = 5;\n\nexport const SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS conversation_files (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n\n absolute_path TEXT NOT NULL UNIQUE,\n parent_dir TEXT NOT NULL,\n file_name TEXT NOT NULL,\n\n account TEXT NOT NULL DEFAULT 'default',\n\n size_bytes INTEGER NOT NULL DEFAULT 0,\n mtime_ms INTEGER NOT NULL DEFAULT 0,\n\n last_indexed_offset INTEGER NOT NULL DEFAULT 0,\n last_indexed_line INTEGER NOT NULL DEFAULT 0,\n\n reducer_state TEXT,\n\n content_fingerprint TEXT,\n\n status TEXT NOT NULL DEFAULT 'active',\n\n last_indexed_at TEXT,\n created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n deleted_at TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_conversation_files_status ON conversation_files(status);\nCREATE INDEX IF NOT EXISTS idx_conversation_files_parent_dir ON conversation_files(parent_dir);\nCREATE INDEX IF NOT EXISTS idx_conversation_files_account ON conversation_files(account);\n\nCREATE TABLE IF NOT EXISTS conversations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n\n file_id INTEGER NOT NULL UNIQUE,\n\n source_path TEXT NOT NULL UNIQUE,\n -- Which provider produced this row. Canonical identity is (provider,\n -- source_path); session_id stays non-unique across providers too.\n provider TEXT NOT NULL DEFAULT 'claude-code',\n kind TEXT,\n external_session_id TEXT,\n session_id TEXT NOT NULL,\n session_name TEXT,\n\n project_path TEXT,\n project_name TEXT,\n account TEXT NOT NULL DEFAULT 'default',\n branch TEXT,\n\n preview TEXT,\n content_snippet TEXT,\n\n message_count INTEGER NOT NULL DEFAULT 0,\n -- Count of messages parseConversation() produces (broader than message_count;\n -- includes tool_use-only and thinking-only lines). The total for bounded paging.\n page_message_count INTEGER NOT NULL DEFAULT 0,\n last_message_sender TEXT NOT NULL DEFAULT 'user',\n timestamp TEXT,\n\n -- Monotonic write counter; the highest value is the most recently indexed\n -- row. Drives last-writer-wins resolution for shared session_ids (matching\n -- the in-memory sessionId map), at the sub-second precision updated_at lacks.\n index_seq INTEGER NOT NULL DEFAULT 0,\n\n first_sent_at TEXT,\n first_sent_text TEXT,\n\n last_sent_at TEXT,\n last_sent_text TEXT,\n\n model TEXT,\n is_subagent INTEGER NOT NULL DEFAULT 0,\n parent_session_id TEXT,\n is_teammate INTEGER NOT NULL DEFAULT 0,\n team_name TEXT,\n tool_names_json TEXT,\n last_prompt TEXT,\n\n status TEXT NOT NULL DEFAULT 'active',\n\n created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n FOREIGN KEY (file_id) REFERENCES conversation_files(id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_conversations_session_id ON conversations(session_id);\nCREATE INDEX IF NOT EXISTS idx_conversations_provider_session ON conversations(provider, session_id);\nCREATE INDEX IF NOT EXISTS idx_conversations_provider_recent ON conversations(provider, timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_conversations_provider_project_branch ON conversations(provider, project_path, branch);\nCREATE INDEX IF NOT EXISTS idx_conversations_recent ON conversations(timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_conversations_project_recent ON conversations(project_path, timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_conversations_project_branch_recent ON conversations(project_path, branch, timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_conversations_account_recent ON conversations(account, timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_conversations_subagent_recent ON conversations(is_subagent, timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_conversations_team_recent ON conversations(team_name, timestamp DESC);\n\n-- Full-text search index over conversation body + metadata. Kept separate from\n-- the metadata tables so list-screen queries stay small and fast. One FTS row\n-- per conversation, replaced on each upsert.\n--\n-- The row's rowid IS conversation_files.id. That is load-bearing, not cosmetic:\n-- FTS5's query planner only handles MATCH, rowid and rank, so any other\n-- constraint (including a source_path equality on an UNINDEXED column) has no\n-- index and linear-scans the whole table. At ~128 KB of body per row that would\n-- mean scanning the entire corpus on every append. Look rows up by rowid.\n-- source_path stays stored-but-UNINDEXED so a search hit can resolve back to a\n-- conversations row.\n--\n-- Body is three columns, not one: text > thinking > tools priority has to\n-- survive an append (a new user message belongs in the text column, not after\n-- the tool output already written). They are deliberately NOT concatenated into\n-- a fourth column - that would double-weight body hits and inflate bm25 length.\nCREATE VIRTUAL TABLE IF NOT EXISTS conversation_messages_fts USING fts5(\n source_path UNINDEXED,\n text,\n thinking,\n tools,\n project_name,\n session_id,\n session_name,\n account,\n model,\n branch,\n tool_names,\n tokenize = 'unicode61'\n);\n\n-- Seek index for large conversations: every N messages, record the byte offset\n-- where the next message's line begins plus the parser's cross-line state\n-- (pending tool_use blocks, team info) needed to resume an equivalent parse\n-- from that point. Lets getConversationPage() read a window near the end of a\n-- huge file without parsing from byte 0.\nCREATE TABLE IF NOT EXISTS message_checkpoints (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_path TEXT NOT NULL,\n message_index INTEGER NOT NULL,\n byte_offset INTEGER NOT NULL,\n line_number INTEGER NOT NULL,\n parser_state TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_message_checkpoints_lookup\n ON message_checkpoints(source_path, message_index);\n\n-- Per-directory mtime watermark for the discovery dir-mtime gate (skips the\n-- glob for a directory whose file/subdir SET hasn't changed; never skips\n-- per-file classify()). One row for a profile's projectsDir itself (parent_root\n-- IS NULL) and one row per immediate project subdirectory discovered under it\n-- (parent_root = the projectsDir path). has_nested marks a project dir known to\n-- contain files below its own top level (e.g. subagents/) — those always\n-- re-glob regardless of mtime, since a project-dir-level watermark can't see a\n-- change two levels down.\nCREATE TABLE IF NOT EXISTS scanned_dirs (\n path TEXT PRIMARY KEY,\n parent_root TEXT,\n mtime_ms INTEGER NOT NULL,\n has_nested INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_scanned_dirs_parent_root ON scanned_dirs(parent_root);\n`;\n","import type { Database } from \"better-sqlite3\";\nimport { getLogger } from \"../logger\";\nimport { SCHEMA_SQL, SCHEMA_VERSION } from \"./schema\";\n\n// Minimal migration runner gated by PRAGMA user_version. The whole schema uses\n// CREATE TABLE/INDEX IF NOT EXISTS, so a fresh DB and an up-to-date DB both\n// no-op safely. When SCHEMA_VERSION bumps in the future, add ALTER statements\n// keyed on the stored version before stamping the new version.\nexport function runMigrations(db: Database): void {\n const current = db.pragma(\"user_version\", { simple: true }) as number;\n if (current >= SCHEMA_VERSION) return;\n\n const log = getLogger();\n log.info({ from: current, to: SCHEMA_VERSION }, \"migrations: applying\");\n\n // v1 → v2: add provider columns to a pre-existing conversations table BEFORE\n // running SCHEMA_SQL, because SCHEMA_SQL now creates indexes on those columns.\n // CREATE TABLE IF NOT EXISTS can't add columns, so ALTER the existing one.\n // Non-destructive: existing rows keep their data and default to 'claude-code'.\n // Guarded on table existence so a fresh DB (table created by SCHEMA_SQL below)\n // skips this entirely.\n if (current >= 1 && current < 2 && tableExists(db, \"conversations\")) {\n for (const [col, ddl] of [\n [\n \"provider\",\n // Keep the historical DEFAULT 'threadbase' — v2→v3 below updates these rows.\n \"ALTER TABLE conversations ADD COLUMN provider TEXT NOT NULL DEFAULT 'threadbase'\",\n ],\n [\"kind\", \"ALTER TABLE conversations ADD COLUMN kind TEXT\"],\n [\"external_session_id\", \"ALTER TABLE conversations ADD COLUMN external_session_id TEXT\"],\n ] as const) {\n if (!hasColumn(db, \"conversations\", col)) db.exec(ddl);\n }\n }\n\n // v2 → v3: rename provider 'threadbase' → 'claude-code'.\n if (current >= 1 && current < 3 && tableExists(db, \"conversations\")) {\n db.exec(\"UPDATE conversations SET provider = 'claude-code' WHERE provider = 'threadbase'\");\n }\n\n // v4 → v5: the FTS body split from a single `content` column (fed by the 5 KB\n // head-biased meta.contentSnippet) into text/thinking/tools columns fed by the\n // tail-biased search document. CREATE VIRTUAL TABLE IF NOT EXISTS cannot\n // reshape an existing table, so drop it and let SCHEMA_SQL recreate it.\n //\n // Then force every known file to reindex from byte 0. classify() already\n // returns \"reindex\" whenever last_indexed_offset === 0, so resetting the\n // cursor is the entire trigger. Without it an unchanged conversation would\n // keep its old FTS row forever — nothing would ever append to it, so nothing\n // would rebuild it.\n //\n // Deliberately cheap: no JSONL is read here. Opening the database must stay in\n // milliseconds; the actual re-parse happens in the next indexAll (newest files\n // first), not inside a pragma migration.\n if (current >= 1 && current < 5) {\n db.exec(\"DROP TABLE IF EXISTS conversation_messages_fts\");\n // A database old enough to still be at v1 can predate some cursor columns,\n // and CREATE TABLE IF NOT EXISTS below won't add them — so reset only what\n // this database actually has.\n if (tableExists(db, \"conversation_files\")) {\n const assignments: string[] = [];\n if (hasColumn(db, \"conversation_files\", \"last_indexed_offset\")) {\n assignments.push(\"last_indexed_offset = 0\");\n }\n if (hasColumn(db, \"conversation_files\", \"last_indexed_line\")) {\n assignments.push(\"last_indexed_line = 0\");\n }\n if (hasColumn(db, \"conversation_files\", \"reducer_state\")) {\n assignments.push(\"reducer_state = NULL\");\n }\n if (assignments.length > 0) {\n db.exec(`UPDATE conversation_files SET ${assignments.join(\", \")}`);\n }\n }\n }\n\n // Fresh DB and re-runs both no-op safely (CREATE ... IF NOT EXISTS). Creates\n // any missing tables/indexes, including the new provider indexes.\n db.exec(SCHEMA_SQL);\n\n db.pragma(`user_version = ${SCHEMA_VERSION}`);\n}\n\nfunction tableExists(db: Database, table: string): boolean {\n return (\n db.prepare(\"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?\").get(table) !==\n undefined\n );\n}\n\nfunction hasColumn(db: Database, table: string, column: string): boolean {\n const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];\n return cols.some((c) => c.name === column);\n}\n","import { readdir, stat } from \"fs/promises\";\nimport { dirname, join } from \"path\";\nimport { canonicalPath } from \"../canonical-path\";\nimport { type DiscoveredFile, discoverJsonlFiles } from \"../discovery\";\nimport { getLogger } from \"../logger\";\nimport type { ConversationFilesRepo } from \"./repositories/conversation-files.repo\";\nimport type { ScannedDirsRepo } from \"./repositories/scanned-dirs.repo\";\n\n// ponytail: full-reconcile backstop runs every Nth gated pass, ignoring all\n// watermarks, so a filesystem that doesn't honor directory mtimes (some\n// NFS/SMB mounts, a few atomic-replace editors) can't permanently hide a\n// missed add/remove. Bump this if that self-heal interval feels too slow for\n// a given deployment; there's no config surface for it yet.\nexport const FULL_RECONCILE_EVERY_N_SCANS = 20;\n\n// Discover JSONL files under each profile's projectsDir, skipping the glob for\n// a directory whose file/subdirectory SET hasn't changed since the last pass.\n//\n// The core invariant (see SCAN-OPTIMIZATION-PROMPT.md Stage 4): the gate only\n// ever skips the GLOB (which exists to catch added/removed files — an event\n// that bumps the containing directory's mtime). It never skips classify() for\n// an already-known file (which exists to catch content changes, e.g. an\n// append — an event that does NOT bump the directory's mtime). Every active\n// file previously indexed under a project dir is always included in the\n// returned list, glob or no glob, so index-engine.ts's deletion-reconcile\n// never mistakes a skipped-but-still-known file for a vanished one.\n//\n// Two-level watermark, matching the real ~/.claude/projects/ shape (one\n// project subdirectory per conversation history, sometimes with nested\n// subagent/workflow trees below that):\n// - projectsDir itself: catches added/removed PROJECT DIRECTORIES.\n// - each immediate project subdirectory: catches added/removed FILES\n// directly in it. A project dir known to contain nested files (subagents/\n// etc) always re-globs regardless of mtime — a project-dir-level\n// watermark can't see a change two levels down, so it isn't trusted to\n// skip those dirs. This still skips the glob for the common case: most\n// project dirs are flat (session-uuid.jsonl directly inside).\nexport async function discoverJsonlFilesGated(\n dirs: { projectsDir: string; account: string }[],\n files: ConversationFilesRepo,\n scannedDirs: ScannedDirsRepo,\n options: { fullRescan?: boolean } = {},\n): Promise<DiscoveredFile[]> {\n const log = getLogger();\n if (options.fullRescan) {\n return discoverJsonlFiles(dirs);\n }\n\n const results: DiscoveredFile[] = [];\n\n for (const rawDir of dirs) {\n const { account } = rawDir;\n // Canonicalize: the reuse branch below compares these dir keys against\n // parent_dir/scanned_dirs.path, which the repositories store canonically.\n // Without this the has_nested/parent_dir lookups silently miss on Windows\n // when a caller's projectsDir came in with the other separator style, and\n // the caller's seen-set then loses those files to the deletion-reconcile.\n const projectsDir = canonicalPath(rawDir.projectsDir);\n\n const resolved = await resolveProjectDirs(projectsDir, scannedDirs);\n if (resolved === null) continue; // projectsDir unreadable/vanished\n\n for (const projectDir of resolved.entries) {\n let dirStat: { mtimeMs: number };\n try {\n dirStat = await stat(projectDir);\n } catch {\n // Project dir vanished between listing and stat — drop its watermark;\n // its previously-known files simply won't appear in this pass's\n // result, so index-engine.ts's deletion-reconcile marks them deleted.\n scannedDirs.remove(projectDir);\n continue;\n }\n\n const watermark = scannedDirs.get(projectDir);\n const canReuse =\n watermark !== undefined &&\n watermark.mtime_ms === dirStat.mtimeMs &&\n watermark.has_nested === 0;\n\n if (canReuse) {\n const known = files.activePathsByParentDir(projectDir);\n // A watermarked, unchanged dir must have had ≥1 indexed file when it was\n // globbed. Zero known files means the watermark is out of sync with\n // conversation_files — most often a race: another connection sharing this\n // index.db committed the scanned_dirs watermark (during its own discovery)\n // before it finished writing that dir's file rows, and we read in that\n // window. Trusting the empty reuse would drop the dir's conversations from\n // `discovered`, so the caller's deletion-reconcile (or a stale allActive)\n // never surfaces them → 404. Fall through to the glob, which reads the\n // real on-disk truth and re-indexes what's actually there.\n if (known.length > 0) {\n for (const row of known) {\n results.push({ filePath: row.absolute_path, account: row.account });\n }\n continue;\n }\n }\n\n const found = await discoverJsonlFiles([{ projectsDir: projectDir, account }]);\n const hasNested = found.some((f) => dirnameOf(f.filePath) !== projectDir);\n scannedDirs.upsert(projectDir, projectsDir, dirStat.mtimeMs, hasNested);\n results.push(...found);\n }\n\n // Commit the root watermark only after every project dir under it has been\n // processed without throwing. Committing it earlier (e.g. right after the\n // readdir) would let a crash mid-loop leave un-globbed project dirs with no\n // scanned_dirs row — the next pass's root-reuse would then return only the\n // dirs that DID get a row, silently dropping the crashed-out dirs' files\n // from `seen` and mass-deleting their conversations.\n if (resolved.commitRoot) {\n scannedDirs.upsert(projectsDir, null, resolved.commitRoot.mtimeMs, false);\n const seen = new Set(resolved.entries);\n for (const known of scannedDirs.childrenOf(projectsDir)) {\n if (!seen.has(known.path)) scannedDirs.remove(known.path);\n }\n }\n }\n\n log.debug(\n { totalFiles: results.length, dirs: dirs.length },\n \"dir-watermark: gated discovery complete\",\n );\n return results;\n}\n\ninterface ResolvedProjectDirs {\n entries: string[];\n // Present only when projectsDir was actually re-listed this pass (root\n // watermark was stale/missing) — the caller commits it after the per-dir\n // loop succeeds. Absent when the root watermark was reused as-is (nothing\n // new to commit).\n commitRoot?: { mtimeMs: number };\n}\n\n// The list of immediate project subdirectories under projectsDir, reusing the\n// stored list when projectsDir's own mtime hasn't moved (no dirs added or\n// removed), otherwise re-listing (but NOT yet persisting — see commitRoot).\n// Returns null if projectsDir itself can't be read (e.g. a profile pointing at\n// a not-yet-created config dir).\nasync function resolveProjectDirs(\n projectsDir: string,\n scannedDirs: ScannedDirsRepo,\n): Promise<ResolvedProjectDirs | null> {\n let rootStat: { mtimeMs: number };\n try {\n rootStat = await stat(projectsDir);\n } catch {\n return null;\n }\n\n const rootWatermark = scannedDirs.get(projectsDir);\n if (rootWatermark !== undefined && rootWatermark.mtime_ms === rootStat.mtimeMs) {\n return { entries: scannedDirs.childrenOf(projectsDir).map((row) => row.path) };\n }\n\n let entries: string[];\n try {\n entries = (await readdir(projectsDir, { withFileTypes: true }))\n .filter((e) => e.isDirectory())\n .map((e) => joinPath(projectsDir, e.name));\n } catch {\n return null;\n }\n\n return { entries, commitRoot: { mtimeMs: rootStat.mtimeMs } };\n}\n\n// Platform-aware: the paths threaded through here are canonical (native\n// separator on Windows), so a hardcoded \"/\" split would read the whole path as\n// the dirname and mis-derive has_nested.\nfunction dirnameOf(filePath: string): string {\n return dirname(filePath);\n}\n\nfunction joinPath(dir: string, name: string): string {\n return join(dir, name);\n}\n","import type { Database } from \"better-sqlite3\";\nimport { canonicalPath } from \"../../canonical-path\";\nimport type { ConvReducerState } from \"../conversation-reducer\";\n\nexport interface Checkpoint {\n messageIndex: number;\n byteOffset: number;\n lineNumber: number;\n state: ConvReducerState;\n}\n\ninterface CheckpointRow {\n message_index: number;\n byte_offset: number;\n line_number: number;\n parser_state: string;\n}\n\n// Stores periodic seek points for a conversation so a page near the end of a\n// large file can be read without parsing from byte 0.\nexport class CheckpointsRepo {\n constructor(private db: Database) {}\n\n replaceAll(sourcePath: string, checkpoints: Checkpoint[]): void {\n const path = canonicalPath(sourcePath);\n const tx = this.db.transaction(() => {\n this.db.prepare(\"DELETE FROM message_checkpoints WHERE source_path = ?\").run(path);\n const insert = this.db.prepare(\n `INSERT INTO message_checkpoints\n (source_path, message_index, byte_offset, line_number, parser_state)\n VALUES (?, ?, ?, ?, ?)`,\n );\n for (const c of checkpoints) {\n insert.run(path, c.messageIndex, c.byteOffset, c.lineNumber, JSON.stringify(c.state));\n }\n });\n tx();\n }\n\n // Insert checkpoints without touching existing rows. Appends never invalidate\n // the chain covering the immutable prefix (Kafka sparse-index style); rows are\n // only ever removed on truncation/replace or deletion.\n append(sourcePath: string, checkpoints: Checkpoint[]): void {\n const path = canonicalPath(sourcePath);\n const tx = this.db.transaction(() => {\n const insert = this.db.prepare(\n `INSERT INTO message_checkpoints\n (source_path, message_index, byte_offset, line_number, parser_state)\n VALUES (?, ?, ?, ?, ?)`,\n );\n for (const c of checkpoints) {\n insert.run(path, c.messageIndex, c.byteOffset, c.lineNumber, JSON.stringify(c.state));\n }\n });\n tx();\n }\n\n // The latest checkpoint at or before `messageIndex`, or null if none (read\n // from the file start). Lets a page seek to the nearest prior anchor.\n floor(sourcePath: string, messageIndex: number): Checkpoint | null {\n const row = this.db\n .prepare(\n `SELECT message_index, byte_offset, line_number, parser_state\n FROM message_checkpoints\n WHERE source_path = ? AND message_index <= ?\n ORDER BY message_index DESC LIMIT 1`,\n )\n .get(canonicalPath(sourcePath), messageIndex) as CheckpointRow | undefined;\n return row ? toCheckpoint(row) : null;\n }\n\n // The highest-index checkpoint for a file, or null if none. The resume point\n // for extending the chain after an append.\n last(sourcePath: string): Checkpoint | null {\n const row = this.db\n .prepare(\n `SELECT message_index, byte_offset, line_number, parser_state\n FROM message_checkpoints\n WHERE source_path = ?\n ORDER BY message_index DESC LIMIT 1`,\n )\n .get(canonicalPath(sourcePath)) as CheckpointRow | undefined;\n return row ? toCheckpoint(row) : null;\n }\n\n count(sourcePath: string): number {\n return (\n this.db\n .prepare(\"SELECT COUNT(*) AS n FROM message_checkpoints WHERE source_path = ?\")\n .get(canonicalPath(sourcePath)) as { n: number }\n ).n;\n }\n\n remove(sourcePath: string): void {\n this.db\n .prepare(\"DELETE FROM message_checkpoints WHERE source_path = ?\")\n .run(canonicalPath(sourcePath));\n }\n}\n\nfunction toCheckpoint(row: CheckpointRow): Checkpoint {\n return {\n messageIndex: row.message_index,\n byteOffset: row.byte_offset,\n lineNumber: row.line_number,\n state: JSON.parse(row.parser_state) as ConvReducerState,\n };\n}\n","import type { Database } from \"better-sqlite3\";\nimport { basename, dirname } from \"path\";\nimport { canonicalPath } from \"../../canonical-path\";\n\n// One tracked JSONL source file + its incremental indexing cursor.\nexport interface FileRow {\n id: number;\n absolute_path: string;\n parent_dir: string;\n file_name: string;\n account: string;\n size_bytes: number;\n mtime_ms: number;\n last_indexed_offset: number;\n last_indexed_line: number;\n reducer_state: string | null;\n content_fingerprint: string | null;\n status: string;\n last_indexed_at: string | null;\n}\n\nexport class ConversationFilesRepo {\n constructor(private db: Database) {}\n\n getByPath(absolutePath: string): FileRow | undefined {\n return this.db\n .prepare(\"SELECT * FROM conversation_files WHERE absolute_path = ?\")\n .get(canonicalPath(absolutePath)) as FileRow | undefined;\n }\n\n // Insert a freshly-discovered file at offset 0; returns its row id. Existing\n // path is left untouched (returns the existing id) so a re-discovery is safe.\n //\n // absolute_path/parent_dir are stored canonicalized so a caller that built the\n // path with the native separator (watcher, path.join) and one that got it from\n // fast-glob (forward slashes, even on Windows) land on the same row instead of\n // inserting a duplicate.\n ensure(absolutePath: string, account: string): number {\n const canonical = canonicalPath(absolutePath);\n const existing = this.getByPath(canonical);\n if (existing) return existing.id;\n\n const info = this.db\n .prepare(\n `INSERT INTO conversation_files (absolute_path, parent_dir, file_name, account)\n VALUES (?, ?, ?, ?)`,\n )\n .run(canonical, dirname(canonical), basename(canonical), account);\n return Number(info.lastInsertRowid);\n }\n\n // Advance the cursor + persisted reducer state after a successful index pass.\n updateCursor(\n id: number,\n fields: {\n sizeBytes: number;\n mtimeMs: number;\n offset: number;\n line: number;\n reducerState: string | null;\n fingerprint: string | null;\n status?: string;\n },\n ): void {\n this.db\n .prepare(\n `UPDATE conversation_files\n SET size_bytes = ?, mtime_ms = ?, last_indexed_offset = ?, last_indexed_line = ?,\n reducer_state = ?, content_fingerprint = ?, status = ?,\n last_indexed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n )\n .run(\n fields.sizeBytes,\n fields.mtimeMs,\n fields.offset,\n fields.line,\n fields.reducerState,\n fields.fingerprint,\n fields.status ?? \"active\",\n id,\n );\n }\n\n // Reset the cursor to 0 for a truncated/replaced file before a full reindex.\n resetCursor(id: number): void {\n this.db\n .prepare(\n `UPDATE conversation_files\n SET last_indexed_offset = 0, last_indexed_line = 0, reducer_state = NULL,\n status = 'needs_reindex', updated_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n )\n .run(id);\n }\n\n setStatus(id: number, status: string): void {\n const deletedAt = status === \"deleted\" ? \"CURRENT_TIMESTAMP\" : \"deleted_at\";\n this.db\n .prepare(\n `UPDATE conversation_files\n SET status = ?, deleted_at = ${deletedAt}, updated_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n )\n .run(status, id);\n }\n\n allActivePaths(): string[] {\n const rows = this.db\n .prepare(\"SELECT absolute_path FROM conversation_files WHERE status != 'deleted'\")\n .all() as { absolute_path: string }[];\n return rows.map((r) => r.absolute_path);\n }\n\n // Active file paths belonging to any of the given accounts. Backs the\n // deletion-reconcile so a scan that covered only some accounts can't mark\n // another account's files deleted (they live in the same shared index.db but\n // a different profile owns them). Returns [] for an empty account list.\n activePathsByAccounts(accounts: string[]): string[] {\n if (accounts.length === 0) return [];\n const placeholders = accounts.map(() => \"?\").join(\", \");\n const rows = this.db\n .prepare(\n `SELECT absolute_path FROM conversation_files\n WHERE status != 'deleted' AND account IN (${placeholders})`,\n )\n .all(...accounts) as { absolute_path: string }[];\n return rows.map((r) => r.absolute_path);\n }\n\n // Active files whose immediate parent is exactly parentDir (no nested\n // subdirectories). Backs the dir-mtime gate's reuse path: a project dir with\n // an unchanged mtime and no nested files can skip the glob entirely.\n activePathsByParentDir(parentDir: string): { absolute_path: string; account: string }[] {\n return this.db\n .prepare(\n \"SELECT absolute_path, account FROM conversation_files WHERE parent_dir = ? AND status != 'deleted'\",\n )\n .all(canonicalPath(parentDir)) as { absolute_path: string; account: string }[];\n }\n}\n","import type { Database } from \"better-sqlite3\";\nimport { canonicalPath } from \"../../canonical-path\";\nimport { CLAUDE_CODE_PROVIDER } from \"../../providers/provider\";\nimport type { ConversationMeta, MessageSender, ProviderName } from \"../../types\";\n\nexport interface ConversationRow {\n id: number;\n file_id: number;\n source_path: string;\n provider: string;\n kind: string | null;\n external_session_id: string | null;\n session_id: string;\n session_name: string | null;\n project_path: string | null;\n project_name: string | null;\n account: string;\n branch: string | null;\n preview: string | null;\n content_snippet: string | null;\n message_count: number;\n page_message_count: number;\n last_message_sender: string;\n timestamp: string | null;\n first_sent_at: string | null;\n first_sent_text: string | null;\n last_sent_at: string | null;\n last_sent_text: string | null;\n model: string | null;\n is_subagent: number;\n parent_session_id: string | null;\n is_teammate: number;\n team_name: string | null;\n tool_names_json: string | null;\n last_prompt: string | null;\n status: string;\n}\n\n// Row -> ConversationMeta. Must reproduce the exact in-memory shape so the\n// persistent and legacy paths return identical ScanResult/SearchResult objects.\nexport function rowToMeta(row: ConversationRow): ConversationMeta {\n return {\n id: row.source_path,\n filePath: row.source_path,\n provider: (row.provider as ProviderName) ?? CLAUDE_CODE_PROVIDER,\n kind: (row.kind as ConversationMeta[\"kind\"]) ?? undefined,\n externalSessionId: row.external_session_id ?? undefined,\n sessionId: row.session_id,\n sessionName: row.session_name ?? \"\",\n projectPath: row.project_path ?? \"\",\n projectName: row.project_name ?? \"\",\n account: row.account,\n timestamp: row.timestamp ?? \"\",\n messageCount: row.message_count,\n lastMessageSender: row.last_message_sender as MessageSender,\n preview: row.preview ?? \"\",\n contentSnippet: row.content_snippet ?? \"\",\n gitBranch: row.branch,\n model: row.model,\n isSubagent: row.is_subagent === 1,\n parentSessionId: row.parent_session_id,\n isTeammate: row.is_teammate === 1,\n teamName: row.team_name,\n toolNames: row.tool_names_json ? (JSON.parse(row.tool_names_json) as string[]) : [],\n firstMessage: row.first_sent_text\n ? { text: row.first_sent_text, timestamp: row.first_sent_at ?? \"\" }\n : null,\n lastMessage: row.last_sent_text\n ? { text: row.last_sent_text, timestamp: row.last_sent_at ?? \"\" }\n : null,\n lastPrompt: row.last_prompt ?? undefined,\n };\n}\n\nexport class ConversationsRepo {\n constructor(private db: Database) {}\n\n // Upsert by file_id (1 file = 1 conversation). Keyed on the unique file_id so\n // a re-index overwrites the prior summary in place.\n upsert(fileId: number, meta: ConversationMeta, pageMessageCount = meta.messageCount): void {\n this.db\n .prepare(\n `INSERT INTO conversations (\n file_id, source_path, provider, kind, external_session_id,\n session_id, session_name, project_path, project_name,\n account, branch, preview, content_snippet, message_count, page_message_count,\n last_message_sender,\n timestamp, index_seq, first_sent_at, first_sent_text, last_sent_at, last_sent_text,\n model, is_subagent, parent_session_id, is_teammate, team_name, tool_names_json,\n last_prompt, status, updated_at\n ) VALUES (\n @file_id, @source_path, @provider, @kind, @external_session_id,\n @session_id, @session_name, @project_path, @project_name,\n @account, @branch, @preview, @content_snippet, @message_count, @page_message_count,\n @last_message_sender,\n @timestamp,\n (SELECT COALESCE(MAX(index_seq), 0) + 1 FROM conversations),\n @first_sent_at, @first_sent_text, @last_sent_at, @last_sent_text,\n @model, @is_subagent, @parent_session_id, @is_teammate, @team_name, @tool_names_json,\n @last_prompt, 'active', CURRENT_TIMESTAMP\n )\n ON CONFLICT(file_id) DO UPDATE SET\n source_path = excluded.source_path,\n provider = excluded.provider,\n kind = excluded.kind,\n external_session_id = excluded.external_session_id,\n session_id = excluded.session_id,\n session_name = excluded.session_name,\n project_path = excluded.project_path,\n project_name = excluded.project_name,\n account = excluded.account,\n branch = excluded.branch,\n preview = excluded.preview,\n content_snippet = excluded.content_snippet,\n message_count = excluded.message_count,\n page_message_count = excluded.page_message_count,\n last_message_sender = excluded.last_message_sender,\n timestamp = excluded.timestamp,\n first_sent_at = excluded.first_sent_at,\n first_sent_text = excluded.first_sent_text,\n last_sent_at = excluded.last_sent_at,\n last_sent_text = excluded.last_sent_text,\n model = excluded.model,\n is_subagent = excluded.is_subagent,\n parent_session_id = excluded.parent_session_id,\n is_teammate = excluded.is_teammate,\n team_name = excluded.team_name,\n tool_names_json = excluded.tool_names_json,\n last_prompt = excluded.last_prompt,\n status = 'active',\n index_seq = (SELECT COALESCE(MAX(index_seq), 0) + 1 FROM conversations),\n updated_at = CURRENT_TIMESTAMP`,\n )\n .run({\n file_id: fileId,\n // Canonical form, matching conversation_files.absolute_path — otherwise\n // a native-separator caller writes a second spelling of the same file\n // that every by-path lookup (and the deletion-reconcile) then misses.\n source_path: canonicalPath(meta.id),\n provider: meta.provider ?? CLAUDE_CODE_PROVIDER,\n kind: meta.kind ?? null,\n external_session_id: meta.externalSessionId ?? null,\n session_id: meta.sessionId,\n session_name: meta.sessionName || null,\n project_path: meta.projectPath || null,\n project_name: meta.projectName || null,\n account: meta.account,\n branch: meta.gitBranch,\n preview: meta.preview || null,\n content_snippet: meta.contentSnippet || null,\n message_count: meta.messageCount,\n page_message_count: pageMessageCount,\n last_message_sender: meta.lastMessageSender,\n timestamp: meta.timestamp || null,\n first_sent_at: meta.firstMessage?.timestamp ?? null,\n first_sent_text: meta.firstMessage?.text ?? null,\n last_sent_at: meta.lastMessage?.timestamp ?? null,\n last_sent_text: meta.lastMessage?.text ?? null,\n model: meta.model,\n is_subagent: meta.isSubagent ? 1 : 0,\n parent_session_id: meta.parentSessionId,\n is_teammate: meta.isTeammate ? 1 : 0,\n team_name: meta.teamName,\n tool_names_json: JSON.stringify(meta.toolNames),\n last_prompt: meta.lastPrompt ?? null,\n });\n }\n\n getBySourcePath(sourcePath: string): ConversationMeta | null {\n const row = this.db\n .prepare(\"SELECT * FROM conversations WHERE source_path = ? AND status = 'active'\")\n .get(canonicalPath(sourcePath)) as ConversationRow | undefined;\n return row ? rowToMeta(row) : null;\n }\n\n // Dual lookup matching scanner.getConversation: resolve by source_path (the\n // canonical id) first, then by session_id.\n //\n // session_id is NOT unique (see schema header) — the sessionId form is a\n // compatibility convenience. Resolution is deterministic and matches the\n // in-memory scanner: among active rows sharing the session_id, newest\n // timestamp wins, index_seq breaks sub-second ties (precision updated_at\n // lacks), then source_path ascending. Collision-safe callers should use\n // getAllBySessionId() instead.\n getByIdOrSession(id: string): ConversationMeta | null {\n const direct = this.getBySourcePath(id);\n if (direct) return direct;\n const row = this.db\n .prepare(\n `SELECT * FROM conversations WHERE session_id = ? AND status = 'active'\n ORDER BY COALESCE(timestamp, '') DESC, index_seq DESC, source_path ASC LIMIT 1`,\n )\n .get(id) as ConversationRow | undefined;\n return row ? rowToMeta(row) : null;\n }\n\n // All active conversations sharing a session_id, newest first (same ordering\n // as getByIdOrSession). Collision-safe counterpart to the convenience\n // getByIdOrSession() sessionId lookup.\n getAllBySessionId(sessionId: string): ConversationMeta[] {\n const rows = this.db\n .prepare(\n `SELECT * FROM conversations WHERE session_id = ? AND status = 'active'\n ORDER BY COALESCE(timestamp, '') DESC, index_seq DESC, source_path ASC`,\n )\n .all(sessionId) as ConversationRow[];\n return rows.map(rowToMeta);\n }\n\n // All active metas (unsorted/unfiltered) — callers apply the existing\n // filters/view transforms. Used by scan() before SQL filtering is wired in.\n allActive(): ConversationMeta[] {\n const rows = this.db\n .prepare(\"SELECT * FROM conversations WHERE status = 'active'\")\n .all() as ConversationRow[];\n return rows.map(rowToMeta);\n }\n\n // Most recent active conversations, newest first. Backs the empty-query\n // search path (mirrors the in-memory indexer's getRecent).\n recent(limit: number): ConversationMeta[] {\n const rows = this.db\n .prepare(\n `SELECT * FROM conversations WHERE status = 'active'\n ORDER BY COALESCE(timestamp, '') DESC, source_path ASC LIMIT ?`,\n )\n .all(limit) as ConversationRow[];\n return rows.map(rowToMeta);\n }\n\n distinctProjects(): string[] {\n const rows = this.db\n .prepare(\n `SELECT DISTINCT project_path FROM conversations\n WHERE status = 'active' AND project_path IS NOT NULL AND project_path != ''\n ORDER BY project_path ASC`,\n )\n .all() as { project_path: string }[];\n return rows.map((r) => r.project_path);\n }\n\n deleteByFileId(fileId: number): void {\n this.db\n .prepare(\n \"UPDATE conversations SET status = 'deleted', updated_at = CURRENT_TIMESTAMP WHERE file_id = ?\",\n )\n .run(fileId);\n }\n\n // The parseConversation message total for a file (for bounded paging), or 0\n // if not indexed.\n pageMessageCount(sourcePath: string): number {\n const row = this.db\n .prepare(\n \"SELECT page_message_count AS n FROM conversations WHERE source_path = ? AND status = 'active'\",\n )\n .get(canonicalPath(sourcePath)) as { n: number } | undefined;\n return row?.n ?? 0;\n }\n\n count(): number {\n return (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM conversations WHERE status = 'active'\").get() as {\n n: number;\n }\n ).n;\n }\n}\n","import type { Database } from \"better-sqlite3\";\nimport { canonicalPath } from \"../../canonical-path\";\nimport type { SearchDocument } from \"../../search-document\";\nimport {\n FTS_ELLIPSIS,\n FTS_HIT_CLOSE,\n FTS_HIT_OPEN,\n FTS_SNIPPET_TOKENS,\n parseFtsSnippet,\n} from \"../../search-matches\";\nimport type { ConversationMeta, SearchHighlight } from \"../../types\";\n\n// Body column positions in conversation_messages_fts, in snippet() priority\n// order. source_path is column 0.\nconst BODY_COLUMNS = [\n { name: \"text\", index: 1 },\n { name: \"thinking\", index: 2 },\n { name: \"tools\", index: 3 },\n] as const;\n\nexport interface FtsSearchFilters {\n account?: string;\n provider?: string;\n project?: string;\n since?: Date;\n include?: \"all\" | \"conversations\" | \"subagents\" | \"teammates\";\n}\n\nexport interface FtsHit {\n sourcePath: string;\n // Present when the match was in a body column. Null for a metadata-only hit,\n // which the caller resolves through generateMatches() instead.\n body: { snippet: string; highlights: SearchHighlight[] } | null;\n}\n\n// FTS5-backed search index over conversation body + metadata. One row per\n// conversation, addressed by rowid == conversation_files.id.\n//\n// Never look a row up by source_path: FTS5's planner handles only MATCH, rowid\n// and rank, so any other constraint linear-scans the table. That was tolerable\n// when a row held <=5 KB; with ~128 KB of body per row it would scan the whole\n// corpus on every append.\nexport class FtsRepo {\n constructor(private db: Database) {}\n\n upsert(rowId: number, meta: ConversationMeta, doc: SearchDocument): void {\n // Canonical form, matching conversations.source_path — a search hit is\n // resolved back through getBySourcePath(), so the two must agree.\n const sourcePath = canonicalPath(meta.id);\n const tx = this.db.transaction(() => {\n this.db.prepare(\"DELETE FROM conversation_messages_fts WHERE rowid = ?\").run(rowId);\n this.db\n .prepare(\n `INSERT INTO conversation_messages_fts\n (rowid, source_path, text, thinking, tools,\n project_name, session_id, session_name, account, model, branch, tool_names)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n rowId,\n sourcePath,\n doc.text,\n doc.thinking,\n doc.tools,\n meta.projectName ?? \"\",\n meta.sessionId ?? \"\",\n meta.sessionName ?? \"\",\n meta.account ?? \"\",\n meta.model ?? \"\",\n meta.gitBranch ?? \"\",\n meta.toolNames.join(\" \"),\n );\n });\n tx();\n }\n\n // Current durable buckets for a conversation, so an append can tail-extend\n // them without reparsing the file. Returns null when no row exists yet.\n readDocument(rowId: number): SearchDocument | null {\n const row = this.db\n .prepare(\"SELECT text, thinking, tools FROM conversation_messages_fts WHERE rowid = ?\")\n .get(rowId) as { text: string; thinking: string; tools: string } | undefined;\n if (!row) return null;\n return { text: row.text ?? \"\", thinking: row.thinking ?? \"\", tools: row.tools ?? \"\" };\n }\n\n // True when the stored row already equals what we would write, so an append\n // can skip re-tokenizing ~128 KB of body for nothing.\n //\n // The check covers the metadata columns too, not just the buckets: a\n // newly-seen tool name or a late-resolved session_name changes metadata while\n // the body is byte-identical, and nothing else ever rewrites this row — so\n // skipping on \"body unchanged\" alone would strand that stale value forever.\n isCurrent(rowId: number, meta: ConversationMeta, doc: SearchDocument): boolean {\n const row = this.db\n .prepare(\n `SELECT text, thinking, tools,\n project_name, session_id, session_name, account, model, branch, tool_names\n FROM conversation_messages_fts WHERE rowid = ?`,\n )\n .get(rowId) as Record<string, string> | undefined;\n if (!row) return false;\n\n return (\n row.text === doc.text &&\n row.thinking === doc.thinking &&\n row.tools === doc.tools &&\n row.project_name === (meta.projectName ?? \"\") &&\n row.session_id === (meta.sessionId ?? \"\") &&\n row.session_name === (meta.sessionName ?? \"\") &&\n row.account === (meta.account ?? \"\") &&\n row.model === (meta.model ?? \"\") &&\n row.branch === (meta.gitBranch ?? \"\") &&\n row.tool_names === meta.toolNames.join(\" \")\n );\n }\n\n remove(rowId: number): void {\n this.db.prepare(\"DELETE FROM conversation_messages_fts WHERE rowid = ?\").run(rowId);\n }\n\n // Ranked hits matching the query, best first. Filters run in SQL BEFORE LIMIT:\n // with a wide corpus a popular term matches far more conversations than one\n // page, so post-filtering an already-truncated list would report \"no results\"\n // for queries that do have them.\n search(query: string, limit: number, filters: FtsSearchFilters = {}): FtsHit[] {\n const match = toMatchQuery(query);\n if (!match) return [];\n\n const snippetSelects = BODY_COLUMNS.map(\n (c) => `snippet(conversation_messages_fts, ${c.index}, ?, ?, ?, ?) AS snippet_${c.name}`,\n ).join(\", \");\n\n const params: unknown[] = [];\n for (const _col of BODY_COLUMNS) {\n params.push(FTS_HIT_OPEN, FTS_HIT_CLOSE, FTS_ELLIPSIS, FTS_SNIPPET_TOKENS);\n }\n params.push(match);\n\n const where: string[] = [\"conversation_messages_fts MATCH ?\", \"c.status = 'active'\"];\n\n if (filters.account) {\n where.push(\"c.account = ?\");\n params.push(filters.account);\n }\n if (filters.provider) {\n where.push(\"c.provider = ?\");\n params.push(filters.provider);\n }\n if (filters.project) {\n where.push(\"(lower(c.project_path) LIKE ? OR lower(c.project_name) LIKE ?)\");\n const like = `%${filters.project.toLowerCase()}%`;\n params.push(like, like);\n }\n if (filters.since) {\n where.push(\"c.timestamp >= ?\");\n params.push(filters.since.toISOString());\n }\n if (filters.include === \"conversations\") {\n where.push(\"c.is_subagent = 0 AND c.is_teammate = 0\");\n } else if (filters.include === \"subagents\") {\n where.push(\"c.is_subagent = 1\");\n } else if (filters.include === \"teammates\") {\n where.push(\"c.is_teammate = 1\");\n }\n\n params.push(limit);\n\n const rows = this.db\n .prepare(\n `SELECT conversation_messages_fts.source_path AS source_path, ${snippetSelects}\n FROM conversation_messages_fts\n JOIN conversations c ON c.source_path = conversation_messages_fts.source_path\n WHERE ${where.join(\" AND \")}\n ORDER BY rank\n LIMIT ?`,\n )\n .all(...params) as Record<string, string>[];\n\n return rows.map((row) => ({ sourcePath: row.source_path, body: pickBodySnippet(row) }));\n }\n\n count(): number {\n return (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM conversation_messages_fts\").get() as { n: number }\n ).n;\n }\n}\n\n// snippet() returns a column's head even when the match was elsewhere, so the\n// hit markers are the only signal that THIS column matched. Take the first\n// marked column in priority order: text, then thinking, then tools.\nfunction pickBodySnippet(row: Record<string, string>) {\n for (const col of BODY_COLUMNS) {\n const parsed = parseFtsSnippet(row[`snippet_${col.name}`]);\n if (parsed) return parsed;\n }\n return null;\n}\n\n// Turn free-text into a safe FTS5 prefix query. Each whitespace-separated term\n// is wrapped in double quotes (so FTS5 special chars are treated literally) and\n// suffixed with * for prefix matching, mirroring FlexSearch's forward tokenizer.\n// Terms are ANDed together. Returns \"\" when nothing usable remains.\nfunction toMatchQuery(query: string): string {\n const terms = query\n .trim()\n .split(/\\s+/)\n .map((t) => t.replace(/\"/g, \"\").trim())\n .filter(Boolean);\n if (terms.length === 0) return \"\";\n return terms.map((t) => `\"${t}\"*`).join(\" AND \");\n}\n","import type { Database } from \"better-sqlite3\";\nimport { canonicalPath } from \"../../canonical-path\";\n\nexport interface ScannedDirRow {\n path: string;\n parent_root: string | null;\n mtime_ms: number;\n has_nested: number;\n}\n\n// Per-directory mtime watermarks backing the discovery dir-mtime gate. One row\n// for a profile's projectsDir (parent_root NULL), one row per immediate\n// project subdirectory (parent_root = the projectsDir path).\nexport class ScannedDirsRepo {\n constructor(private db: Database) {}\n\n get(path: string): ScannedDirRow | undefined {\n return this.db.prepare(\"SELECT * FROM scanned_dirs WHERE path = ?\").get(canonicalPath(path)) as\n | ScannedDirRow\n | undefined;\n }\n\n // Known project subdirectories under a root, path ascending (stable order).\n childrenOf(parentRoot: string): ScannedDirRow[] {\n return this.db\n .prepare(\"SELECT * FROM scanned_dirs WHERE parent_root = ? ORDER BY path ASC\")\n .all(canonicalPath(parentRoot)) as ScannedDirRow[];\n }\n\n upsert(path: string, parentRoot: string | null, mtimeMs: number, hasNested: boolean): void {\n this.db\n .prepare(\n `INSERT INTO scanned_dirs (path, parent_root, mtime_ms, has_nested, updated_at)\n VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT(path) DO UPDATE SET\n parent_root = excluded.parent_root,\n mtime_ms = excluded.mtime_ms,\n has_nested = excluded.has_nested,\n updated_at = CURRENT_TIMESTAMP`,\n )\n .run(\n canonicalPath(path),\n parentRoot === null ? null : canonicalPath(parentRoot),\n mtimeMs,\n hasNested ? 1 : 0,\n );\n }\n\n // Drop a project subdirectory's watermark row (it vanished from disk).\n remove(path: string): void {\n this.db.prepare(\"DELETE FROM scanned_dirs WHERE path = ?\").run(canonicalPath(path));\n }\n}\n","import type { FSWatcher } from \"chokidar\";\nimport chokidar from \"chokidar\";\nimport { getLogger } from \"../logger\";\nimport { getProjectsDir } from \"../profiles\";\nimport type { Profile } from \"../types\";\n\nconst EXCLUDED_SEGMENTS = [\"/memory/\", \"/tool-results/\"];\n\nexport interface WatchEvent {\n filePath: string;\n account: string;\n type: \"add\" | \"change\" | \"unlink\";\n}\n\nexport interface FileWatcherOptions {\n // Debounce window (ms) coalescing rapid events for one file. Spec §8.\n debounceMs?: number;\n}\n\n// Watches each active profile's projects dir for *.jsonl changes and forwards\n// debounced add/change/unlink events. Thin wrapper over chokidar; the scanner\n// owns what to do with the events (enqueue index jobs, emit to host apps).\nexport class FileWatcher {\n private watchers: FSWatcher[] = [];\n private timers = new Map<string, ReturnType<typeof setTimeout>>();\n private readonly debounceMs: number;\n\n constructor(\n private profiles: Profile[],\n private onEvent: (event: WatchEvent) => void,\n options: FileWatcherOptions = {},\n ) {\n this.debounceMs = options.debounceMs ?? 400;\n }\n\n // Resolves once every underlying chokidar watcher has finished its initial\n // scan, so the caller knows subsequent FS changes will be observed.\n async start(): Promise<void> {\n const log = getLogger();\n const ready: Promise<void>[] = [];\n for (const profile of this.profiles) {\n const dir = getProjectsDir(profile);\n const watcher = chokidar.watch(dir, {\n ignoreInitial: true,\n ignored: (p: string) => EXCLUDED_SEGMENTS.some((seg) => p.includes(seg)),\n awaitWriteFinish: { stabilityThreshold: this.debounceMs, pollInterval: 100 },\n });\n watcher\n .on(\"add\", (p) => this.dispatch(p, profile.id, \"add\"))\n .on(\"change\", (p) => this.dispatch(p, profile.id, \"change\"))\n .on(\"unlink\", (p) => this.dispatch(p, profile.id, \"unlink\"));\n ready.push(new Promise<void>((resolve) => watcher.once(\"ready\", () => resolve())));\n this.watchers.push(watcher);\n log.debug({ dir, account: profile.id }, \"watcher: watching\");\n }\n await Promise.all(ready);\n }\n\n async stop(): Promise<void> {\n for (const t of this.timers.values()) clearTimeout(t);\n this.timers.clear();\n await Promise.all(this.watchers.map((w) => w.close()));\n this.watchers = [];\n }\n\n private dispatch(filePath: string, account: string, type: WatchEvent[\"type\"]): void {\n if (!filePath.endsWith(\".jsonl\")) return;\n // Debounce per path: collapse a flurry of events into the latest one.\n const existing = this.timers.get(filePath);\n if (existing) clearTimeout(existing);\n this.timers.set(\n filePath,\n setTimeout(() => {\n this.timers.delete(filePath);\n this.onEvent({ filePath, account, type });\n }, this.debounceMs),\n );\n }\n}\n","import { getLogger } from \"../logger\";\n\nexport type JobReason = \"watcher\" | \"manual\" | \"periodic\";\n\nexport interface IndexJob {\n filePath: string;\n account: string;\n reason: JobReason;\n}\n\n// In-process job queue for incremental indexing. Jobs are deduplicated by file\n// path (the latest job for a path wins) and processed one at a time, because\n// SQLite has a single writer. A burst of filesystem events for the same file\n// collapses to one index pass.\nexport class IndexQueue {\n private pending = new Map<string, IndexJob>();\n private running = false;\n private idleResolvers: Array<() => void> = [];\n\n constructor(private process: (job: IndexJob) => Promise<void>) {}\n\n enqueue(job: IndexJob): void {\n this.pending.set(job.filePath, job);\n void this.drain();\n }\n\n // Resolves when the queue has fully drained — useful for tests and for a\n // clean shutdown.\n onIdle(): Promise<void> {\n if (!this.running && this.pending.size === 0) return Promise.resolve();\n return new Promise((resolve) => this.idleResolvers.push(resolve));\n }\n\n get size(): number {\n return this.pending.size;\n }\n\n private async drain(): Promise<void> {\n if (this.running) return;\n this.running = true;\n const log = getLogger();\n\n while (this.pending.size > 0) {\n const [path, job] = this.pending.entries().next().value as [string, IndexJob];\n this.pending.delete(path);\n try {\n await this.process(job);\n } catch (err) {\n log.warn({ filePath: job.filePath, err }, \"index-queue: job failed\");\n }\n }\n\n this.running = false;\n const resolvers = this.idleResolvers;\n this.idleResolvers = [];\n for (const r of resolvers) r();\n }\n}\n","// ─── Primitives ─────────────────────────────────────────────────────\n\nexport type MessageSender = \"user\" | \"assistant\";\nexport type Include = \"all\" | \"conversations\" | \"subagents\" | \"teammates\";\nexport type View = \"flat\" | \"tree\" | \"grouped\";\nexport type SortOrder = \"recent\" | \"oldest\" | \"messages-desc\" | \"messages-asc\" | \"alpha\";\n\nexport const VALID_SORT_ORDERS: SortOrder[] = [\n \"recent\",\n \"oldest\",\n \"messages-desc\",\n \"messages-asc\",\n \"alpha\",\n];\n\n// ─── Profile ────────────────────────────────────────────────────────\n\nexport interface Profile {\n id: string;\n label: string;\n configDir: string;\n enabled: boolean;\n emoji?: string;\n scanHistory?: boolean;\n}\n\n// ─── Content Tiers ──────────────────────────────────────────────────\n\nexport interface ContentTier {\n name: string;\n previewMax: number;\n snippetMax: number;\n}\n\n// ─── ConversationMeta (full superset) ───────────────────────────────\n\nexport interface MessageSnapshot {\n text: string;\n timestamp: string;\n}\n\nexport type ProviderName = \"claude-code\" | \"codex-cli\";\n\nexport interface ConversationMeta {\n id: string;\n filePath: string;\n sessionId: string;\n // Which provider produced this meta. Optional/additive: existing claude-code\n // metas default to \"claude-code\" when unset.\n provider?: ProviderName;\n // Distinguishes plain chat conversations from task-oriented logs (Codex).\n kind?: \"conversation\" | \"task\";\n // Provider-native session id when it differs from the (non-unique) sessionId.\n externalSessionId?: string;\n sessionName: string;\n projectPath: string;\n projectName: string;\n account: string;\n timestamp: string;\n messageCount: number;\n lastMessageSender: MessageSender;\n preview: string;\n contentSnippet: string;\n gitBranch: string | null;\n model: string | null;\n isSubagent: boolean;\n parentSessionId: string | null;\n isTeammate: boolean;\n teamName: string | null;\n toolNames: string[];\n firstMessage: MessageSnapshot | null;\n lastMessage: MessageSnapshot | null;\n lastPrompt?: string;\n}\n\n// ─── View Variants ──────────────────────────────────────────────────\n\nexport interface TreeConversation extends ConversationMeta {\n subagents: ConversationMeta[];\n}\n\nexport interface GroupedConversations {\n [groupKey: string]: ConversationMeta[];\n}\n\n// ─── Options ────────────────────────────────────────────────────────\n\nexport interface FileStatEntry {\n mtimeMs: number;\n size: number;\n}\n\nexport interface ScanOptions {\n profiles?: Profile[];\n // Providers to scan. Defaults to [\"claude-code\"]. Including \"codex-cli\"\n // requires codexRoots (no default home scan).\n providers?: ProviderName[];\n // Absolute roots to discover Codex CLI history under (e.g. ~/.codex/sessions).\n codexRoots?: string[];\n tier?: string;\n tiers?: Record<string, ContentTier>;\n include?: Include;\n view?: View;\n sort?: SortOrder;\n since?: string;\n project?: string;\n account?: string;\n limit?: number;\n offset?: number;\n onProgress?: (scanned: number, total: number) => void;\n onBatch?: (metas: ConversationMeta[]) => void;\n /** Known file stats from a previous scan. Files whose (mtimeMs, size) match\n * are skipped — the cached ConversationMeta is reused instead. */\n statCache?: Map<string, { stat: FileStatEntry; meta: ConversationMeta }>;\n /** Persistent mode only: bypass the dir-mtime discovery gate and force a\n * full glob of every profile's projectsDir, ignoring stored watermarks.\n * Use for an explicit user-triggered refresh that shouldn't trust the\n * gate's optimization. */\n fullRescan?: boolean;\n}\n\nexport interface ScanResult {\n conversations: ConversationMeta[] | TreeConversation[] | GroupedConversations;\n total: number;\n scanned: number;\n}\n\nexport interface SearchOptions extends ScanOptions {\n fields?: string[];\n // Restrict results to one provider.\n provider?: ProviderName;\n}\n\n// Half-open [start, end) character range into a SearchMatch.snippet marking one\n// matched term, so a client can emphasise it without parsing markup. Ranges\n// follow FTS token boundaries, not the query string: a prefix search for\n// \"timeout\" highlights the whole token \"timeoutMs\".\nexport interface SearchHighlight {\n start: number;\n end: number;\n}\n\nexport interface SearchMatch {\n field: string;\n snippet: string;\n // Present on body hits (field === \"content\"), where the excerpt comes from\n // FTS5 snippet(). Absent on metadata hits from generateMatches().\n highlights?: SearchHighlight[];\n}\n\nexport interface SearchResult {\n meta: ConversationMeta;\n score: number;\n matches: SearchMatch[];\n}\n\nexport interface GetConversationOptions {\n profiles?: Profile[];\n}\n\nexport interface GetConversationPageOptions {\n beforeIndex?: number;\n limit: number;\n}\n\nexport interface ConversationPage {\n messages: ConversationMessage[];\n total: number;\n fromIndex: number;\n}\n\n// A page sliced directly from a single parsed JSONL file, carrying the parsed\n// Conversation alongside the window so a caller can build a full response\n// (meta + messages) from one parse, without a prior scan().\nexport interface SingleFilePage extends ConversationPage {\n conversation: Conversation;\n}\n\n// ─── Full Conversation ──────────────────────────────────────────────\n\nexport interface TurnDuration {\n durationMs: number;\n messageCount: number;\n uuid?: string;\n}\n\nexport interface DeferredToolsDeltaAttachment {\n type: \"deferred_tools_delta\";\n addedNames: string[];\n addedLines: unknown[];\n removedNames: string[];\n}\n\nexport type AttachmentSidecar =\n | DeferredToolsDeltaAttachment\n | { type: string; [key: string]: unknown };\n\nexport interface ToolUseBlock {\n id: string;\n name: string;\n input: Record<string, unknown>;\n}\n\nexport interface ToolResultBlock {\n toolUseId: string;\n type:\n | \"edit\"\n | \"write\"\n | \"read\"\n | \"bash\"\n | \"grep\"\n | \"glob\"\n | \"taskAgent\"\n | \"taskCreate\"\n | \"taskUpdate\"\n | \"generic\";\n content: Record<string, unknown>;\n isError?: boolean;\n}\n\nexport interface TeamInfo {\n teammateId: string;\n summary?: string;\n color?: string;\n}\n\nexport interface MessageMetadata {\n model?: string;\n stopReason?: string | null;\n inputTokens?: number;\n outputTokens?: number;\n cacheReadTokens?: number;\n cacheCreationTokens?: number;\n gitBranch?: string;\n version?: string;\n toolUses?: string[];\n toolUseBlocks?: ToolUseBlock[];\n toolResults?: ToolResultBlock[];\n teamName?: string;\n teamInfo?: TeamInfo;\n}\n\nexport interface ConversationMessage {\n role: MessageSender;\n text: string;\n timestamp: string;\n uuid?: string;\n metadata?: MessageMetadata;\n isToolResult?: boolean;\n isThinking?: boolean;\n thinkingContent?: string;\n thinkingSignature?: string;\n parentUuid?: string | null;\n requestId?: string;\n promptId?: string;\n isSidechain?: boolean;\n permissionMode?: string;\n hasImages?: boolean;\n attachment?: AttachmentSidecar;\n}\n\nexport interface Conversation {\n id: string;\n filePath: string;\n projectPath: string;\n projectName: string;\n sessionId: string;\n sessionName: string;\n messages: ConversationMessage[];\n fullText: string;\n timestamp: string;\n messageCount: number;\n account: string;\n turnDurations?: TurnDuration[];\n lastPrompt?: string;\n}\n","export {\n applyAccountFilter,\n applyIncludeFilter,\n applyPagination,\n applyProjectFilter,\n applySinceFilter,\n applySort,\n} from \"./filters\";\nexport { readGitBranch } from \"./git\";\nexport { SearchIndexer } from \"./indexer\";\nexport type { Logger, LoggerOptions } from \"./logger\";\nexport { createLogger, getLogger, setLogger } from \"./logger\";\nexport type { ConvReducerState as JsonlParseState } from \"./persistent/conversation-reducer\";\nexport {\n initialConvState as createJsonlParseState,\n parseJsonlLine,\n} from \"./persistent/conversation-reducer\";\nexport type { Sidecar } from \"./persistent/sidecar\";\nexport { readSidecar, sidecarPath } from \"./persistent/sidecar\";\nexport {\n detectDefaultProfile,\n getProjectsDir,\n loadProfiles,\n resolveConfigDir,\n saveProfiles,\n} from \"./profiles\";\nexport { CodexCliProvider } from \"./providers/codex-cli\";\nexport type {\n DiscoveredConversationFile,\n ScannerProvider,\n ScannerProviderName,\n} from \"./providers/provider\";\nexport { ThreadbaseProvider } from \"./providers/threadbase\";\nexport type {\n ConversationScannerOptions,\n PersistentConfig,\n ScannerChangeEvent,\n WatchOptions,\n} from \"./scanner\";\nexport { ConversationScanner } from \"./scanner\";\nexport { cleanSystemTags } from \"./tags\";\nexport { DEFAULT_TIERS, resolveTier } from \"./tiers\";\nexport * from \"./types\";\n\n// ─── Standalone Convenience Functions ───────────────────────────────\n\nimport { ConversationScanner } from \"./scanner\";\nimport type {\n Conversation,\n GetConversationOptions,\n ScanOptions,\n ScanResult,\n SearchOptions,\n SearchResult,\n} from \"./types\";\n\nlet defaultScanner: ConversationScanner | undefined;\n\nfunction getDefaultScanner(): ConversationScanner {\n if (!defaultScanner) {\n defaultScanner = new ConversationScanner();\n }\n return defaultScanner;\n}\n\nexport function resetDefaultScanner(): void {\n defaultScanner = undefined;\n}\n\nexport async function scan(\n options?: ScanOptions,\n scanner?: ConversationScanner,\n): Promise<ScanResult> {\n return (scanner ?? getDefaultScanner()).scan(options);\n}\n\nexport async function search(\n query: string,\n options?: SearchOptions,\n scanner?: ConversationScanner,\n): Promise<SearchResult[]> {\n return (scanner ?? getDefaultScanner()).search(query, options);\n}\n\nexport async function getConversation(\n id: string,\n options?: GetConversationOptions,\n scanner?: ConversationScanner,\n): Promise<Conversation | null> {\n return (scanner ?? getDefaultScanner()).getConversation(id, options);\n}\n"],"mappings":"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","names":["basename","dirname","join","dirname","join","getShortProjectName","basename","readFileSync","readFileSync","join","join","createReadStream","basename","createInterface","basename","getShortProjectName","createInterface","createReadStream","fg","stat","fg","stat","closeSync","openSync","readSync","statSync","homedir","join","basename","createReadStream","yieldToEventLoop","createReadStream","createReadStream","createReadStream","yieldToEventLoop","getShortProjectName","basename","stat","statSync","createReadStream","createInterface","createInterface","createReadStream","extractCodexText","dirname","dirname","stat","dirname","join","stat","dirname","join","basename","dirname","dirname","basename","stat","meta","total","beforeIndex","fromIndex","statSync","EXCLUDED_SEGMENTS","process","BATCH_SIZE","join","homedir","statSync","previous","resolvedAccount","meta","openSync","readSync","closeSync"]}
1
+ {"version":3,"sources":["../src/filters.ts","../src/git.ts","../src/logger.ts","../src/indexer.ts","../src/search-matches.ts","../src/parser.ts","../src/persistent/metadata-reducer.ts","../src/providers/provider.ts","../src/tags.ts","../src/persistent/conversation-reducer.ts","../src/persistent/sidecar.ts","../src/profiles.ts","../src/providers/codex-cli.ts","../src/canonical-path.ts","../src/discovery.ts","../src/providers/threadbase.ts","../src/scanner.ts","../src/cache.ts","../src/persistent/conversation-stream.ts","../src/persistent/paged-reader.ts","../src/persistent/jsonl-tail-reader.ts","../src/persistent/cursor.ts","../src/persistent/index-engine.ts","../src/providers/parse.ts","../src/search-document.ts","../src/tiers.ts","../src/persistent/db.ts","../src/persistent/schema.ts","../src/persistent/migrations.ts","../src/persistent/dir-watermark.ts","../src/persistent/repositories/checkpoints.repo.ts","../src/persistent/repositories/conversation-files.repo.ts","../src/persistent/repositories/conversations.repo.ts","../src/persistent/repositories/fts.repo.ts","../src/persistent/repositories/scanned-dirs.repo.ts","../src/watcher/file-watcher.ts","../src/watcher/index-queue.ts","../src/types.ts","../src/index.ts"],"sourcesContent":["import type { ConversationMeta, Include, SortOrder } from \"./types\";\n\nexport function applySort(metas: ConversationMeta[], order: SortOrder): ConversationMeta[] {\n const out = [...metas];\n // Tie-break by id (the file path) so the order is fully deterministic and does\n // not depend on input order. This matters because metas can arrive in\n // different orders (filesystem discovery vs. an SQLite query), and a tie on\n // the primary key would otherwise resolve differently between them.\n const tie = (a: ConversationMeta, b: ConversationMeta) => a.id.localeCompare(b.id);\n switch (order) {\n case \"recent\":\n out.sort((a, b) => b.timestamp.localeCompare(a.timestamp) || tie(a, b));\n break;\n case \"oldest\":\n out.sort((a, b) => a.timestamp.localeCompare(b.timestamp) || tie(a, b));\n break;\n case \"messages-desc\":\n out.sort((a, b) => b.messageCount - a.messageCount || tie(a, b));\n break;\n case \"messages-asc\":\n out.sort((a, b) => a.messageCount - b.messageCount || tie(a, b));\n break;\n case \"alpha\":\n out.sort((a, b) => {\n const cmp = a.projectName.localeCompare(b.projectName);\n if (cmp !== 0) return cmp;\n return a.preview.localeCompare(b.preview) || tie(a, b);\n });\n break;\n }\n return out;\n}\n\nexport function applySinceFilter(metas: ConversationMeta[], since: string): ConversationMeta[] {\n const cutoff = parseSinceCutoff(since);\n return metas.filter((m) => new Date(m.timestamp).getTime() >= cutoff.getTime());\n}\n\nexport function applyIncludeFilter(\n metas: ConversationMeta[],\n include: Include,\n): ConversationMeta[] {\n switch (include) {\n case \"all\":\n return metas;\n case \"conversations\":\n return metas.filter((m) => !m.isSubagent && !m.isTeammate);\n case \"subagents\":\n return metas.filter((m) => m.isSubagent);\n case \"teammates\":\n return metas.filter((m) => m.isTeammate);\n }\n}\n\nexport function applyProjectFilter(metas: ConversationMeta[], project: string): ConversationMeta[] {\n const lower = project.toLowerCase();\n return metas.filter(\n (m) =>\n m.projectPath.toLowerCase().includes(lower) || m.projectName.toLowerCase().includes(lower),\n );\n}\n\nexport function applyAccountFilter(metas: ConversationMeta[], account: string): ConversationMeta[] {\n return metas.filter((m) => m.account === account);\n}\n\nexport function applyPagination<T>(\n items: T[],\n limit: number,\n offset: number,\n): { items: T[]; total: number } {\n return {\n items: items.slice(offset, offset + limit),\n total: items.length,\n };\n}\n\nexport function parseSinceCutoff(value: string): Date {\n const s = value.trim();\n if (!s) throw new Error(\"Empty --since value\");\n\n // Try ISO date\n const isoMatch = s.match(/^\\d{4}-\\d{2}-\\d{2}$/);\n if (isoMatch) {\n const d = new Date(`${s}T00:00:00Z`);\n if (!Number.isNaN(d.getTime())) return d;\n }\n\n // Try duration: digits + unit\n const durationMatch = s.match(/^(\\d+)([hdw])$/);\n if (!durationMatch) {\n throw new Error(\n `Invalid --since value \"${s}\": expected duration like \"7d\", \"24h\", \"2w\" or ISO date \"2006-01-02\"`,\n );\n }\n\n const n = parseInt(durationMatch[1], 10);\n const unit = durationMatch[2];\n let ms: number;\n switch (unit) {\n case \"h\":\n ms = n * 60 * 60 * 1000;\n break;\n case \"d\":\n ms = n * 24 * 60 * 60 * 1000;\n break;\n case \"w\":\n ms = n * 7 * 24 * 60 * 60 * 1000;\n break;\n default:\n throw new Error(`Invalid unit \"${unit}\"`);\n }\n\n return new Date(Date.now() - ms);\n}\n","import { readFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport { getLogger } from \"./logger\";\n\nconst REF_PREFIX = \"ref: refs/heads/\";\nconst MAX_DEPTH = 6;\n\nexport function readGitBranch(projectPath: string): string | null {\n if (!projectPath) return null;\n const log = getLogger();\n\n let dir = projectPath;\n let depth = 0;\n\n while (depth < MAX_DEPTH) {\n const headPath = join(dir, \".git\", \"HEAD\");\n try {\n const content = readFileSync(headPath, \"utf-8\").trim();\n if (content.startsWith(REF_PREFIX)) {\n const branch = content.slice(REF_PREFIX.length);\n log.trace({ projectPath, dir, branch }, \"git: branch resolved\");\n return branch;\n }\n // Detached HEAD: raw commit SHA\n if (content.length >= 7) {\n log.trace({ projectPath, dir }, \"git: detached HEAD\");\n return \"(detached)\";\n }\n return null;\n } catch {\n // .git/HEAD not found at this level\n }\n\n const parent = dirname(dir);\n if (parent === dir) return null; // filesystem root\n dir = parent;\n depth++;\n }\n\n log.trace({ projectPath }, \"git: no .git found within depth\");\n return null;\n}\n","import pino, { type Logger, type LoggerOptions } from \"pino\";\n\nlet currentLogger: Logger = pino({ level: \"silent\" });\n\nexport function createLogger(options?: LoggerOptions | Logger): Logger {\n if (options && typeof (options as Logger).child === \"function\") {\n return options as Logger;\n }\n return pino((options as LoggerOptions) ?? { level: \"silent\" });\n}\n\nexport function setLogger(logger: Logger): void {\n currentLogger = logger;\n}\n\nexport function getLogger(): Logger {\n return currentLogger;\n}\n\nexport type { Logger, LoggerOptions };\n","import FlexSearchModule from \"flexsearch\";\n\n// FlexSearch has inconsistent default export across ESM/CJS\nconst FlexSearch = (FlexSearchModule as any).default ?? FlexSearchModule;\n\nimport { getLogger } from \"./logger\";\nimport { buildContentMatch, generateMatches } from \"./search-matches\";\nimport type { ConversationMeta, SearchMatch, SearchResult } from \"./types\";\n\nexport class SearchIndexer {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private index: any;\n private documents = new Map<string, ConversationMeta>();\n // The indexed body per conversation, kept so a hit can produce a real excerpt\n // instead of falling back to an unrelated preview. Sized by the caller (the\n // scanner tail-caps it to the content tier) — see the note on addDocument.\n private searchContents = new Map<string, string>();\n\n constructor() {\n this.index = this.createIndex();\n }\n\n private createIndex() {\n return new (FlexSearch as any).Document({\n document: {\n id: \"id\",\n index: [\n \"content\",\n \"projectName\",\n \"projectPath\",\n \"sessionId\",\n \"sessionName\",\n \"account\",\n \"model\",\n \"gitBranch\",\n \"toolNames\",\n ],\n store: [\"id\"],\n },\n tokenize: \"forward\",\n resolution: 9,\n cache: 100,\n });\n }\n\n // `searchContent` is the combined search document (text + thinking + tools),\n // NOT meta.contentSnippet.\n //\n // It arrives pre-capped. This index is `tokenize: \"forward\"` at resolution 9,\n // which stores every prefix of every token, so it is a resident-memory\n // structure whose cost is very different from the on-disk FTS index. Feeding\n // it the full ~128 KB budget across a few hundred conversations would be a\n // multi-hundred-MB-to-GB index. The scanner therefore caps this to the active\n // tier's snippetMax and accepts that `persistent: false` has lower recall than\n // SQLite — an honest, documented divergence rather than a silent one.\n addDocument(meta: ConversationMeta, searchContent = \"\"): void {\n this.documents.set(meta.id, meta);\n this.searchContents.set(meta.id, searchContent);\n this.index.add(toIndexDoc(meta, searchContent));\n }\n\n buildIndex(metas: ConversationMeta[], searchContents?: Map<string, string>): void {\n this.clear();\n for (const meta of metas) {\n this.addDocument(meta, searchContents?.get(meta.id) ?? \"\");\n }\n getLogger().debug({ docCount: metas.length }, \"indexer: built\");\n }\n\n search(query: string, options?: { fields?: string[]; limit?: number }): SearchResult[] {\n const limit = options?.limit ?? 50;\n\n if (!query.trim()) {\n return this.getRecent(limit);\n }\n\n const results = this.index.search(query, { limit: limit * 2, enrich: true });\n\n const seen = new Set<string>();\n const searchResults: SearchResult[] = [];\n\n for (const fieldResult of results) {\n if (!fieldResult.result) continue;\n for (const item of fieldResult.result) {\n const id = typeof item === \"object\" ? (item as { id: string }).id : String(item);\n if (seen.has(id)) continue;\n seen.add(id);\n\n const meta = this.documents.get(id);\n if (!meta) continue;\n\n searchResults.push({\n meta,\n score: 1,\n matches: this.matchesFor(meta, query),\n });\n if (searchResults.length >= limit) break;\n }\n if (searchResults.length >= limit) break;\n }\n\n return searchResults;\n }\n\n // Body context first (that is what explains why the result appeared), then any\n // metadata matches. Only when neither hits does generateMatches' preview\n // fallback stand in.\n private matchesFor(meta: ConversationMeta, query: string): SearchMatch[] {\n const contentMatch = buildContentMatch(this.searchContents.get(meta.id) ?? \"\", query);\n const metaMatches = generateMatches(meta, query);\n if (!contentMatch) return metaMatches;\n return [contentMatch, ...metaMatches.filter((m) => m.field !== \"preview\")];\n }\n\n private getRecent(limit: number): SearchResult[] {\n return Array.from(this.documents.values())\n .sort((a, b) => b.timestamp.localeCompare(a.timestamp))\n .slice(0, limit)\n .map((meta) => ({\n meta,\n score: 1,\n matches: [{ field: \"timestamp\", snippet: meta.preview }],\n }));\n }\n\n getDocumentCount(): number {\n return this.documents.size;\n }\n\n // Replace an already-indexed document in place. FlexSearch's `add` does not\n // overwrite an existing id, so a single-file refresh must go through\n // `update` to avoid stale matches lingering in the index.\n updateDocument(meta: ConversationMeta, searchContent = \"\"): void {\n this.documents.set(meta.id, meta);\n this.searchContents.set(meta.id, searchContent);\n this.index.update(toIndexDoc(meta, searchContent));\n }\n\n removeDocument(id: string): void {\n this.documents.delete(id);\n this.searchContents.delete(id);\n this.index.remove(id);\n }\n\n clear(): void {\n this.documents.clear();\n this.searchContents.clear();\n this.index = this.createIndex();\n getLogger().trace(\"indexer: cleared\");\n }\n}\n\nfunction toIndexDoc(meta: ConversationMeta, searchContent: string) {\n return {\n id: meta.id,\n content: searchContent,\n projectName: meta.projectName,\n projectPath: meta.projectPath,\n sessionId: meta.sessionId,\n sessionName: meta.sessionName,\n account: meta.account,\n model: meta.model || \"\",\n gitBranch: meta.gitBranch || \"\",\n toolNames: meta.toolNames.join(\" \"),\n };\n}\n","import type { ConversationMeta, SearchHighlight, SearchMatch } from \"./types\";\n\n// Delimiters handed to FTS5 snippet() to mark matched tokens, stripped again\n// before the excerpt reaches a caller. Control characters (STX/ETX) rather than\n// a textual sentinel like [[HIT]]: a transcript can legitimately contain wiki\n// link syntax, and a collision would silently shift every highlight offset.\nexport const FTS_HIT_OPEN = \"\\u0002\";\nexport const FTS_HIT_CLOSE = \"\\u0003\";\n\n// Tokens of context FTS5 puts around a hit. Clients decide final layout.\nexport const FTS_SNIPPET_TOKENS = 16;\n\nexport const FTS_ELLIPSIS = \"…\";\n\n// Field name used for any conversation-body hit, whichever body column matched.\n// Column identity (text/thinking/tools) is an indexing detail; consumers only\n// need to distinguish \"body\" from \"metadata\".\nexport const CONTENT_FIELD = \"content\";\n\n// Convert one raw FTS5 snippet() string into plain text plus highlight ranges.\n// Returns null when the column carried no marked hit — snippet() returns the\n// head of a column even when the match was in a different column, so marker\n// presence is the only reliable \"did THIS column match\" signal.\nexport function parseFtsSnippet(\n raw: string | null | undefined,\n): { snippet: string; highlights: SearchHighlight[] } | null {\n if (!raw?.includes(FTS_HIT_OPEN)) return null;\n\n const highlights: SearchHighlight[] = [];\n let snippet = \"\";\n let openAt = -1;\n\n for (const ch of raw) {\n if (ch === FTS_HIT_OPEN) {\n openAt = snippet.length;\n continue;\n }\n if (ch === FTS_HIT_CLOSE) {\n if (openAt >= 0 && snippet.length > openAt) {\n highlights.push({ start: openAt, end: snippet.length });\n }\n openAt = -1;\n continue;\n }\n snippet += ch;\n }\n\n return highlights.length > 0 ? { snippet, highlights } : null;\n}\n\n// Per-field match snippets for a METADATA hit. Body context no longer comes from\n// here: it comes from FTS5 snippet() (persistent) or buildContentMatch()\n// (in-memory), because meta.contentSnippet is a 5 KB head-biased preview, not\n// the search corpus.\n//\n// Field ids are camelCase, matching ConversationMeta — never the snake_case FTS\n// column names. Consumers switch on matches[].field, so the two vocabularies\n// must not mix. Order follows the documented precedence.\nexport function generateMatches(meta: ConversationMeta, query: string): SearchMatch[] {\n const matches: SearchMatch[] = [];\n const lowerQuery = query.toLowerCase();\n\n const fields: [string, string][] = [\n [\"sessionName\", meta.sessionName],\n [\"projectName\", meta.projectName],\n [\"gitBranch\", meta.gitBranch || \"\"],\n [\"toolNames\", meta.toolNames.join(\" \")],\n [\"model\", meta.model || \"\"],\n [\"account\", meta.account],\n [\"sessionId\", meta.sessionId],\n ];\n\n for (const [field, value] of fields) {\n const idx = value.toLowerCase().indexOf(lowerQuery);\n if (idx !== -1) {\n const start = Math.max(0, idx - 80);\n const end = Math.min(value.length, idx + query.length + 120);\n let snippet = value.slice(start, end);\n if (start > 0) snippet = `...${snippet}`;\n if (end < value.length) snippet = `${snippet}...`;\n matches.push({ field, snippet });\n }\n }\n\n return matches.length > 0 ? matches : [{ field: \"preview\", snippet: meta.preview }];\n}\n\n// Body context for the in-memory path, which has no FTS5 snippet(). Locates the\n// query literally in the combined search document and returns the same\n// {field, snippet, highlights} shape the persistent path produces.\nexport function buildContentMatch(searchContent: string, query: string): SearchMatch | null {\n if (!searchContent || !query.trim()) return null;\n const idx = searchContent.toLowerCase().indexOf(query.toLowerCase());\n if (idx === -1) return null;\n\n const start = Math.max(0, idx - 80);\n const end = Math.min(searchContent.length, idx + query.length + 120);\n const body = searchContent.slice(start, end).replace(/\\s+/g, \" \").trim();\n\n // Measure the hit AFTER the whitespace collapse, so the offsets describe the\n // string actually being returned.\n const hitAt = body.toLowerCase().indexOf(query.toLowerCase());\n const prefix = start > 0 ? FTS_ELLIPSIS : \"\";\n const suffix = end < searchContent.length ? FTS_ELLIPSIS : \"\";\n const snippet = `${prefix}${body}${suffix}`;\n\n const highlights: SearchHighlight[] =\n hitAt === -1\n ? []\n : [{ start: hitAt + prefix.length, end: hitAt + prefix.length + query.length }];\n\n return { field: CONTENT_FIELD, snippet, highlights };\n}\n","import { createReadStream } from \"fs\";\nimport { basename } from \"path\";\nimport { createInterface } from \"readline\";\nimport { getLogger } from \"./logger\";\nimport { applyTeamInfo, initialConvState, reduceConvLine } from \"./persistent/conversation-reducer\";\nimport {\n deriveSessionNameFromFirstMessage,\n finalizeMeta,\n initialReducerState,\n reduceLine,\n} from \"./persistent/metadata-reducer\";\nimport { cleanSystemTags } from \"./tags\";\nimport type {\n ContentTier,\n Conversation,\n ConversationMessage,\n ConversationMeta,\n TeamInfo,\n ToolResultBlock,\n ToolUseBlock,\n TurnDuration,\n} from \"./types\";\n\n// `onEntry` lets a caller fold a second accumulator (the search document) over\n// the same lines without re-reading the file.\nexport async function parseMeta(\n filePath: string,\n account: string,\n tier: ContentTier,\n onEntry?: (entry: Record<string, unknown>) => void,\n): Promise<ConversationMeta | null> {\n const log = getLogger();\n log.trace({ filePath, account, tier: tier.name }, \"parseMeta: start\");\n\n // Stream the whole file through the same per-line fold the incremental\n // indexer uses, so a full parse and an append-then-resume produce identical\n // metadata by construction.\n const state = initialReducerState();\n const fileStream = createReadStream(filePath);\n const rl = createInterface({ input: fileStream, crlfDelay: Infinity });\n\n try {\n for await (const line of rl) {\n if (!line.trim()) continue;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n state.badJsonLines++;\n continue;\n }\n reduceLine(state, entry, tier);\n onEntry?.(entry);\n }\n } catch (err) {\n log.warn({ filePath, err }, \"parseMeta: read failed\");\n return null;\n }\n\n if (state.badJsonLines > 0) {\n log.warn(\n { filePath, badJsonLines: state.badJsonLines },\n \"parseMeta: skipped malformed JSON lines\",\n );\n }\n\n const meta = finalizeMeta(state, filePath, account, tier);\n if (!meta) log.trace({ filePath }, \"parseMeta: no messages\");\n return meta;\n}\n\nexport async function parseConversation(\n filePath: string,\n account: string,\n): Promise<Conversation | null> {\n const log = getLogger();\n log.trace({ filePath, account }, \"parseConversation: start\");\n const messages: ConversationMessage[] = [];\n let badJsonLines = 0;\n const textParts: string[] = [];\n const turnDurations: TurnDuration[] = [];\n\n // The message line→message reduction lives in reduceConvLine (shared with the\n // bounded page reader). turn_duration collection is parseConversation-only\n // (pages don't need it), so it stays inline here.\n const state = initialConvState();\n\n const fileStream = createReadStream(filePath);\n const rl = createInterface({ input: fileStream, crlfDelay: Infinity });\n\n try {\n for await (const line of rl) {\n if (!line.trim()) continue;\n\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n badJsonLines++;\n continue;\n }\n\n if (\n entry.type === \"system\" &&\n entry.subtype === \"turn_duration\" &&\n typeof entry.durationMs === \"number\"\n ) {\n turnDurations.push({\n durationMs: entry.durationMs as number,\n messageCount: (entry.messageCount as number) || 0,\n uuid: entry.uuid as string | undefined,\n });\n continue;\n }\n\n const message = reduceConvLine(state, entry);\n if (message) {\n messages.push(message);\n if (message.text) textParts.push(message.text);\n }\n }\n } catch (err) {\n log.warn({ filePath, err }, \"parseConversation: read failed\");\n return null;\n }\n\n if (badJsonLines > 0) {\n log.warn({ filePath, badJsonLines }, \"parseConversation: skipped malformed JSON lines\");\n }\n\n if (messages.length === 0) {\n log.trace({ filePath }, \"parseConversation: no messages\");\n return null;\n }\n\n log.debug({ filePath, messageCount: messages.length }, \"parseConversation: complete\");\n applyTeamInfo(messages, state);\n\n return {\n id: filePath,\n filePath,\n projectPath: state.cwd,\n projectName: getShortProjectName(state.cwd),\n sessionId: state.sessionId || basename(filePath, \".jsonl\"),\n sessionName:\n state.sessionName ||\n deriveSessionNameFromFirstMessage(messages.find((m) => m.role === \"user\" && m.text) ?? null),\n messages,\n fullText: textParts.join(\" \"),\n timestamp: state.latestTimestamp || new Date().toISOString(),\n messageCount: messages.length,\n account,\n turnDurations: turnDurations.length > 0 ? turnDurations : undefined,\n lastPrompt: state.lastPrompt || undefined,\n };\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────\n\nexport function extractTextContent(content: unknown): string {\n if (!content) return \"\";\n if (typeof content === \"string\") return cleanSystemTags(content);\n if (Array.isArray(content)) {\n return content\n .map((item) => {\n if (typeof item === \"string\") return item;\n if (item?.type === \"text\" && item?.text) return item.text;\n if (item?.type === \"tool_result\" && typeof item?.content === \"string\") return item.content;\n return \"\";\n })\n .filter(Boolean)\n .map(cleanSystemTags)\n .join(\" \");\n }\n return \"\";\n}\n\nexport function extractToolUseNames(content: unknown): string[] {\n if (!Array.isArray(content)) return [];\n return content\n .filter((item) => item?.type === \"tool_use\" && item?.name)\n .map((item) => item.name as string);\n}\n\nexport function extractToolUseBlocks(content: unknown): ToolUseBlock[] {\n if (!Array.isArray(content)) return [];\n return content\n .filter((item) => item?.type === \"tool_use\" && item?.name && item?.id)\n .map((item) => ({\n id: item.id as string,\n name: item.name as string,\n input: (item.input as Record<string, unknown>) || {},\n }));\n}\n\nconst TOOL_NAME_TO_TYPE: Record<string, ToolResultBlock[\"type\"]> = {\n Edit: \"edit\",\n Write: \"write\",\n Read: \"read\",\n Bash: \"bash\",\n Grep: \"grep\",\n Glob: \"glob\",\n Agent: \"taskAgent\",\n TaskCreate: \"taskCreate\",\n TaskUpdate: \"taskUpdate\",\n};\n\nexport function extractToolResultBlocks(\n content: unknown,\n pendingToolUses: Map<string, ToolUseBlock>,\n): ToolResultBlock[] {\n if (!Array.isArray(content)) return [];\n return content\n .filter((item) => item?.type === \"tool_result\" && item?.tool_use_id)\n .map((item) => {\n const toolName = pendingToolUses.get(item.tool_use_id as string)?.name ?? \"\";\n return {\n toolUseId: item.tool_use_id as string,\n type: TOOL_NAME_TO_TYPE[toolName] ?? \"generic\",\n content:\n typeof item.content === \"string\"\n ? { text: item.content as string }\n : ((item.content as Record<string, unknown>) ?? {}),\n isError: typeof item.is_error === \"boolean\" ? item.is_error : undefined,\n };\n });\n}\n\nexport function collectToolNames(content: unknown, toolSet: Set<string>): void {\n if (!Array.isArray(content)) return;\n for (const item of content) {\n if (item?.type === \"tool_use\" && item?.name) {\n toolSet.add(item.name as string);\n }\n }\n}\n\nexport function isOnlyToolResultContent(content: unknown): boolean {\n if (!Array.isArray(content)) return false;\n return content.length > 0 && content.every((item) => item?.type === \"tool_result\");\n}\n\nexport function isTeammateContent(content: unknown): boolean {\n const raw =\n typeof content === \"string\"\n ? content\n : Array.isArray(content)\n ? content\n .map((item) =>\n typeof item === \"string\" ? item : item?.type === \"text\" ? (item.text ?? \"\") : \"\",\n )\n .join(\"\")\n : \"\";\n return raw.includes(\"<teammate-message\");\n}\n\nexport function extractThinking(content: unknown): { content: string; signature: string } {\n if (!Array.isArray(content)) return { content: \"\", signature: \"\" };\n const blocks = content.filter((item) => item?.type === \"thinking\");\n return {\n content: blocks\n .map((b) => b.thinking as string)\n .filter(Boolean)\n .join(\"\\n\\n\"),\n signature: blocks\n .map((b) => b.signature as string)\n .filter(Boolean)\n .join(\"\"),\n };\n}\n\nexport function hasImageBlocks(content: unknown): boolean {\n if (!Array.isArray(content)) return false;\n return content.some(\n (item) =>\n item?.type === \"image\" &&\n (item?.source?.type === \"base64\" || item?.file?.base64 !== undefined),\n );\n}\n\nexport function parseTeammateMessageTag(content: string): TeamInfo | null {\n const match = content.match(/<teammate-message\\s+([^>]*)>/);\n if (!match) return null;\n const attrs = match[1];\n const id = attrs.match(/teammate_id=\"([^\"]*)\"/)?.[1];\n if (!id) return null;\n const summary = attrs.match(/summary=\"([^\"]*)\"/)?.[1];\n const color = attrs.match(/color=\"([^\"]*)\"/)?.[1];\n return { teammateId: id, summary, color };\n}\n\nexport function getShortProjectName(fullPath: string): string {\n const parts = fullPath.split(\"/\").filter(Boolean);\n return parts.slice(-3).join(\"/\");\n}\n","import { basename, dirname, join } from \"path\";\nimport {\n collectToolNames,\n extractTextContent,\n extractThinking,\n extractToolUseBlocks,\n isOnlyToolResultContent,\n isTeammateContent,\n} from \"../parser\";\nimport { CLAUDE_CODE_PROVIDER } from \"../providers/provider\";\nimport type { ContentTier, ConversationMeta, MessageSender, MessageSnapshot } from \"../types\";\n\n// Serializable fold state for incremental metadata indexing. parseMeta()'s\n// per-line loop is expressed as reduceLine(state, entry); finalizeMeta(state)\n// produces the ConversationMeta. Because the state is plain JSON, it can be\n// persisted in conversation_files.reducer_state and resumed when a file grows,\n// so an append only needs to fold the newly-read lines.\nexport interface ReducerState {\n sessionId: string;\n sessionName: string;\n latestTimestamp: string;\n cwd: string;\n teamName: string;\n model: string | null;\n messageCount: number;\n // Count of messages parseConversation() would produce — broader than\n // messageCount (also counts tool_use-only and thinking-only lines). Used as\n // the page total for bounded reads, since metadata messageCount differs.\n pageMessageCount: number;\n lastMessageSender: MessageSender;\n isTeammate: boolean;\n firstUserSeen: boolean;\n firstMessage: MessageSnapshot | null;\n lastMessage: MessageSnapshot | null;\n lastPrompt: string;\n toolNames: string[];\n previewParts: string[];\n snippetParts: string[];\n previewLength: number;\n snippetLength: number;\n badJsonLines: number;\n}\n\nexport function initialReducerState(): ReducerState {\n return {\n sessionId: \"\",\n sessionName: \"\",\n latestTimestamp: \"\",\n cwd: \"\",\n teamName: \"\",\n model: null,\n messageCount: 0,\n lastMessageSender: \"user\",\n isTeammate: false,\n firstUserSeen: false,\n firstMessage: null,\n lastMessage: null,\n lastPrompt: \"\",\n pageMessageCount: 0,\n toolNames: [],\n previewParts: [],\n snippetParts: [],\n previewLength: 0,\n snippetLength: 0,\n badJsonLines: 0,\n };\n}\n\n// Fold one already-parsed JSONL entry into the state, mutating it in place.\n// Mirrors parseMeta()'s loop body exactly so a full reduce equals a streamed\n// parseMeta. `entry` is the JSON.parse() of one non-empty line.\nexport function reduceLine(\n state: ReducerState,\n entry: Record<string, unknown>,\n tier: ContentTier,\n): void {\n if (entry.cwd && !state.cwd) state.cwd = entry.cwd as string;\n if (entry.sessionId && !state.sessionId) state.sessionId = entry.sessionId as string;\n if (entry.slug && !state.sessionName) state.sessionName = entry.slug as string;\n if (entry.teamName && !state.teamName) state.teamName = entry.teamName as string;\n if (entry.timestamp) {\n const ts = entry.timestamp as string;\n if (!state.latestTimestamp || ts > state.latestTimestamp) state.latestTimestamp = ts;\n }\n\n const type = entry.type as string;\n\n if (type === \"last-prompt\") {\n if (entry.lastPrompt && !state.lastPrompt) state.lastPrompt = entry.lastPrompt as string;\n return;\n }\n\n // file-history-snapshot and other non-message entries are excluded.\n if (type !== \"user\" && type !== \"assistant\") return;\n if (entry.isMeta) return;\n\n const msg = entry.message as Record<string, unknown> | undefined;\n\n if (state.model === null && msg?.model) state.model = msg.model as string;\n\n if (type === \"user\" && !state.firstUserSeen) {\n state.firstUserSeen = true;\n if (isTeammateContent(msg?.content)) state.isTeammate = true;\n }\n\n const content = extractTextContent(msg?.content);\n const hasToolUseResult = type === \"user\" && entry.toolUseResult != null;\n const isOnlyToolResult = hasToolUseResult && isOnlyToolResultContent(msg?.content);\n\n const toolSet = new Set(state.toolNames);\n collectToolNames(msg?.content, toolSet);\n state.toolNames = Array.from(toolSet);\n\n // Count messages the same way parseConversation does, so bounded paging has a\n // correct total. (Broader than messageCount: includes tool_use-only and\n // thinking-only lines.)\n const toolUseBlocks = extractToolUseBlocks(msg?.content);\n const thinking = type === \"assistant\" ? extractThinking(msg?.content) : null;\n const hasThinking = !!(thinking?.content || thinking?.signature);\n if (content || isOnlyToolResult || toolUseBlocks.length > 0 || hasThinking) {\n state.pageMessageCount++;\n }\n\n if (content || isOnlyToolResult) {\n state.messageCount++;\n state.lastMessageSender = type as MessageSender;\n\n if (content) {\n const ts = (entry.timestamp as string) || \"\";\n if (!state.firstMessage) state.firstMessage = { text: content.slice(0, 200), timestamp: ts };\n state.lastMessage = { text: content.slice(0, 200), timestamp: ts };\n\n if (state.previewLength < tier.previewMax) {\n state.previewParts.push(content);\n state.previewLength += content.length;\n }\n if (state.snippetLength < tier.snippetMax) {\n const remaining = tier.snippetMax - state.snippetLength;\n const chunk = content.length > remaining ? content.slice(0, remaining) : content;\n state.snippetParts.push(chunk);\n state.snippetLength += chunk.length;\n }\n }\n }\n}\n\n// Build the ConversationMeta from accumulated state, or null when no messages\n// were seen — matching parseMeta()'s final shape exactly.\nexport function finalizeMeta(\n state: ReducerState,\n filePath: string,\n account: string,\n tier: ContentTier,\n): ConversationMeta | null {\n if (state.messageCount === 0) return null;\n\n const isSubagent = filePath.includes(\"/subagents/\");\n let parentSessionId: string | null = null;\n if (isSubagent) {\n const uuidDir = dirname(dirname(filePath));\n parentSessionId = join(dirname(uuidDir), `${basename(uuidDir)}.jsonl`);\n }\n\n const projectPath = state.cwd;\n return {\n id: filePath,\n filePath,\n provider: CLAUDE_CODE_PROVIDER,\n sessionId: state.sessionId || basename(filePath, \".jsonl\"),\n sessionName: state.sessionName || deriveSessionNameFromFirstMessage(state.firstMessage),\n projectPath,\n projectName: getShortProjectName(projectPath),\n account,\n timestamp: state.latestTimestamp || new Date().toISOString(),\n messageCount: state.messageCount,\n lastMessageSender: state.lastMessageSender,\n preview: state.previewParts.join(\" \").slice(0, tier.previewMax),\n contentSnippet: state.snippetParts.join(\" \"),\n gitBranch: null,\n model: state.model,\n isSubagent,\n parentSessionId,\n isTeammate: state.isTeammate,\n teamName: state.teamName || null,\n toolNames: state.toolNames,\n firstMessage: state.firstMessage,\n lastMessage: state.lastMessage,\n lastPrompt: state.lastPrompt || undefined,\n };\n}\n\n// Fallback session name for interactive Claude Code conversations, which carry\n// no `slug` (only SDK/agent sessions do). The first user message is the only\n// human-readable name source in a normal JSONL, so use its first line, trimmed\n// to a title-sized length. Empty when there is no message text.\nexport function deriveSessionNameFromFirstMessage(firstMessage: { text: string } | null): string {\n const firstLine = firstMessage?.text.split(\"\\n\", 1)[0]?.trim() ?? \"\";\n return firstLine.slice(0, 80);\n}\n\nfunction getShortProjectName(fullPath: string): string {\n const parts = fullPath.split(\"/\").filter(Boolean);\n return parts.slice(-3).join(\"/\");\n}\n","import type { ContentTier, ConversationMeta } from \"../types\";\n\n// A scanner provider knows how to discover and parse one local conversation-log\n// format into the shared ConversationMeta model. The reducer triplet\n// (createEmptyAccumulator / reduceEntry / finalize) is the same fold used by a\n// full streamed parse and — by design — a future offset-resumed incremental\n// parse, since the accumulator is plain serializable state. See\n// metadata-reducer.ts for the Threadbase implementation this mirrors.\nexport interface ScannerProvider<Acc = unknown> {\n name: ScannerProviderName;\n\n // Discover candidate files for this provider under the given roots. Roots are\n // already absolute. Returns one entry per file worth parsing.\n discover(roots: string[]): Promise<DiscoveredConversationFile[]>;\n\n // Cheap structural sniff: does this file look like this provider's format?\n // `sample` is the first few lines (newline-joined). Used so a single roots\n // list can be shared across providers without misclassifying files.\n canParse(filePath: string, sample: string): boolean;\n\n createEmptyAccumulator(): Acc;\n\n // Fold one already-JSON.parsed line into the accumulator, in place. Must never\n // throw on an unknown entry shape — ignore what it doesn't understand.\n reduceEntry(acc: Acc, entry: Record<string, unknown>, tier: ContentTier): void;\n\n // Build the ConversationMeta from the accumulator, or null if no messages.\n finalize(acc: Acc, filePath: string, account: string, tier: ContentTier): ConversationMeta | null;\n}\n\nexport type ScannerProviderName = \"claude-code\" | \"codex-cli\";\n\nexport const CLAUDE_CODE_PROVIDER = \"claude-code\" as const;\nexport const CODEX_CLI_PROVIDER = \"codex-cli\" as const;\n\nexport interface DiscoveredConversationFile {\n filePath: string;\n account: string;\n}\n","const SYSTEM_TAGS = [\n \"system-reminder\",\n \"command-name\",\n \"command-message\",\n \"command-args\",\n \"ide_selection\",\n \"ide_opened_file\",\n \"local-command-stdout\",\n \"local-command-caveat\",\n \"retrieval_status\",\n \"task_id\",\n \"task_type\",\n \"task-id\",\n \"task-notification\",\n \"fast_mode_info\",\n \"persisted-output\",\n \"tool_use_error\",\n \"user-prompt-submit-hook\",\n \"thinking\",\n \"ask_user\",\n \"teammate-message\",\n];\n\nconst SYSTEM_TAG_RE = new RegExp(`<(${SYSTEM_TAGS.join(\"|\")})[^>]*>[\\\\s\\\\S]*?<\\\\/\\\\1>`, \"g\");\n\nexport function cleanSystemTags(text: string): string {\n return text\n .replace(SYSTEM_TAG_RE, \"\")\n .replace(/[^\\S\\n]+/g, \" \")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n}\n","import {\n extractTextContent,\n extractThinking,\n extractToolResultBlocks,\n extractToolUseBlocks,\n extractToolUseNames,\n hasImageBlocks,\n isOnlyToolResultContent,\n parseTeammateMessageTag,\n} from \"../parser\";\nimport type {\n AttachmentSidecar,\n ConversationMessage,\n MessageMetadata,\n MessageSender,\n TeamInfo,\n ToolUseBlock,\n} from \"../types\";\n\n// Resumable, serializable state for the conversation line→message reduction.\n// Mirrors parseConversation()'s loop so a full reduce equals a streamed parse,\n// and so a bounded page read can resume an *equivalent* parse from a checkpoint\n// by restoring the cross-line state (pending tool_use blocks + team info).\nexport interface ConvReducerState {\n cwd: string;\n sessionId: string;\n sessionName: string;\n latestTimestamp: string;\n lastPrompt: string;\n // id -> tool_use block, so a later tool_result line resolves its tool type.\n pendingToolUses: Record<string, ToolUseBlock>;\n // teamName -> info, collected as lines are read and applied to messages.\n teamInfo: Record<string, TeamInfo>;\n}\n\nexport function initialConvState(): ConvReducerState {\n return {\n cwd: \"\",\n sessionId: \"\",\n sessionName: \"\",\n latestTimestamp: \"\",\n lastPrompt: \"\",\n pendingToolUses: {},\n teamInfo: {},\n };\n}\n\n// Fold one JSONL entry. Returns the produced ConversationMessage (its message\n// index is the count of messages emitted so far), or null if the line produced\n// no message. Mutates `state` (header fields + cross-line maps).\nexport function reduceConvLine(\n state: ConvReducerState,\n entry: Record<string, unknown>,\n): ConversationMessage | null {\n if (entry.cwd && !state.cwd) state.cwd = entry.cwd as string;\n if (entry.sessionId && !state.sessionId) state.sessionId = entry.sessionId as string;\n if (entry.slug && !state.sessionName) state.sessionName = entry.slug as string;\n if (entry.timestamp) {\n const ts = entry.timestamp as string;\n if (!state.latestTimestamp || ts > state.latestTimestamp) state.latestTimestamp = ts;\n }\n\n const type = entry.type as string;\n\n if (type === \"last-prompt\") {\n if (entry.lastPrompt && !state.lastPrompt) state.lastPrompt = entry.lastPrompt as string;\n return null;\n }\n // system (incl. turn_duration), file-history-snapshot, etc. produce no message.\n if (type !== \"user\" && type !== \"assistant\") return null;\n if (entry.isMeta) return null;\n\n const msg = entry.message as Record<string, unknown> | undefined;\n\n const toolUseBlocks = extractToolUseBlocks(msg?.content);\n for (const block of toolUseBlocks) state.pendingToolUses[block.id] = block;\n\n const hasToolUseResult = type === \"user\" && entry.toolUseResult != null;\n const isToolResultOnly = hasToolUseResult && isOnlyToolResultContent(msg?.content);\n const content = extractTextContent(msg?.content);\n const thinking = type === \"assistant\" ? extractThinking(msg?.content) : null;\n const hasThinking = !!(thinking?.content || thinking?.signature);\n\n if (!(content || isToolResultOnly || toolUseBlocks.length > 0 || hasThinking)) return null;\n\n const metadata: MessageMetadata = {};\n if (msg?.model) metadata.model = msg.model as string;\n if (msg?.stop_reason !== undefined) metadata.stopReason = msg.stop_reason as string | null;\n if (entry.gitBranch) metadata.gitBranch = entry.gitBranch as string;\n if (entry.version) metadata.version = entry.version as string;\n\n const usage = msg?.usage as Record<string, number> | undefined;\n if (usage) {\n if (usage.input_tokens) metadata.inputTokens = usage.input_tokens;\n if (usage.output_tokens) metadata.outputTokens = usage.output_tokens;\n if (usage.cache_read_input_tokens) metadata.cacheReadTokens = usage.cache_read_input_tokens;\n if (usage.cache_creation_input_tokens)\n metadata.cacheCreationTokens = usage.cache_creation_input_tokens;\n }\n\n const toolUseNames = extractToolUseNames(msg?.content);\n if (toolUseNames.length > 0) metadata.toolUses = toolUseNames;\n if (toolUseBlocks.length > 0) metadata.toolUseBlocks = toolUseBlocks;\n\n if (isToolResultOnly) {\n const pending = new Map(Object.entries(state.pendingToolUses));\n const toolResultBlocks = extractToolResultBlocks(msg?.content, pending);\n if (toolResultBlocks.length > 0) {\n metadata.toolResults = toolResultBlocks;\n // A result consumes its tool_use: prune it so the cross-line map stays\n // bounded by IN-FLIGHT tool calls instead of growing with the file.\n // Unpruned, every checkpoint snapshot/serialization of this state costs\n // O(file) — the structuredClone stall on large live conversations.\n for (const block of toolResultBlocks) delete state.pendingToolUses[block.toolUseId];\n }\n }\n\n if (entry.teamName) {\n metadata.teamName = entry.teamName as string;\n if (!state.teamInfo[metadata.teamName] && content) {\n const info = parseTeammateMessageTag(content);\n if (info) state.teamInfo[metadata.teamName] = info;\n }\n }\n\n const thinkingContent = thinking?.content || undefined;\n const thinkingSignature = thinking?.signature || undefined;\n const hasMetadata = Object.keys(metadata).length > 0;\n\n return {\n role: type as MessageSender,\n text: content || \"\",\n timestamp: (entry.timestamp as string) || \"\",\n uuid: (entry.uuid as string) || undefined,\n metadata: hasMetadata ? metadata : undefined,\n isToolResult: isToolResultOnly || undefined,\n isThinking: thinkingContent || thinkingSignature ? true : undefined,\n thinkingContent,\n thinkingSignature,\n parentUuid: entry.parentUuid !== undefined ? (entry.parentUuid as string | null) : undefined,\n requestId: type === \"assistant\" ? (entry.requestId as string | undefined) : undefined,\n promptId: type === \"user\" ? (entry.promptId as string | undefined) : undefined,\n isSidechain: typeof entry.isSidechain === \"boolean\" ? entry.isSidechain : undefined,\n permissionMode: type === \"user\" ? (entry.permissionMode as string | undefined) : undefined,\n hasImages: hasImageBlocks(msg?.content) || undefined,\n attachment:\n entry.attachment !== undefined ? (entry.attachment as AttachmentSidecar) : undefined,\n };\n}\n\n// Parse/classify one raw JSONL line — the exact line→message mapping the\n// scanner uses internally, exported for downstream indexers (e.g. a watcher\n// tailing appended lines) so their semantics cannot drift from the scanner's.\n// Returns the produced ConversationMessage, or null when the line yields none\n// (system/summary/sidecar records, blank or malformed lines). Whether a line\n// produces a message — and its core fields (role, uuid, timestamp) — is\n// state-independent, so calls without `state` classify correctly on their own;\n// pass one shared `state` across successive lines of a file to also resolve\n// cross-line references (tool_result → tool_use types, team info) identically\n// to a full parse.\nexport function parseJsonlLine(\n line: string,\n state: ConvReducerState = initialConvState(),\n): ConversationMessage | null {\n const text = line.trimEnd();\n if (text.trim().length === 0) return null;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(text);\n } catch {\n return null;\n }\n return reduceConvLine(state, entry);\n}\n\n// Back-apply collected team info to a set of messages (the post-pass\n// parseConversation does). Safe to call on a window: teamInfo for a given name\n// is stable once first seen, so a window restored from a checkpoint that\n// carries earlier teamInfo gets the same result.\nexport function applyTeamInfo(messages: ConversationMessage[], state: ConvReducerState): void {\n if (Object.keys(state.teamInfo).length === 0) return;\n for (const m of messages) {\n const name = m.metadata?.teamName;\n if (name && state.teamInfo[name] && m.metadata) m.metadata.teamInfo = state.teamInfo[name];\n }\n}\n","import { readFileSync, writeFileSync } from \"fs\";\nimport { getLogger } from \"../logger\";\nimport type { ConversationMeta } from \"../types\";\n\nexport const SIDECAR_VERSION = 1;\n\n// Portable, human-readable index summary written next to a JSONL file as\n// `<file>.idx.json`. Not the canonical index (SQLite is) — useful for\n// debugging, portability, and rebuilding the DB if it's lost. Off by default.\nexport interface Sidecar {\n version: number;\n sourcePath: string;\n sizeBytes: number;\n mtimeMs: number;\n lastIndexedOffset: number;\n lastIndexedLine: number;\n messageCount: number;\n projectPath: string;\n projectName: string;\n branch: string | null;\n firstSentAt: string | null;\n firstSentText: string | null;\n lastSentAt: string | null;\n lastSentText: string | null;\n updatedAt: string;\n}\n\nexport function sidecarPath(jsonlPath: string): string {\n return `${jsonlPath}.idx.json`;\n}\n\nexport function buildSidecar(\n meta: ConversationMeta,\n cursor: { sizeBytes: number; mtimeMs: number; offset: number; line: number },\n updatedAt: string,\n): Sidecar {\n return {\n version: SIDECAR_VERSION,\n sourcePath: meta.filePath,\n sizeBytes: cursor.sizeBytes,\n mtimeMs: cursor.mtimeMs,\n lastIndexedOffset: cursor.offset,\n lastIndexedLine: cursor.line,\n messageCount: meta.messageCount,\n projectPath: meta.projectPath,\n projectName: meta.projectName,\n branch: meta.gitBranch,\n firstSentAt: meta.firstMessage?.timestamp ?? null,\n firstSentText: meta.firstMessage?.text ?? null,\n lastSentAt: meta.lastMessage?.timestamp ?? null,\n lastSentText: meta.lastMessage?.text ?? null,\n updatedAt,\n };\n}\n\n// Write the sidecar; never throw into the indexing path — a sidecar is a\n// best-effort artifact, so a write failure (read-only dir, etc.) is logged and\n// swallowed.\nexport function writeSidecar(jsonlPath: string, sidecar: Sidecar): void {\n try {\n writeFileSync(sidecarPath(jsonlPath), JSON.stringify(sidecar, null, 2));\n } catch (err) {\n getLogger().warn({ jsonlPath, err }, \"sidecar: write failed\");\n }\n}\n\nexport function readSidecar(jsonlPath: string): Sidecar | null {\n try {\n return JSON.parse(readFileSync(sidecarPath(jsonlPath), \"utf-8\")) as Sidecar;\n } catch {\n return null;\n }\n}\n","import { mkdir, readFile, writeFile } from \"fs/promises\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { getLogger } from \"./logger\";\nimport type { Profile } from \"./types\";\n\nconst PROFILES_FILE = \"profiles.json\";\n\nexport function resolveConfigDir(configDir: string): string {\n return configDir.replace(/^~/, homedir());\n}\n\nexport function getProjectsDir(profile: Profile): string {\n return join(resolveConfigDir(profile.configDir), \"projects\");\n}\n\nexport async function detectDefaultProfile(): Promise<Profile> {\n return {\n id: \"default\",\n label: \"Default\",\n configDir: join(homedir(), \".claude\"),\n enabled: true,\n emoji: \"🤖\",\n };\n}\n\nexport async function loadProfiles(configPath: string): Promise<Profile[]> {\n const log = getLogger();\n try {\n const resolved = resolveConfigDir(configPath);\n const data = await readFile(join(resolved, PROFILES_FILE), \"utf-8\");\n const profiles = JSON.parse(data) as Profile[];\n log.debug({ configPath, count: profiles.length }, \"profiles: loaded\");\n return profiles;\n } catch (err) {\n log.debug({ configPath, err }, \"profiles: load failed, using default\");\n const defaultProfile = await detectDefaultProfile();\n return [defaultProfile];\n }\n}\n\nexport async function saveProfiles(profiles: Profile[], configPath: string): Promise<void> {\n const resolved = resolveConfigDir(configPath);\n await mkdir(resolved, { recursive: true });\n await writeFile(join(resolved, PROFILES_FILE), JSON.stringify(profiles, null, 2));\n getLogger().debug({ configPath, count: profiles.length }, \"profiles: saved\");\n}\n","import fg from \"fast-glob\";\nimport { createReadStream } from \"fs\";\nimport { stat } from \"fs/promises\";\nimport { basename } from \"path\";\nimport { createInterface } from \"readline\";\nimport { canonicalPath } from \"../canonical-path\";\nimport { getLogger } from \"../logger\";\nimport { cleanSystemTags } from \"../tags\";\nimport type {\n ContentTier,\n Conversation,\n ConversationMessage,\n ConversationMeta,\n MessageSender,\n MessageSnapshot,\n} from \"../types\";\nimport {\n CODEX_CLI_PROVIDER,\n type DiscoveredConversationFile,\n type ScannerProvider,\n} from \"./provider\";\n\n// Serializable fold state for a Codex CLI rollout session. Plain JSON so a\n// future persistent engine can resume it from a byte offset (same contract as\n// the Threadbase ReducerState).\nexport interface CodexAccumulator {\n sessionId: string;\n cwd: string;\n gitBranch: string | null;\n model: string | null;\n latestTimestamp: string;\n messageCount: number;\n lastMessageSender: MessageSender;\n firstUser: MessageSnapshot | null;\n lastUser: MessageSnapshot | null;\n lastAssistant: MessageSnapshot | null;\n toolNames: string[];\n previewParts: string[];\n previewLength: number;\n snippetParts: string[];\n snippetLength: number;\n}\n\n// Codex CLI rollout sessions live as rollout-*.jsonl under a date-partitioned\n// tree. The Codex provider is opt-in: the scanner only discovers under roots the\n// caller passes explicitly (no default home scan).\nexport class CodexCliProvider implements ScannerProvider<CodexAccumulator> {\n readonly name = CODEX_CLI_PROVIDER;\n\n async discover(roots: string[]): Promise<DiscoveredConversationFile[]> {\n const log = getLogger();\n const results: DiscoveredConversationFile[] = [];\n for (const root of roots) {\n let paths: string[];\n try {\n paths = await fg([\"**/rollout-*.jsonl\", \"**/*.jsonl\"], {\n cwd: root,\n absolute: true,\n dot: false,\n unique: true,\n });\n } catch (err) {\n log.warn({ root, err }, \"codex discovery: glob failed\");\n continue;\n }\n for (const filePath of paths) {\n try {\n const s = await stat(filePath);\n // Canonical form — fast-glob emits forward slashes even on Windows,\n // but every by-path lookup keys off the canonical spelling.\n if (s.size > 0) results.push({ filePath: canonicalPath(filePath), account: \"codex\" });\n } catch (err) {\n log.warn({ filePath, err }, \"codex discovery: stat failed\");\n }\n }\n }\n return results;\n }\n\n // Codex rollout lines carry distinctive top-level types.\n canParse(_filePath: string, sample: string): boolean {\n for (const line of sample.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const e = JSON.parse(line) as Record<string, unknown>;\n if (e.type === \"session_meta\" || e.type === \"response_item\" || e.type === \"event_msg\") {\n return true;\n }\n if (e.type === \"user\" || e.type === \"assistant\") return false;\n } catch {}\n }\n return false;\n }\n\n createEmptyAccumulator(): CodexAccumulator {\n return {\n sessionId: \"\",\n cwd: \"\",\n gitBranch: null,\n model: null,\n latestTimestamp: \"\",\n messageCount: 0,\n lastMessageSender: \"user\",\n firstUser: null,\n lastUser: null,\n lastAssistant: null,\n toolNames: [],\n previewParts: [],\n previewLength: 0,\n snippetParts: [],\n snippetLength: 0,\n };\n }\n\n reduceEntry(acc: CodexAccumulator, entry: Record<string, unknown>, tier: ContentTier): void {\n reduceCodexEntry(acc, entry, tier);\n }\n\n finalize(\n acc: CodexAccumulator,\n filePath: string,\n account: string,\n tier: ContentTier,\n ): ConversationMeta | null {\n return finalizeCodexMeta(acc, filePath, account, tier);\n }\n}\n\n// ─── Reducer (exported for tests / future incremental engine) ───────────\n\nconst asString = (v: unknown): string => (typeof v === \"string\" ? v : \"\");\n\n// Pull display text out of a Codex content array of {type,text} blocks.\nfunction extractCodexText(content: unknown): string {\n if (typeof content === \"string\") return cleanSystemTags(content);\n if (!Array.isArray(content)) return \"\";\n return content\n .map((item) => {\n if (typeof item === \"string\") return item;\n const t = item?.type;\n if ((t === \"input_text\" || t === \"output_text\" || t === \"text\") && item?.text) {\n return item.text as string;\n }\n return \"\";\n })\n .filter(Boolean)\n .map(cleanSystemTags)\n .join(\" \");\n}\n\n// Fold one Codex rollout line into the accumulator. Tolerant by construction:\n// any line it doesn't recognise is ignored, never thrown on.\nexport function reduceCodexEntry(\n acc: CodexAccumulator,\n entry: Record<string, unknown>,\n tier: ContentTier,\n): void {\n const ts = asString(entry.timestamp);\n if (ts && (!acc.latestTimestamp || ts > acc.latestTimestamp)) acc.latestTimestamp = ts;\n\n const payload = entry.payload as Record<string, unknown> | undefined;\n if (!payload || typeof payload !== \"object\") return;\n\n const type = entry.type as string;\n\n if (type === \"session_meta\") {\n if (!acc.sessionId) acc.sessionId = asString(payload.id);\n if (!acc.cwd) acc.cwd = asString(payload.cwd);\n const git = payload.git as Record<string, unknown> | undefined;\n if (acc.gitBranch === null && git?.branch) acc.gitBranch = asString(git.branch) || null;\n return;\n }\n\n // Model lives on turn_context records (and occasionally elsewhere).\n if (acc.model === null && payload.model) acc.model = asString(payload.model) || null;\n\n if (type !== \"response_item\") return;\n\n const ptype = payload.type as string;\n\n // Tool calls — collect the tool name.\n if (ptype === \"function_call\" || ptype === \"custom_tool_call\") {\n const name = asString(payload.name);\n if (name && !acc.toolNames.includes(name)) acc.toolNames.push(name);\n return;\n }\n\n if (ptype !== \"message\") return;\n\n const role = payload.role;\n // developer/system/tool roles carry sandbox boilerplate — not user-visible turns.\n if (role !== \"user\" && role !== \"assistant\") return;\n\n const text = extractCodexText(payload.content);\n if (!text) return;\n\n const sender = role as MessageSender;\n acc.messageCount++;\n acc.lastMessageSender = sender;\n\n const snapshot: MessageSnapshot = { text: text.slice(0, 200), timestamp: ts };\n if (sender === \"user\") {\n if (!acc.firstUser) acc.firstUser = snapshot;\n acc.lastUser = snapshot;\n } else {\n acc.lastAssistant = snapshot;\n }\n\n if (acc.previewLength < tier.previewMax) {\n acc.previewParts.push(text);\n acc.previewLength += text.length;\n }\n if (acc.snippetLength < tier.snippetMax) {\n const remaining = tier.snippetMax - acc.snippetLength;\n const chunk = text.length > remaining ? text.slice(0, remaining) : text;\n acc.snippetParts.push(chunk);\n acc.snippetLength += chunk.length;\n }\n}\n\nexport function finalizeCodexMeta(\n acc: CodexAccumulator,\n filePath: string,\n account: string,\n tier: ContentTier,\n): ConversationMeta | null {\n if (acc.messageCount === 0) return null;\n\n const sessionId = acc.sessionId || basename(filePath, \".jsonl\");\n const projectPath = acc.cwd;\n // Treat a session with tool calls but no plain back-and-forth as task-shaped.\n const kind: \"conversation\" | \"task\" =\n acc.lastAssistant === null && acc.toolNames.length > 0 ? \"task\" : \"conversation\";\n\n return {\n id: filePath,\n filePath,\n provider: CODEX_CLI_PROVIDER,\n kind,\n externalSessionId: acc.sessionId || undefined,\n sessionId,\n sessionName: \"\",\n projectPath,\n projectName: getShortProjectName(projectPath),\n account,\n timestamp: acc.latestTimestamp || new Date().toISOString(),\n messageCount: acc.messageCount,\n lastMessageSender: acc.lastMessageSender,\n preview: acc.previewParts.join(\" \").slice(0, tier.previewMax),\n contentSnippet: acc.snippetParts.join(\" \"),\n gitBranch: acc.gitBranch,\n model: acc.model,\n isSubagent: false,\n parentSessionId: null,\n isTeammate: false,\n teamName: null,\n toolNames: acc.toolNames,\n firstMessage: acc.firstUser,\n lastMessage: acc.lastAssistant ?? acc.lastUser,\n lastPrompt: acc.lastUser?.text || undefined,\n };\n}\n\nfunction getShortProjectName(fullPath: string): string {\n return fullPath.split(\"/\").filter(Boolean).slice(-3).join(\"/\");\n}\n\n// Full conversation parse for a Codex rollout session — the Codex analogue of\n// parseConversation(). Streams the file, emitting one ConversationMessage per\n// user/assistant message item. Used by getConversation()/getConversationPage()\n// when the resolved meta is a codex-cli conversation.\nexport async function parseCodexConversation(\n filePath: string,\n account: string,\n): Promise<Conversation | null> {\n const log = getLogger();\n const messages: ConversationMessage[] = [];\n const textParts: string[] = [];\n let sessionId = \"\";\n let cwd = \"\";\n let latestTimestamp = \"\";\n let lastUserText = \"\";\n\n const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });\n try {\n for await (const line of rl) {\n if (!line.trim()) continue;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n const ts = asString(entry.timestamp);\n if (ts && (!latestTimestamp || ts > latestTimestamp)) latestTimestamp = ts;\n const payload = entry.payload as Record<string, unknown> | undefined;\n if (!payload || typeof payload !== \"object\") continue;\n\n if (entry.type === \"session_meta\") {\n if (!sessionId) sessionId = asString(payload.id);\n if (!cwd) cwd = asString(payload.cwd);\n continue;\n }\n if (entry.type !== \"response_item\" || payload.type !== \"message\") continue;\n const role = payload.role;\n if (role !== \"user\" && role !== \"assistant\") continue;\n const text = extractCodexText(payload.content);\n if (!text) continue;\n messages.push({ role: role as MessageSender, text, timestamp: ts });\n textParts.push(text);\n if (role === \"user\") lastUserText = text;\n }\n } catch (err) {\n log.warn({ filePath, err }, \"parseCodexConversation: read failed\");\n return null;\n }\n\n if (messages.length === 0) return null;\n\n return {\n id: filePath,\n filePath,\n projectPath: cwd,\n projectName: getShortProjectName(cwd),\n sessionId: sessionId || basename(filePath, \".jsonl\"),\n sessionName: \"\",\n messages,\n fullText: textParts.join(\" \"),\n timestamp: latestTimestamp || new Date().toISOString(),\n messageCount: messages.length,\n account,\n lastPrompt: lastUserText || undefined,\n };\n}\n","import { sep } from \"path\";\n\n// Canonical absolute-path form used as the identity key for a conversation file\n// across the whole index (SQLite rows, the in-memory metadata cache, the\n// dir-mtime watermarks, and every by-path lookup).\n//\n// The problem this solves: fast-glob (discovery) emits forward-slash paths even\n// on Windows, while the file watcher (chokidar), path.join(), and every caller\n// that builds a path with the platform API emit native (back)slashes. The same\n// real file could therefore be stored/looked-up under two different string keys\n// on Windows, so by-path APIs miss rows depending on the caller's separator\n// style. Normalizing collapses both forms onto one key.\n//\n// The native separator is the canonical form, not fast-glob's forward slashes:\n// a path handed back to a caller (ConversationMeta.filePath/id, a change event,\n// a metadata-cache key) is compared against platform-built paths by every\n// consumer, so the native spelling is the one the public surface has to speak.\n// The cost is a one-time reindex for an index.db written before this change\n// (its forward-slash rows miss, get reinserted in native form, and the same\n// pass's deletion-reconcile retires the old ones) — bounded and self-healing,\n// unlike the recurrent duplicate-row churn this fixes.\n//\n// On POSIX this is a no-op: the separator is already \"/\", and a backslash is a\n// legal filename character there, so it must be left untouched.\nexport function canonicalPath(p: string): string {\n return sep === \"\\\\\" ? p.replace(/\\//g, \"\\\\\") : p;\n}\n","import fg from \"fast-glob\";\nimport { stat } from \"fs/promises\";\nimport { canonicalPath } from \"./canonical-path\";\nimport { getLogger } from \"./logger\";\n\nexport interface DiscoveredFile {\n filePath: string;\n account: string;\n}\n\nconst EXCLUDED_SEGMENTS = [\"/memory/\", \"/tool-results/\"];\n\n// stat() is cheap and IO-bound, so a large history scans much faster when the\n// calls run concurrently rather than one-at-a-time. Cap concurrency well under\n// typical fd limits.\nconst STAT_CONCURRENCY = 32;\n\nexport async function discoverJsonlFiles(\n dirs: { projectsDir: string; account: string }[],\n onProgress?: (found: number) => void,\n): Promise<DiscoveredFile[]> {\n const log = getLogger();\n const results: DiscoveredFile[] = [];\n\n for (const { projectsDir, account } of dirs) {\n let filePaths: string[];\n try {\n filePaths = await fg(\"**/*.jsonl\", {\n cwd: projectsDir,\n absolute: true,\n dot: false,\n });\n } catch (err) {\n log.warn({ projectsDir, account, err }, \"discovery: glob failed\");\n continue;\n }\n\n // Filter out excluded directory segments\n const filtered = filePaths.filter((fp) => !EXCLUDED_SEGMENTS.some((seg) => fp.includes(seg)));\n\n // Filter out empty files. Stat concurrently in bounded chunks; chunk order\n // is preserved and intra-chunk order follows the input, so discovery order\n // is stable (downstream sorts anyway).\n let kept = 0;\n let skippedEmpty = 0;\n let skippedInaccessible = 0;\n for (let i = 0; i < filtered.length; i += STAT_CONCURRENCY) {\n const chunk = filtered.slice(i, i + STAT_CONCURRENCY);\n const statted = await Promise.all(\n chunk.map(async (filePath) => {\n try {\n const s = await stat(filePath);\n return { filePath, size: s.size };\n } catch (err) {\n log.warn({ filePath, err }, \"discovery: stat failed\");\n return { filePath, size: -1 };\n }\n }),\n );\n for (const { filePath, size } of statted) {\n if (size < 0) {\n skippedInaccessible++;\n } else if (size > 0) {\n // fast-glob emits forward slashes even on Windows. Emit the canonical\n // form instead, so a discovered path is the same string as the one a\n // watcher/path.join caller produces for that file — discovery output\n // keys the in-memory metadata cache and the has_nested comparison in\n // dir-watermark, neither of which goes through a repository.\n results.push({ filePath: canonicalPath(filePath), account });\n kept++;\n } else {\n skippedEmpty++;\n }\n }\n }\n\n log.debug(\n {\n projectsDir,\n account,\n globMatches: filePaths.length,\n afterExclusions: filtered.length,\n kept,\n skippedEmpty,\n skippedInaccessible,\n },\n \"discovery: directory scanned\",\n );\n\n onProgress?.(results.length);\n }\n\n log.debug({ totalFiles: results.length, dirs: dirs.length }, \"discovery: complete\");\n return results;\n}\n","import { discoverJsonlFiles } from \"../discovery\";\nimport {\n finalizeMeta,\n initialReducerState,\n type ReducerState,\n reduceLine,\n} from \"../persistent/metadata-reducer\";\nimport type { ContentTier, ConversationMeta } from \"../types\";\nimport {\n CLAUDE_CODE_PROVIDER,\n type DiscoveredConversationFile,\n type ScannerProvider,\n} from \"./provider\";\n\n// The existing Claude/Threadbase format, expressed as a provider. All behavior\n// is the already-shared reducer (metadata-reducer.ts) — no logic is duplicated\n// here. Discovery is delegated to discoverJsonlFiles; the scanner passes the\n// profiles' projects dirs as roots paired with their account ids.\nexport class ThreadbaseProvider implements ScannerProvider<ReducerState> {\n readonly name = CLAUDE_CODE_PROVIDER;\n\n // Roots are passed as \"<projectsDir>\\0<account>\" so the scanner can carry the\n // per-root account through the shared interface. The scanner builds these.\n async discover(roots: string[]): Promise<DiscoveredConversationFile[]> {\n const dirs = roots.map((r) => {\n const [projectsDir, account = \"default\"] = r.split(\"\\0\");\n return { projectsDir, account };\n });\n return discoverJsonlFiles(dirs);\n }\n\n // Threadbase JSONL has top-level type \"user\"/\"assistant\" with a cwd/sessionId.\n canParse(_filePath: string, sample: string): boolean {\n for (const line of sample.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const e = JSON.parse(line) as Record<string, unknown>;\n if (e.type === \"user\" || e.type === \"assistant\") return true;\n // Codex's distinctive top-level types — definitively not us.\n if (e.type === \"session_meta\" || e.type === \"response_item\") return false;\n } catch {}\n }\n return false;\n }\n\n createEmptyAccumulator(): ReducerState {\n return initialReducerState();\n }\n\n reduceEntry(acc: ReducerState, entry: Record<string, unknown>, tier: ContentTier): void {\n reduceLine(acc, entry, tier);\n }\n\n finalize(\n acc: ReducerState,\n filePath: string,\n account: string,\n tier: ContentTier,\n ): ConversationMeta | null {\n // finalizeMeta already tags provider: \"claude-code\".\n return finalizeMeta(acc, filePath, account, tier);\n }\n}\n","import { EventEmitter } from \"events\";\nimport { closeSync, openSync, readSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { LRUCache } from \"./cache\";\nimport { canonicalPath } from \"./canonical-path\";\nimport { discoverJsonlFiles } from \"./discovery\";\nimport {\n applyAccountFilter,\n applyIncludeFilter,\n applyPagination,\n applyProjectFilter,\n applySinceFilter,\n applySort,\n parseSinceCutoff,\n} from \"./filters\";\nimport { readGitBranch } from \"./git\";\nimport { SearchIndexer } from \"./indexer\";\nimport { getLogger } from \"./logger\";\nimport { parseConversation, parseMeta } from \"./parser\";\nimport {\n type CachedConversation,\n extendConversation,\n parseConversationResumable,\n} from \"./persistent/conversation-stream\";\nimport { classify } from \"./persistent/cursor\";\nimport { PersistentEngine } from \"./persistent/index-engine\";\nimport { getProjectsDir, loadProfiles } from \"./profiles\";\nimport { CodexCliProvider, parseCodexConversation } from \"./providers/codex-cli\";\nimport { parseMetaWithProvider } from \"./providers/parse\";\nimport {\n CLAUDE_CODE_PROVIDER,\n CODEX_CLI_PROVIDER,\n type ScannerProvider,\n} from \"./providers/provider\";\nimport { ThreadbaseProvider } from \"./providers/threadbase\";\nimport {\n appendSearchDelta,\n combineSearchContent,\n emptySearchDocument,\n extractSearchDelta,\n type SearchDocument,\n} from \"./search-document\";\nimport { CONTENT_FIELD, generateMatches } from \"./search-matches\";\nimport { resolveTier } from \"./tiers\";\nimport type {\n ContentTier,\n Conversation,\n ConversationMeta,\n ConversationPage,\n GetConversationOptions,\n GetConversationPageOptions,\n GroupedConversations,\n Profile,\n ScanOptions,\n ScanResult,\n SearchOptions,\n SearchResult,\n SingleFilePage,\n TreeConversation,\n} from \"./types\";\nimport { FileWatcher } from \"./watcher/file-watcher\";\nimport { type IndexJob, IndexQueue } from \"./watcher/index-queue\";\n\nconst BATCH_SIZE = 12;\nconst DEFAULT_CONFIG_PATH = \"~/.config/threadbase-scanner\";\n\n// A test process that reaches the default path writes its fixtures into the\n// developer's real index. Measured on one live machine: 579 of 1,144 rows were\n// test fixtures, 357 of them from tb-streamer's suite, which constructs\n// scanners with no dbPath and has no TB_SCANNER_DB of its own. Those rows are\n// indistinguishable from real conversations in every coverage query, so they\n// silently corrupt any measurement taken of the index — including the\n// completeness metrics a durable archive would report.\n//\n// The env var already existed for this; it just failed open. Failing loudly is\n// what makes the omission visible at the moment it is introduced rather than a\n// year later, and it belongs here rather than in each consumer's setup because\n// the scanner is the thing that knows which path is the real one.\nfunction isTestProcess(): boolean {\n return (\n Boolean(process.env.VITEST) ||\n Boolean(process.env.JEST_WORKER_ID) ||\n process.env.NODE_ENV === \"test\"\n );\n}\n\n// Default persistent-index location. Overridable via TB_SCANNER_DB (required\n// under a test runner, and handy for pointing at an alternate DB in ops).\nfunction defaultDbPath(): string {\n const override = process.env.TB_SCANNER_DB;\n if (override) return override;\n if (isTestProcess()) {\n throw new Error(\n \"TB_SCANNER_DB must be set under a test runner. Refusing to open the default index at \" +\n \"~/.config/threadbase-scanner/index.db, because test fixtures written there are \" +\n \"indistinguishable from real conversations and corrupt every measurement of the index. \" +\n \"Point it at a temp directory in your test setup, or pass persistent.dbPath explicitly.\",\n );\n }\n return join(homedir(), \".config\", \"threadbase-scanner\", \"index.db\");\n}\n\n// Flatten a search document for the in-memory index, tail-capped to the content\n// tier. Tail rather than head for the same reason the buckets themselves are\n// tail-biased: people search for what they did recently.\n//\n// This is where `persistent: false` deliberately diverges from SQLite. The\n// FlexSearch index is `tokenize: \"forward\"` (every prefix of every token, held\n// in RAM), so handing it the full ~128 KB budget per conversation would cost\n// hundreds of MB to GB. Extraction RULES are shared with FTS5; indexed VOLUME\n// is not. Raise this only against a measurement, never on speculation.\nfunction capForMemory(doc: SearchDocument, max: number): string {\n const combined = combineSearchContent(doc);\n return combined.length > max ? combined.slice(-max) : combined;\n}\n\nexport interface PersistentConfig {\n dbPath?: string;\n // Write a portable <file>.idx.json sidecar next to each indexed JSONL for\n // debugging/portability/recovery. Off by default. SQLite stays canonical.\n sidecar?: boolean;\n}\n\nexport interface ConversationScannerOptions {\n metadataCacheSize?: number;\n conversationCacheSize?: number;\n // SQLite-backed persistent index. Enabled by default (at DEFAULT_DB_PATH).\n // Pass `false` for the legacy in-memory path (no native dependency, no DB\n // file). Pass `{ dbPath }` to override the database location.\n persistent?: false | PersistentConfig;\n}\n\nexport interface WatchOptions {\n profiles?: Profile[];\n // Watcher debounce window (ms). Default 400.\n debounceMs?: number;\n // Periodic full rescan as a correctness fallback for missed FS events.\n // Default 60_000ms; set 0 to disable.\n periodicMs?: number;\n}\n\n// Emitted after the index changes for one file. `meta` is the fresh metadata,\n// or null when the file was removed/emptied (deleted from the index).\nexport interface ScannerChangeEvent {\n filePath: string;\n account: string;\n meta: ConversationMeta | null;\n reason: IndexJob[\"reason\"];\n}\n\nexport class ConversationScanner {\n private metadataCache: Map<string, ConversationMeta> = new Map();\n // Parsed conversations plus (persistent claude-code entries only) the resume\n // point that lets refreshFile extend them in place when the file grows.\n private conversationLRU: LRUCache<string, CachedConversation>;\n // session_id is NOT unique, so this maps a sessionId to every active meta that\n // carries it. Resolution picks deterministically (newest timestamp, then path\n // ascending) so dropping one file never hides another with the same id.\n private sessionIdIndex: Map<string, ConversationMeta[]> = new Map();\n private projects: Set<string> = new Set();\n private indexer: SearchIndexer = new SearchIndexer();\n // Search body per conversation for the in-memory path, already tail-capped to\n // the content tier. Survives scan() so a statCache hit — which skips the parse\n // entirely — can still index a body rather than an empty string.\n private searchContents = new Map<string, string>();\n // Tier the most recent scan() ran with, so refreshFile() re-parses a single\n // file at the same content depth. Defaults to the standard tier.\n private lastTier: ContentTier = resolveTier(\"standard\");\n\n // null when persistent mode is disabled (legacy in-memory path). Lazily\n // opened on first use so merely constructing a scanner never touches disk.\n private readonly dbPath: string | null;\n private readonly sidecarEnabled: boolean;\n private engineInstance: PersistentEngine | null = null;\n // The most recent scan()'s promise while it is still running, or null when\n // idle. close() awaits it before closing the SQLite handle so a fire-and-\n // forget scan can't have its DB shut mid-indexAll (\"database connection is\n // not open\"). Tracks the latest scan only — concurrent scans on one instance\n // aren't a supported pattern (a single writer DB), and the latest resolving\n // implies earlier ones already settled in practice.\n private inFlightScan: Promise<unknown> | null = null;\n\n private emitter = new EventEmitter();\n private watcher: FileWatcher | null = null;\n private queue: IndexQueue | null = null;\n private periodicTimer: ReturnType<typeof setInterval> | null = null;\n // The in-flight periodicReconcile() job, if one is mid-run. unwatch() awaits\n // it before dropping the queue/engine so its post-await engine.files access\n // can't hit a closed DB (the watch-mode half of Bug #4).\n private inFlightReconcile: Promise<void> | null = null;\n\n constructor(options?: ConversationScannerOptions) {\n this.conversationLRU = new LRUCache<string, CachedConversation>(\n options?.conversationCacheSize ?? 5,\n );\n if (options?.persistent === false) {\n this.dbPath = null;\n this.sidecarEnabled = false;\n } else {\n this.dbPath = options?.persistent?.dbPath ?? defaultDbPath();\n this.sidecarEnabled = options?.persistent?.sidecar ?? false;\n }\n }\n\n private get persistent(): boolean {\n return this.dbPath !== null;\n }\n\n private engine(): PersistentEngine {\n if (!this.engineInstance) {\n this.engineInstance = new PersistentEngine(this.dbPath as string, {\n sidecar: this.sidecarEnabled,\n });\n }\n return this.engineInstance;\n }\n\n // Release the SQLite connection. No-op in legacy mode. Safe to call\n // repeatedly. Awaits watcher teardown AND any in-flight scan before closing\n // the DB handle, so a fire-and-forget scan() can never have its connection\n // shut mid-indexAll() (\"database connection is not open\"). This is why close()\n // is async — callers should await it during shutdown. (Scanner review Bug #4.)\n async close(): Promise<void> {\n if (this.watcher || this.queue || this.periodicTimer) await this.unwatch();\n // Let any running scan finish touching the DB first. Swallow its result/\n // rejection here — the caller that started the scan owns its outcome; close()\n // only needs it to stop using the handle before we close it.\n if (this.inFlightScan) {\n try {\n await this.inFlightScan;\n } catch {\n // scan failed on its own; close() proceeds to release the handle\n }\n }\n this.engineInstance?.close();\n this.engineInstance = null;\n }\n\n async scan(options: ScanOptions = {}): Promise<ScanResult> {\n // Track this scan so close() can await it before shutting the DB handle.\n // Cleared on settle — but only if it's still the current one (a newer scan\n // may have replaced it). Errors propagate to the caller unchanged.\n const promise = this.runScan(options);\n this.inFlightScan = promise;\n try {\n return await promise;\n } finally {\n if (this.inFlightScan === promise) this.inFlightScan = null;\n }\n }\n\n private async runScan(options: ScanOptions): Promise<ScanResult> {\n const profiles = await this.resolveProfiles(options.profiles);\n const activeProfiles = profiles.filter((p) => p.enabled && p.scanHistory !== false);\n this.lastTier = resolveTier(options.tier ?? \"standard\", options.tiers);\n\n if (this.persistent) {\n return this.scanPersistent(activeProfiles, options);\n }\n return this.scanInMemory(activeProfiles, options);\n }\n\n // SQLite-backed scan: (re)index changed files into the DB, then query all\n // active metas and run the identical filter/sort/view/paginate pipeline as\n // the in-memory path — guaranteeing an identical ScanResult shape.\n private async scanPersistent(\n activeProfiles: Profile[],\n options: ScanOptions,\n ): Promise<ScanResult> {\n const log = getLogger();\n const startedAt = Date.now();\n const engine = this.engine();\n\n const { scanned } = await engine.indexAll(activeProfiles, options);\n const allMetas = engine.allActive();\n\n const { conversations, total } = this.finalize(allMetas, options);\n log.info(\n { scanned, kept: allMetas.length, filteredTotal: total, elapsedMs: Date.now() - startedAt },\n \"scan: complete (persistent)\",\n );\n return { conversations, total, scanned };\n }\n\n // Apply include/project/account/since filters, sort, view transform, and\n // pagination. Shared by both backends so results never diverge.\n private finalize(\n allMetas: ConversationMeta[],\n options: ScanOptions,\n ): { conversations: ScanResult[\"conversations\"]; total: number } {\n let filtered = allMetas;\n if (options.include && options.include !== \"all\") {\n filtered = applyIncludeFilter(filtered, options.include);\n }\n if (options.project) filtered = applyProjectFilter(filtered, options.project);\n if (options.account) filtered = applyAccountFilter(filtered, options.account);\n if (options.since) filtered = applySinceFilter(filtered, options.since);\n\n filtered = applySort(filtered, options.sort ?? \"recent\");\n const total = filtered.length;\n const conversations = this.transformView(filtered, options);\n\n if (Array.isArray(conversations)) {\n const limit = options.limit ?? 50;\n const offset = options.offset ?? 0;\n return { conversations: applyPagination(conversations, limit, offset).items, total };\n }\n return { conversations, total };\n }\n\n private async scanInMemory(activeProfiles: Profile[], options: ScanOptions): Promise<ScanResult> {\n const log = getLogger();\n const startedAt = Date.now();\n const tier = this.lastTier;\n\n log.info(\n {\n activeProfiles: activeProfiles.length,\n tier: tier.name,\n sort: options.sort ?? \"recent\",\n include: options.include ?? \"all\",\n view: options.view ?? \"flat\",\n },\n \"scan: start\",\n );\n\n // Clear caches. searchContents is deliberately NOT cleared: it is keyed by\n // file path and only ever replaced by a fresh parse, so keeping it lets a\n // statCache hit (which skips parsing) still index a real body.\n this.metadataCache.clear();\n this.conversationLRU.clear();\n this.sessionIdIndex.clear();\n this.projects.clear();\n this.indexer.clear();\n\n const files = await this.discoverWithProviders(activeProfiles, options);\n const totalFiles = files.length;\n let scanned = 0;\n let parseFailures = 0;\n\n const allMetas: ConversationMeta[] = [];\n // Memoize git-branch lookups per project path for the duration of this\n // scan. readGitBranch walks the filesystem for .git/HEAD; without this it\n // runs once per conversation (thousands of times) even though there are\n // only a handful of distinct project roots. Scoped to one scan() call —\n // branches can change between scans, so a longer-lived memo would go stale.\n const gitBranchMemo = new Map<string, string | null>();\n const resolveGitBranch = (projectPath: string): string | null => {\n let branch = gitBranchMemo.get(projectPath);\n if (branch === undefined) {\n branch = readGitBranch(projectPath);\n gitBranchMemo.set(projectPath, branch);\n }\n return branch;\n };\n const { statCache } = options;\n for (let i = 0; i < files.length; i += BATCH_SIZE) {\n const batch = files.slice(i, i + BATCH_SIZE);\n const results = await Promise.all(\n batch.map(async ({ filePath, account, provider }) => {\n if (statCache) {\n const cached = statCache.get(filePath);\n if (cached) {\n try {\n const s = statSync(filePath);\n if (s.mtimeMs === cached.stat.mtimeMs && s.size === cached.stat.size) {\n // Unchanged file: whatever body we extracted last time is\n // still accurate, so reuse it instead of indexing nothing.\n return {\n meta: cached.meta,\n searchContent: this.searchContents.get(cached.meta.id),\n };\n }\n } catch {\n // file disappeared — fall through to parse which will return null\n }\n }\n }\n try {\n // Fold the search document over the same lines as the metadata\n // parse — one read of the file, not two.\n let doc = emptySearchDocument();\n const meta = await parseMetaWithProvider(provider, filePath, account, tier, (entry) => {\n doc = appendSearchDelta(doc, extractSearchDelta(entry));\n });\n // The provider may already know its branch (Codex reads it from\n // session_meta). Only walk the filesystem when it doesn't.\n if (meta && meta.gitBranch === null && meta.projectPath) {\n meta.gitBranch = resolveGitBranch(meta.projectPath);\n }\n return { meta, searchContent: capForMemory(doc, tier.snippetMax) };\n } catch (err) {\n parseFailures++;\n log.warn({ filePath, account, provider: provider.name, err }, \"scan: parse threw\");\n return { meta: null, searchContent: undefined };\n }\n }),\n );\n\n const batchMetas: ConversationMeta[] = [];\n for (const { meta, searchContent } of results) {\n if (meta && meta.messageCount > 0) {\n this.metadataCache.set(meta.id, meta);\n this.addToSessionIndex(meta);\n this.projects.add(meta.projectPath);\n allMetas.push(meta);\n batchMetas.push(meta);\n const content = searchContent ?? this.searchContents.get(meta.id) ?? \"\";\n this.searchContents.set(meta.id, content);\n this.indexer.addDocument(meta, content);\n }\n }\n\n if (batchMetas.length > 0) {\n options.onBatch?.(batchMetas);\n }\n\n scanned += batch.length;\n log.debug({ scanned, totalFiles, batchKept: batchMetas.length }, \"scan: batch complete\");\n options.onProgress?.(scanned, totalFiles);\n }\n\n const { conversations, total } = this.finalize(allMetas, options);\n\n const elapsedMs = Date.now() - startedAt;\n log.info(\n {\n totalFiles,\n scanned,\n kept: allMetas.length,\n filteredTotal: total,\n parseFailures,\n elapsedMs,\n },\n \"scan: complete\",\n );\n\n return { conversations, total, scanned };\n }\n\n async search(query: string, options: SearchOptions = {}): Promise<SearchResult[]> {\n const log = getLogger();\n log.debug({ query, indexSize: this.indexer.getDocumentCount() }, \"search: start\");\n\n let results: SearchResult[];\n if (this.persistent) {\n // SQLite FTS5 is the persistent search engine. Index once if the DB is\n // empty (mirroring the in-memory \"scan on first search\" contract), then\n // query FTS directly — no in-memory index to warm.\n const engine = this.engine();\n if (engine.conversations.count() === 0) {\n log.debug(\"search: persistent index empty, triggering scan\");\n const profiles = await this.resolveProfiles(options.profiles);\n const activeProfiles = profiles.filter((p) => p.enabled && p.scanHistory !== false);\n await engine.indexAll(activeProfiles, { ...options, limit: undefined, offset: undefined });\n }\n if (query.trim()) {\n // Filters go down into SQL so they run BEFORE the LIMIT. Applying them\n // to an already-truncated hit list would report \"no results\" whenever\n // the surviving conversations sit outside the unfiltered top-N — much\n // more likely now that the corpus is wide.\n const want = (options.limit ?? 50) + (options.offset ?? 0);\n results = engine\n .searchHits(query, want, {\n account: options.account,\n provider: options.provider,\n project: options.project,\n since: options.since ? parseSinceCutoff(options.since) : undefined,\n include: options.include,\n })\n .map(({ meta, body }) => ({\n meta,\n score: 1,\n matches: body\n ? [\n { field: CONTENT_FIELD, snippet: body.snippet, highlights: body.highlights },\n ...generateMatches(meta, query).filter((m) => m.field !== \"preview\"),\n ]\n : generateMatches(meta, query),\n }));\n } else {\n results = engine.recentMetas((options.limit ?? 50) + (options.offset ?? 0)).map((meta) => ({\n meta,\n score: 1,\n matches: [{ field: \"timestamp\", snippet: meta.preview }],\n }));\n }\n } else {\n if (this.indexer.getDocumentCount() === 0) {\n log.debug(\"search: index empty, triggering scan\");\n await this.scan({ ...options, limit: undefined, offset: undefined });\n }\n results = this.indexer.search(query, {\n fields: options.fields,\n limit: (options.limit ?? 50) * 2,\n });\n }\n\n if (options.include && options.include !== \"all\") {\n results = results.filter((r) => {\n switch (options.include) {\n case \"conversations\":\n return !r.meta.isSubagent && !r.meta.isTeammate;\n case \"subagents\":\n return r.meta.isSubagent;\n case \"teammates\":\n return r.meta.isTeammate;\n default:\n return true;\n }\n });\n }\n if (options.project) {\n const lower = options.project.toLowerCase();\n results = results.filter(\n (r) =>\n r.meta.projectPath.toLowerCase().includes(lower) ||\n r.meta.projectName.toLowerCase().includes(lower),\n );\n }\n if (options.account) {\n results = results.filter((r) => r.meta.account === options.account);\n }\n if (options.provider) {\n results = results.filter(\n (r) => (r.meta.provider ?? CLAUDE_CODE_PROVIDER) === options.provider,\n );\n }\n if (options.since) {\n const cutoff = parseSinceCutoff(options.since);\n results = results.filter((r) => new Date(r.meta.timestamp).getTime() >= cutoff.getTime());\n }\n\n const limit = options.limit ?? 50;\n const offset = options.offset ?? 0;\n const sliced = results.slice(offset, offset + limit);\n log.debug({ query, matched: results.length, returned: sliced.length }, \"search: complete\");\n return sliced;\n }\n\n async getConversation(\n id: string,\n _options?: GetConversationOptions,\n ): Promise<Conversation | null> {\n const log = getLogger();\n // Canonicalize the file-path form of the id so a native (join/watcher) and a\n // forward-slash (fast-glob) spelling of the same file resolve to one entry.\n // A sessionId (no separators) is unaffected.\n const cid = canonicalPath(id);\n const cached = this.conversationLRU.get(cid);\n if (cached) {\n log.debug({ id }, \"getConversation: cache hit\");\n return cached.conversation;\n }\n\n const meta = this.persistent\n ? this.engine().getByIdOrSession(cid)\n : (this.metadataCache.get(cid) ?? this.resolveSessionId(cid));\n if (!meta) {\n log.debug({ id }, \"getConversation: not found in metadata\");\n return null;\n }\n\n log.debug({ id, filePath: meta.filePath }, \"getConversation: cache miss, parsing\");\n try {\n // Persistent claude-code files parse through the resumable streaming fold\n // so the cached entry carries its resume point — refreshFile can then\n // extend it with only the appended bytes instead of evicting it. Codex\n // and legacy-mode parses stay on their existing parsers (no resume state;\n // a refresh evicts them as before).\n if (this.persistent && meta.provider !== CODEX_CLI_PROVIDER) {\n const parsed = await parseConversationResumable(meta.filePath, meta.account);\n if (parsed) this.conversationLRU.set(cid, parsed);\n return parsed?.conversation ?? null;\n }\n const conversation =\n meta.provider === CODEX_CLI_PROVIDER\n ? await parseCodexConversation(meta.filePath, meta.account)\n : await parseConversation(meta.filePath, meta.account);\n if (conversation) {\n this.conversationLRU.set(cid, { conversation });\n }\n return conversation;\n } catch (err) {\n log.warn({ id, filePath: meta.filePath, err }, \"getConversation: parse failed\");\n return null;\n }\n }\n\n // Return one bounded window of a conversation's messages plus the total\n // message count, so a caller can serve the last page and scroll back without\n // holding the whole conversation itself.\n //\n // The window is `[max(0, beforeIndex - limit), beforeIndex)` in chronological\n // order; `beforeIndex` defaults to `total` (the newest page). `fromIndex` is\n // the window's start index, so the caller can derive `has_more_older`\n // (fromIndex > 0). Returns null when the id can't be resolved/parsed — the\n // same contract as getConversation.\n //\n // Persistent mode: a true bounded read — the engine seeks from the nearest\n // checkpoint and parses only the requested window (checkpoints are built\n // lazily for large conversations). Windowed message indices are proven\n // identical to parseConversation().messages.slice(...) by the paged-reader\n // equivalence test.\n //\n // Legacy mode: parse-once-then-slice via getConversation (cached in the LRU),\n // which is identical by construction.\n async getConversationPage(\n id: string,\n options: GetConversationPageOptions,\n ): Promise<ConversationPage | null> {\n if (this.persistent) {\n return this.engine().getPage(canonicalPath(id), options);\n }\n\n const conversation = await this.getConversation(id);\n if (!conversation) return null;\n\n const { messages } = conversation;\n const total = messages.length;\n const { limit } = options;\n const beforeIndex = options.beforeIndex ?? total;\n\n const fromIndex = Math.max(0, beforeIndex - limit);\n const window = messages.slice(fromIndex, beforeIndex);\n\n return { messages: window, total, fromIndex };\n }\n\n // Parse one JSONL file directly and slice a page window — without any prior\n // scan() or metadata index. This is the cold-start fast path: a single\n // conversation can be served from one file parse (~ms) instead of waiting\n // for a full filesystem scan. The window is the same\n // `[max(0, beforeIndex - limit), beforeIndex)` slice as getConversationPage,\n // and the parsed Conversation is returned alongside so the caller can build\n // the response meta (projectPath, timestamp, messageCount, …) without a\n // second parse. Returns null when the file can't be parsed. `account` only\n // feeds the conversation's account field; \"default\" is the single-profile\n // fallback, matching refreshFile.\n async parseSingleFilePage(\n filePath: string,\n account: string | undefined,\n options: GetConversationPageOptions,\n ): Promise<SingleFilePage | null> {\n // There is no index row on this path — it is the cold/fallback read — so\n // sniff the file for its provider. Handing a Codex rollout to\n // parseConversation() returns nothing, which reads to the caller as a\n // conversation that does not exist.\n const provider = await this.resolveProviderForFile(filePath, null);\n const conversation =\n provider?.name === CODEX_CLI_PROVIDER\n ? await parseCodexConversation(filePath, account ?? \"default\")\n : await parseConversation(filePath, account ?? \"default\");\n if (!conversation) return null;\n\n const { messages } = conversation;\n const total = messages.length;\n const { limit } = options;\n const beforeIndex = options.beforeIndex ?? total;\n\n const fromIndex = Math.max(0, beforeIndex - limit);\n const window = messages.slice(fromIndex, beforeIndex);\n\n return { messages: window, total, fromIndex, conversation };\n }\n\n // Re-parse a single JSONL file and update every in-memory index in place —\n // metadata cache, sessionId index, project set, search index — and evict the\n // file's parsed conversation from the LRU so the next getConversation()\n // re-reads it. This lets a long-lived scanner stay current with a file that\n // grew after the initial scan() without paying for a full rescan.\n //\n // `account` defaults to the account already recorded for this file (the id\n // is the file path), falling back to \"default\" for a file the scanner has\n // not seen before. Returns the fresh ConversationMeta, or null when the file\n // no longer parses (missing/empty) — in which case any prior entry for it is\n // dropped from all indexes.\n //\n // Single-flighted per path: concurrent callers (stacked client retries, a\n // watcher tick racing a caller) await the one in-flight refresh instead of\n // each re-reading the file.\n private readonly refreshesInFlight = new Map<string, Promise<ConversationMeta | null>>();\n\n refreshFile(filePath: string, account?: string): Promise<ConversationMeta | null> {\n // Key every refresh off the canonical path so a fast-glob (forward slash)\n // and a watcher/join (native) spelling of the same file share one in-flight\n // refresh and hit the same stored row.\n const cpath = canonicalPath(filePath);\n const inFlight = this.refreshesInFlight.get(cpath);\n if (inFlight) return inFlight;\n const refresh = this.doRefreshFile(cpath, account).finally(() => {\n if (this.refreshesInFlight.get(cpath) === refresh) {\n this.refreshesInFlight.delete(cpath);\n }\n });\n this.refreshesInFlight.set(cpath, refresh);\n return refresh;\n }\n\n private async doRefreshFile(\n filePath: string,\n account?: string,\n ): Promise<ConversationMeta | null> {\n const log = getLogger();\n\n if (this.persistent) {\n const engine = this.engine();\n const previous = engine.getByIdOrSession(filePath);\n const resolvedAccount = account ?? previous?.account ?? \"default\";\n // Resolve the provider so a Codex file refreshes through the Codex reducer\n // rather than the Threadbase tail-read. Prefer stored metadata; fall back\n // to a structural sniff for a file the index hasn't seen yet.\n const provider = await this.resolveProviderForFile(filePath, previous);\n // force=false: let classify() decide. It already distinguishes\n // unchanged/appended/reindex/vanished correctly (including the\n // replace-with-larger-file case, cursor.ts), so the watcher's live-append\n // path resumes from the byte cursor instead of reparsing the whole file\n // on every debounced tick.\n const { meta, change } = await engine.indexFile(\n filePath,\n resolvedAccount,\n this.lastTier.name,\n undefined,\n readGitBranch,\n false,\n provider,\n );\n\n // Cached parses are keyed by whatever id getConversation() was asked for\n // — the file-path id or the sessionId — so collect every candidate key.\n const cacheKeys = new Set<string>();\n for (const m of [previous, meta]) {\n if (m) {\n cacheKeys.add(m.id);\n cacheKeys.add(m.sessionId);\n }\n }\n if (!meta || change === \"reindex\" || change === \"vanished\") {\n // Truncated/replaced/gone: the cached parse no longer matches the\n // bytes on disk — drop it so the next read re-parses.\n for (const key of cacheKeys) this.conversationLRU.delete(key);\n } else if (change === \"appended\") {\n // The file only grew: advance any cached parse by folding just the\n // appended bytes, leaving the LRU warm instead of evicting it.\n await this.extendCachedConversations(cacheKeys, filePath, meta.account);\n }\n // unchanged → the cached parse is still valid; leave it warm.\n\n log.debug({ filePath, change, kept: !!meta }, \"refreshFile: updated persistent index\");\n return meta;\n }\n\n const previous = this.metadataCache.get(filePath);\n const resolvedAccount = account ?? previous?.account ?? \"default\";\n\n let meta: ConversationMeta | null = null;\n let searchDoc = emptySearchDocument();\n const onEntry = (entry: Record<string, unknown>) => {\n searchDoc = appendSearchDelta(searchDoc, extractSearchDelta(entry));\n };\n // Same provider resolution the persistent branch above does: without it a\n // Codex file is folded by the Threadbase reducer, yields no meta, and is\n // dropped from the index by the eviction below.\n const provider = await this.resolveProviderForFile(filePath, previous ?? null);\n try {\n meta = provider\n ? await parseMetaWithProvider(provider, filePath, resolvedAccount, this.lastTier, onEntry)\n : await parseMeta(filePath, resolvedAccount, this.lastTier, onEntry);\n } catch (err) {\n log.warn({ filePath, err }, \"refreshFile: parseMeta threw\");\n meta = null;\n }\n\n // The LRU is keyed by the id used at getConversation() time, which can be\n // either the file-path id or the sessionId — evict both for the prior and\n // refreshed metas so no stale parse survives.\n const evict = (m: ConversationMeta | undefined | null) => {\n if (!m) return;\n this.conversationLRU.delete(m.id);\n this.conversationLRU.delete(m.sessionId);\n };\n evict(previous);\n evict(meta);\n\n if (!meta || meta.messageCount === 0) {\n if (previous) {\n this.metadataCache.delete(previous.id);\n this.removeFromSessionIndex(previous);\n this.indexer.removeDocument(previous.id);\n this.searchContents.delete(previous.id);\n }\n log.debug({ filePath }, \"refreshFile: dropped (no parseable messages)\");\n return null;\n }\n\n meta.gitBranch = readGitBranch(meta.projectPath);\n\n // A re-parse can change the sessionId mapping; clear the old entry first so\n // we don't leave this file listed under a stale sessionId.\n if (previous) this.removeFromSessionIndex(previous);\n this.metadataCache.set(meta.id, meta);\n this.addToSessionIndex(meta);\n this.projects.add(meta.projectPath);\n const searchContent = capForMemory(searchDoc, this.lastTier.snippetMax);\n this.searchContents.set(meta.id, searchContent);\n if (previous) {\n this.indexer.updateDocument(meta, searchContent);\n } else {\n this.indexer.addDocument(meta, searchContent);\n }\n\n log.debug(\n { filePath, messageCount: meta.messageCount },\n \"refreshFile: updated in-memory indexes\",\n );\n return meta;\n }\n\n // Advance every cached parse of an appended file by folding only the new\n // bytes through the conversation reducer — the in-memory analogue of the\n // persisted metadata fold. Entries without resume state (Codex) and entries\n // whose extension fails are evicted so the next read re-parses from scratch.\n private async extendCachedConversations(\n cacheKeys: Set<string>,\n filePath: string,\n account: string,\n ): Promise<void> {\n // Multiple keys can point at the same wrapper (path id + sessionId) —\n // group by wrapper so each cached parse is extended exactly once.\n const wrappers = new Map<CachedConversation, string[]>();\n for (const key of cacheKeys) {\n const wrapper = this.conversationLRU.get(key);\n if (!wrapper) continue;\n const keys = wrappers.get(wrapper) ?? [];\n keys.push(key);\n wrappers.set(wrapper, keys);\n }\n for (const [wrapper, keys] of wrappers) {\n if (!wrapper.resume) {\n for (const key of keys) this.conversationLRU.delete(key);\n continue;\n }\n try {\n const extended = await extendConversation(\n wrapper.conversation,\n wrapper.resume,\n filePath,\n account,\n );\n // Mutate the wrapper in place so every key holding it sees the update.\n wrapper.conversation = extended.conversation;\n wrapper.resume = extended.resume;\n } catch (err) {\n getLogger().warn({ filePath, err }, \"refreshFile: cache extension failed, evicting\");\n for (const key of keys) this.conversationLRU.delete(key);\n }\n }\n }\n\n getMetadataCache(): Map<string, ConversationMeta> {\n if (this.persistent) {\n const map = new Map<string, ConversationMeta>();\n for (const meta of this.engine().allActive()) map.set(meta.id, meta);\n return map;\n }\n return this.metadataCache;\n }\n\n // Collision-safe sessionId lookup. session_id is NOT unique (the parser falls\n // back to the file basename, and resumed/subagent sessions repeat ids), so\n // getConversation(sessionId) is a convenience that resolves to one match;\n // this returns every active conversation sharing the id, newest first.\n getConversationsBySessionId(sessionId: string): ConversationMeta[] {\n if (this.persistent) {\n return this.engine().getAllBySessionId(sessionId);\n }\n return this.sortBySessionPriority(this.sessionIdIndex.get(sessionId) ?? []);\n }\n\n getProjects(): string[] {\n const source = this.persistent ? this.engine().getProjects() : this.projects;\n const normalized = new Set<string>();\n for (const p of source) {\n normalized.add(p.replace(/\\/+$/, \"\"));\n }\n return Array.from(normalized).sort();\n }\n\n // ── File watching (persistent mode only) ────────────────────────────────\n\n // Subscribe to index changes. Events: \"change\" → ScannerChangeEvent,\n // \"error\" → Error. Returns this for chaining.\n on(event: \"change\", listener: (e: ScannerChangeEvent) => void): this;\n on(event: \"error\", listener: (err: Error) => void): this;\n on(event: string, listener: (...args: any[]) => void): this {\n this.emitter.on(event, listener);\n return this;\n }\n\n off(event: string, listener: (...args: any[]) => void): this {\n this.emitter.off(event, listener);\n return this;\n }\n\n // Start watching the active profiles' project dirs. A filesystem watcher\n // feeds debounced, path-deduplicated index jobs through a single-writer\n // queue; a periodic full rescan backstops any events the watcher misses\n // (sleep/wake, network FS, restarts). Emits \"change\" per indexed file.\n // Persistent mode only — throws in legacy mode (no durable index to update).\n async watch(options: WatchOptions = {}): Promise<void> {\n if (!this.persistent) {\n throw new Error(\"watch() requires persistent mode; construct with persistent enabled\");\n }\n if (this.watcher) return; // already watching\n\n const profiles = await this.resolveProfiles(options.profiles);\n const activeProfiles = profiles.filter((p) => p.enabled && p.scanHistory !== false);\n\n this.queue = new IndexQueue(async (job) => {\n try {\n // Create/change and delete both route through refreshFile: a removed\n // file stat-fails or parses empty, so refreshFile drops its row and\n // returns null. The index stays correct either way.\n const meta = await this.refreshFile(job.filePath, job.account);\n this.emitter.emit(\"change\", {\n filePath: job.filePath,\n account: job.account,\n meta,\n reason: job.reason,\n } satisfies ScannerChangeEvent);\n } catch (err) {\n this.emitter.emit(\"error\", err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n this.watcher = new FileWatcher(\n activeProfiles,\n (e) => {\n this.queue?.enqueue({\n filePath: e.filePath,\n account: e.account,\n reason: \"watcher\",\n });\n },\n { debounceMs: options.debounceMs },\n );\n await this.watcher.start();\n\n const periodicMs = options.periodicMs ?? 60_000;\n if (periodicMs > 0) {\n this.periodicTimer = setInterval(() => {\n // Track the job so unwatch() can await it — otherwise its post-await\n // engine.files access can land after the DB is closed.\n const job = this.periodicReconcile(activeProfiles).finally(() => {\n if (this.inFlightReconcile === job) this.inFlightReconcile = null;\n });\n this.inFlightReconcile = job;\n }, periodicMs);\n // Don't keep the process alive solely for the rescan timer.\n this.periodicTimer.unref?.();\n }\n\n getLogger().info({ profiles: activeProfiles.length, periodicMs }, \"watch: started\");\n }\n\n // Periodic correctness backstop: re-discover all files and enqueue them\n // through the same queue the watcher uses, so any add/change the watcher\n // missed still gets indexed and emits a \"change\" event. Vanished files\n // (active in the DB but no longer on disk) are enqueued too — refreshFile\n // drops them. Routing through the queue (not a direct scan) keeps event\n // emission and single-writer serialization unified.\n private async periodicReconcile(activeProfiles: Profile[]): Promise<void> {\n if (!this.queue) return;\n try {\n const engine = this.engine();\n const configDirs = activeProfiles.map((p) => ({\n projectsDir: getProjectsDir(p),\n account: p.id,\n }));\n const discovered = await discoverJsonlFiles(configDirs);\n const seen = new Set<string>();\n for (const { filePath, account } of discovered) {\n seen.add(filePath);\n // Only enqueue genuinely-changed files so an idle tick stays silent\n // (no spurious \"change\" events for thousands of unchanged files).\n const { change } = classify(filePath, engine.files.getByPath(filePath));\n if (change !== \"unchanged\") {\n this.queue.enqueue({ filePath, account, reason: \"periodic\" });\n }\n }\n // Files in the index but gone from disk → enqueue so refreshFile drops them.\n for (const path of engine.files.allActivePaths()) {\n if (!seen.has(path)) {\n this.queue.enqueue({ filePath: path, account: \"default\", reason: \"periodic\" });\n }\n }\n } catch (err) {\n this.emitter.emit(\"error\", err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n // Stop watching and drain any in-flight index jobs.\n async unwatch(): Promise<void> {\n if (this.periodicTimer) {\n clearInterval(this.periodicTimer);\n this.periodicTimer = null;\n }\n // Let a reconcile that already started finish touching the DB before we\n // drop the queue/engine below. Its own catch swallows errors; we only wait.\n if (this.inFlightReconcile) {\n await this.inFlightReconcile;\n }\n if (this.watcher) {\n await this.watcher.stop();\n this.watcher = null;\n }\n if (this.queue) {\n await this.queue.onIdle();\n this.queue = null;\n }\n }\n\n // ── Provider plumbing (in-memory path) ──────────────────────────────────\n\n // Build the flat parse worklist for the enabled providers. Threadbase\n // discovers under the active profiles' project dirs; Codex discovers under\n // the explicit codexRoots (opt-in — no default home scan).\n private async discoverWithProviders(\n activeProfiles: Profile[],\n options: ScanOptions,\n ): Promise<{ filePath: string; account: string; provider: ScannerProvider }[]> {\n const enabled = options.providers ?? [CLAUDE_CODE_PROVIDER];\n const work: { filePath: string; account: string; provider: ScannerProvider }[] = [];\n\n if (enabled.includes(CLAUDE_CODE_PROVIDER)) {\n const provider = new ThreadbaseProvider();\n const roots = activeProfiles.map((p) => `${getProjectsDir(p)}\\0${p.id}`);\n for (const f of await provider.discover(roots)) {\n work.push({ ...f, provider });\n }\n }\n if (enabled.includes(CODEX_CLI_PROVIDER) && (options.codexRoots?.length ?? 0) > 0) {\n const provider = new CodexCliProvider();\n for (const f of await provider.discover(options.codexRoots as string[])) {\n work.push({ ...f, provider });\n }\n }\n return work;\n }\n\n // Resolve which provider should (re)parse a file. Stored metadata wins; for a\n // file the index has not seen, sniff the first lines with each non-Threadbase\n // provider's canParse. Returns undefined for Threadbase (the engine's default\n // tail-read path).\n private async resolveProviderForFile(\n filePath: string,\n previous: ConversationMeta | null,\n ): Promise<ScannerProvider | undefined> {\n if (previous?.provider === CODEX_CLI_PROVIDER) return new CodexCliProvider();\n if (previous?.provider) return undefined; // known Threadbase\n let sample = \"\";\n try {\n const fd = openSync(filePath, \"r\");\n try {\n const buf = Buffer.alloc(8192);\n const n = readSync(fd, buf, 0, buf.length, 0);\n sample = buf.subarray(0, n).toString(\"utf8\");\n } finally {\n closeSync(fd);\n }\n } catch {\n return undefined;\n }\n const codex = new CodexCliProvider();\n if (codex.canParse(filePath, sample)) return codex;\n return undefined;\n }\n\n private addToSessionIndex(meta: ConversationMeta): void {\n const list = this.sessionIdIndex.get(meta.sessionId);\n if (list) {\n // Replace any existing entry for the same file (re-index), else append.\n const i = list.findIndex((m) => m.id === meta.id);\n if (i >= 0) list[i] = meta;\n else list.push(meta);\n } else {\n this.sessionIdIndex.set(meta.sessionId, [meta]);\n }\n }\n\n private removeFromSessionIndex(meta: ConversationMeta): void {\n const list = this.sessionIdIndex.get(meta.sessionId);\n if (!list) return;\n const next = list.filter((m) => m.id !== meta.id);\n if (next.length > 0) this.sessionIdIndex.set(meta.sessionId, next);\n else this.sessionIdIndex.delete(meta.sessionId);\n }\n\n // Deterministic single-result sessionId resolution: newest timestamp first,\n // tie-broken by absolute path ascending.\n private resolveSessionId(sessionId: string): ConversationMeta | undefined {\n return this.sortBySessionPriority(this.sessionIdIndex.get(sessionId) ?? [])[0];\n }\n\n private sortBySessionPriority(metas: ConversationMeta[]): ConversationMeta[] {\n return [...metas].sort((a, b) => {\n if (a.timestamp !== b.timestamp) return a.timestamp < b.timestamp ? 1 : -1;\n return a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0;\n });\n }\n\n private async resolveProfiles(profiles?: Profile[]): Promise<Profile[]> {\n // An explicit array (including an empty one) means \"use exactly these\" —\n // `[]` scans zero profiles. Only an absent argument falls back to defaults.\n // Treating `[]` as \"load defaults\" silently scanned the real ~/.claude\n // history, which looked like a hang for callers isolating from it.\n if (profiles) return profiles;\n return loadProfiles(DEFAULT_CONFIG_PATH);\n }\n\n private transformView(\n metas: ConversationMeta[],\n options: ScanOptions,\n ): ConversationMeta[] | TreeConversation[] | GroupedConversations {\n switch (options.view) {\n case \"tree\":\n return this.toTree(metas);\n case \"grouped\":\n return this.toGrouped(metas);\n default:\n return metas;\n }\n }\n\n private toTree(metas: ConversationMeta[]): TreeConversation[] {\n const parents: TreeConversation[] = [];\n const subagents: ConversationMeta[] = [];\n\n for (const meta of metas) {\n if (meta.isSubagent) {\n subagents.push(meta);\n } else {\n parents.push({ ...meta, subagents: [] });\n }\n }\n\n const parentById = new Map(parents.map((p) => [p.id, p]));\n for (const sub of subagents) {\n const parent = sub.parentSessionId ? parentById.get(sub.parentSessionId) : undefined;\n if (parent) {\n parent.subagents.push(sub);\n } else {\n parents.push({ ...sub, subagents: [] });\n }\n }\n\n return parents;\n }\n\n private toGrouped(metas: ConversationMeta[]): GroupedConversations {\n const groups: GroupedConversations = {};\n for (const meta of metas) {\n const key = meta.teamName || \"_default\";\n if (!groups[key]) groups[key] = [];\n groups[key].push(meta);\n }\n return groups;\n }\n}\n","export class LRUCache<K, V> {\n private map = new Map<K, V>();\n private readonly capacity: number;\n\n constructor(capacity: number) {\n this.capacity = capacity;\n }\n\n get(key: K): V | undefined {\n const value = this.map.get(key);\n if (value === undefined) return undefined;\n // Move to end (most recently used)\n this.map.delete(key);\n this.map.set(key, value);\n return value;\n }\n\n set(key: K, value: V): void {\n this.map.delete(key);\n this.map.set(key, value);\n if (this.map.size > this.capacity) {\n const oldest = this.map.keys().next();\n if (!oldest.done) this.map.delete(oldest.value);\n }\n }\n\n has(key: K): boolean {\n return this.map.has(key);\n }\n\n delete(key: K): boolean {\n return this.map.delete(key);\n }\n\n clear(): void {\n this.map.clear();\n }\n\n get size(): number {\n return this.map.size;\n }\n}\n","import { basename } from \"path\";\nimport { getShortProjectName } from \"../parser\";\nimport type { Conversation, ConversationMessage, TurnDuration } from \"../types\";\nimport { applyTeamInfo, type ConvReducerState, initialConvState } from \"./conversation-reducer\";\nimport { deriveSessionNameFromFirstMessage } from \"./metadata-reducer\";\nimport { streamMessages } from \"./paged-reader\";\n\n// A parsed conversation held in the scanner's LRU together with the resume\n// point needed to extend it in place when its file grows: the conversation\n// reducer state at EOF plus the byte/line offset just past the last complete\n// line. `resume` is absent for providers without a resumable reducer (Codex)\n// — a refresh evicts those entries instead of extending them.\nexport interface ConversationResume {\n state: ConvReducerState;\n offset: number;\n line: number;\n}\n\nexport interface CachedConversation {\n conversation: Conversation;\n resume?: ConversationResume;\n}\n\n// Fold [resume.offset, EOF) through the conversation reducer, collecting the\n// produced messages plus turn_duration entries. turn_duration lines are\n// consumed BEFORE the reducer — exactly as parseConversation does — so their\n// header fields (timestamps) never leak into the conversation state. Mutates\n// resume.state in place; on failure the caller must discard the cached entry.\nasync function foldTail(filePath: string, resume: ConversationResume) {\n const messages: ConversationMessage[] = [];\n const textParts: string[] = [];\n const turnDurations: TurnDuration[] = [];\n const end = await streamMessages(\n filePath,\n resume.offset,\n resume.line,\n resume.state,\n (message) => {\n messages.push(message);\n if (message.text) textParts.push(message.text);\n return false; // never stop early — fold to EOF\n },\n (entry) => {\n if (\n entry.type === \"system\" &&\n entry.subtype === \"turn_duration\" &&\n typeof entry.durationMs === \"number\"\n ) {\n turnDurations.push({\n durationMs: entry.durationMs as number,\n messageCount: (entry.messageCount as number) || 0,\n uuid: entry.uuid as string | undefined,\n });\n return true;\n }\n return false;\n },\n );\n return { messages, textParts, turnDurations, end };\n}\n\n// Assemble the Conversation shape exactly as parseConversation() does.\nfunction assemble(\n filePath: string,\n account: string,\n messages: ConversationMessage[],\n fullText: string,\n turnDurations: TurnDuration[],\n state: ConvReducerState,\n): Conversation {\n return {\n id: filePath,\n filePath,\n projectPath: state.cwd,\n projectName: getShortProjectName(state.cwd),\n sessionId: state.sessionId || basename(filePath, \".jsonl\"),\n sessionName:\n state.sessionName ||\n deriveSessionNameFromFirstMessage(messages.find((m) => m.role === \"user\" && m.text) ?? null),\n messages,\n fullText,\n timestamp: state.latestTimestamp || new Date().toISOString(),\n messageCount: messages.length,\n account,\n turnDurations: turnDurations.length > 0 ? turnDurations : undefined,\n lastPrompt: state.lastPrompt || undefined,\n };\n}\n\n// Full parse that also captures the resume point, so a later append can extend\n// the result in place instead of re-parsing the file. Identical to\n// parseConversation() by construction (same reducer, same assembly); the one\n// deliberate difference is torn-write discipline: a final line with no\n// trailing newline is treated as in-flight (not parsed yet) rather than\n// parsed, matching the persistent index's byte-cursor semantics.\nexport async function parseConversationResumable(\n filePath: string,\n account: string,\n): Promise<CachedConversation | null> {\n const resume: ConversationResume = { state: initialConvState(), offset: 0, line: 0 };\n const { messages, textParts, turnDurations, end } = await foldTail(filePath, resume);\n if (messages.length === 0) return null;\n applyTeamInfo(messages, resume.state);\n const conversation = assemble(\n filePath,\n account,\n messages,\n textParts.join(\" \"),\n turnDurations,\n resume.state,\n );\n return { conversation, resume: { state: resume.state, offset: end.offset, line: end.line } };\n}\n\n// Extend a cached conversation with only the bytes appended since its resume\n// point. Returns a NEW Conversation with a fresh messages array — the old\n// ARRAY is never mutated, but team info discovered in the delta enriches the\n// shared message objects in place (the same enrichment a full re-parse would\n// emit), so element-level immutability is not guaranteed.\nexport async function extendConversation(\n previous: Conversation,\n resume: ConversationResume,\n filePath: string,\n account: string,\n): Promise<CachedConversation> {\n const { messages: fresh, textParts, turnDurations, end } = await foldTail(filePath, resume);\n const messages = fresh.length > 0 ? previous.messages.concat(fresh) : previous.messages;\n // Team info discovered in the delta back-applies to earlier messages, the\n // same way a full parse applies it once at the end.\n applyTeamInfo(messages, resume.state);\n const fullText =\n textParts.length === 0\n ? previous.fullText\n : previous.fullText\n ? `${previous.fullText} ${textParts.join(\" \")}`\n : textParts.join(\" \");\n const allTurnDurations = (previous.turnDurations ?? []).concat(turnDurations);\n const conversation = assemble(\n filePath,\n account,\n messages,\n fullText,\n allTurnDurations,\n resume.state,\n );\n return { conversation, resume: { state: resume.state, offset: end.offset, line: end.line } };\n}\n","import { createReadStream } from \"fs\";\nimport { setImmediate as yieldToEventLoop } from \"timers/promises\";\nimport type { ConversationMessage, ConversationPage } from \"../types\";\nimport {\n applyTeamInfo,\n type ConvReducerState,\n initialConvState,\n reduceConvLine,\n} from \"./conversation-reducer\";\nimport { YIELD_EVERY_LINES } from \"./jsonl-tail-reader\";\nimport type { Checkpoint } from \"./repositories/checkpoints.repo\";\n\n// Snapshot the parser state every CHECKPOINT_INTERVAL messages.\nexport const CHECKPOINT_INTERVAL = 500;\n\n// Stream a JSONL file from `startOffset`, folding each complete line through the\n// conversation reducer. `onMessage` receives every produced message plus the\n// byte offset of the line that FOLLOWS it (a safe resume point) and the current\n// state. `onEntry` (optional) sees every parsed entry first — return true to\n// consume it before it reaches the reducer (parseConversation's turn_duration\n// handling). Returns the offset/line just past the last COMPLETE line consumed\n// — a trailing torn line is never included, so the result is a safe resume\n// point for a later incremental fold.\nexport async function streamMessages(\n filePath: string,\n startOffset: number,\n startLine: number,\n state: ConvReducerState,\n // Return true to stop streaming (window filled).\n onMessage: (message: ConversationMessage, nextByteOffset: number, nextLine: number) => boolean,\n onEntry?: (entry: Record<string, unknown>) => boolean,\n): Promise<{ offset: number; line: number }> {\n const stream = createReadStream(filePath, { start: startOffset, encoding: \"utf8\" });\n let buffer = \"\";\n let offset = startOffset;\n let line = startLine;\n let sinceYield = 0;\n\n for await (const chunk of stream) {\n buffer += chunk;\n let nl: number;\n // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic line scan\n while ((nl = buffer.indexOf(\"\\n\")) >= 0) {\n const lineWithNewline = buffer.slice(0, nl + 1);\n const text = lineWithNewline.trimEnd();\n buffer = buffer.slice(nl + 1);\n offset += Buffer.byteLength(lineWithNewline, \"utf8\");\n line += 1;\n\n // Same cooperative yield as the tail reader: without it, buffered chunks\n // drain through microtasks and a full-file walk blocks the event loop.\n if (++sinceYield >= YIELD_EVERY_LINES) {\n sinceYield = 0;\n await yieldToEventLoop();\n }\n\n if (text.length === 0) continue;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(text);\n } catch {\n continue;\n }\n if (onEntry?.(entry)) continue;\n const message = reduceConvLine(state, entry);\n if (message && onMessage(message, offset, line)) {\n stream.destroy();\n return { offset, line };\n }\n }\n }\n return { offset, line };\n}\n\n// Build checkpoints by streaming the file once. Each checkpoint captures the\n// byte offset + reducer state immediately AFTER message (k*interval - 1), i.e.\n// a clean resume point for message index k*interval. Pass `from` (the last\n// persisted checkpoint) to EXTEND an existing chain: the stream resumes from\n// its offset/state and only checkpoints past it are returned — an append costs\n// O(new bytes), never a re-walk of the immutable prefix.\nexport async function buildCheckpoints(\n filePath: string,\n interval = CHECKPOINT_INTERVAL,\n from: Checkpoint | null = null,\n): Promise<Checkpoint[]> {\n const checkpoints: Checkpoint[] = [];\n const state = from ? from.state : initialConvState();\n let index = from ? from.messageIndex : 0;\n\n await streamMessages(\n filePath,\n from?.byteOffset ?? 0,\n from?.lineNumber ?? 0,\n state,\n (_msg, nextOffset, nextLine) => {\n index += 1;\n // After consuming `index` messages, if the next message index is a multiple\n // of the interval, snapshot a resume point for it.\n if (index % interval === 0) {\n checkpoints.push({\n messageIndex: index,\n byteOffset: nextOffset,\n lineNumber: nextLine,\n state: structuredClone(state),\n });\n }\n return false; // never stop early — we want the whole file\n },\n );\n\n return checkpoints;\n}\n\n// Read a bounded message window [fromIndex, beforeIndex) plus the total message\n// count, seeking from the nearest checkpoint <= fromIndex. `total` requires\n// knowing the message count; callers pass it (from the indexed summary) so we\n// never scan the whole file just to count.\nexport async function readPage(\n filePath: string,\n total: number,\n options: { beforeIndex?: number; limit: number },\n floor: Checkpoint | null,\n): Promise<ConversationPage> {\n const beforeIndex = options.beforeIndex ?? total;\n const fromIndex = Math.max(0, beforeIndex - options.limit);\n\n const state = floor ? structuredClone(floor.state) : initialConvState();\n const startOffset = floor ? floor.byteOffset : 0;\n const startLine = floor ? floor.lineNumber : 0;\n let index = floor ? floor.messageIndex : 0;\n\n const window: ConversationMessage[] = [];\n await streamMessages(filePath, startOffset, startLine, state, (message) => {\n const current = index;\n index += 1;\n if (current >= fromIndex && current < beforeIndex) window.push(message);\n return index >= beforeIndex; // stop once the window is filled\n });\n\n applyTeamInfo(window, state);\n return { messages: window, total, fromIndex };\n}\n","import { createReadStream } from \"fs\";\nimport { setImmediate as yieldToEventLoop } from \"timers/promises\";\nimport type { ContentTier } from \"../types\";\nimport { type ReducerState, reduceLine } from \"./metadata-reducer\";\n\n// Cooperatively yield every N folded lines so a full parse of a large file\n// (cold index, truncation fallback) never blocks the event loop for its whole\n// duration — buffered stream chunks otherwise drain through microtasks without\n// ever reaching the macrotask queue.\nexport const YIELD_EVERY_LINES = 500;\n\nexport interface TailReadResult {\n // Byte offset of the first un-consumed byte (start of the trailing partial\n // line, or EOF). Safe to persist as the next cursor.\n newOffset: number;\n newLine: number;\n parsedLines: number;\n badJsonLines: number;\n}\n\n// Stream a JSONL file from `startOffset` to EOF, folding each COMPLETE line into\n// `state`. A trailing line with no newline (a writer mid-append) is left\n// un-consumed: the offset is not advanced past it, so the next pass re-reads it\n// once it's complete. Offsets are byte offsets (Buffer.byteLength), never string\n// .length, so multibyte content stays aligned. (Spec §7.2–7.4.)\n// `onEntry` rides the same line loop as the metadata fold, so search-document\n// extraction costs no extra file read — important when a cold index or a v5\n// migration re-reads the whole corpus.\nexport async function tailReduce(\n filePath: string,\n startOffset: number,\n startLine: number,\n state: ReducerState,\n tier: ContentTier,\n onEntry?: (entry: Record<string, unknown>) => void,\n): Promise<TailReadResult> {\n const stream = createReadStream(filePath, { start: startOffset, encoding: \"utf8\" });\n\n let buffer = \"\";\n let offset = startOffset;\n let line = startLine;\n let parsedLines = 0;\n let sinceYield = 0;\n\n for await (const chunk of stream) {\n buffer += chunk;\n let nl: number;\n // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic line scan\n while ((nl = buffer.indexOf(\"\\n\")) >= 0) {\n const lineWithNewline = buffer.slice(0, nl + 1);\n const text = lineWithNewline.trimEnd();\n buffer = buffer.slice(nl + 1);\n\n if (text.length > 0) {\n try {\n const entry = JSON.parse(text) as Record<string, unknown>;\n reduceLine(state, entry, tier);\n onEntry?.(entry);\n } catch {\n state.badJsonLines++;\n }\n parsedLines++;\n }\n\n offset += Buffer.byteLength(lineWithNewline, \"utf8\");\n line++;\n if (++sinceYield >= YIELD_EVERY_LINES) {\n sinceYield = 0;\n await yieldToEventLoop();\n }\n }\n }\n\n return { newOffset: offset, newLine: line, parsedLines, badJsonLines: state.badJsonLines };\n}\n","import { createHash } from \"crypto\";\nimport { closeSync, openSync, readSync, statSync } from \"fs\";\nimport type { FileRow } from \"./repositories/conversation-files.repo\";\n\nexport type FileChange = \"unchanged\" | \"appended\" | \"reindex\" | \"vanished\";\n\nexport interface FileStat {\n size: number;\n mtimeMs: number;\n}\n\n// Cheap content fingerprint: hash of the first + last 4KB plus the size. Lets\n// us detect a same-size in-place rewrite (e.g. a file replaced atomically)\n// without hashing an entire 200MB file. (Spec §9.4.)\nconst FP_BYTES = 4096;\n\nexport function fingerprint(filePath: string, size: number): string {\n const hash = createHash(\"sha1\");\n hash.update(String(size));\n const fd = openSync(filePath, \"r\");\n try {\n const head = Buffer.alloc(Math.min(FP_BYTES, size));\n if (head.length > 0) {\n readSync(fd, head, 0, head.length, 0);\n hash.update(head);\n }\n if (size > FP_BYTES) {\n const tailLen = Math.min(FP_BYTES, size);\n const tail = Buffer.alloc(tailLen);\n readSync(fd, tail, 0, tailLen, size - tailLen);\n hash.update(tail);\n }\n } finally {\n closeSync(fd);\n }\n return hash.digest(\"hex\");\n}\n\n// Classify how a file changed relative to its persisted cursor row. Drives\n// whether the indexer skips, tail-reads from the cursor, or reindexes from 0.\n// (Spec §9.)\nexport function classify(\n filePath: string,\n existing: FileRow | undefined,\n): { change: FileChange; stat?: FileStat } {\n let stat: FileStat;\n try {\n const s = statSync(filePath);\n stat = { size: s.size, mtimeMs: s.mtimeMs };\n } catch {\n return { change: \"vanished\" };\n }\n\n if (!existing || existing.status !== \"active\" || existing.last_indexed_offset === 0) {\n return { change: \"reindex\", stat };\n }\n\n // Smaller than where we left off → truncated/replaced. Reindex.\n if (stat.size < existing.last_indexed_offset) {\n return { change: \"reindex\", stat };\n }\n\n // Same size + same mtime → nothing to do.\n if (stat.size === existing.size_bytes && stat.mtimeMs === existing.mtime_ms) {\n return { change: \"unchanged\", stat };\n }\n\n // Same size but mtime moved → possible in-place rewrite. Fingerprint to be\n // sure; a changed fingerprint means the bytes we already indexed are stale.\n if (stat.size === existing.last_indexed_offset) {\n const fp = fingerprint(filePath, stat.size);\n if (existing.content_fingerprint && fp !== existing.content_fingerprint) {\n return { change: \"reindex\", stat };\n }\n return { change: \"unchanged\", stat };\n }\n\n // Grew past the cursor. Usually a genuine append — but an atomic replace with\n // a DIFFERENT, LONGER file also lands here (size > offset, mtime moved). If we\n // trusted \"appended\" blindly we'd resume the OLD reducer state and fold only\n // the new tail, blending two conversations. So re-check the region we already\n // folded before taking the fast path.\n //\n // The stored content_fingerprint was computed as fingerprint(_, size_bytes)\n // over the old file. It describes the same byte range as fingerprint(current,\n // last_indexed_offset) ONLY when size_bytes === last_indexed_offset (no\n // trailing partial line was pending at index time — the normal case). When\n // that holds and the fingerprints match, we treat it as a real append and\n // resume the cursor; a mismatch means the indexed region changed → reindex.\n // When it doesn't hold (a partial line was pending), we can't reconstruct the\n // stored window, so we reindex to be safe.\n //\n // ponytail: fingerprint() only covers the first 4KB + the 4KB ending at the\n // offset — NOT the whole [0, offset) prefix. So this catches every real atomic\n // replace (any rewrite changes line 1 → different head 4KB) but NOT a rewrite\n // that keeps both 8KB windows byte-identical and differs only in the middle.\n // That's the same bounded ceiling the existing same-size edge-fingerprint has,\n // accepted by design. Upgrade to a full-prefix hash (or an offset-keyed head\n // fingerprint column) only if a mid-file-preserving-envelope rewrite is ever\n // observed in practice — and that same column would also let the pending-\n // partial-then-grew case resume instead of reindexing.\n if (\n existing.content_fingerprint &&\n existing.size_bytes === existing.last_indexed_offset &&\n fingerprint(filePath, existing.last_indexed_offset) === existing.content_fingerprint\n ) {\n return { change: \"appended\", stat };\n }\n return { change: \"reindex\", stat };\n}\n","import { statSync } from \"fs\";\nimport { canonicalPath } from \"../canonical-path\";\nimport { readGitBranch } from \"../git\";\nimport { getLogger } from \"../logger\";\nimport { getProjectsDir } from \"../profiles\";\nimport { CodexCliProvider, parseCodexConversation } from \"../providers/codex-cli\";\nimport { parseMetaWithProvider } from \"../providers/parse\";\nimport {\n CLAUDE_CODE_PROVIDER,\n CODEX_CLI_PROVIDER,\n type ScannerProvider,\n} from \"../providers/provider\";\nimport { appendSearchDelta, emptySearchDocument, extractSearchDelta } from \"../search-document\";\nimport { resolveTier } from \"../tiers\";\nimport type {\n ConversationMeta,\n ConversationPage,\n GetConversationPageOptions,\n Profile,\n ScanOptions,\n SearchHighlight,\n} from \"../types\";\nimport { classify, type FileChange, fingerprint } from \"./cursor\";\nimport { type DB, openDatabase } from \"./db\";\nimport { discoverJsonlFilesGated, FULL_RECONCILE_EVERY_N_SCANS } from \"./dir-watermark\";\nimport { type TailReadResult, tailReduce } from \"./jsonl-tail-reader\";\nimport { finalizeMeta, initialReducerState, type ReducerState } from \"./metadata-reducer\";\nimport { buildCheckpoints, CHECKPOINT_INTERVAL, readPage } from \"./paged-reader\";\nimport { CheckpointsRepo } from \"./repositories/checkpoints.repo\";\nimport { ConversationFilesRepo } from \"./repositories/conversation-files.repo\";\nimport { ConversationsRepo } from \"./repositories/conversations.repo\";\nimport { FtsRepo, type FtsSearchFilters } from \"./repositories/fts.repo\";\nimport { ScannedDirsRepo } from \"./repositories/scanned-dirs.repo\";\nimport { buildSidecar, writeSidecar } from \"./sidecar\";\n\nconst BATCH_SIZE = 12;\n\nexport interface EngineSearchHit {\n meta: ConversationMeta;\n // Null when only metadata matched — the caller falls back to generateMatches.\n body: { snippet: string; highlights: SearchHighlight[] } | null;\n}\n\n// Persistent indexing engine. Owns the SQLite connection and the discover ->\n// classify -> tail-read -> upsert pipeline. Query helpers return ConversationMeta\n// straight from the DB so the scanner facade can run the existing\n// filter/sort/view pipeline over them, guaranteeing parity with the in-memory\n// path.\n//\n// Indexing is incremental: an appended file resumes the persisted reducer fold\n// from its byte cursor and reads only the new bytes (cursor.ts + the tail\n// reader); a truncated/replaced file reindexes from offset 0.\nexport class PersistentEngine {\n readonly db: DB;\n readonly files: ConversationFilesRepo;\n readonly conversations: ConversationsRepo;\n readonly fts: FtsRepo;\n readonly checkpoints: CheckpointsRepo;\n readonly scannedDirs: ScannedDirsRepo;\n // When true, write a portable <file>.idx.json sidecar next to each indexed\n // JSONL. Off by default.\n private readonly sidecar: boolean;\n // Counts indexAll() passes so the dir-mtime gate's full-reconcile backstop\n // (FULL_RECONCILE_EVERY_N_SCANS) can fire periodically. In-memory only: a\n // restart just means the first few post-restart scans don't force an early\n // backstop pass, which is harmless (watermarks themselves persist in the DB).\n private scanCount = 0;\n // In-flight checkpoint build/extension per file, so concurrent getPage\n // callers share one stream instead of each walking the file.\n private readonly checkpointBuilds = new Map<string, Promise<void>>();\n\n constructor(dbPath: string, options: { sidecar?: boolean } = {}) {\n this.db = openDatabase(dbPath);\n this.files = new ConversationFilesRepo(this.db);\n this.conversations = new ConversationsRepo(this.db);\n this.fts = new FtsRepo(this.db);\n this.checkpoints = new CheckpointsRepo(this.db);\n this.scannedDirs = new ScannedDirsRepo(this.db);\n this.sidecar = options.sidecar ?? false;\n }\n\n close(): void {\n this.db.close();\n }\n\n // Discover all JSONL files under the active profiles, (re)parse files that\n // changed since the last index, and upsert their metadata. Returns the number\n // of files seen on disk this pass (the scan \"scanned\" count).\n async indexAll(activeProfiles: Profile[], options: ScanOptions): Promise<{ scanned: number }> {\n const log = getLogger();\n const tier = resolveTier(options.tier ?? \"standard\", options.tiers);\n\n const enabled = options.providers ?? [CLAUDE_CODE_PROVIDER];\n\n // Each discovered file carries the provider that should parse it. claude-code\n // files fold through the byte-offset-resumable tail reader; Codex files\n // (opt-in, only under explicit codexRoots) reparse from offset 0.\n const discovered: { filePath: string; account: string; provider?: ScannerProvider }[] = [];\n\n if (enabled.includes(CLAUDE_CODE_PROVIDER)) {\n const configDirs = activeProfiles.map((p) => ({\n projectsDir: getProjectsDir(p),\n account: p.id,\n }));\n // Escape hatch: an explicit fullRescan bypasses the dir-mtime gate\n // entirely — an explicit refresh is precisely the \"don't trust the\n // gate, check for real\" signal. Otherwise every FULL_RECONCILE_EVERY_N_SCANS\n // passes also bypasses it, self-healing any add/remove a filesystem that\n // doesn't honor directory mtimes might have hidden from the gate.\n this.scanCount++;\n const forceFullGlob =\n options.fullRescan === true || this.scanCount % FULL_RECONCILE_EVERY_N_SCANS === 0;\n const gated = await discoverJsonlFilesGated(configDirs, this.files, this.scannedDirs, {\n fullRescan: forceFullGlob,\n });\n for (const f of gated) discovered.push(f);\n }\n\n const codex = new CodexCliProvider();\n if (enabled.includes(CODEX_CLI_PROVIDER) && (options.codexRoots?.length ?? 0) > 0) {\n for (const f of await codex.discover(options.codexRoots as string[])) {\n discovered.push({ ...f, provider: codex });\n }\n }\n let scanned = 0;\n\n const gitBranchMemo = new Map<string, string | null>();\n const resolveGitBranch = (projectPath: string): string | null => {\n if (gitBranchMemo.has(projectPath)) {\n return gitBranchMemo.get(projectPath) ?? null;\n }\n const branch = readGitBranch(projectPath);\n gitBranchMemo.set(projectPath, branch);\n return branch;\n };\n\n for (let i = 0; i < discovered.length; i += BATCH_SIZE) {\n const batch = discovered.slice(i, i + BATCH_SIZE);\n const results = await Promise.all(\n batch.map(async ({ filePath, account, provider }) => {\n const { meta } = await this.indexFile(\n filePath,\n account,\n tier.name,\n options.tiers,\n resolveGitBranch,\n false,\n provider,\n );\n return meta;\n }),\n );\n const kept = results.filter((m): m is ConversationMeta => m != null);\n if (kept.length > 0) options.onBatch?.(kept);\n scanned += batch.length;\n options.onProgress?.(scanned, discovered.length);\n }\n\n // Reconcile deletions: any previously-active file that wasn't discovered\n // this pass is gone from disk — mark it deleted. This is the correctness\n // backstop for unlink events the watcher may have missed (spec §9.5).\n //\n // Scope the reconcile to the accounts THIS scan actually covered. The index\n // is shared across profiles/accounts (one index.db), so a scan of account A\n // must not mark account B's files deleted just because B's files weren't in\n // A's discovered set — B is simply out of this scan's scope, not gone from\n // disk. Covered = the claude-code profile ids scanned (when that provider is\n // enabled) plus \"codex\" (when codex roots were scanned).\n const coveredAccounts = new Set<string>();\n if (enabled.includes(CLAUDE_CODE_PROVIDER)) {\n for (const p of activeProfiles) coveredAccounts.add(p.id);\n }\n if (enabled.includes(CODEX_CLI_PROVIDER) && (options.codexRoots?.length ?? 0) > 0) {\n coveredAccounts.add(\"codex\");\n }\n // Canonical form on both sides: the stored paths are canonical, and a\n // discovery source that emits native separators would otherwise leave every\n // one of its files out of the seen-set and get them marked deleted.\n const seen = new Set(discovered.map((d) => canonicalPath(d.filePath)));\n for (const path of this.files.activePathsByAccounts([...coveredAccounts])) {\n if (!seen.has(path)) this.markDeleted(path);\n }\n\n log.info({ scanned, indexed: this.conversations.count() }, \"persistent: indexAll complete\");\n return { scanned };\n }\n\n // (Re)index a single file. Classifies the change vs. the persisted cursor:\n // unchanged → return the stored summary; appended → resume the fold and read\n // only new bytes; reindex/force → fold from offset 0. Writes the summary +\n // cursor + reducer state in one transaction so a crash never leaves a\n // half-written row or an over-advanced cursor. Returns the classification\n // alongside the meta so callers (refreshFile) can keep, extend, or evict\n // their own per-file caches without re-stat'ing the file (racy) themselves.\n async indexFile(\n rawFilePath: string,\n account: string,\n tierName: string,\n customTiers: ScanOptions[\"tiers\"],\n resolveGitBranch: (projectPath: string) => string | null,\n force = false,\n provider?: ScannerProvider,\n ): Promise<{ meta: ConversationMeta | null; change: FileChange }> {\n const log = getLogger();\n const tier = resolveTier(tierName, customTiers);\n\n // Canonicalize once, here: everything downstream (the cursor row, the\n // conversation's source_path/id, the checkpoint + FTS keys, the sidecar, and\n // the meta handed back to the caller) then keys off the one canonical\n // spelling, whichever separator style the caller passed in.\n const filePath = canonicalPath(rawFilePath);\n const existing = this.files.getByPath(filePath);\n const { change, stat } = classify(filePath, existing);\n\n if (change === \"vanished\" || !stat) {\n // File gone between discovery and indexing — treat as deleted.\n this.markDeleted(filePath);\n return { meta: null, change: \"vanished\" };\n }\n // Unchanged: serve the persisted summary without re-reading (unless a\n // caller forces a re-parse, e.g. refreshFile after a same-size edit).\n if (change === \"unchanged\" && !force) {\n return { meta: this.conversations.getBySourcePath(filePath), change };\n }\n\n // ponytail: Codex reparses from offset 0 on every change rather than\n // resuming a byte cursor like claude-code. Codex rollout sessions are small,\n // so a full reparse is cheap; the resumable-fold path stays claude-code-only.\n // Upgrade path if Codex files ever get large: give CodexAccumulator the same\n // serialized-reducer-state treatment and route it through tailReduce.\n if (provider && provider.name !== CLAUDE_CODE_PROVIDER) {\n const meta = await this.indexFileWithProvider(\n provider,\n filePath,\n account,\n tier,\n stat,\n resolveGitBranch,\n );\n return { meta, change };\n }\n\n // appended → resume the persisted fold from the cursor; reindex/force →\n // start fresh from offset 0. This is the incremental win: an append only\n // reads the newly-written bytes.\n const resume = change === \"appended\" && !force && existing?.reducer_state;\n const state: ReducerState = resume\n ? (JSON.parse(existing.reducer_state as string) as ReducerState)\n : initialReducerState();\n const startOffset = resume ? existing.last_indexed_offset : 0;\n const startLine = resume ? existing.last_indexed_line : 0;\n\n // Search extraction is a sibling fold over the same lines as the metadata\n // reducer — no second read of the file, and an append only ever extracts\n // from the newly-read region.\n let searchDelta = emptySearchDocument();\n const collectSearch = (entry: Record<string, unknown>) => {\n searchDelta = appendSearchDelta(searchDelta, extractSearchDelta(entry));\n };\n\n let result: TailReadResult;\n try {\n result = await tailReduce(filePath, startOffset, startLine, state, tier, collectSearch);\n } catch (err) {\n log.warn({ filePath, err }, \"persistent: tail read failed\");\n return { meta: null, change };\n }\n\n const meta = finalizeMeta(state, filePath, account, tier);\n if (!meta) {\n this.markDeleted(filePath);\n return { meta: null, change };\n }\n meta.gitBranch = resolveGitBranch(meta.projectPath);\n\n const fp = stat.size > 0 ? fingerprint(filePath, stat.size) : null;\n const fileId = this.files.ensure(filePath, account);\n\n // A resumed fold extends the buckets already in FTS; a from-0 fold replaces\n // them. Tail-append means a full bucket still admits the new material and\n // drops its oldest instead of freezing.\n const base = resume\n ? (this.fts.readDocument(fileId) ?? emptySearchDocument())\n : emptySearchDocument();\n const searchDoc = appendSearchDelta(base, searchDelta);\n\n const upsert = this.db.transaction(() => {\n this.conversations.upsert(fileId, meta, state.pageMessageCount);\n // Re-tokenizing ~128 KB is the expensive part of an append; skip it when\n // the row (body + metadata) would come out identical.\n if (!this.fts.isCurrent(fileId, meta, searchDoc)) {\n this.fts.upsert(fileId, meta, searchDoc);\n }\n // Checkpoints cover the file's immutable prefix, so an append (a resumed\n // fold) leaves them valid — drop them only when the fold restarted from\n // offset 0 (truncate/replace/new). They're rebuilt lazily on page access.\n if (!resume) this.checkpoints.remove(filePath);\n this.files.updateCursor(fileId, {\n sizeBytes: stat.size,\n mtimeMs: stat.mtimeMs,\n // Advance only to the last fully-parsed line; a trailing partial line\n // is left for the next pass.\n offset: result.newOffset,\n line: result.newLine,\n reducerState: JSON.stringify(state),\n fingerprint: fp,\n status: \"active\",\n });\n });\n upsert();\n\n if (this.sidecar) {\n writeSidecar(\n filePath,\n buildSidecar(\n meta,\n {\n sizeBytes: stat.size,\n mtimeMs: stat.mtimeMs,\n offset: result.newOffset,\n line: result.newLine,\n },\n new Date().toISOString(),\n ),\n );\n }\n\n log.debug(\n { filePath, change, bytesRead: result.newOffset - startOffset, msgs: meta.messageCount },\n \"persistent: indexed file\",\n );\n return { meta, change };\n }\n\n // Index a non-Threadbase provider file: full reparse from offset 0 through the\n // provider's reducer/finalize, then the same upsert + FTS write + cursor bump\n // the Threadbase path uses. The cursor records size/mtime (and offset = size)\n // so the next pass classifies an unchanged file as \"unchanged\" and skips it;\n // any change reparses from 0 again. No reducer_state is persisted.\n private async indexFileWithProvider(\n provider: ScannerProvider,\n filePath: string,\n account: string,\n tier: ReturnType<typeof resolveTier>,\n stat: { size: number; mtimeMs: number },\n resolveGitBranch: (projectPath: string) => string | null,\n ): Promise<ConversationMeta | null> {\n const log = getLogger();\n\n // Codex always reparses from byte 0, so the search document is rebuilt whole\n // from the same pass rather than tail-extended.\n let searchDoc = emptySearchDocument();\n const meta = await parseMetaWithProvider(provider, filePath, account, tier, (entry) => {\n searchDoc = appendSearchDelta(searchDoc, extractSearchDelta(entry));\n });\n if (!meta) {\n this.markDeleted(filePath);\n return null;\n }\n // The provider may already know its branch (Codex reads it from\n // session_meta). Only walk the filesystem when it doesn't.\n if (meta.gitBranch === null && meta.projectPath) {\n meta.gitBranch = resolveGitBranch(meta.projectPath);\n }\n\n const fp = stat.size > 0 ? fingerprint(filePath, stat.size) : null;\n const fileId = this.files.ensure(filePath, account);\n const upsert = this.db.transaction(() => {\n this.conversations.upsert(fileId, meta, meta.messageCount);\n if (!this.fts.isCurrent(fileId, meta, searchDoc)) {\n this.fts.upsert(fileId, meta, searchDoc);\n }\n this.checkpoints.remove(filePath);\n this.files.updateCursor(fileId, {\n sizeBytes: stat.size,\n mtimeMs: stat.mtimeMs,\n // offset = size marks the file fully consumed (non-zero so the next pass\n // can classify it \"unchanged\"); no resumable reducer state is kept.\n offset: stat.size,\n line: 0,\n reducerState: null,\n fingerprint: fp,\n status: \"active\",\n });\n });\n upsert();\n\n log.debug(\n { filePath, provider: provider.name, msgs: meta.messageCount },\n \"persistent: indexed provider file\",\n );\n return meta;\n }\n\n private markDeleted(filePath: string): void {\n const existing = this.files.getByPath(filePath);\n if (!existing) return;\n const tx = this.db.transaction(() => {\n this.conversations.deleteByFileId(existing.id);\n this.fts.remove(existing.id);\n this.checkpoints.remove(filePath);\n this.files.setStatus(existing.id, \"deleted\");\n });\n tx();\n }\n\n // ── Query helpers (read straight from SQLite) ───────────────────────────\n\n allActive(): ConversationMeta[] {\n return this.conversations.allActive();\n }\n\n getByIdOrSession(id: string): ConversationMeta | null {\n return this.conversations.getByIdOrSession(id);\n }\n\n getAllBySessionId(sessionId: string): ConversationMeta[] {\n return this.conversations.getAllBySessionId(sessionId);\n }\n\n // Ranked hits matching the FTS query, best first, each already resolved to its\n // active conversation row and carrying the body excerpt when the match was in\n // the conversation body.\n //\n // Filters are passed down into SQL rather than applied to the result: with a\n // wide corpus, filtering an already-LIMITed list drops conversations that\n // would have matched.\n searchHits(query: string, limit: number, filters: FtsSearchFilters = {}): EngineSearchHit[] {\n const hits: EngineSearchHit[] = [];\n for (const hit of this.fts.search(query, limit, filters)) {\n const meta = this.conversations.getBySourcePath(hit.sourcePath);\n if (meta) hits.push({ meta, body: hit.body });\n }\n return hits;\n }\n\n // Empty-query listing, mirroring the in-memory indexer's behavior.\n recentMetas(limit: number): ConversationMeta[] {\n return this.conversations.recent(limit);\n }\n\n getProjects(): string[] {\n return this.conversations.distinctProjects();\n }\n\n // Bounded conversation page: read only the requested window from the file,\n // seeking from the nearest checkpoint. Returns null if the id can't be\n // resolved to an indexed conversation. For conversations large enough to span\n // a checkpoint interval, checkpoints are built lazily on first access and\n // reused thereafter. Smaller conversations read from the start (cheap).\n async getPage(id: string, options: GetConversationPageOptions): Promise<ConversationPage | null> {\n const meta = this.conversations.getByIdOrSession(id);\n if (!meta) return null;\n const filePath = meta.filePath;\n\n // Codex files don't fold through the conversation-reducer the bounded reader\n // (readPage) uses — that reducer only understands the claude-code line shape,\n // so readPage would return an empty window for a Codex file. Codex rollout\n // sessions are small (already reparsed from offset 0 on every change), so\n // parse the whole conversation and slice the window — identical math to the\n // claude-code path and to the legacy getConversationPage slice.\n if (meta.provider === CODEX_CLI_PROVIDER) {\n const conversation = await parseCodexConversation(filePath, meta.account);\n if (!conversation) return null;\n const { messages } = conversation;\n const total = messages.length;\n const beforeIndex = options.beforeIndex ?? total;\n const fromIndex = Math.max(0, beforeIndex - options.limit);\n return { messages: messages.slice(fromIndex, beforeIndex), total, fromIndex };\n }\n\n // Bounded paging uses the parseConversation message total, which differs\n // from meta.messageCount (the metadata count excludes tool_use-only and\n // thinking-only lines).\n const total = this.conversations.pageMessageCount(filePath);\n\n await this.ensureCheckpoints(filePath, total);\n\n const beforeIndex = options.beforeIndex ?? total;\n const fromIndex = Math.max(0, beforeIndex - options.limit);\n let floor = this.checkpoints.floor(filePath, fromIndex);\n // A checkpoint past the current EOF is stale — the file was truncated or\n // replaced on disk and no refresh has landed yet. Seeking there would read\n // nothing (or garbage), so fall back to byte 0: correct, just slower, and\n // the next refresh drops the stale chain. A failed stat is left to\n // readPage, which surfaces the read error exactly as before.\n if (floor) {\n try {\n if (floor.byteOffset > statSync(filePath).size) floor = null;\n } catch {\n // fall through to readPage's own error handling\n }\n }\n return readPage(filePath, total, options, floor);\n }\n\n // Build or extend the checkpoint chain so it covers `total` messages. Cold\n // file → full build; a file that grew → extend from the last persisted\n // checkpoint (reads only past its offset, never the prefix). Single-flighted\n // per path: concurrent getPage callers await the same build instead of\n // streaming the file in parallel.\n private ensureCheckpoints(filePath: string, total: number): Promise<void> {\n if (total <= CHECKPOINT_INTERVAL) return Promise.resolve();\n const inFlight = this.checkpointBuilds.get(filePath);\n if (inFlight) return inFlight;\n\n const build = (async () => {\n const last = this.checkpoints.last(filePath);\n // Chain already covers every checkpointable index — nothing to do.\n if (last && total < last.messageIndex + CHECKPOINT_INTERVAL) return;\n const fresh = await buildCheckpoints(filePath, CHECKPOINT_INTERVAL, last);\n if (fresh.length === 0) return;\n if (last) this.checkpoints.append(filePath, fresh);\n else this.checkpoints.replaceAll(filePath, fresh);\n })().finally(() => {\n if (this.checkpointBuilds.get(filePath) === build) this.checkpointBuilds.delete(filePath);\n });\n this.checkpointBuilds.set(filePath, build);\n return build;\n }\n}\n","import { createReadStream } from \"fs\";\nimport { createInterface } from \"readline\";\nimport { getLogger } from \"../logger\";\nimport type { ContentTier, ConversationMeta } from \"../types\";\nimport type { ScannerProvider } from \"./provider\";\n\n// Stream a file through a provider's reducer triplet. This is the same fold a\n// future offset-resumed incremental parse would run (createEmptyAccumulator →\n// reduceEntry per line → finalize), just starting from offset 0. Bad JSON lines\n// and unknown entry shapes are skipped, never fatal.\n// `onEntry` lets a caller fold a second, independent accumulator (the search\n// document) over the same lines without re-reading the file. It never affects\n// the provider's own reduce.\nexport async function parseMetaWithProvider(\n provider: ScannerProvider,\n filePath: string,\n account: string,\n tier: ContentTier,\n onEntry?: (entry: Record<string, unknown>) => void,\n): Promise<ConversationMeta | null> {\n const log = getLogger();\n const acc = provider.createEmptyAccumulator();\n const rl = createInterface({\n input: createReadStream(filePath),\n crlfDelay: Infinity,\n });\n\n try {\n for await (const line of rl) {\n if (!line.trim()) continue;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n try {\n provider.reduceEntry(acc, entry, tier);\n onEntry?.(entry);\n } catch (err) {\n // A provider reducer should never throw, but one bad line must not kill\n // the whole file.\n log.warn({ filePath, provider: provider.name, err }, \"provider reduce threw; line skipped\");\n }\n }\n } catch (err) {\n log.warn({ filePath, provider: provider.name, err }, \"provider parse: read failed\");\n return null;\n }\n\n return provider.finalize(acc, filePath, account, tier);\n}\n","import { extractThinking } from \"./parser\";\nimport { cleanSystemTags } from \"./tags\";\n\n// The derived search corpus: one document per conversation, split into three\n// independently budgeted buckets so a later user message always lands in `text`\n// rather than after previously-indexed tool output. Assembled into FTS body\n// columns (persistent) or one combined string (FlexSearch).\n//\n// Budgets are JavaScript string lengths (UTF-16 code units), NOT bytes. ASCII\n// makes the two coincide; Hebrew/CJK/emoji cost 2-3 UTF-8 bytes per unit, so a\n// 64K `textMax` can hold ~128-192 KB on disk. Size index-growth estimates from\n// that, and don't describe these as byte caps.\nexport const SEARCH_BUDGET = {\n textMax: 64 * 1024,\n thinkingMax: 32 * 1024,\n toolsMax: 32 * 1024,\n\n toolPayloadMax: 4 * 1024,\n} as const;\n\nexport interface SearchDocument {\n text: string;\n thinking: string;\n tools: string;\n}\n\n// Same shape as SearchDocument, but holding only the material extracted from\n// newly-read lines — what gets tail-appended onto the durable buckets.\nexport type SearchDocumentDelta = SearchDocument;\n\nconst SEP = \"\\n\\n\";\n\nexport function emptySearchDocument(): SearchDocument {\n return { text: \"\", thinking: \"\", tools: \"\" };\n}\n\nexport function isEmptyDelta(delta: SearchDocumentDelta): boolean {\n return !delta.text && !delta.thinking && !delta.tools;\n}\n\nexport function searchDocumentsEqual(a: SearchDocument, b: SearchDocument): boolean {\n return a.text === b.text && a.thinking === b.thinking && a.tools === b.tools;\n}\n\n// Tail-biased append: each bucket is a sliding window over the NEWEST material.\n// Head-biased fill (stop once full) would freeze a conversation the moment it\n// crossed the budget — the same bug as the old 5 KB contentSnippet, just larger,\n// and it would miss exactly the actively-growing conversations search exists for.\nexport function appendSearchDelta(doc: SearchDocument, delta: SearchDocumentDelta): SearchDocument {\n return {\n text: tailAppend(doc.text, delta.text, SEARCH_BUDGET.textMax),\n thinking: tailAppend(doc.thinking, delta.thinking, SEARCH_BUDGET.thinkingMax),\n tools: tailAppend(doc.tools, delta.tools, SEARCH_BUDGET.toolsMax),\n };\n}\n\n// ponytail: slice(-max) can cut mid-token; the truncated head token simply\n// doesn't match. Grapheme/token-aware windowing isn't worth it for v1.\nfunction tailAppend(current: string, incoming: string, max: number): string {\n if (!incoming) return current;\n const joined = current ? current + SEP + incoming : incoming;\n return joined.length <= max ? joined : joined.slice(-max);\n}\n\n// Flatten to one string for the in-memory FlexSearch index, which has a single\n// body field. The persistent path keeps the buckets as separate FTS columns\n// instead (concatenating there would double-weight body hits).\nexport function combineSearchContent(doc: SearchDocument): string {\n return [doc.text, doc.thinking, doc.tools].filter(Boolean).join(SEP);\n}\n\n// Truncate one tool payload before it joins the tools bucket, so a single huge\n// Read/Bash/compiler dump can't consume the whole window.\nexport function capToolPayload(value: unknown): string {\n const raw = stringifyPayload(value);\n if (!raw) return \"\";\n return raw.length > SEARCH_BUDGET.toolPayloadMax\n ? raw.slice(0, SEARCH_BUDGET.toolPayloadMax)\n : raw;\n}\n\nfunction stringifyPayload(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"string\") return value;\n try {\n return JSON.stringify(value) ?? \"\";\n } catch {\n return \"\";\n }\n}\n\n// Extract the search-document delta from one already-parsed JSONL entry.\n// Handles both Claude/Threadbase and Codex CLI line shapes so every indexing\n// path — persistent, in-memory, both providers — shares one corpus definition.\nexport function extractSearchDelta(entry: Record<string, unknown>): SearchDocumentDelta {\n if (entry.type === \"response_item\" || entry.type === \"session_meta\") {\n return extractCodexDelta(entry);\n }\n return extractClaudeDelta(entry);\n}\n\nfunction extractClaudeDelta(entry: Record<string, unknown>): SearchDocumentDelta {\n const type = entry.type;\n if (type !== \"user\" && type !== \"assistant\") return emptySearchDocument();\n if (entry.isMeta) return emptySearchDocument();\n\n const msg = entry.message as Record<string, unknown> | undefined;\n const content = msg?.content;\n\n const tools = [\n extractClaudeToolContent(content),\n // Claude stores the rich/structured tool result at the JSONL entry's top\n // level, not inside message.content — indexing only message.content would\n // miss most real tool output (file reads, command stdout).\n capToolPayload(entry.toolUseResult),\n ]\n .filter(Boolean)\n .join(SEP);\n\n return {\n text: extractClaudeText(content),\n thinking: type === \"assistant\" ? extractThinking(content).content : \"\",\n tools,\n };\n}\n\n// Human/assistant prose only. Deliberately NOT parser.extractTextContent():\n// that helper folds `tool_result` strings into the same string as user text,\n// which would let a large tool result eat the high-priority text window.\n//\n// It DOES keep extractTextContent's cleanSystemTags step, and that is\n// mandatory: without it every user turn's <system-reminder> (CLAUDE.md dump,\n// skill listing, memory block) lands in `text`, exhausting the window in a\n// couple of turns and making every conversation on the machine a hit for tokens\n// that only ever appear in those wrappers.\nfunction extractClaudeText(content: unknown): string {\n if (typeof content === \"string\") return cleanSystemTags(content);\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const item of content) {\n if (typeof item === \"string\") {\n const cleaned = cleanSystemTags(item);\n if (cleaned) parts.push(cleaned);\n } else if (item?.type === \"text\" && typeof item.text === \"string\") {\n const cleaned = cleanSystemTags(item.text);\n if (cleaned) parts.push(cleaned);\n }\n }\n return parts.join(SEP);\n}\n\n// Tool I/O is the payload, so it is NOT tag-cleaned — stripping it as if it\n// were a prompt wrapper would delete the content being indexed. The per-payload\n// cap is the safety valve instead.\nfunction extractClaudeToolContent(content: unknown): string {\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const item of content) {\n if (item?.type === \"tool_use\") {\n const capped = capToolPayload(item.input);\n if (capped) parts.push(capped);\n } else if (item?.type === \"tool_result\") {\n const capped = capToolPayload(item.content);\n if (capped) parts.push(capped);\n }\n }\n return parts.join(SEP);\n}\n\nfunction extractCodexDelta(entry: Record<string, unknown>): SearchDocumentDelta {\n const payload = entry.payload as Record<string, unknown> | undefined;\n if (!payload || typeof payload !== \"object\") return emptySearchDocument();\n\n const ptype = payload.type as string;\n\n if (ptype === \"function_call\" || ptype === \"custom_tool_call\") {\n return { text: \"\", thinking: \"\", tools: capToolPayload(payload.arguments) };\n }\n if (ptype === \"function_call_output\" || ptype === \"custom_tool_call_output\") {\n return { text: \"\", thinking: \"\", tools: capToolPayload(payload.output) };\n }\n if (ptype === \"reasoning\") {\n return { text: \"\", thinking: extractCodexReasoning(payload), tools: \"\" };\n }\n if (ptype === \"message\") {\n const role = payload.role;\n if (role !== \"user\" && role !== \"assistant\") return emptySearchDocument();\n return { text: extractCodexText(payload.content), thinking: \"\", tools: \"\" };\n }\n return emptySearchDocument();\n}\n\n// Codex reasoning carries its text under `summary` (and sometimes `content`),\n// each a list of {type, text} blocks. Never a signature — there isn't one here,\n// and on the Claude side the thinking signature is a base64 crypto blob that is\n// pure noise in a search index.\nfunction extractCodexReasoning(payload: Record<string, unknown>): string {\n const parts: string[] = [];\n for (const key of [\"summary\", \"content\"] as const) {\n const blocks = payload[key];\n if (!Array.isArray(blocks)) continue;\n for (const block of blocks) {\n if (typeof block === \"string\") parts.push(block);\n else if (typeof block?.text === \"string\") parts.push(block.text);\n }\n }\n return parts.filter(Boolean).join(SEP);\n}\n\nfunction extractCodexText(content: unknown): string {\n if (typeof content === \"string\") return cleanSystemTags(content);\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const item of content) {\n if (typeof item === \"string\") {\n const cleaned = cleanSystemTags(item);\n if (cleaned) parts.push(cleaned);\n continue;\n }\n const t = item?.type;\n if (\n (t === \"input_text\" || t === \"output_text\" || t === \"text\") &&\n typeof item.text === \"string\"\n ) {\n const cleaned = cleanSystemTags(item.text);\n if (cleaned) parts.push(cleaned);\n }\n }\n return parts.join(SEP);\n}\n","import type { ContentTier } from \"./types\";\n\nexport const DEFAULT_TIERS: Record<string, ContentTier> = {\n standard: { name: \"standard\", previewMax: 200, snippetMax: 5_000 },\n full: { name: \"full\", previewMax: 1_200, snippetMax: 50_000 },\n};\n\nexport function resolveTier(\n tierName: string,\n customTiers?: Record<string, ContentTier>,\n): ContentTier {\n const tier = customTiers?.[tierName] ?? DEFAULT_TIERS[tierName];\n if (!tier) {\n throw new Error(\n `Unknown tier \"${tierName}\". Available: ${Object.keys({ ...DEFAULT_TIERS, ...customTiers }).join(\", \")}`,\n );\n }\n return tier;\n}\n","import Database from \"better-sqlite3\";\nimport { mkdirSync } from \"fs\";\nimport { dirname } from \"path\";\nimport { getLogger } from \"../logger\";\nimport { runMigrations } from \"./migrations\";\n\nexport type DB = Database.Database;\n\n// Open (creating if needed) the SQLite index at dbPath, apply WAL pragmas, and\n// run migrations. \":memory:\" is supported for tests.\nexport function openDatabase(dbPath: string): DB {\n if (dbPath !== \":memory:\") {\n mkdirSync(dirname(dbPath), { recursive: true });\n }\n\n let db: DB;\n try {\n db = new Database(dbPath);\n } catch (err) {\n if (isNativeBindingFailure(err)) throw nativeBindingError(err, dbPath);\n throw err;\n }\n\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"synchronous = NORMAL\");\n db.pragma(\"temp_store = MEMORY\");\n db.pragma(\"foreign_keys = ON\");\n // Let a reader briefly wait out a concurrent indexing write instead of failing\n // immediately with SQLITE_BUSY. Operational resilience only — search recall is\n // unaffected.\n db.pragma(\"busy_timeout = 5000\");\n\n runMigrations(db);\n\n getLogger().debug({ dbPath }, \"db: opened\");\n return db;\n}\n\n// better-sqlite3 is a native addon, and `require()` of it succeeds even when its\n// binary is absent — the JS entry point loads fine and the addon is only touched\n// when a Database is constructed. So a broken install stays invisible until this\n// exact line, then fails with an opaque list of candidate .node paths that reads\n// like an ABI mismatch and sends people chasing Node versions.\n//\n// It usually isn't an ABI mismatch: the path in the message is normally correct\n// for the running Node, and the file simply was never produced.\nfunction isNativeBindingFailure(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n return /could not locate the bindings file|NODE_MODULE_VERSION|was compiled against|\\.node['\"\\s]/i.test(\n message,\n );\n}\n\nfunction nativeBindingError(err: unknown, dbPath: string): Error {\n const detail = err instanceof Error ? err.message : String(err);\n return new Error(\n [\n `Could not load better-sqlite3's native binary, so the persistent index at ${dbPath} cannot be opened.`,\n \"This usually means the binary was never built or downloaded — not that your Node version is wrong.\",\n \"\",\n \"Try, in order:\",\n \" 1. npm rebuild better-sqlite3\",\n \" 2. rm -rf node_modules && npm install\",\n \" (npm 12 blocks package install scripts by default; approve better-sqlite3 if asked)\",\n \" 3. Run without SQLite entirely: new ConversationScanner({ persistent: false })\",\n \" or pass --no-persist on the CLI. Search falls back to the in-memory index.\",\n \"\",\n `Original error: ${detail}`,\n ].join(\"\\n\"),\n );\n}\n","// Persistent index schema for @threadbase-sh/scanner.\n//\n// Embedded as a string (not a sibling .sql file) so it survives tsup bundling\n// into dist/ without runtime file I/O.\n//\n// One JSONL file == one conversation, so conversation_files.id maps 1:1 to\n// conversations.file_id.\n//\n// NOTE: session_id is intentionally NOT unique. parseMeta() falls back to the\n// file's basename when a line carries no sessionId, and resumed/subagent\n// sessions can repeat a sessionId across files. The stable unique key is the\n// file's absolute_path (and conversations.file_id).\n\n// Bumped whenever DDL below changes; drives migrations.ts via PRAGMA user_version.\n// v4: added the scanned_dirs table (dir-mtime discovery gate). runMigrations()\n// early-returns when the stored user_version >= SCHEMA_VERSION, so a new table\n// in SCHEMA_SQL is only reached on an existing DB if this number is bumped —\n// otherwise every DB already at v3 skips the migration and never gets the table.\n// v5: FTS body split from one `content` column into text/thinking/tools, fed by\n// the tail-biased search document instead of meta.contentSnippet.\nexport const SCHEMA_VERSION = 5;\n\nexport const SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS conversation_files (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n\n absolute_path TEXT NOT NULL UNIQUE,\n parent_dir TEXT NOT NULL,\n file_name TEXT NOT NULL,\n\n account TEXT NOT NULL DEFAULT 'default',\n\n size_bytes INTEGER NOT NULL DEFAULT 0,\n mtime_ms INTEGER NOT NULL DEFAULT 0,\n\n last_indexed_offset INTEGER NOT NULL DEFAULT 0,\n last_indexed_line INTEGER NOT NULL DEFAULT 0,\n\n reducer_state TEXT,\n\n content_fingerprint TEXT,\n\n status TEXT NOT NULL DEFAULT 'active',\n\n last_indexed_at TEXT,\n created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n deleted_at TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_conversation_files_status ON conversation_files(status);\nCREATE INDEX IF NOT EXISTS idx_conversation_files_parent_dir ON conversation_files(parent_dir);\nCREATE INDEX IF NOT EXISTS idx_conversation_files_account ON conversation_files(account);\n\nCREATE TABLE IF NOT EXISTS conversations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n\n file_id INTEGER NOT NULL UNIQUE,\n\n source_path TEXT NOT NULL UNIQUE,\n -- Which provider produced this row. Canonical identity is (provider,\n -- source_path); session_id stays non-unique across providers too.\n provider TEXT NOT NULL DEFAULT 'claude-code',\n kind TEXT,\n external_session_id TEXT,\n session_id TEXT NOT NULL,\n session_name TEXT,\n\n project_path TEXT,\n project_name TEXT,\n account TEXT NOT NULL DEFAULT 'default',\n branch TEXT,\n\n preview TEXT,\n content_snippet TEXT,\n\n message_count INTEGER NOT NULL DEFAULT 0,\n -- Count of messages parseConversation() produces (broader than message_count;\n -- includes tool_use-only and thinking-only lines). The total for bounded paging.\n page_message_count INTEGER NOT NULL DEFAULT 0,\n last_message_sender TEXT NOT NULL DEFAULT 'user',\n timestamp TEXT,\n\n -- Monotonic write counter; the highest value is the most recently indexed\n -- row. Drives last-writer-wins resolution for shared session_ids (matching\n -- the in-memory sessionId map), at the sub-second precision updated_at lacks.\n index_seq INTEGER NOT NULL DEFAULT 0,\n\n first_sent_at TEXT,\n first_sent_text TEXT,\n\n last_sent_at TEXT,\n last_sent_text TEXT,\n\n model TEXT,\n is_subagent INTEGER NOT NULL DEFAULT 0,\n parent_session_id TEXT,\n is_teammate INTEGER NOT NULL DEFAULT 0,\n team_name TEXT,\n tool_names_json TEXT,\n last_prompt TEXT,\n\n status TEXT NOT NULL DEFAULT 'active',\n\n created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n FOREIGN KEY (file_id) REFERENCES conversation_files(id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_conversations_session_id ON conversations(session_id);\nCREATE INDEX IF NOT EXISTS idx_conversations_provider_session ON conversations(provider, session_id);\nCREATE INDEX IF NOT EXISTS idx_conversations_provider_recent ON conversations(provider, timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_conversations_provider_project_branch ON conversations(provider, project_path, branch);\nCREATE INDEX IF NOT EXISTS idx_conversations_recent ON conversations(timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_conversations_project_recent ON conversations(project_path, timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_conversations_project_branch_recent ON conversations(project_path, branch, timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_conversations_account_recent ON conversations(account, timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_conversations_subagent_recent ON conversations(is_subagent, timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_conversations_team_recent ON conversations(team_name, timestamp DESC);\n\n-- Full-text search index over conversation body + metadata. Kept separate from\n-- the metadata tables so list-screen queries stay small and fast. One FTS row\n-- per conversation, replaced on each upsert.\n--\n-- The row's rowid IS conversation_files.id. That is load-bearing, not cosmetic:\n-- FTS5's query planner only handles MATCH, rowid and rank, so any other\n-- constraint (including a source_path equality on an UNINDEXED column) has no\n-- index and linear-scans the whole table. At ~128 KB of body per row that would\n-- mean scanning the entire corpus on every append. Look rows up by rowid.\n-- source_path stays stored-but-UNINDEXED so a search hit can resolve back to a\n-- conversations row.\n--\n-- Body is three columns, not one: text > thinking > tools priority has to\n-- survive an append (a new user message belongs in the text column, not after\n-- the tool output already written). They are deliberately NOT concatenated into\n-- a fourth column - that would double-weight body hits and inflate bm25 length.\nCREATE VIRTUAL TABLE IF NOT EXISTS conversation_messages_fts USING fts5(\n source_path UNINDEXED,\n text,\n thinking,\n tools,\n project_name,\n session_id,\n session_name,\n account,\n model,\n branch,\n tool_names,\n tokenize = 'unicode61'\n);\n\n-- Seek index for large conversations: every N messages, record the byte offset\n-- where the next message's line begins plus the parser's cross-line state\n-- (pending tool_use blocks, team info) needed to resume an equivalent parse\n-- from that point. Lets getConversationPage() read a window near the end of a\n-- huge file without parsing from byte 0.\nCREATE TABLE IF NOT EXISTS message_checkpoints (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_path TEXT NOT NULL,\n message_index INTEGER NOT NULL,\n byte_offset INTEGER NOT NULL,\n line_number INTEGER NOT NULL,\n parser_state TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_message_checkpoints_lookup\n ON message_checkpoints(source_path, message_index);\n\n-- Per-directory mtime watermark for the discovery dir-mtime gate (skips the\n-- glob for a directory whose file/subdir SET hasn't changed; never skips\n-- per-file classify()). One row for a profile's projectsDir itself (parent_root\n-- IS NULL) and one row per immediate project subdirectory discovered under it\n-- (parent_root = the projectsDir path). has_nested marks a project dir known to\n-- contain files below its own top level (e.g. subagents/) — those always\n-- re-glob regardless of mtime, since a project-dir-level watermark can't see a\n-- change two levels down.\nCREATE TABLE IF NOT EXISTS scanned_dirs (\n path TEXT PRIMARY KEY,\n parent_root TEXT,\n mtime_ms INTEGER NOT NULL,\n has_nested INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_scanned_dirs_parent_root ON scanned_dirs(parent_root);\n`;\n","import type { Database } from \"better-sqlite3\";\nimport { getLogger } from \"../logger\";\nimport { SCHEMA_SQL, SCHEMA_VERSION } from \"./schema\";\n\n// Minimal migration runner gated by PRAGMA user_version. The whole schema uses\n// CREATE TABLE/INDEX IF NOT EXISTS, so a fresh DB and an up-to-date DB both\n// no-op safely. When SCHEMA_VERSION bumps in the future, add ALTER statements\n// keyed on the stored version before stamping the new version.\nexport function runMigrations(db: Database): void {\n const current = db.pragma(\"user_version\", { simple: true }) as number;\n if (current >= SCHEMA_VERSION) return;\n\n const log = getLogger();\n log.info({ from: current, to: SCHEMA_VERSION }, \"migrations: applying\");\n\n // v1 → v2: add provider columns to a pre-existing conversations table BEFORE\n // running SCHEMA_SQL, because SCHEMA_SQL now creates indexes on those columns.\n // CREATE TABLE IF NOT EXISTS can't add columns, so ALTER the existing one.\n // Non-destructive: existing rows keep their data and default to 'claude-code'.\n // Guarded on table existence so a fresh DB (table created by SCHEMA_SQL below)\n // skips this entirely.\n if (current >= 1 && current < 2 && tableExists(db, \"conversations\")) {\n for (const [col, ddl] of [\n [\n \"provider\",\n // Keep the historical DEFAULT 'threadbase' — v2→v3 below updates these rows.\n \"ALTER TABLE conversations ADD COLUMN provider TEXT NOT NULL DEFAULT 'threadbase'\",\n ],\n [\"kind\", \"ALTER TABLE conversations ADD COLUMN kind TEXT\"],\n [\"external_session_id\", \"ALTER TABLE conversations ADD COLUMN external_session_id TEXT\"],\n ] as const) {\n if (!hasColumn(db, \"conversations\", col)) db.exec(ddl);\n }\n }\n\n // v2 → v3: rename provider 'threadbase' → 'claude-code'.\n if (current >= 1 && current < 3 && tableExists(db, \"conversations\")) {\n db.exec(\"UPDATE conversations SET provider = 'claude-code' WHERE provider = 'threadbase'\");\n }\n\n // v4 → v5: the FTS body split from a single `content` column (fed by the 5 KB\n // head-biased meta.contentSnippet) into text/thinking/tools columns fed by the\n // tail-biased search document. CREATE VIRTUAL TABLE IF NOT EXISTS cannot\n // reshape an existing table, so drop it and let SCHEMA_SQL recreate it.\n //\n // Then force every known file to reindex from byte 0. classify() already\n // returns \"reindex\" whenever last_indexed_offset === 0, so resetting the\n // cursor is the entire trigger. Without it an unchanged conversation would\n // keep its old FTS row forever — nothing would ever append to it, so nothing\n // would rebuild it.\n //\n // Deliberately cheap: no JSONL is read here. Opening the database must stay in\n // milliseconds; the actual re-parse happens in the next indexAll (newest files\n // first), not inside a pragma migration.\n if (current >= 1 && current < 5) {\n db.exec(\"DROP TABLE IF EXISTS conversation_messages_fts\");\n // A database old enough to still be at v1 can predate some cursor columns,\n // and CREATE TABLE IF NOT EXISTS below won't add them — so reset only what\n // this database actually has.\n if (tableExists(db, \"conversation_files\")) {\n const assignments: string[] = [];\n if (hasColumn(db, \"conversation_files\", \"last_indexed_offset\")) {\n assignments.push(\"last_indexed_offset = 0\");\n }\n if (hasColumn(db, \"conversation_files\", \"last_indexed_line\")) {\n assignments.push(\"last_indexed_line = 0\");\n }\n if (hasColumn(db, \"conversation_files\", \"reducer_state\")) {\n assignments.push(\"reducer_state = NULL\");\n }\n if (assignments.length > 0) {\n db.exec(`UPDATE conversation_files SET ${assignments.join(\", \")}`);\n }\n }\n }\n\n // Fresh DB and re-runs both no-op safely (CREATE ... IF NOT EXISTS). Creates\n // any missing tables/indexes, including the new provider indexes.\n db.exec(SCHEMA_SQL);\n\n db.pragma(`user_version = ${SCHEMA_VERSION}`);\n}\n\nfunction tableExists(db: Database, table: string): boolean {\n return (\n db.prepare(\"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?\").get(table) !==\n undefined\n );\n}\n\nfunction hasColumn(db: Database, table: string, column: string): boolean {\n const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];\n return cols.some((c) => c.name === column);\n}\n","import { readdir, stat } from \"fs/promises\";\nimport { dirname, join } from \"path\";\nimport { canonicalPath } from \"../canonical-path\";\nimport { type DiscoveredFile, discoverJsonlFiles } from \"../discovery\";\nimport { getLogger } from \"../logger\";\nimport type { ConversationFilesRepo } from \"./repositories/conversation-files.repo\";\nimport type { ScannedDirsRepo } from \"./repositories/scanned-dirs.repo\";\n\n// ponytail: full-reconcile backstop runs every Nth gated pass, ignoring all\n// watermarks, so a filesystem that doesn't honor directory mtimes (some\n// NFS/SMB mounts, a few atomic-replace editors) can't permanently hide a\n// missed add/remove. Bump this if that self-heal interval feels too slow for\n// a given deployment; there's no config surface for it yet.\nexport const FULL_RECONCILE_EVERY_N_SCANS = 20;\n\n// Discover JSONL files under each profile's projectsDir, skipping the glob for\n// a directory whose file/subdirectory SET hasn't changed since the last pass.\n//\n// The core invariant (see SCAN-OPTIMIZATION-PROMPT.md Stage 4): the gate only\n// ever skips the GLOB (which exists to catch added/removed files — an event\n// that bumps the containing directory's mtime). It never skips classify() for\n// an already-known file (which exists to catch content changes, e.g. an\n// append — an event that does NOT bump the directory's mtime). Every active\n// file previously indexed under a project dir is always included in the\n// returned list, glob or no glob, so index-engine.ts's deletion-reconcile\n// never mistakes a skipped-but-still-known file for a vanished one.\n//\n// Two-level watermark, matching the real ~/.claude/projects/ shape (one\n// project subdirectory per conversation history, sometimes with nested\n// subagent/workflow trees below that):\n// - projectsDir itself: catches added/removed PROJECT DIRECTORIES.\n// - each immediate project subdirectory: catches added/removed FILES\n// directly in it. A project dir known to contain nested files (subagents/\n// etc) always re-globs regardless of mtime — a project-dir-level\n// watermark can't see a change two levels down, so it isn't trusted to\n// skip those dirs. This still skips the glob for the common case: most\n// project dirs are flat (session-uuid.jsonl directly inside).\nexport async function discoverJsonlFilesGated(\n dirs: { projectsDir: string; account: string }[],\n files: ConversationFilesRepo,\n scannedDirs: ScannedDirsRepo,\n options: { fullRescan?: boolean } = {},\n): Promise<DiscoveredFile[]> {\n const log = getLogger();\n if (options.fullRescan) {\n return discoverJsonlFiles(dirs);\n }\n\n const results: DiscoveredFile[] = [];\n\n for (const rawDir of dirs) {\n const { account } = rawDir;\n // Canonicalize: the reuse branch below compares these dir keys against\n // parent_dir/scanned_dirs.path, which the repositories store canonically.\n // Without this the has_nested/parent_dir lookups silently miss on Windows\n // when a caller's projectsDir came in with the other separator style, and\n // the caller's seen-set then loses those files to the deletion-reconcile.\n const projectsDir = canonicalPath(rawDir.projectsDir);\n\n const resolved = await resolveProjectDirs(projectsDir, scannedDirs);\n if (resolved === null) continue; // projectsDir unreadable/vanished\n\n for (const projectDir of resolved.entries) {\n let dirStat: { mtimeMs: number };\n try {\n dirStat = await stat(projectDir);\n } catch {\n // Project dir vanished between listing and stat — drop its watermark;\n // its previously-known files simply won't appear in this pass's\n // result, so index-engine.ts's deletion-reconcile marks them deleted.\n scannedDirs.remove(projectDir);\n continue;\n }\n\n const watermark = scannedDirs.get(projectDir);\n const canReuse =\n watermark !== undefined &&\n watermark.mtime_ms === dirStat.mtimeMs &&\n watermark.has_nested === 0;\n\n if (canReuse) {\n const known = files.activePathsByParentDir(projectDir);\n // A watermarked, unchanged dir must have had ≥1 indexed file when it was\n // globbed. Zero known files means the watermark is out of sync with\n // conversation_files — most often a race: another connection sharing this\n // index.db committed the scanned_dirs watermark (during its own discovery)\n // before it finished writing that dir's file rows, and we read in that\n // window. Trusting the empty reuse would drop the dir's conversations from\n // `discovered`, so the caller's deletion-reconcile (or a stale allActive)\n // never surfaces them → 404. Fall through to the glob, which reads the\n // real on-disk truth and re-indexes what's actually there.\n if (known.length > 0) {\n for (const row of known) {\n results.push({ filePath: row.absolute_path, account: row.account });\n }\n continue;\n }\n }\n\n const found = await discoverJsonlFiles([{ projectsDir: projectDir, account }]);\n const hasNested = found.some((f) => dirnameOf(f.filePath) !== projectDir);\n scannedDirs.upsert(projectDir, projectsDir, dirStat.mtimeMs, hasNested);\n results.push(...found);\n }\n\n // Commit the root watermark only after every project dir under it has been\n // processed without throwing. Committing it earlier (e.g. right after the\n // readdir) would let a crash mid-loop leave un-globbed project dirs with no\n // scanned_dirs row — the next pass's root-reuse would then return only the\n // dirs that DID get a row, silently dropping the crashed-out dirs' files\n // from `seen` and mass-deleting their conversations.\n if (resolved.commitRoot) {\n scannedDirs.upsert(projectsDir, null, resolved.commitRoot.mtimeMs, false);\n const seen = new Set(resolved.entries);\n for (const known of scannedDirs.childrenOf(projectsDir)) {\n if (!seen.has(known.path)) scannedDirs.remove(known.path);\n }\n }\n }\n\n log.debug(\n { totalFiles: results.length, dirs: dirs.length },\n \"dir-watermark: gated discovery complete\",\n );\n return results;\n}\n\ninterface ResolvedProjectDirs {\n entries: string[];\n // Present only when projectsDir was actually re-listed this pass (root\n // watermark was stale/missing) — the caller commits it after the per-dir\n // loop succeeds. Absent when the root watermark was reused as-is (nothing\n // new to commit).\n commitRoot?: { mtimeMs: number };\n}\n\n// The list of immediate project subdirectories under projectsDir, reusing the\n// stored list when projectsDir's own mtime hasn't moved (no dirs added or\n// removed), otherwise re-listing (but NOT yet persisting — see commitRoot).\n// Returns null if projectsDir itself can't be read (e.g. a profile pointing at\n// a not-yet-created config dir).\nasync function resolveProjectDirs(\n projectsDir: string,\n scannedDirs: ScannedDirsRepo,\n): Promise<ResolvedProjectDirs | null> {\n let rootStat: { mtimeMs: number };\n try {\n rootStat = await stat(projectsDir);\n } catch {\n return null;\n }\n\n const rootWatermark = scannedDirs.get(projectsDir);\n if (rootWatermark !== undefined && rootWatermark.mtime_ms === rootStat.mtimeMs) {\n return { entries: scannedDirs.childrenOf(projectsDir).map((row) => row.path) };\n }\n\n let entries: string[];\n try {\n entries = (await readdir(projectsDir, { withFileTypes: true }))\n .filter((e) => e.isDirectory())\n .map((e) => joinPath(projectsDir, e.name));\n } catch {\n return null;\n }\n\n return { entries, commitRoot: { mtimeMs: rootStat.mtimeMs } };\n}\n\n// Platform-aware: the paths threaded through here are canonical (native\n// separator on Windows), so a hardcoded \"/\" split would read the whole path as\n// the dirname and mis-derive has_nested.\nfunction dirnameOf(filePath: string): string {\n return dirname(filePath);\n}\n\nfunction joinPath(dir: string, name: string): string {\n return join(dir, name);\n}\n","import type { Database } from \"better-sqlite3\";\nimport { canonicalPath } from \"../../canonical-path\";\nimport type { ConvReducerState } from \"../conversation-reducer\";\n\nexport interface Checkpoint {\n messageIndex: number;\n byteOffset: number;\n lineNumber: number;\n state: ConvReducerState;\n}\n\ninterface CheckpointRow {\n message_index: number;\n byte_offset: number;\n line_number: number;\n parser_state: string;\n}\n\n// Stores periodic seek points for a conversation so a page near the end of a\n// large file can be read without parsing from byte 0.\nexport class CheckpointsRepo {\n constructor(private db: Database) {}\n\n replaceAll(sourcePath: string, checkpoints: Checkpoint[]): void {\n const path = canonicalPath(sourcePath);\n const tx = this.db.transaction(() => {\n this.db.prepare(\"DELETE FROM message_checkpoints WHERE source_path = ?\").run(path);\n const insert = this.db.prepare(\n `INSERT INTO message_checkpoints\n (source_path, message_index, byte_offset, line_number, parser_state)\n VALUES (?, ?, ?, ?, ?)`,\n );\n for (const c of checkpoints) {\n insert.run(path, c.messageIndex, c.byteOffset, c.lineNumber, JSON.stringify(c.state));\n }\n });\n tx();\n }\n\n // Insert checkpoints without touching existing rows. Appends never invalidate\n // the chain covering the immutable prefix (Kafka sparse-index style); rows are\n // only ever removed on truncation/replace or deletion.\n append(sourcePath: string, checkpoints: Checkpoint[]): void {\n const path = canonicalPath(sourcePath);\n const tx = this.db.transaction(() => {\n const insert = this.db.prepare(\n `INSERT INTO message_checkpoints\n (source_path, message_index, byte_offset, line_number, parser_state)\n VALUES (?, ?, ?, ?, ?)`,\n );\n for (const c of checkpoints) {\n insert.run(path, c.messageIndex, c.byteOffset, c.lineNumber, JSON.stringify(c.state));\n }\n });\n tx();\n }\n\n // The latest checkpoint at or before `messageIndex`, or null if none (read\n // from the file start). Lets a page seek to the nearest prior anchor.\n floor(sourcePath: string, messageIndex: number): Checkpoint | null {\n const row = this.db\n .prepare(\n `SELECT message_index, byte_offset, line_number, parser_state\n FROM message_checkpoints\n WHERE source_path = ? AND message_index <= ?\n ORDER BY message_index DESC LIMIT 1`,\n )\n .get(canonicalPath(sourcePath), messageIndex) as CheckpointRow | undefined;\n return row ? toCheckpoint(row) : null;\n }\n\n // The highest-index checkpoint for a file, or null if none. The resume point\n // for extending the chain after an append.\n last(sourcePath: string): Checkpoint | null {\n const row = this.db\n .prepare(\n `SELECT message_index, byte_offset, line_number, parser_state\n FROM message_checkpoints\n WHERE source_path = ?\n ORDER BY message_index DESC LIMIT 1`,\n )\n .get(canonicalPath(sourcePath)) as CheckpointRow | undefined;\n return row ? toCheckpoint(row) : null;\n }\n\n count(sourcePath: string): number {\n return (\n this.db\n .prepare(\"SELECT COUNT(*) AS n FROM message_checkpoints WHERE source_path = ?\")\n .get(canonicalPath(sourcePath)) as { n: number }\n ).n;\n }\n\n remove(sourcePath: string): void {\n this.db\n .prepare(\"DELETE FROM message_checkpoints WHERE source_path = ?\")\n .run(canonicalPath(sourcePath));\n }\n}\n\nfunction toCheckpoint(row: CheckpointRow): Checkpoint {\n return {\n messageIndex: row.message_index,\n byteOffset: row.byte_offset,\n lineNumber: row.line_number,\n state: JSON.parse(row.parser_state) as ConvReducerState,\n };\n}\n","import type { Database } from \"better-sqlite3\";\nimport { basename, dirname } from \"path\";\nimport { canonicalPath } from \"../../canonical-path\";\n\n// One tracked JSONL source file + its incremental indexing cursor.\nexport interface FileRow {\n id: number;\n absolute_path: string;\n parent_dir: string;\n file_name: string;\n account: string;\n size_bytes: number;\n mtime_ms: number;\n last_indexed_offset: number;\n last_indexed_line: number;\n reducer_state: string | null;\n content_fingerprint: string | null;\n status: string;\n last_indexed_at: string | null;\n}\n\nexport class ConversationFilesRepo {\n constructor(private db: Database) {}\n\n getByPath(absolutePath: string): FileRow | undefined {\n return this.db\n .prepare(\"SELECT * FROM conversation_files WHERE absolute_path = ?\")\n .get(canonicalPath(absolutePath)) as FileRow | undefined;\n }\n\n // Insert a freshly-discovered file at offset 0; returns its row id. Existing\n // path is left untouched (returns the existing id) so a re-discovery is safe.\n //\n // absolute_path/parent_dir are stored canonicalized so a caller that built the\n // path with the native separator (watcher, path.join) and one that got it from\n // fast-glob (forward slashes, even on Windows) land on the same row instead of\n // inserting a duplicate.\n ensure(absolutePath: string, account: string): number {\n const canonical = canonicalPath(absolutePath);\n const existing = this.getByPath(canonical);\n if (existing) return existing.id;\n\n const info = this.db\n .prepare(\n `INSERT INTO conversation_files (absolute_path, parent_dir, file_name, account)\n VALUES (?, ?, ?, ?)`,\n )\n .run(canonical, dirname(canonical), basename(canonical), account);\n return Number(info.lastInsertRowid);\n }\n\n // Advance the cursor + persisted reducer state after a successful index pass.\n updateCursor(\n id: number,\n fields: {\n sizeBytes: number;\n mtimeMs: number;\n offset: number;\n line: number;\n reducerState: string | null;\n fingerprint: string | null;\n status?: string;\n },\n ): void {\n this.db\n .prepare(\n `UPDATE conversation_files\n SET size_bytes = ?, mtime_ms = ?, last_indexed_offset = ?, last_indexed_line = ?,\n reducer_state = ?, content_fingerprint = ?, status = ?,\n last_indexed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n )\n .run(\n fields.sizeBytes,\n fields.mtimeMs,\n fields.offset,\n fields.line,\n fields.reducerState,\n fields.fingerprint,\n fields.status ?? \"active\",\n id,\n );\n }\n\n // Reset the cursor to 0 for a truncated/replaced file before a full reindex.\n resetCursor(id: number): void {\n this.db\n .prepare(\n `UPDATE conversation_files\n SET last_indexed_offset = 0, last_indexed_line = 0, reducer_state = NULL,\n status = 'needs_reindex', updated_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n )\n .run(id);\n }\n\n setStatus(id: number, status: string): void {\n const deletedAt = status === \"deleted\" ? \"CURRENT_TIMESTAMP\" : \"deleted_at\";\n this.db\n .prepare(\n `UPDATE conversation_files\n SET status = ?, deleted_at = ${deletedAt}, updated_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n )\n .run(status, id);\n }\n\n allActivePaths(): string[] {\n const rows = this.db\n .prepare(\"SELECT absolute_path FROM conversation_files WHERE status != 'deleted'\")\n .all() as { absolute_path: string }[];\n return rows.map((r) => r.absolute_path);\n }\n\n // Active file paths belonging to any of the given accounts. Backs the\n // deletion-reconcile so a scan that covered only some accounts can't mark\n // another account's files deleted (they live in the same shared index.db but\n // a different profile owns them). Returns [] for an empty account list.\n activePathsByAccounts(accounts: string[]): string[] {\n if (accounts.length === 0) return [];\n const placeholders = accounts.map(() => \"?\").join(\", \");\n const rows = this.db\n .prepare(\n `SELECT absolute_path FROM conversation_files\n WHERE status != 'deleted' AND account IN (${placeholders})`,\n )\n .all(...accounts) as { absolute_path: string }[];\n return rows.map((r) => r.absolute_path);\n }\n\n // Active files whose immediate parent is exactly parentDir (no nested\n // subdirectories). Backs the dir-mtime gate's reuse path: a project dir with\n // an unchanged mtime and no nested files can skip the glob entirely.\n activePathsByParentDir(parentDir: string): { absolute_path: string; account: string }[] {\n return this.db\n .prepare(\n \"SELECT absolute_path, account FROM conversation_files WHERE parent_dir = ? AND status != 'deleted'\",\n )\n .all(canonicalPath(parentDir)) as { absolute_path: string; account: string }[];\n }\n}\n","import type { Database } from \"better-sqlite3\";\nimport { canonicalPath } from \"../../canonical-path\";\nimport { CLAUDE_CODE_PROVIDER } from \"../../providers/provider\";\nimport type { ConversationMeta, MessageSender, ProviderName } from \"../../types\";\n\nexport interface ConversationRow {\n id: number;\n file_id: number;\n source_path: string;\n provider: string;\n kind: string | null;\n external_session_id: string | null;\n session_id: string;\n session_name: string | null;\n project_path: string | null;\n project_name: string | null;\n account: string;\n branch: string | null;\n preview: string | null;\n content_snippet: string | null;\n message_count: number;\n page_message_count: number;\n last_message_sender: string;\n timestamp: string | null;\n first_sent_at: string | null;\n first_sent_text: string | null;\n last_sent_at: string | null;\n last_sent_text: string | null;\n model: string | null;\n is_subagent: number;\n parent_session_id: string | null;\n is_teammate: number;\n team_name: string | null;\n tool_names_json: string | null;\n last_prompt: string | null;\n status: string;\n}\n\n// Row -> ConversationMeta. Must reproduce the exact in-memory shape so the\n// persistent and legacy paths return identical ScanResult/SearchResult objects.\nexport function rowToMeta(row: ConversationRow): ConversationMeta {\n return {\n id: row.source_path,\n filePath: row.source_path,\n provider: (row.provider as ProviderName) ?? CLAUDE_CODE_PROVIDER,\n kind: (row.kind as ConversationMeta[\"kind\"]) ?? undefined,\n externalSessionId: row.external_session_id ?? undefined,\n sessionId: row.session_id,\n sessionName: row.session_name ?? \"\",\n projectPath: row.project_path ?? \"\",\n projectName: row.project_name ?? \"\",\n account: row.account,\n timestamp: row.timestamp ?? \"\",\n messageCount: row.message_count,\n lastMessageSender: row.last_message_sender as MessageSender,\n preview: row.preview ?? \"\",\n contentSnippet: row.content_snippet ?? \"\",\n gitBranch: row.branch,\n model: row.model,\n isSubagent: row.is_subagent === 1,\n parentSessionId: row.parent_session_id,\n isTeammate: row.is_teammate === 1,\n teamName: row.team_name,\n toolNames: row.tool_names_json ? (JSON.parse(row.tool_names_json) as string[]) : [],\n firstMessage: row.first_sent_text\n ? { text: row.first_sent_text, timestamp: row.first_sent_at ?? \"\" }\n : null,\n lastMessage: row.last_sent_text\n ? { text: row.last_sent_text, timestamp: row.last_sent_at ?? \"\" }\n : null,\n lastPrompt: row.last_prompt ?? undefined,\n };\n}\n\nexport class ConversationsRepo {\n constructor(private db: Database) {}\n\n // Upsert by file_id (1 file = 1 conversation). Keyed on the unique file_id so\n // a re-index overwrites the prior summary in place.\n upsert(fileId: number, meta: ConversationMeta, pageMessageCount = meta.messageCount): void {\n this.db\n .prepare(\n `INSERT INTO conversations (\n file_id, source_path, provider, kind, external_session_id,\n session_id, session_name, project_path, project_name,\n account, branch, preview, content_snippet, message_count, page_message_count,\n last_message_sender,\n timestamp, index_seq, first_sent_at, first_sent_text, last_sent_at, last_sent_text,\n model, is_subagent, parent_session_id, is_teammate, team_name, tool_names_json,\n last_prompt, status, updated_at\n ) VALUES (\n @file_id, @source_path, @provider, @kind, @external_session_id,\n @session_id, @session_name, @project_path, @project_name,\n @account, @branch, @preview, @content_snippet, @message_count, @page_message_count,\n @last_message_sender,\n @timestamp,\n (SELECT COALESCE(MAX(index_seq), 0) + 1 FROM conversations),\n @first_sent_at, @first_sent_text, @last_sent_at, @last_sent_text,\n @model, @is_subagent, @parent_session_id, @is_teammate, @team_name, @tool_names_json,\n @last_prompt, 'active', CURRENT_TIMESTAMP\n )\n ON CONFLICT(file_id) DO UPDATE SET\n source_path = excluded.source_path,\n provider = excluded.provider,\n kind = excluded.kind,\n external_session_id = excluded.external_session_id,\n session_id = excluded.session_id,\n session_name = excluded.session_name,\n project_path = excluded.project_path,\n project_name = excluded.project_name,\n account = excluded.account,\n branch = excluded.branch,\n preview = excluded.preview,\n content_snippet = excluded.content_snippet,\n message_count = excluded.message_count,\n page_message_count = excluded.page_message_count,\n last_message_sender = excluded.last_message_sender,\n timestamp = excluded.timestamp,\n first_sent_at = excluded.first_sent_at,\n first_sent_text = excluded.first_sent_text,\n last_sent_at = excluded.last_sent_at,\n last_sent_text = excluded.last_sent_text,\n model = excluded.model,\n is_subagent = excluded.is_subagent,\n parent_session_id = excluded.parent_session_id,\n is_teammate = excluded.is_teammate,\n team_name = excluded.team_name,\n tool_names_json = excluded.tool_names_json,\n last_prompt = excluded.last_prompt,\n status = 'active',\n index_seq = (SELECT COALESCE(MAX(index_seq), 0) + 1 FROM conversations),\n updated_at = CURRENT_TIMESTAMP`,\n )\n .run({\n file_id: fileId,\n // Canonical form, matching conversation_files.absolute_path — otherwise\n // a native-separator caller writes a second spelling of the same file\n // that every by-path lookup (and the deletion-reconcile) then misses.\n source_path: canonicalPath(meta.id),\n provider: meta.provider ?? CLAUDE_CODE_PROVIDER,\n kind: meta.kind ?? null,\n external_session_id: meta.externalSessionId ?? null,\n session_id: meta.sessionId,\n session_name: meta.sessionName || null,\n project_path: meta.projectPath || null,\n project_name: meta.projectName || null,\n account: meta.account,\n branch: meta.gitBranch,\n preview: meta.preview || null,\n content_snippet: meta.contentSnippet || null,\n message_count: meta.messageCount,\n page_message_count: pageMessageCount,\n last_message_sender: meta.lastMessageSender,\n timestamp: meta.timestamp || null,\n first_sent_at: meta.firstMessage?.timestamp ?? null,\n first_sent_text: meta.firstMessage?.text ?? null,\n last_sent_at: meta.lastMessage?.timestamp ?? null,\n last_sent_text: meta.lastMessage?.text ?? null,\n model: meta.model,\n is_subagent: meta.isSubagent ? 1 : 0,\n parent_session_id: meta.parentSessionId,\n is_teammate: meta.isTeammate ? 1 : 0,\n team_name: meta.teamName,\n tool_names_json: JSON.stringify(meta.toolNames),\n last_prompt: meta.lastPrompt ?? null,\n });\n }\n\n getBySourcePath(sourcePath: string): ConversationMeta | null {\n const row = this.db\n .prepare(\"SELECT * FROM conversations WHERE source_path = ? AND status = 'active'\")\n .get(canonicalPath(sourcePath)) as ConversationRow | undefined;\n return row ? rowToMeta(row) : null;\n }\n\n // Dual lookup matching scanner.getConversation: resolve by source_path (the\n // canonical id) first, then by session_id.\n //\n // session_id is NOT unique (see schema header) — the sessionId form is a\n // compatibility convenience. Resolution is deterministic and matches the\n // in-memory scanner: among active rows sharing the session_id, newest\n // timestamp wins, index_seq breaks sub-second ties (precision updated_at\n // lacks), then source_path ascending. Collision-safe callers should use\n // getAllBySessionId() instead.\n getByIdOrSession(id: string): ConversationMeta | null {\n const direct = this.getBySourcePath(id);\n if (direct) return direct;\n const row = this.db\n .prepare(\n `SELECT * FROM conversations WHERE session_id = ? AND status = 'active'\n ORDER BY COALESCE(timestamp, '') DESC, index_seq DESC, source_path ASC LIMIT 1`,\n )\n .get(id) as ConversationRow | undefined;\n return row ? rowToMeta(row) : null;\n }\n\n // All active conversations sharing a session_id, newest first (same ordering\n // as getByIdOrSession). Collision-safe counterpart to the convenience\n // getByIdOrSession() sessionId lookup.\n getAllBySessionId(sessionId: string): ConversationMeta[] {\n const rows = this.db\n .prepare(\n `SELECT * FROM conversations WHERE session_id = ? AND status = 'active'\n ORDER BY COALESCE(timestamp, '') DESC, index_seq DESC, source_path ASC`,\n )\n .all(sessionId) as ConversationRow[];\n return rows.map(rowToMeta);\n }\n\n // All active metas (unsorted/unfiltered) — callers apply the existing\n // filters/view transforms. Used by scan() before SQL filtering is wired in.\n allActive(): ConversationMeta[] {\n const rows = this.db\n .prepare(\"SELECT * FROM conversations WHERE status = 'active'\")\n .all() as ConversationRow[];\n return rows.map(rowToMeta);\n }\n\n // Most recent active conversations, newest first. Backs the empty-query\n // search path (mirrors the in-memory indexer's getRecent).\n recent(limit: number): ConversationMeta[] {\n const rows = this.db\n .prepare(\n `SELECT * FROM conversations WHERE status = 'active'\n ORDER BY COALESCE(timestamp, '') DESC, source_path ASC LIMIT ?`,\n )\n .all(limit) as ConversationRow[];\n return rows.map(rowToMeta);\n }\n\n distinctProjects(): string[] {\n const rows = this.db\n .prepare(\n `SELECT DISTINCT project_path FROM conversations\n WHERE status = 'active' AND project_path IS NOT NULL AND project_path != ''\n ORDER BY project_path ASC`,\n )\n .all() as { project_path: string }[];\n return rows.map((r) => r.project_path);\n }\n\n deleteByFileId(fileId: number): void {\n this.db\n .prepare(\n \"UPDATE conversations SET status = 'deleted', updated_at = CURRENT_TIMESTAMP WHERE file_id = ?\",\n )\n .run(fileId);\n }\n\n // The parseConversation message total for a file (for bounded paging), or 0\n // if not indexed.\n pageMessageCount(sourcePath: string): number {\n const row = this.db\n .prepare(\n \"SELECT page_message_count AS n FROM conversations WHERE source_path = ? AND status = 'active'\",\n )\n .get(canonicalPath(sourcePath)) as { n: number } | undefined;\n return row?.n ?? 0;\n }\n\n count(): number {\n return (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM conversations WHERE status = 'active'\").get() as {\n n: number;\n }\n ).n;\n }\n}\n","import type { Database } from \"better-sqlite3\";\nimport { canonicalPath } from \"../../canonical-path\";\nimport type { SearchDocument } from \"../../search-document\";\nimport {\n FTS_ELLIPSIS,\n FTS_HIT_CLOSE,\n FTS_HIT_OPEN,\n FTS_SNIPPET_TOKENS,\n parseFtsSnippet,\n} from \"../../search-matches\";\nimport type { ConversationMeta, SearchHighlight } from \"../../types\";\n\n// Body column positions in conversation_messages_fts, in snippet() priority\n// order. source_path is column 0.\nconst BODY_COLUMNS = [\n { name: \"text\", index: 1 },\n { name: \"thinking\", index: 2 },\n { name: \"tools\", index: 3 },\n] as const;\n\nexport interface FtsSearchFilters {\n account?: string;\n provider?: string;\n project?: string;\n since?: Date;\n include?: \"all\" | \"conversations\" | \"subagents\" | \"teammates\";\n}\n\nexport interface FtsHit {\n sourcePath: string;\n // Present when the match was in a body column. Null for a metadata-only hit,\n // which the caller resolves through generateMatches() instead.\n body: { snippet: string; highlights: SearchHighlight[] } | null;\n}\n\n// FTS5-backed search index over conversation body + metadata. One row per\n// conversation, addressed by rowid == conversation_files.id.\n//\n// Never look a row up by source_path: FTS5's planner handles only MATCH, rowid\n// and rank, so any other constraint linear-scans the table. That was tolerable\n// when a row held <=5 KB; with ~128 KB of body per row it would scan the whole\n// corpus on every append.\nexport class FtsRepo {\n constructor(private db: Database) {}\n\n upsert(rowId: number, meta: ConversationMeta, doc: SearchDocument): void {\n // Canonical form, matching conversations.source_path — a search hit is\n // resolved back through getBySourcePath(), so the two must agree.\n const sourcePath = canonicalPath(meta.id);\n const tx = this.db.transaction(() => {\n this.db.prepare(\"DELETE FROM conversation_messages_fts WHERE rowid = ?\").run(rowId);\n this.db\n .prepare(\n `INSERT INTO conversation_messages_fts\n (rowid, source_path, text, thinking, tools,\n project_name, session_id, session_name, account, model, branch, tool_names)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n rowId,\n sourcePath,\n doc.text,\n doc.thinking,\n doc.tools,\n meta.projectName ?? \"\",\n meta.sessionId ?? \"\",\n meta.sessionName ?? \"\",\n meta.account ?? \"\",\n meta.model ?? \"\",\n meta.gitBranch ?? \"\",\n meta.toolNames.join(\" \"),\n );\n });\n tx();\n }\n\n // Current durable buckets for a conversation, so an append can tail-extend\n // them without reparsing the file. Returns null when no row exists yet.\n readDocument(rowId: number): SearchDocument | null {\n const row = this.db\n .prepare(\"SELECT text, thinking, tools FROM conversation_messages_fts WHERE rowid = ?\")\n .get(rowId) as { text: string; thinking: string; tools: string } | undefined;\n if (!row) return null;\n return { text: row.text ?? \"\", thinking: row.thinking ?? \"\", tools: row.tools ?? \"\" };\n }\n\n // True when the stored row already equals what we would write, so an append\n // can skip re-tokenizing ~128 KB of body for nothing.\n //\n // The check covers the metadata columns too, not just the buckets: a\n // newly-seen tool name or a late-resolved session_name changes metadata while\n // the body is byte-identical, and nothing else ever rewrites this row — so\n // skipping on \"body unchanged\" alone would strand that stale value forever.\n isCurrent(rowId: number, meta: ConversationMeta, doc: SearchDocument): boolean {\n const row = this.db\n .prepare(\n `SELECT text, thinking, tools,\n project_name, session_id, session_name, account, model, branch, tool_names\n FROM conversation_messages_fts WHERE rowid = ?`,\n )\n .get(rowId) as Record<string, string> | undefined;\n if (!row) return false;\n\n return (\n row.text === doc.text &&\n row.thinking === doc.thinking &&\n row.tools === doc.tools &&\n row.project_name === (meta.projectName ?? \"\") &&\n row.session_id === (meta.sessionId ?? \"\") &&\n row.session_name === (meta.sessionName ?? \"\") &&\n row.account === (meta.account ?? \"\") &&\n row.model === (meta.model ?? \"\") &&\n row.branch === (meta.gitBranch ?? \"\") &&\n row.tool_names === meta.toolNames.join(\" \")\n );\n }\n\n remove(rowId: number): void {\n this.db.prepare(\"DELETE FROM conversation_messages_fts WHERE rowid = ?\").run(rowId);\n }\n\n // Ranked hits matching the query, best first. Filters run in SQL BEFORE LIMIT:\n // with a wide corpus a popular term matches far more conversations than one\n // page, so post-filtering an already-truncated list would report \"no results\"\n // for queries that do have them.\n search(query: string, limit: number, filters: FtsSearchFilters = {}): FtsHit[] {\n const match = toMatchQuery(query);\n if (!match) return [];\n\n const snippetSelects = BODY_COLUMNS.map(\n (c) => `snippet(conversation_messages_fts, ${c.index}, ?, ?, ?, ?) AS snippet_${c.name}`,\n ).join(\", \");\n\n const params: unknown[] = [];\n for (const _col of BODY_COLUMNS) {\n params.push(FTS_HIT_OPEN, FTS_HIT_CLOSE, FTS_ELLIPSIS, FTS_SNIPPET_TOKENS);\n }\n params.push(match);\n\n const where: string[] = [\"conversation_messages_fts MATCH ?\", \"c.status = 'active'\"];\n\n if (filters.account) {\n where.push(\"c.account = ?\");\n params.push(filters.account);\n }\n if (filters.provider) {\n where.push(\"c.provider = ?\");\n params.push(filters.provider);\n }\n if (filters.project) {\n where.push(\"(lower(c.project_path) LIKE ? OR lower(c.project_name) LIKE ?)\");\n const like = `%${filters.project.toLowerCase()}%`;\n params.push(like, like);\n }\n if (filters.since) {\n where.push(\"c.timestamp >= ?\");\n params.push(filters.since.toISOString());\n }\n if (filters.include === \"conversations\") {\n where.push(\"c.is_subagent = 0 AND c.is_teammate = 0\");\n } else if (filters.include === \"subagents\") {\n where.push(\"c.is_subagent = 1\");\n } else if (filters.include === \"teammates\") {\n where.push(\"c.is_teammate = 1\");\n }\n\n params.push(limit);\n\n const rows = this.db\n .prepare(\n `SELECT conversation_messages_fts.source_path AS source_path, ${snippetSelects}\n FROM conversation_messages_fts\n JOIN conversations c ON c.source_path = conversation_messages_fts.source_path\n WHERE ${where.join(\" AND \")}\n ORDER BY rank\n LIMIT ?`,\n )\n .all(...params) as Record<string, string>[];\n\n return rows.map((row) => ({ sourcePath: row.source_path, body: pickBodySnippet(row) }));\n }\n\n count(): number {\n return (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM conversation_messages_fts\").get() as { n: number }\n ).n;\n }\n}\n\n// snippet() returns a column's head even when the match was elsewhere, so the\n// hit markers are the only signal that THIS column matched. Take the first\n// marked column in priority order: text, then thinking, then tools.\nfunction pickBodySnippet(row: Record<string, string>) {\n for (const col of BODY_COLUMNS) {\n const parsed = parseFtsSnippet(row[`snippet_${col.name}`]);\n if (parsed) return parsed;\n }\n return null;\n}\n\n// Turn free-text into a safe FTS5 prefix query. Each whitespace-separated term\n// is wrapped in double quotes (so FTS5 special chars are treated literally) and\n// suffixed with * for prefix matching, mirroring FlexSearch's forward tokenizer.\n// Terms are ANDed together. Returns \"\" when nothing usable remains.\nfunction toMatchQuery(query: string): string {\n const terms = query\n .trim()\n .split(/\\s+/)\n .map((t) => t.replace(/\"/g, \"\").trim())\n .filter(Boolean);\n if (terms.length === 0) return \"\";\n return terms.map((t) => `\"${t}\"*`).join(\" AND \");\n}\n","import type { Database } from \"better-sqlite3\";\nimport { canonicalPath } from \"../../canonical-path\";\n\nexport interface ScannedDirRow {\n path: string;\n parent_root: string | null;\n mtime_ms: number;\n has_nested: number;\n}\n\n// Per-directory mtime watermarks backing the discovery dir-mtime gate. One row\n// for a profile's projectsDir (parent_root NULL), one row per immediate\n// project subdirectory (parent_root = the projectsDir path).\nexport class ScannedDirsRepo {\n constructor(private db: Database) {}\n\n get(path: string): ScannedDirRow | undefined {\n return this.db.prepare(\"SELECT * FROM scanned_dirs WHERE path = ?\").get(canonicalPath(path)) as\n | ScannedDirRow\n | undefined;\n }\n\n // Known project subdirectories under a root, path ascending (stable order).\n childrenOf(parentRoot: string): ScannedDirRow[] {\n return this.db\n .prepare(\"SELECT * FROM scanned_dirs WHERE parent_root = ? ORDER BY path ASC\")\n .all(canonicalPath(parentRoot)) as ScannedDirRow[];\n }\n\n upsert(path: string, parentRoot: string | null, mtimeMs: number, hasNested: boolean): void {\n this.db\n .prepare(\n `INSERT INTO scanned_dirs (path, parent_root, mtime_ms, has_nested, updated_at)\n VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT(path) DO UPDATE SET\n parent_root = excluded.parent_root,\n mtime_ms = excluded.mtime_ms,\n has_nested = excluded.has_nested,\n updated_at = CURRENT_TIMESTAMP`,\n )\n .run(\n canonicalPath(path),\n parentRoot === null ? null : canonicalPath(parentRoot),\n mtimeMs,\n hasNested ? 1 : 0,\n );\n }\n\n // Drop a project subdirectory's watermark row (it vanished from disk).\n remove(path: string): void {\n this.db.prepare(\"DELETE FROM scanned_dirs WHERE path = ?\").run(canonicalPath(path));\n }\n}\n","import type { FSWatcher } from \"chokidar\";\nimport chokidar from \"chokidar\";\nimport { getLogger } from \"../logger\";\nimport { getProjectsDir } from \"../profiles\";\nimport type { Profile } from \"../types\";\n\nconst EXCLUDED_SEGMENTS = [\"/memory/\", \"/tool-results/\"];\n\nexport interface WatchEvent {\n filePath: string;\n account: string;\n type: \"add\" | \"change\" | \"unlink\";\n}\n\nexport interface FileWatcherOptions {\n // Debounce window (ms) coalescing rapid events for one file. Spec §8.\n debounceMs?: number;\n}\n\n// Watches each active profile's projects dir for *.jsonl changes and forwards\n// debounced add/change/unlink events. Thin wrapper over chokidar; the scanner\n// owns what to do with the events (enqueue index jobs, emit to host apps).\nexport class FileWatcher {\n private watchers: FSWatcher[] = [];\n private timers = new Map<string, ReturnType<typeof setTimeout>>();\n private readonly debounceMs: number;\n\n constructor(\n private profiles: Profile[],\n private onEvent: (event: WatchEvent) => void,\n options: FileWatcherOptions = {},\n ) {\n this.debounceMs = options.debounceMs ?? 400;\n }\n\n // Resolves once every underlying chokidar watcher has finished its initial\n // scan, so the caller knows subsequent FS changes will be observed.\n async start(): Promise<void> {\n const log = getLogger();\n const ready: Promise<void>[] = [];\n for (const profile of this.profiles) {\n const dir = getProjectsDir(profile);\n const watcher = chokidar.watch(dir, {\n ignoreInitial: true,\n ignored: (p: string) => EXCLUDED_SEGMENTS.some((seg) => p.includes(seg)),\n awaitWriteFinish: { stabilityThreshold: this.debounceMs, pollInterval: 100 },\n });\n watcher\n .on(\"add\", (p) => this.dispatch(p, profile.id, \"add\"))\n .on(\"change\", (p) => this.dispatch(p, profile.id, \"change\"))\n .on(\"unlink\", (p) => this.dispatch(p, profile.id, \"unlink\"));\n ready.push(new Promise<void>((resolve) => watcher.once(\"ready\", () => resolve())));\n this.watchers.push(watcher);\n log.debug({ dir, account: profile.id }, \"watcher: watching\");\n }\n await Promise.all(ready);\n }\n\n async stop(): Promise<void> {\n for (const t of this.timers.values()) clearTimeout(t);\n this.timers.clear();\n await Promise.all(this.watchers.map((w) => w.close()));\n this.watchers = [];\n }\n\n private dispatch(filePath: string, account: string, type: WatchEvent[\"type\"]): void {\n if (!filePath.endsWith(\".jsonl\")) return;\n // Debounce per path: collapse a flurry of events into the latest one.\n const existing = this.timers.get(filePath);\n if (existing) clearTimeout(existing);\n this.timers.set(\n filePath,\n setTimeout(() => {\n this.timers.delete(filePath);\n this.onEvent({ filePath, account, type });\n }, this.debounceMs),\n );\n }\n}\n","import { getLogger } from \"../logger\";\n\nexport type JobReason = \"watcher\" | \"manual\" | \"periodic\";\n\nexport interface IndexJob {\n filePath: string;\n account: string;\n reason: JobReason;\n}\n\n// In-process job queue for incremental indexing. Jobs are deduplicated by file\n// path (the latest job for a path wins) and processed one at a time, because\n// SQLite has a single writer. A burst of filesystem events for the same file\n// collapses to one index pass.\nexport class IndexQueue {\n private pending = new Map<string, IndexJob>();\n private running = false;\n private idleResolvers: Array<() => void> = [];\n\n constructor(private process: (job: IndexJob) => Promise<void>) {}\n\n enqueue(job: IndexJob): void {\n this.pending.set(job.filePath, job);\n void this.drain();\n }\n\n // Resolves when the queue has fully drained — useful for tests and for a\n // clean shutdown.\n onIdle(): Promise<void> {\n if (!this.running && this.pending.size === 0) return Promise.resolve();\n return new Promise((resolve) => this.idleResolvers.push(resolve));\n }\n\n get size(): number {\n return this.pending.size;\n }\n\n private async drain(): Promise<void> {\n if (this.running) return;\n this.running = true;\n const log = getLogger();\n\n while (this.pending.size > 0) {\n const [path, job] = this.pending.entries().next().value as [string, IndexJob];\n this.pending.delete(path);\n try {\n await this.process(job);\n } catch (err) {\n log.warn({ filePath: job.filePath, err }, \"index-queue: job failed\");\n }\n }\n\n this.running = false;\n const resolvers = this.idleResolvers;\n this.idleResolvers = [];\n for (const r of resolvers) r();\n }\n}\n","// ─── Primitives ─────────────────────────────────────────────────────\n\nexport type MessageSender = \"user\" | \"assistant\";\nexport type Include = \"all\" | \"conversations\" | \"subagents\" | \"teammates\";\nexport type View = \"flat\" | \"tree\" | \"grouped\";\nexport type SortOrder = \"recent\" | \"oldest\" | \"messages-desc\" | \"messages-asc\" | \"alpha\";\n\nexport const VALID_SORT_ORDERS: SortOrder[] = [\n \"recent\",\n \"oldest\",\n \"messages-desc\",\n \"messages-asc\",\n \"alpha\",\n];\n\n// ─── Profile ────────────────────────────────────────────────────────\n\nexport interface Profile {\n id: string;\n label: string;\n configDir: string;\n enabled: boolean;\n emoji?: string;\n scanHistory?: boolean;\n}\n\n// ─── Content Tiers ──────────────────────────────────────────────────\n\nexport interface ContentTier {\n name: string;\n previewMax: number;\n snippetMax: number;\n}\n\n// ─── ConversationMeta (full superset) ───────────────────────────────\n\nexport interface MessageSnapshot {\n text: string;\n timestamp: string;\n}\n\nexport type ProviderName = \"claude-code\" | \"codex-cli\";\n\nexport interface ConversationMeta {\n id: string;\n filePath: string;\n sessionId: string;\n // Which provider produced this meta. Optional/additive: existing claude-code\n // metas default to \"claude-code\" when unset.\n provider?: ProviderName;\n // Distinguishes plain chat conversations from task-oriented logs (Codex).\n kind?: \"conversation\" | \"task\";\n // Provider-native session id when it differs from the (non-unique) sessionId.\n externalSessionId?: string;\n sessionName: string;\n projectPath: string;\n projectName: string;\n account: string;\n timestamp: string;\n messageCount: number;\n lastMessageSender: MessageSender;\n preview: string;\n contentSnippet: string;\n gitBranch: string | null;\n model: string | null;\n isSubagent: boolean;\n parentSessionId: string | null;\n isTeammate: boolean;\n teamName: string | null;\n toolNames: string[];\n firstMessage: MessageSnapshot | null;\n lastMessage: MessageSnapshot | null;\n lastPrompt?: string;\n}\n\n// ─── View Variants ──────────────────────────────────────────────────\n\nexport interface TreeConversation extends ConversationMeta {\n subagents: ConversationMeta[];\n}\n\nexport interface GroupedConversations {\n [groupKey: string]: ConversationMeta[];\n}\n\n// ─── Options ────────────────────────────────────────────────────────\n\nexport interface FileStatEntry {\n mtimeMs: number;\n size: number;\n}\n\nexport interface ScanOptions {\n profiles?: Profile[];\n // Providers to scan. Defaults to [\"claude-code\"]. Including \"codex-cli\"\n // requires codexRoots (no default home scan).\n providers?: ProviderName[];\n // Absolute roots to discover Codex CLI history under (e.g. ~/.codex/sessions).\n codexRoots?: string[];\n tier?: string;\n tiers?: Record<string, ContentTier>;\n include?: Include;\n view?: View;\n sort?: SortOrder;\n since?: string;\n project?: string;\n account?: string;\n limit?: number;\n offset?: number;\n onProgress?: (scanned: number, total: number) => void;\n onBatch?: (metas: ConversationMeta[]) => void;\n /** Known file stats from a previous scan. Files whose (mtimeMs, size) match\n * are skipped — the cached ConversationMeta is reused instead. */\n statCache?: Map<string, { stat: FileStatEntry; meta: ConversationMeta }>;\n /** Persistent mode only: bypass the dir-mtime discovery gate and force a\n * full glob of every profile's projectsDir, ignoring stored watermarks.\n * Use for an explicit user-triggered refresh that shouldn't trust the\n * gate's optimization. */\n fullRescan?: boolean;\n}\n\nexport interface ScanResult {\n conversations: ConversationMeta[] | TreeConversation[] | GroupedConversations;\n total: number;\n scanned: number;\n}\n\nexport interface SearchOptions extends ScanOptions {\n fields?: string[];\n // Restrict results to one provider.\n provider?: ProviderName;\n}\n\n// Half-open [start, end) character range into a SearchMatch.snippet marking one\n// matched term, so a client can emphasise it without parsing markup. Ranges\n// follow FTS token boundaries, not the query string: a prefix search for\n// \"timeout\" highlights the whole token \"timeoutMs\".\nexport interface SearchHighlight {\n start: number;\n end: number;\n}\n\nexport interface SearchMatch {\n field: string;\n snippet: string;\n // Present on body hits (field === \"content\"), where the excerpt comes from\n // FTS5 snippet(). Absent on metadata hits from generateMatches().\n highlights?: SearchHighlight[];\n}\n\nexport interface SearchResult {\n meta: ConversationMeta;\n score: number;\n matches: SearchMatch[];\n}\n\nexport interface GetConversationOptions {\n profiles?: Profile[];\n}\n\nexport interface GetConversationPageOptions {\n beforeIndex?: number;\n limit: number;\n}\n\nexport interface ConversationPage {\n messages: ConversationMessage[];\n total: number;\n fromIndex: number;\n}\n\n// A page sliced directly from a single parsed JSONL file, carrying the parsed\n// Conversation alongside the window so a caller can build a full response\n// (meta + messages) from one parse, without a prior scan().\nexport interface SingleFilePage extends ConversationPage {\n conversation: Conversation;\n}\n\n// ─── Full Conversation ──────────────────────────────────────────────\n\nexport interface TurnDuration {\n durationMs: number;\n messageCount: number;\n uuid?: string;\n}\n\nexport interface DeferredToolsDeltaAttachment {\n type: \"deferred_tools_delta\";\n addedNames: string[];\n addedLines: unknown[];\n removedNames: string[];\n}\n\nexport type AttachmentSidecar =\n | DeferredToolsDeltaAttachment\n | { type: string; [key: string]: unknown };\n\nexport interface ToolUseBlock {\n id: string;\n name: string;\n input: Record<string, unknown>;\n}\n\nexport interface ToolResultBlock {\n toolUseId: string;\n type:\n | \"edit\"\n | \"write\"\n | \"read\"\n | \"bash\"\n | \"grep\"\n | \"glob\"\n | \"taskAgent\"\n | \"taskCreate\"\n | \"taskUpdate\"\n | \"generic\";\n content: Record<string, unknown>;\n isError?: boolean;\n}\n\nexport interface TeamInfo {\n teammateId: string;\n summary?: string;\n color?: string;\n}\n\nexport interface MessageMetadata {\n model?: string;\n stopReason?: string | null;\n inputTokens?: number;\n outputTokens?: number;\n cacheReadTokens?: number;\n cacheCreationTokens?: number;\n gitBranch?: string;\n version?: string;\n toolUses?: string[];\n toolUseBlocks?: ToolUseBlock[];\n toolResults?: ToolResultBlock[];\n teamName?: string;\n teamInfo?: TeamInfo;\n}\n\nexport interface ConversationMessage {\n role: MessageSender;\n text: string;\n timestamp: string;\n uuid?: string;\n metadata?: MessageMetadata;\n isToolResult?: boolean;\n isThinking?: boolean;\n thinkingContent?: string;\n thinkingSignature?: string;\n parentUuid?: string | null;\n requestId?: string;\n promptId?: string;\n isSidechain?: boolean;\n permissionMode?: string;\n hasImages?: boolean;\n attachment?: AttachmentSidecar;\n}\n\nexport interface Conversation {\n id: string;\n filePath: string;\n projectPath: string;\n projectName: string;\n sessionId: string;\n sessionName: string;\n messages: ConversationMessage[];\n fullText: string;\n timestamp: string;\n messageCount: number;\n account: string;\n turnDurations?: TurnDuration[];\n lastPrompt?: string;\n}\n","export {\n applyAccountFilter,\n applyIncludeFilter,\n applyPagination,\n applyProjectFilter,\n applySinceFilter,\n applySort,\n} from \"./filters\";\nexport { readGitBranch } from \"./git\";\nexport { SearchIndexer } from \"./indexer\";\nexport type { Logger, LoggerOptions } from \"./logger\";\nexport { createLogger, getLogger, setLogger } from \"./logger\";\nexport type { ConvReducerState as JsonlParseState } from \"./persistent/conversation-reducer\";\nexport {\n initialConvState as createJsonlParseState,\n parseJsonlLine,\n} from \"./persistent/conversation-reducer\";\nexport type { Sidecar } from \"./persistent/sidecar\";\nexport { readSidecar, sidecarPath } from \"./persistent/sidecar\";\nexport {\n detectDefaultProfile,\n getProjectsDir,\n loadProfiles,\n resolveConfigDir,\n saveProfiles,\n} from \"./profiles\";\nexport { CodexCliProvider } from \"./providers/codex-cli\";\nexport type {\n DiscoveredConversationFile,\n ScannerProvider,\n ScannerProviderName,\n} from \"./providers/provider\";\nexport { ThreadbaseProvider } from \"./providers/threadbase\";\nexport type {\n ConversationScannerOptions,\n PersistentConfig,\n ScannerChangeEvent,\n WatchOptions,\n} from \"./scanner\";\nexport { ConversationScanner } from \"./scanner\";\nexport { cleanSystemTags } from \"./tags\";\nexport { DEFAULT_TIERS, resolveTier } from \"./tiers\";\nexport * from \"./types\";\n\n// ─── Standalone Convenience Functions ───────────────────────────────\n\nimport { ConversationScanner } from \"./scanner\";\nimport type {\n Conversation,\n GetConversationOptions,\n ScanOptions,\n ScanResult,\n SearchOptions,\n SearchResult,\n} from \"./types\";\n\nlet defaultScanner: ConversationScanner | undefined;\n\nfunction getDefaultScanner(): ConversationScanner {\n if (!defaultScanner) {\n defaultScanner = new ConversationScanner();\n }\n return defaultScanner;\n}\n\nexport function resetDefaultScanner(): void {\n defaultScanner = undefined;\n}\n\nexport async function scan(\n options?: ScanOptions,\n scanner?: ConversationScanner,\n): Promise<ScanResult> {\n return (scanner ?? getDefaultScanner()).scan(options);\n}\n\nexport async function search(\n query: string,\n options?: SearchOptions,\n scanner?: ConversationScanner,\n): Promise<SearchResult[]> {\n return (scanner ?? getDefaultScanner()).search(query, options);\n}\n\nexport async function getConversation(\n id: string,\n options?: GetConversationOptions,\n scanner?: ConversationScanner,\n): Promise<Conversation | null> {\n return (scanner ?? getDefaultScanner()).getConversation(id, options);\n}\n"],"mappings":"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","names":["basename","dirname","join","dirname","join","getShortProjectName","basename","readFileSync","readFileSync","join","join","createReadStream","basename","createInterface","basename","getShortProjectName","createInterface","createReadStream","fg","stat","fg","stat","closeSync","openSync","readSync","statSync","homedir","join","basename","createReadStream","yieldToEventLoop","createReadStream","createReadStream","createReadStream","yieldToEventLoop","getShortProjectName","basename","stat","statSync","createReadStream","createInterface","createInterface","createReadStream","extractCodexText","dirname","dirname","stat","dirname","join","stat","dirname","join","basename","dirname","dirname","basename","stat","meta","total","beforeIndex","fromIndex","statSync","EXCLUDED_SEGMENTS","process","BATCH_SIZE","join","homedir","statSync","previous","resolvedAccount","provider","meta","openSync","readSync","closeSync"]}