@tekmidian/pai 0.65.2 → 0.66.1
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/{chain-CT3SI-3z.mjs → chain-CZfKIEq9.mjs} +4 -4
- package/dist/{chain-CT3SI-3z.mjs.map → chain-CZfKIEq9.mjs.map} +1 -1
- package/dist/cli/index.mjs +9 -9
- package/dist/cli/program.mjs +9 -9
- package/dist/{context-handover-cache-CRSFdkNi.mjs → context-handover-cache-pkHzmL3c.mjs} +3 -3
- package/dist/{context-handover-cache-CRSFdkNi.mjs.map → context-handover-cache-pkHzmL3c.mjs.map} +1 -1
- package/dist/daemon/index.mjs +7 -7
- package/dist/{daemon-SaPnmoyg.mjs → daemon-DXlaVCkw.mjs} +9 -9
- package/dist/{daemon-SaPnmoyg.mjs.map → daemon-DXlaVCkw.mjs.map} +1 -1
- package/dist/{daemon-DoTVK9z6.mjs → daemon-DyORmxZu.mjs} +7 -7
- package/dist/daemon-mcp/index.mjs +8 -6
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{fallback-vHjFZP0Y.mjs → fallback-CIiq2Raw.mjs} +18 -8
- package/dist/fallback-CIiq2Raw.mjs.map +1 -0
- package/dist/hooks/block-sleep-poll.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs +16 -0
- package/dist/hooks/load-project-context.mjs.map +2 -2
- package/dist/hooks/route-agents-to-worker.mjs +16 -0
- package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
- package/dist/hooks/security-validator.mjs +16 -0
- package/dist/hooks/security-validator.mjs.map +2 -2
- package/dist/hooks/worker-guard.mjs +15 -0
- package/dist/hooks/worker-guard.mjs.map +3 -3
- package/dist/hooks/worker-proxy.mjs +16 -0
- package/dist/hooks/worker-proxy.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +17 -0
- package/dist/hooks/worker-status-line.mjs.map +2 -2
- package/dist/hooks/worker-supervision.mjs +16 -0
- package/dist/hooks/worker-supervision.mjs.map +2 -2
- package/dist/{main-resolver-VSM5ytnA.mjs → main-resolver-BKz_OuEh.mjs} +2 -2
- package/dist/{main-resolver-CKNGux1B.mjs → main-resolver-DPPwHNtn.mjs} +2 -2
- package/dist/{main-resolver-CKNGux1B.mjs.map → main-resolver-DPPwHNtn.mjs.map} +1 -1
- package/dist/{planner-Dzs9-8AS.mjs → planner-Dm5Acyo1.mjs} +5 -5
- package/dist/{planner-Dzs9-8AS.mjs.map → planner-Dm5Acyo1.mjs.map} +1 -1
- package/dist/{program-DK0mRRlf.mjs → program-JJ0TwSHI.mjs} +59 -16
- package/dist/program-JJ0TwSHI.mjs.map +1 -0
- package/dist/{run-DMaCpvMW.mjs → run-BCpbxqA5.mjs} +490 -26
- package/dist/run-BCpbxqA5.mjs.map +1 -0
- package/dist/{run-env-BHWnXqle.mjs → run-env-DRcp7A8K.mjs} +16 -2
- package/dist/run-env-DRcp7A8K.mjs.map +1 -0
- package/dist/{server-DlL1QI2k.mjs → server-DgmAHyFK.mjs} +2 -2
- package/dist/{server-DlL1QI2k.mjs.map → server-DgmAHyFK.mjs.map} +1 -1
- package/dist/{session-keepalive-DVZ9-7qg.mjs → session-keepalive-DxjTHGcK.mjs} +4 -4
- package/dist/{session-keepalive-DVZ9-7qg.mjs.map → session-keepalive-DxjTHGcK.mjs.map} +1 -1
- package/dist/{work-queue-worker-DZiEMvBQ.mjs → work-queue-worker-4tLa5gpQ.mjs} +2 -2
- package/dist/{work-queue-worker-DZiEMvBQ.mjs.map → work-queue-worker-4tLa5gpQ.mjs.map} +1 -1
- package/dist/{work-queue-worker-D7GHuj2I.mjs → work-queue-worker-rFZvTO3p.mjs} +4 -4
- package/docs/commands/README.md +1 -0
- package/docs/commands/worker.md +30 -0
- package/package.json +1 -1
- package/src/hooks/ts/lib/worker-guard.test.ts +12 -0
- package/src/hooks/ts/lib/worker-guard.ts +13 -0
- package/dist/fallback-vHjFZP0Y.mjs.map +0 -1
- package/dist/program-DK0mRRlf.mjs.map +0 -1
- package/dist/run-DMaCpvMW.mjs.map +0 -1
- package/dist/run-env-BHWnXqle.mjs.map +0 -1
package/dist/{context-handover-cache-CRSFdkNi.mjs.map → context-handover-cache-pkHzmL3c.mjs.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-handover-cache-CRSFdkNi.mjs","names":["CLAUDE_PROJECTS_DIR","resolve","homedir","existsSync","readFileSync","join","join","findNotesDir","basename","existsSync","readdirSync","join","existsSync","readdirSync","basename","readFileSync","existsSync","join","basename","readFileSync","findClaudeBinary","extractAndStoreTriples","findNotesDir","kgExtractAndStoreTriples"],"sources":["../src/registry/moved.ts","../src/cli/commands/registry/utils.ts","../src/cli/commands/registry/scan.ts","../src/hooks/ts/lib/pai-paths.ts","../src/hooks/ts/lib/project-utils/paths.ts","../src/hooks/ts/lib/project-utils/session-notes.ts","../src/session/checkpoint-block.ts","../src/hooks/ts/lib/project-utils/todo.ts","../src/daemon/templates/session-summary-prompt.ts","../src/daemon/templates/triple-extraction-prompt.ts","../src/workers/daemon-llm.ts","../src/memory/kg-extraction.ts","../src/daemon/session-summary-worker.ts","../src/daemon/work-queue.ts","../src/hooks/ts/lib/context-handover-cache.ts"],"sourcesContent":["/**\n * moved.ts — reconnect a project to transcripts that moved out from under it.\n *\n * The registry records an `encoded_dir`: the name Claude Code gave the folder\n * holding a project's transcripts. It is written once, when the project is\n * added, and nothing updates it when the project moves. So a project that has\n * been relocated points at a directory that is empty or gone, and every lookup\n * — checkpoints, handovers, session digests — quietly returns nothing.\n *\n * `resolveTranscriptDir` already re-derives the name from the project's current\n * root path, which fixes the case where the encoding is stale but the path is\n * right. It cannot fix the case where the ENCODING RULE itself does not\n * reproduce the folder name: iCloud paths with `~` in them, emoji segments, or\n * a folder Claude Code created under a path the project no longer has.\n *\n * This module answers that case by asking the transcripts instead of guessing.\n * Every transcript entry records the `cwd` it was written in, so the mapping\n * from a project root to its transcript folder is a fact sitting on disk rather\n * than something to be inferred from a naming convention. Reading it is slower\n * than a string transform and it is right, which is the correct trade for a\n * repair that runs on demand.\n */\n\nimport { existsSync, readdirSync, statSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n/** Where Claude Code keeps per-project transcript folders. */\nexport function claudeProjectsDir(): string {\n return join(homedir(), \".claude\", \"projects\");\n}\n\n/**\n * Every transcript file for one project folder — live and archived.\n *\n * The archived half matters: `session-stop` moves all but the newest transcript\n * into `sessions/`, so a folder whose work is finished has an empty top level\n * and everything underneath. Counting only the top level reports a busy project\n * as unused, which is how an earlier audit of this same problem overstated the\n * breakage by a factor of three.\n */\nexport function transcriptFiles(projectDir: string): string[] {\n const out: string[] = [];\n for (const dir of [projectDir, join(projectDir, \"sessions\")]) {\n if (!existsSync(dir)) continue;\n try {\n for (const entry of readdirSync(dir)) {\n if (entry.endsWith(\".jsonl\")) out.push(join(dir, entry));\n }\n } catch {\n /* unreadable — treat as empty rather than failing the scan */\n }\n }\n return out;\n}\n\n/**\n * The working directory a transcript was recorded in.\n *\n * Reads only the head of the file. `cwd` is present on the first entry and does\n * not change within a session, so scanning further would cost time to learn\n * nothing. Returns null rather than throwing: a truncated or half-written\n * transcript is normal for a session that is still running.\n */\nexport function cwdOfTranscript(file: string, maxBytes = 64 * 1024): string | null {\n let head: string;\n try {\n const buf = readFileSync(file);\n head = buf.subarray(0, maxBytes).toString(\"utf-8\");\n } catch {\n return null;\n }\n\n for (const line of head.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const cwd = (JSON.parse(line) as { cwd?: string }).cwd;\n if (cwd) return cwd;\n } catch {\n // A partial final line is expected when the head is cut mid-entry.\n continue;\n }\n }\n return null;\n}\n\nexport interface TranscriptFolder {\n /** Folder name under ~/.claude/projects. */\n name: string;\n /** Number of transcripts, live plus archived. */\n count: number;\n /** Newest transcript mtime, for choosing between candidates. */\n newest: number;\n /**\n * How many sampled transcripts in this folder began in the cwd being looked\n * up, and how many began somewhere else.\n *\n * One folder routinely holds transcripts for more than one directory: a\n * session started in a subdirectory can land in the parent's folder. So a\n * project appearing in a folder is not evidence the folder is ITS folder.\n * Measured 2026-08-02 — `apps/youdrill` held 5 transcripts for itself and 3\n * for `apps/youdrill/app`, and reconnecting `app` there would have attached a\n * subdirectory to its parent's history.\n */\n matching: number;\n total: number;\n}\n\n/**\n * Map every working directory seen on disk to the folders that hold its\n * transcripts.\n *\n * A directory can legitimately map to more than one folder — Claude Code's\n * encoding is lossy, so two different paths can collide, and a project moved\n * and moved back leaves both. Callers pick; this reports.\n */\nexport function scanTranscriptFolders(\n projectsDir = claudeProjectsDir(),\n sampleFilesPerFolder = 8\n): Map<string, TranscriptFolder[]> {\n const byCwd = new Map<string, TranscriptFolder[]>();\n if (!existsSync(projectsDir)) return byCwd;\n\n let entries: string[];\n try {\n entries = readdirSync(projectsDir);\n } catch {\n return byCwd;\n }\n\n for (const name of entries) {\n const dir = join(projectsDir, name);\n try {\n if (!statSync(dir).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const files = transcriptFiles(dir);\n if (files.length === 0) continue;\n\n // Newest first: a moved project's most recent transcripts carry its current\n // path, and older ones may still carry the old one.\n const mtimes = new Map<string, number>();\n for (const f of files) {\n try {\n mtimes.set(f, statSync(f).mtimeMs);\n } catch {\n mtimes.set(f, 0);\n }\n }\n const ordered = files.sort((a, b) => (mtimes.get(b) ?? 0) - (mtimes.get(a) ?? 0));\n const newest = mtimes.get(ordered[0]) ?? 0;\n\n // Count how many sampled transcripts began in each cwd, so a caller can\n // tell \"this folder is that project's\" from \"that project appears in this\n // folder\" — which are very different claims.\n const tally = new Map<string, number>();\n let sampled = 0;\n for (const f of ordered.slice(0, sampleFilesPerFolder)) {\n const cwd = cwdOfTranscript(f);\n if (!cwd) continue;\n sampled++;\n tally.set(cwd, (tally.get(cwd) ?? 0) + 1);\n }\n\n for (const [cwd, matching] of tally) {\n const record: TranscriptFolder = {\n name,\n count: files.length,\n newest,\n matching,\n total: sampled,\n };\n const list = byCwd.get(cwd);\n if (list) list.push(record);\n else byCwd.set(cwd, [record]);\n }\n }\n\n return byCwd;\n}\n\nexport interface MovedProject {\n id: number;\n slug: string;\n rootPath: string;\n /** What the registry currently claims. Null when it has never been set. */\n storedDir: string | null;\n /** The folder that actually holds this project's transcripts. */\n correctDir: string;\n transcripts: number;\n /** Sessions the registry believes exist — how much is currently unreachable. */\n sessions: number;\n}\n\nexport interface RegistryProjectRow {\n id: number;\n slug: string;\n root_path: string;\n encoded_dir: string | null;\n sessions: number;\n}\n\n/**\n * Which projects point at the wrong transcript folder, and where they belong.\n *\n * A project is only reported when its stored folder yields nothing AND its root\n * path is recorded as a `cwd` somewhere else. Both halves matter: without the\n * first this would rewrite entries that work, and without the second it would\n * have nothing better to offer than the value already there.\n *\n * `resolvesNow` is supplied by the caller so this module does not have to\n * duplicate the resolver's fallback logic — a project the shipped resolver\n * already handles is not broken and must not be \"repaired\".\n */\nexport function findMovedProjects(\n rows: RegistryProjectRow[],\n byCwd: Map<string, TranscriptFolder[]>,\n resolvesNow: (row: RegistryProjectRow) => boolean\n): MovedProject[] {\n const out: MovedProject[] = [];\n\n for (const row of rows) {\n if (resolvesNow(row)) continue;\n\n const candidates = byCwd.get(row.root_path);\n if (!candidates || candidates.length === 0) continue;\n\n // Only a folder this project DOMINATES. A folder where most transcripts\n // began somewhere else belongs to that somewhere else, and attaching a\n // subdirectory to its parent's folder would hand it a history that is\n // mostly not its own — then the no-session-id fallback, which takes the\n // newest transcript in the folder, would read a sibling's work as this\n // project's. A missed repair costs nothing; a wrong one is silent and\n // wrong in the direction of confidently reporting someone else's data.\n const owned = candidates.filter((c) => c.matching * 2 > c.total);\n if (owned.length === 0) continue;\n\n // Most transcripts wins, newest breaks a tie: the folder with the most\n // history is the one worth reconnecting to, and recency separates a live\n // folder from an abandoned duplicate.\n const best = [...owned].sort((a, b) => b.count - a.count || b.newest - a.newest)[0];\n\n if (best.name === row.encoded_dir) continue;\n\n out.push({\n id: row.id,\n slug: row.slug,\n rootPath: row.root_path,\n storedDir: row.encoded_dir,\n correctDir: best.name,\n transcripts: best.count,\n sessions: row.sessions,\n });\n }\n\n // Most sessions first: those are the projects where the breakage costs most.\n return out.sort((a, b) => b.sessions - a.sessions || b.transcripts - a.transcripts);\n}\n","/** Shared registry helpers for registry command operations. */\n\nimport type { RegistryBackend } from \"../../../storage/registry-interface.js\";\nimport { now } from \"../../utils.js\";\nimport { basename, join } from \"node:path\";\nimport { transcriptFiles, claudeProjectsDir } from \"../../../registry/moved.js\";\n\n/**\n * Would writing this encoded_dir replace a working one with a broken one?\n *\n * `encodeDir` is lossy: `~` and emoji do not survive it, so a value derived\n * from a project's path can name a folder Claude Code never created. Every\n * caller here derives its value that way, and this function is the single\n * point they all pass through.\n *\n * `pai registry reconnect` repairs those by reading the cwd recorded inside the\n * transcripts. Without this check the next scan silently reverts the repair —\n * and it reverts it in the most confusing possible way, because a read taken\n * straight after the repair still shows it. Observed twice on 2026-08-02:\n * three projects reconnected, verified, and rediscovered as broken within the\n * hour, with the repair command reporting success both times.\n *\n * Permits the write when the new value resolves to transcripts, or when the\n * existing one resolves to nothing — i.e. whenever it cannot make things worse.\n */\nasync function worthWriting(backend: RegistryBackend, projectId: number, encodedDir: string): Promise<boolean> {\n if (transcriptFiles(join(claudeProjectsDir(), encodedDir)).length > 0) return true;\n\n const project = await backend.getProjectById(projectId);\n const current = project?.encoded_dir;\n if (!current) return true;\n\n return transcriptFiles(join(claudeProjectsDir(), current)).length === 0;\n}\n\n/**\n * Upsert a project row. Returns { id, isNew }.\n *\n * Matching priority:\n * 1. root_path — most reliable; handles slug collisions\n * 2. encoded_dir — Claude project dirs are canonical\n * 3. Insert with suffix-deduplication on slug collision\n *\n * display_name is set to basename(rootPath) on INSERT so that the unified\n * listing always shows a human-readable name rather than the kebab-case slug.\n */\nexport async function upsertProject(\n backend: RegistryBackend,\n slug: string,\n rootPath: string,\n encodedDir: string\n): Promise<{ id: number; isNew: boolean }> {\n const ts = now();\n\n const byPath = await backend.getProjectByRootPath(rootPath);\n\n if (byPath) {\n const encodedOwner = await backend.getProjectByEncodedDir(encodedDir);\n\n if (\n (!encodedOwner || encodedOwner.id === byPath.id) &&\n (await worthWriting(backend, byPath.id, encodedDir))\n ) {\n await backend.updateProjectPath(byPath.id, { encodedDir }, ts);\n }\n return { id: byPath.id, isNew: false };\n }\n\n const byEncoded = await backend.getProjectByEncodedDir(encodedDir);\n\n if (byEncoded) {\n const pathOwner = await backend.getProjectByRootPath(rootPath);\n\n if (!pathOwner || pathOwner.id === byEncoded.id) {\n await backend.updateProjectPath(byEncoded.id, { rootPath }, ts);\n }\n return { id: byEncoded.id, isNew: false };\n }\n\n // Use basename(rootPath) as the human display name. If rootPath is empty or\n // just \"/\" fall back to the slug so we always have something non-empty.\n const displayName = basename(rootPath) || slug;\n\n const result = await backend.createProjectWithSlugRetry({\n baseSlug: slug,\n displayName,\n rootPath,\n encodedDir,\n createdAt: ts,\n updatedAt: ts,\n });\n\n return { id: result.id, isNew: result.created };\n}\n\n/** Upsert a session note. Returns true if newly inserted. */\nexport async function upsertSession(\n backend: RegistryBackend,\n projectId: number,\n number: number,\n date: string,\n slug: string,\n title: string,\n filename: string\n): Promise<boolean> {\n return backend.upsertSessionIfAbsent({\n projectId,\n number,\n date,\n slug,\n title,\n filename,\n createdAt: now(),\n });\n}\n","/** Registry scan command: walk ~/.claude/projects/ and populate the registry. */\n\nimport { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { realpathSync } from \"node:fs\";\nimport { join, basename, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { ok, warn, err, dim, bold } from \"../../utils.js\";\nimport { encodeDir } from \"../../utils.js\";\nimport { decodeEncodedDir, slugify, parseSessionFilename, buildEncodedDirMap } from \"../../../registry/migrate.js\";\nimport { ensurePaiMarker, discoverPaiMarkers } from \"../../../registry/pai-marker.js\";\nimport { transcriptFiles, claudeProjectsDir } from \"../../../registry/moved.js\";\nimport { upsertProject, upsertSession } from \"./utils.js\";\nimport { paiHomePath, resolvePaiFile, migratePaiFile, type MigrateFileResult } from \"../../../config/pai-home.js\";\nimport { getRegistryBackend } from \"../../../storage/factory.js\";\nimport type { RegistryBackend } from \"../../../storage/registry-interface.js\";\n\n// ---------------------------------------------------------------------------\n// clc session.json fallback map\n// ---------------------------------------------------------------------------\n\n/**\n * Build a reverse map from encoded-dir → real path using the clc session\n * registry (~/.claude/session.json).\n *\n * The clc registry stores { sessions: [{ directory, ... }, ...] }. For each\n * entry with a resolvable directory, we encode the realpath and add it to\n * the map. This lets the scanner recover project paths that are not in\n * Claude's session-registry.json but were registered by clc.\n */\nfunction buildClcDirMap(): Map<string, string> {\n const map = new Map<string, string>();\n const sessionFile = join(homedir(), \".claude\", \"session.json\");\n if (!existsSync(sessionFile)) return map;\n\n try {\n const raw = readFileSync(sessionFile, \"utf8\");\n const parsed = JSON.parse(raw) as { sessions?: Array<{ directory?: string }> };\n for (const entry of parsed.sessions ?? []) {\n const dir = entry.directory;\n if (!dir) continue;\n try {\n const real = realpathSync(dir);\n if (!existsSync(real)) continue;\n const encoded = encodeDir(real);\n map.set(encoded, real);\n } catch {\n // Unresolvable path — skip\n }\n }\n } catch {\n // Unparseable — return empty map\n }\n return map;\n}\n\n// ---------------------------------------------------------------------------\n// Config helpers\n// ---------------------------------------------------------------------------\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\n/** Old registry-scan config path: ~/.pai/config.json (pre-2026-09-19). Named\n * \"config.json\" there, but that name is taken at PAI_HOME by the daemon\n * config, so the new location is registry-scan.json instead. */\nfunction oldScanConfigFile(): string {\n return join(homedir(), \".pai\", \"config.json\");\n}\n\nfunction scanConfigFilePath(): string {\n return resolvePaiFile(paiHomePath(\"registry-scan.json\"), [oldScanConfigFile()], \"pai config migrate --registry-scan\");\n}\n\nexport function migrateScanConfig(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"registry-scan.json\"), [oldScanConfigFile()], opts);\n}\n\ninterface PaiConfig {\n scan_dirs: string[];\n}\n\nexport function loadScanConfig(): PaiConfig {\n const file = scanConfigFilePath();\n if (!existsSync(file)) return { scan_dirs: [] };\n try {\n return JSON.parse(readFileSync(file, \"utf8\")) as PaiConfig;\n } catch {\n return { scan_dirs: [] };\n }\n}\n\nexport function saveScanConfig(config: PaiConfig): void {\n const file = paiHomePath(\"registry-scan.json\");\n mkdirSync(paiHomePath(), { recursive: true });\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\", \"utf8\");\n}\n\n/**\n * Resolve a path to its canonical form, falling back to the input.\n *\n * Every path that becomes a `projects.root_path` must go through this. That\n * column is UNIQUE, so two spellings of one directory create two projects and\n * split session history between them. The spellings are not hypothetical: a\n * configured scan_dir of `~/dev/ai` (where `~/dev` is a symlink) makes every\n * project under it register under the symlinked path, while a session started\n * from the resolved path registers a rival row.\n */\nexport function canonicalPath(p: string): string {\n try {\n return realpathSync(p);\n } catch {\n return p;\n }\n}\n\nexport function resolveHome(p: string): string {\n if (p.startsWith(\"~/\")) return join(homedir(), p.slice(2));\n return resolve(p);\n}\n\n// ---------------------------------------------------------------------------\n// File discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively find all .md files in a directory, including YYYY/MM subdirectories.\n * Returns filenames (basename only).\n */\nexport function findNoteFiles(dir: string): string[] {\n const results: string[] = [];\n if (!existsSync(dir)) return results;\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isFile() && entry.name.endsWith(\".md\")) {\n results.push(entry.name);\n } else if (entry.isDirectory() && /^\\d{4}$/.test(entry.name)) {\n const yearDir = join(dir, entry.name);\n for (const monthEntry of readdirSync(yearDir, { withFileTypes: true })) {\n if (monthEntry.isDirectory() && /^\\d{2}$/.test(monthEntry.name)) {\n const monthDir = join(yearDir, monthEntry.name);\n for (const noteEntry of readdirSync(monthDir, { withFileTypes: true })) {\n if (noteEntry.isFile() && noteEntry.name.endsWith(\".md\")) {\n results.push(noteEntry.name);\n }\n }\n }\n }\n }\n }\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Scan result type\n// ---------------------------------------------------------------------------\n\nexport interface ScanResult {\n projectsScanned: number;\n projectsNew: number;\n projectsUpdated: number;\n sessionsScanned: number;\n sessionsNew: number;\n skipped: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Core scan logic\n// ---------------------------------------------------------------------------\n\nexport async function performScan(): Promise<ScanResult> {\n const backend = await getRegistryBackend();\n const result: ScanResult = {\n projectsScanned: 0,\n projectsNew: 0,\n projectsUpdated: 0,\n sessionsScanned: 0,\n sessionsNew: 0,\n skipped: [],\n };\n\n if (!existsSync(CLAUDE_PROJECTS_DIR)) {\n throw new Error(`Claude projects directory not found: ${CLAUDE_PROJECTS_DIR}`);\n }\n\n const entries = readdirSync(CLAUDE_PROJECTS_DIR).filter((name) => {\n const full = join(CLAUDE_PROJECTS_DIR, name);\n return statSync(full).isDirectory();\n });\n\n const lookupMap = buildEncodedDirMap();\n const clcMap = buildClcDirMap();\n\n for (const encodedDir of entries) {\n let rootPath = decodeEncodedDir(encodedDir, lookupMap);\n\n // If the decoded path doesn't exist, try the clc session.json fallback map.\n // The clc map is keyed by encodeDir(realpathSync(directory)), so paths that\n // were registered by clc but not in session-registry.json can be recovered.\n if (!existsSync(rootPath) && clcMap.has(encodedDir)) {\n const clcPath = clcMap.get(encodedDir)!;\n if (existsSync(clcPath)) {\n rootPath = clcPath;\n }\n }\n\n if (!existsSync(rootPath)) {\n result.skipped.push(`${encodedDir} (decoded: ${rootPath} — path not found on disk)`);\n result.projectsScanned++;\n continue;\n }\n\n // Canonicalize before upserting.\n //\n // ~/.claude/projects/ holds one encoded entry per path *spelling*, so a\n // directory reachable through a symlinked prefix (~/dev -> Cloud/Development)\n // appears twice. Decoding each to its literal path registers two projects\n // for one directory, and session history then splits between them —\n // whichever spelling the shell used gets the session.\n //\n // upsertProject looks up by root_path first, so resolving here makes the\n // second spelling land on the row the first one created instead of\n // creating a rival. Both encoded_dir values stay valid for locating\n // central notes; only the project identity is unified.\n rootPath = canonicalPath(rootPath);\n\n const slug = slugify(basename(rootPath) || encodedDir);\n const { id, isNew } = await upsertProject(backend, slug, rootPath, encodedDir);\n\n result.projectsScanned++;\n if (isNew) result.projectsNew++;\n else result.projectsUpdated++;\n\n try {\n ensurePaiMarker(rootPath, slug);\n } catch {\n // Non-fatal\n }\n\n const claudeNotesDir = join(CLAUDE_PROJECTS_DIR, encodedDir, \"Notes\");\n\n if (existsSync(claudeNotesDir)) {\n const rootNotesDir = join(rootPath, \"Notes\");\n if (claudeNotesDir !== rootNotesDir) {\n await backend.updateProjectClaudeNotesDir(id, claudeNotesDir, Date.now());\n }\n }\n\n if (!existsSync(claudeNotesDir)) continue;\n\n const noteFiles = findNoteFiles(claudeNotesDir);\n\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n result.sessionsScanned++;\n const isNewSession = await upsertSession(backend, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename);\n if (isNewSession) result.sessionsNew++;\n }\n }\n\n // Phase 2: Scan project-root Notes/ for all registered active projects\n {\n const activeProjects = await backend.listProjects({ status: \"active\" });\n\n for (const project of activeProjects) {\n const notesDir = join(project.root_path, \"Notes\");\n if (!existsSync(notesDir)) continue;\n\n let files: string[];\n try {\n files = findNoteFiles(notesDir);\n } catch {\n continue;\n }\n\n for (const filename of files) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n result.sessionsScanned++;\n const isNewSession = await upsertSession(backend, project.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename);\n if (isNewSession) result.sessionsNew++;\n }\n }\n }\n\n // Phase 3: Scan extra directories from config\n const config = loadScanConfig();\n if (config.scan_dirs.length) {\n for (const rawDir of config.scan_dirs) {\n const scanDir = resolveHome(rawDir);\n if (!existsSync(scanDir)) {\n result.skipped.push(`${rawDir} (configured scan_dir not found)`);\n continue;\n }\n\n const children = readdirSync(scanDir).filter((name) => {\n if (name.startsWith(\".\")) return false;\n const full = join(scanDir, name);\n try { return statSync(full).isDirectory(); } catch { return false; }\n });\n\n for (const child of children) {\n // Canonicalize: a scan_dir of \"~/dev/ai\" (symlinked prefix) would\n // otherwise register every project under the symlinked spelling and\n // duplicate anything already registered under the resolved one.\n const childPath = canonicalPath(join(scanDir, child));\n const childSlug = slugify(child);\n const childEncoded = encodeDir(childPath);\n\n const existing = await backend.getProjectByRootPath(childPath);\n\n if (existing) {\n result.projectsScanned++;\n result.projectsUpdated++;\n\n try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n const notesDir = join(childPath, \"Notes\");\n if (existsSync(notesDir)) {\n const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n result.sessionsScanned++;\n if (await upsertSession(backend, existing.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n result.sessionsNew++;\n }\n }\n }\n continue;\n }\n\n const { id, isNew } = await upsertProject(backend, childSlug, childPath, childEncoded);\n result.projectsScanned++;\n if (isNew) result.projectsNew++;\n else result.projectsUpdated++;\n\n try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n const notesDir = join(childPath, \"Notes\");\n if (existsSync(notesDir)) {\n const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n result.sessionsScanned++;\n if (await upsertSession(backend, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n result.sessionsNew++;\n }\n }\n }\n }\n }\n }\n\n // Phase 4: Discover PAI.md markers in scan_dirs\n if (config.scan_dirs.length) {\n const resolvedScanDirs = config.scan_dirs.map(resolveHome).filter(existsSync);\n const markers = discoverPaiMarkers(resolvedScanDirs);\n\n for (const marker of markers) {\n const registeredRow = await backend.getProjectBySlug(marker.slug);\n\n if (!registeredRow) continue;\n\n // The marker was found by walking a scan_dir, so its projectRoot carries\n // whatever spelling that dir used. Canonicalize before comparing —\n // otherwise this \"repair\" step rewrites a correct root_path back to the\n // symlinked form on every scan, undoing any deduplication.\n const markerRoot = canonicalPath(marker.projectRoot);\n\n if (registeredRow.root_path !== markerRoot) {\n const newEncoded = encodeDir(markerRoot);\n const now4 = Date.now();\n\n const encodedOwner = await backend.getProjectByEncodedDir(newEncoded);\n const pathOwner = await backend.getProjectByRootPath(markerRoot);\n\n const encodedSafe = !encodedOwner || encodedOwner.id === registeredRow.id;\n const pathSafe = !pathOwner || pathOwner.id === registeredRow.id;\n\n // Never trade a working encoded_dir for one that resolves to nothing.\n //\n // encodeDir is lossy — `~` and emoji do not survive it — so re-deriving\n // from the path can produce a folder name Claude Code never created.\n // `pai registry reconnect` repairs exactly those by reading the cwd out\n // of the transcripts, and this scan runs every 30 minutes from the\n // daemon: without this check it silently reverts that repair, and the\n // repair looks like it worked because a fresh read right afterwards\n // still shows it. Observed on 2026-08-02 — three projects reconnected,\n // reverted, and rediscovered as broken within the hour.\n const derivedResolves = transcriptFiles(join(claudeProjectsDir(), newEncoded)).length > 0;\n const currentResolves =\n Boolean(registeredRow.encoded_dir) &&\n transcriptFiles(join(claudeProjectsDir(), registeredRow.encoded_dir!)).length > 0;\n const encodedWorthWriting = derivedResolves || !currentResolves;\n\n if (encodedSafe && pathSafe && encodedWorthWriting) {\n await backend.updateProjectPath(registeredRow.id, { rootPath: markerRoot, encodedDir: newEncoded }, now4);\n } else if (pathSafe) {\n await backend.updateProjectPath(registeredRow.id, { rootPath: markerRoot }, now4);\n }\n }\n }\n }\n\n // Phase 5: Backfill display_name for rows where it still equals the slug.\n // This converts legacy entries created before the basename-based naming was\n // introduced: \"jobs-beta\" → \"Jobs Beta\" (from basename of root_path).\n {\n const all = await backend.listProjects();\n const stale = all.filter((p) => p.display_name === p.slug && p.root_path);\n\n for (const row of stale) {\n const name = basename(row.root_path);\n if (name && name !== row.slug) {\n await backend.updateProjectDisplayName(row.id, name, Date.now());\n }\n }\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// cmdScan\n// ---------------------------------------------------------------------------\n\n/**\n * Run the registry scan CLI command.\n *\n * @param opts.quick When true, skips verbose output (same scan, less noise).\n * The underlying scan is always incremental via upsert —\n * this flag exists for hook/daemon-triggered invocations that\n * want minimal log output.\n */\nexport async function cmdScan(opts: { quick?: boolean } = {}): Promise<void> {\n const config = loadScanConfig();\n if (!opts.quick) {\n console.log(dim(\"Scanning ~/.claude/projects/ ...\"));\n if (config.scan_dirs.length) {\n console.log(dim(`Scanning ${config.scan_dirs.length} extra dir(s): ${config.scan_dirs.join(\", \")}`));\n }\n console.log(dim(\"Scanning project-root Notes/ directories ...\"));\n }\n\n let result: ScanResult;\n try {\n result = await performScan();\n } catch (e) {\n console.error(err(String(e)));\n process.exitCode = 1;\n return;\n }\n\n if (!opts.quick) {\n console.log(\n ok(`Scanned ${bold(String(result.projectsScanned))} projects, ${bold(String(result.sessionsScanned))} session notes.`)\n );\n console.log(dim(` Projects: ${result.projectsNew} new, ${result.projectsUpdated} updated`));\n console.log(dim(` Sessions: ${result.sessionsNew} new`));\n\n if (result.skipped.length) {\n console.log();\n console.log(warn(` ${result.skipped.length} project(s) skipped (path not found on disk):`));\n for (const s of result.skipped.slice(0, 10)) {\n console.log(dim(` ${s}`));\n }\n if (result.skipped.length > 10) {\n console.log(dim(` ... and ${result.skipped.length - 10} more`));\n }\n }\n } else {\n // Quick mode: single compact line (stderr so it doesn't pollute pipe output)\n process.stderr.write(\n `[registry-scan] ${result.projectsScanned} projects, ${result.sessionsScanned} sessions ` +\n `(${result.projectsNew}+${result.sessionsNew} new).\\n`\n );\n }\n}\n","/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n *\n * Two different things live here, and they must not be confused:\n *\n * - ADAPTER_DIR (~/.claude by default) is the Claude Code harness adapter —\n * fixed, hardcoded paths the harness itself loads from (Hooks/, skills/,\n * agents/, commands/, settings.json, statusline-command.sh,\n * tab-color-command.sh). It is harness-specific: a future harness would\n * need its own adapter directory with its own conventions.\n * - PAI_HOME (~/.claude/pai by default — see ../../../config/pai-home.ts) is\n * where PAI's own state lives: everything hooks WRITE (History/,\n * agent-sessions.json, session-routing.json, ...) resolves there, with a\n * fallback to the pre-2026-09-19 ADAPTER_DIR location and a one-time\n * stderr notice, exactly like every other PAI_HOME file.\n *\n * ALSO loads .env file from ADAPTER_DIR so all hooks get environment\n * variables without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { ADAPTER_DIR, HOOKS_DIR, SKILLS_DIR, historyDir } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\nimport {\n paiHomePath,\n resolvePaiFile,\n migratePaiFile,\n migratePaiDir,\n type MigrateFileResult,\n type MigrateDirResult,\n} from '../../../config/pai-home.js';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE ADAPTER_DIR resolution so .env can set ADAPTER_DIR/PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.ADAPTER_DIR || process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\nfunction defaultAdapterDir(): string {\n return resolve(homedir(), '.claude');\n}\n\n/**\n * Never print when stdout/stderr is a data channel, not a human: hook\n * bundles and worker-status-line.mjs set PAI_QUIET_NOTICES=1 via their\n * esbuild banner (scripts/build-hooks.mjs), and a `pai worker run\n * --output-format json` invocation is detected directly off argv since its\n * env can't be set before this module's static imports resolve.\n */\nfunction suppressDeprecationNotices(): boolean {\n if (process.env.PAI_QUIET_NOTICES === '1') return true;\n const idx = process.argv.indexOf('--output-format');\n return idx !== -1 && process.argv[idx + 1] === 'json';\n}\n\n/**\n * At most once per process — a `globalThis` flag rather than a module-level\n * `let` because hooks and the CLI can end up with more than one instance of\n * this module loaded into the same process (separate bundles), each with its\n * own module scope; only a property on the shared global survives that.\n */\nfunction warnPaiDirDeprecatedOnce(message: string): void {\n const g = globalThis as typeof globalThis & { __paiDirNoticePrinted?: boolean };\n if (g.__paiDirNoticePrinted || suppressDeprecationNotices()) return;\n g.__paiDirNoticePrinted = true;\n process.stderr.write(`pai: ${message}\\n`);\n}\n\n/**\n * Smart ADAPTER_DIR detection with fallback\n * Priority:\n * 1. ADAPTER_DIR environment variable (if set) — warns once if PAI_DIR is\n * ALSO set and resolves to a different path (naming both).\n * 2. PAI_DIR environment variable (deprecated alias, one release) — warns\n * once only when it differs from the default adapter root; PAI_DIR set to\n * the same value as the default is the operator's current, correct\n * setting and stays silent.\n * 3. ~/.claude (standard location)\n */\nfunction resolveAdapterDir(): string {\n if (process.env.ADAPTER_DIR) {\n const adapterDir = resolve(process.env.ADAPTER_DIR);\n if (process.env.PAI_DIR) {\n const paiDir = resolve(process.env.PAI_DIR);\n if (paiDir !== adapterDir) {\n warnPaiDirDeprecatedOnce(\n `ADAPTER_DIR (${adapterDir}) and PAI_DIR (${paiDir}) are both set and differ — ADAPTER_DIR wins. PAI_DIR still works for one release.`\n );\n }\n }\n return adapterDir;\n }\n if (process.env.PAI_DIR) {\n const paiDir = resolve(process.env.PAI_DIR);\n if (paiDir !== defaultAdapterDir()) {\n warnPaiDirDeprecatedOnce(\n 'PAI_DIR is deprecated — use ADAPTER_DIR for the harness adapter root (~/.claude). PAI_DIR still works for one release.'\n );\n }\n return paiDir;\n }\n return defaultAdapterDir();\n}\n\n/** The Claude Code harness adapter root — NOT PAI's state home. See module doc above. */\nexport const ADAPTER_DIR = resolveAdapterDir();\n\n/** @deprecated alias for ADAPTER_DIR, kept for one release. Prefer ADAPTER_DIR. */\nexport const PAI_DIR = ADAPTER_DIR;\n\n/**\n * Adapter directories — fixed paths the Claude Code harness itself loads\n * from. These stay under ADAPTER_DIR; they are not PAI state.\n */\nexport const HOOKS_DIR = join(ADAPTER_DIR, 'Hooks');\nexport const SKILLS_DIR = join(ADAPTER_DIR, 'skills');\nexport const AGENTS_DIR = join(ADAPTER_DIR, 'agents');\nexport const COMMANDS_DIR = join(ADAPTER_DIR, 'commands');\n\n// ---------------------------------------------------------------------------\n// PAI state written by hooks — resolves under PAI_HOME, falling back to the\n// pre-2026-09-19 ADAPTER_DIR location (one-time stderr notice) until\n// `pai config migrate --history` moves it. Actively written on every hook\n// event across every session, so unlike most PAI_HOME files the live move is\n// deliberately NOT automatic — see `pai config migrate --history`.\n// ---------------------------------------------------------------------------\n\nfunction oldHistoryDir(): string {\n return join(ADAPTER_DIR, 'History');\n}\n\n/** Read/write location for hook-captured history: PAI_HOME/History if\n * present, else the old ADAPTER_DIR/History (one-time stderr notice). */\nexport function historyDir(): string {\n return resolvePaiFile(paiHomePath('History'), [oldHistoryDir()], 'pai config migrate --history');\n}\n\nexport function migrateHistoryDir(opts: { dryRun?: boolean } = {}): MigrateDirResult {\n return migratePaiDir(paiHomePath('History'), [oldHistoryDir()], opts);\n}\n\nfunction oldAgentSessionsPath(): string {\n return join(ADAPTER_DIR, 'agent-sessions.json');\n}\n\nexport function agentSessionsPath(): string {\n return resolvePaiFile(paiHomePath('agent-sessions.json'), [oldAgentSessionsPath()], 'pai config migrate --history');\n}\n\nexport function migrateAgentSessions(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath('agent-sessions.json'), [oldAgentSessionsPath()], opts);\n}\n\nfunction oldSecurityEventsPath(): string {\n return join(ADAPTER_DIR, 'history', 'security', 'security-events.jsonl');\n}\n\nexport function securityEventsPath(): string {\n return resolvePaiFile(\n paiHomePath('History', 'security', 'security-events.jsonl'),\n [oldSecurityEventsPath()],\n 'pai config migrate --history'\n );\n}\n\nexport function migrateSecurityEvents(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath('History', 'security', 'security-events.jsonl'), [oldSecurityEventsPath()], opts);\n}\n\nfunction oldSessionRoutingPath(): string {\n return join(ADAPTER_DIR, 'session-routing.json');\n}\n\nexport function sessionRoutingPath(): string {\n return resolvePaiFile(paiHomePath('session-routing.json'), [oldSessionRoutingPath()], 'pai config migrate --history');\n}\n\nexport function migrateSessionRouting(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath('session-routing.json'), [oldSessionRoutingPath()], opts);\n}\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(historyDir(), subdir, `${year}-${month}`, filename);\n}\n","/**\n * Path utilities — encoding, Notes/Sessions directory discovery and creation.\n */\n\nimport { existsSync, mkdirSync, readdirSync, linkSync, copyFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { ADAPTER_DIR, PAI_DIR } from '../pai-paths.js';\n\n// Re-export for consumers. PROJECTS_DIR is a harness-adjacent data dir, not\n// PAI_HOME state — out of scope for the PAI_DIR→PAI_HOME fold (see pai-paths.ts).\nexport { ADAPTER_DIR, PAI_DIR };\nexport const PROJECTS_DIR = join(ADAPTER_DIR, 'projects');\n\n/**\n * Directories known to be automated health-check / probe sessions.\n * Hooks should exit early for these to avoid registry clutter and wasted work.\n */\nconst PROBE_CWD_PATTERNS = [\n '/CodexBar/ClaudeProbe',\n '/ClaudeProbe',\n];\n\n/**\n * Check if the current working directory belongs to a probe/health-check session.\n * Returns true if hooks should skip this session entirely.\n */\nexport function isProbeSession(cwd?: string): boolean {\n const dir = cwd || process.cwd();\n return PROBE_CWD_PATTERNS.some(pattern => dir.includes(pattern));\n}\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with -\n * - Replace space with -\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-')\n .replace(/\\./g, '-')\n .replace(/ /g, '-');\n}\n\n/** Get the project directory for a given working directory. */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/** Get the Notes directory for a project (central location). */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory — checks local first, falls back to central.\n * Does NOT create the directory.\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n const localPaths = [\n join(cwd, 'Notes'),\n join(cwd, 'notes'),\n join(cwd, '.claude', 'Notes'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return { path, isLocal: true };\n }\n }\n\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/** Get the sessions/ directory for a project (stores .jsonl transcripts). */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/** Get the sessions/ directory from a project directory path. */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n// ---------------------------------------------------------------------------\n// Directory creation helpers\n// ---------------------------------------------------------------------------\n\n/** Ensure the Notes directory exists for a project. @deprecated Use ensureNotesDirSmart() */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\n return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists → use it (don't create anything new)\n * - If no local Notes/ → ensure central exists and use that\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n if (found.isLocal) return found;\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n return found;\n}\n\n/** Ensure the sessions/ directory exists for a project. */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/** Ensure the sessions/ directory exists (from project dir path). */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/**\n * Publish every project-root .jsonl into sessions/ as well, WITHOUT removing it.\n *\n * This used to renameSync, and that is how PAI destroyed its users' sessions.\n *\n * `claude --resume <uuid>` finds a transcript only at the project root. Move it\n * into sessions/ and the session becomes permanently unresumable — measured\n * 2026-08-04: `claude --resume b3462801` (867 KB of real work, sessions/ only)\n * answers \"No conversation found with session ID\", while a top-level id is found\n * fine. Nothing warned; the id still looked valid everywhere PAI displayed it.\n *\n * The damage was not occasional. This ran from a UserPromptSubmit hook excluding\n * only the CURRENT session, so every prompt anyone typed unresumed every other\n * session in the project. One PAI project measured 1 transcript at top level\n * against 52 underneath. Among the casualties was 046bb712 — the exact id PAI's\n * own handover tells the user to resume.\n *\n * A hardlink satisfies both sides, which is why this is a two-line fix rather\n * than a redesign: the archive genuinely has consumers that read sessions/\n * (session-summary-worker, registry/moved, session/autosave), and `--resume`\n * needs the root path. One inode, two names, no copy, no window where the file\n * is missing from either place.\n *\n * Never unlink the source. Tidying up another tool's store was the whole\n * mistake; a stale duplicate is free, a lost session is not. Every caller of\n * `transcriptFiles()` was checked before choosing this — they all test emptiness\n * (`.length > 0`), never count, so the duplicate cannot skew a project's stats.\n *\n * `excludeFile` keeps a hook from archiving the transcript it is itself watching\n * being written. It is not a \"finished sessions only\" guard and must not be read\n * as one: it excludes exactly one file, the caller's own, so with two sessions\n * live in one project each still archives the other mid-turn. A consumer that\n * needs \"finished only\" has to enforce that itself — this function cannot know\n * what is live.\n *\n * Returns the number of files newly archived.\n */\nexport function archiveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) return 0;\n\n const files = readdirSync(projectDir);\n let archivedCount = 0;\n\n for (const file of files) {\n if (!file.endsWith('.jsonl') || file === excludeFile) continue;\n\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n\n // Already archived — including by an earlier rename, before this was a\n // hardlink. Those are the sessions that need restoring to the root, which is\n // a separate repair and not this function's job.\n if (existsSync(destPath)) continue;\n\n try {\n linkSync(sourcePath, destPath);\n if (!silent) console.error(`Archived ${file} → sessions/ (still resumable)`);\n archivedCount++;\n } catch (error) {\n // Cross-device (EXDEV) is the realistic failure: sessions/ on another\n // volume. Copy instead, and still leave the original alone.\n try {\n copyFileSync(sourcePath, destPath);\n if (!silent) console.error(`Copied ${file} → sessions/ (hardlink unavailable)`);\n archivedCount++;\n } catch {\n if (!silent) console.error(`Could not archive ${file}: ${error}`);\n }\n }\n }\n\n return archivedCount;\n}\n\n/**\n * @deprecated Renamed to `archiveSessionFilesToSessionsDir`, which is what it\n * now does. Kept so an out-of-tree caller fails loudly at the type level rather\n * than silently keeping the old destructive name for a non-destructive action.\n */\nexport const moveSessionFilesToSessionsDir = archiveSessionFilesToSessionsDir;\n\n// ---------------------------------------------------------------------------\n// CLAUDE.md / TODO.md discovery\n// ---------------------------------------------------------------------------\n\n/** Find TODO.md — check local first, fallback to central. */\nexport function findTodoPath(cwd: string): string {\n const localPaths = [\n join(cwd, 'TODO.md'),\n join(cwd, 'notes', 'TODO.md'),\n join(cwd, 'Notes', 'TODO.md'),\n join(cwd, '.claude', 'TODO.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) return path;\n }\n\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/** Find CLAUDE.md — returns the FIRST found path. */\nexport function findClaudeMdPath(cwd: string): string | null {\n const paths = findAllClaudeMdPaths(cwd);\n return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations in priority order.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n const localPaths = [\n join(cwd, '.claude', 'CLAUDE.md'),\n join(cwd, 'CLAUDE.md'),\n join(cwd, 'Notes', 'CLAUDE.md'),\n join(cwd, 'notes', 'CLAUDE.md'),\n join(cwd, 'Prompts', 'CLAUDE.md'),\n join(cwd, 'prompts', 'CLAUDE.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) foundPaths.push(path);\n }\n\n return foundPaths;\n}\n","/**\n * Session note creation, editing, checkpointing, renaming, and finalization.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Get or create the YYYY/MM subdirectory for the current month inside notesDir. */\nfunction getMonthDir(notesDir: string): string {\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const monthDir = join(notesDir, year, month);\n if (!existsSync(monthDir)) {\n mkdirSync(monthDir, { recursive: true });\n }\n return monthDir;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Get the next note number (4-digit format: 0001, 0002, etc.).\n * Numbers are scoped per YYYY/MM directory.\n */\nexport function getNextNoteNumber(notesDir: string): string {\n const monthDir = getMonthDir(notesDir);\n\n const files = readdirSync(monthDir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-]/))\n .sort();\n\n if (files.length === 0) return '0001';\n\n let maxNumber = 0;\n for (const file of files) {\n const digitMatch = file.match(/^(\\d+)/);\n if (digitMatch) {\n const num = parseInt(digitMatch[1], 10);\n if (num > maxNumber) maxNumber = num;\n }\n }\n\n return String(maxNumber + 1).padStart(4, '0');\n}\n\n/**\n * Get the current (latest) note file path, or null if none exists.\n * Searches current month → previous month → flat notesDir (legacy).\n */\nexport function getCurrentNotePath(notesDir: string): string | null {\n if (!existsSync(notesDir)) return null;\n\n const findLatestIn = (dir: string): string | null => {\n if (!existsSync(dir)) return null;\n const files = readdirSync(dir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-].*\\.md$/))\n .sort((a, b) => {\n const numA = parseInt(a.match(/^(\\d+)/)?.[1] || '0', 10);\n const numB = parseInt(b.match(/^(\\d+)/)?.[1] || '0', 10);\n return numA - numB;\n });\n if (files.length === 0) return null;\n return join(dir, files[files.length - 1]);\n };\n\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const currentMonthDir = join(notesDir, year, month);\n const found = findLatestIn(currentMonthDir);\n if (found) return found;\n\n const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const prevYear = String(prevDate.getFullYear());\n const prevMonth = String(prevDate.getMonth() + 1).padStart(2, '0');\n const prevMonthDir = join(notesDir, prevYear, prevMonth);\n const prevFound = findLatestIn(prevMonthDir);\n if (prevFound) return prevFound;\n\n return findLatestIn(notesDir);\n}\n\n/**\n * Create a new session note.\n * Format: \"NNNN - YYYY-MM-DD - New Session.md\" filed into YYYY/MM subdirectory.\n * Claude MUST rename at session end with a meaningful description.\n */\nexport function createSessionNote(notesDir: string, description: string): string {\n const noteNumber = getNextNoteNumber(notesDir);\n const date = new Date().toISOString().split('T')[0];\n const monthDir = getMonthDir(notesDir);\n const filename = `${noteNumber} - ${date} - New Session.md`;\n const filepath = join(monthDir, filename);\n\n const content = `# Session ${noteNumber}: ${description}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n<!-- PAI will add completed work here during session -->\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n writeFileSync(filepath, content);\n console.error(`Created session note: ${filename}`);\n\n return filepath;\n}\n\n/** Append a checkpoint to the current session note. */\nexport function appendCheckpoint(notePath: string, checkpoint: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found, recreating: ${notePath}`);\n try {\n const parentDir = join(notePath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n const noteFilename = basename(notePath);\n const numberMatch = noteFilename.match(/^(\\d+)/);\n const noteNumber = numberMatch ? numberMatch[1] : '0000';\n const date = new Date().toISOString().split('T')[0];\n const content = `# Session ${noteNumber}: Recovered\\n\\n**Date:** ${date}\\n**Status:** In Progress\\n\\n---\\n\\n## Work Done\\n\\n<!-- PAI will add completed work here during session -->\\n\\n---\\n\\n## Next Steps\\n\\n<!-- To be filled at session end -->\\n\\n---\\n\\n**Tags:** #Session\\n`;\n writeFileSync(notePath, content);\n console.error(`Recreated session note: ${noteFilename}`);\n } catch (err) {\n console.error(`Failed to recreate note: ${err}`);\n return;\n }\n }\n\n const content = readFileSync(notePath, 'utf-8');\n const timestamp = new Date().toISOString();\n const checkpointText = `\\n### Checkpoint ${timestamp}\\n\\n${checkpoint}\\n`;\n\n const nextStepsIndex = content.indexOf('## Next Steps');\n const newContent = nextStepsIndex !== -1\n ? content.substring(0, nextStepsIndex) + checkpointText + content.substring(nextStepsIndex)\n : content + checkpointText;\n\n writeFileSync(notePath, newContent);\n console.error(`Checkpoint added to: ${basename(notePath)}`);\n}\n\n/** Work item for session notes. */\nexport interface WorkItem {\n title: string;\n details?: string[];\n completed?: boolean;\n}\n\n/** Add work items to the \"Work Done\" section of a session note. */\nexport function addWorkToSessionNote(notePath: string, workItems: WorkItem[], sectionTitle?: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n let workText = '';\n if (sectionTitle) workText += `\\n### ${sectionTitle}\\n\\n`;\n\n for (const item of workItems) {\n const checkbox = item.completed !== false ? '[x]' : '[ ]';\n workText += `- ${checkbox} **${item.title}**\\n`;\n if (item.details && item.details.length > 0) {\n for (const detail of item.details) {\n workText += ` - ${detail}\\n`;\n }\n }\n }\n\n const workDoneMatch = content.match(/## Work Done\\n\\n(<!-- .*? -->)?/);\n if (workDoneMatch) {\n const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;\n content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);\n } else {\n const nextStepsIndex = content.indexOf('## Next Steps');\n if (nextStepsIndex !== -1) {\n content = content.substring(0, nextStepsIndex) + workText + '\\n' + content.substring(nextStepsIndex);\n }\n }\n\n writeFileSync(notePath, content);\n console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);\n}\n\n/**\n * Check if a candidate title is meaningless / garbage.\n * Public wrapper around the internal filter for use by other hooks.\n */\nexport function isMeaningfulTitle(text: string): boolean {\n return !isMeaninglessCandidate(text);\n}\n\n/** Sanitize a string for use in a filename. */\nexport function sanitizeForFilename(str: string): string {\n return str\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .substring(0, 50);\n}\n\n/**\n * Return true if the candidate string should be rejected as a meaningful name.\n * Rejects file paths, shebangs, timestamps, system noise, XML tags, hashes, etc.\n */\nfunction isMeaninglessCandidate(text: string): boolean {\n const t = text.trim();\n if (!t) return true;\n if (t.length < 5) return true; // too short to be meaningful\n if (t.startsWith('/') || t.startsWith('~')) return true; // file path\n if (t.startsWith('#!')) return true; // shebang\n if (t.includes('[object Object]')) return true; // serialization artifact\n if (/^\\d{4}-\\d{2}-\\d{2}(T[\\d:.Z+-]+)?$/.test(t)) return true; // ISO timestamp\n if (/^\\d{1,2}:\\d{2}(:\\d{2})?(\\s*(AM|PM))?$/i.test(t)) return true; // time-only\n if (/^<[a-z-]+[\\s/>]/i.test(t)) return true; // XML/HTML tags (<task-notification>, etc.)\n if (/^[0-9a-f]{10,}$/i.test(t)) return true; // hex hash strings\n if (/^Exit code \\d+/i.test(t)) return true; // exit code messages\n if (/^Error:/i.test(t)) return true; // error messages\n if (/^This session is being continued/i.test(t)) return true; // continuation boilerplate\n if (/^\\(Bash completed/i.test(t)) return true; // bash output noise\n if (/^Task Notification$/i.test(t)) return true; // literal \"Task Notification\"\n if (/^New Session$/i.test(t)) return true; // placeholder title\n if (/^Recovered Session$/i.test(t)) return true; // placeholder title\n if (/^Continued Session$/i.test(t)) return true; // placeholder title\n if (/^Untitled Session$/i.test(t)) return true; // placeholder title\n if (/^Context Compression$/i.test(t)) return true; // compression artifact\n if (/^[A-Fa-f0-9]{8,}\\s+Output$/i.test(t)) return true; // hash + \"Output\" pattern\n return false;\n}\n\n/**\n * Extract a meaningful name from session note content and summary.\n * Looks at Work Done section headers, bold text, and summary.\n */\nexport function extractMeaningfulName(noteContent: string, summary: string): string {\n const workDoneMatch = noteContent.match(/## Work Done\\n\\n([\\s\\S]*?)(?=\\n---|\\n## Next)/);\n\n if (workDoneMatch) {\n const workDoneSection = workDoneMatch[1];\n\n const subheadings = workDoneSection.match(/### ([^\\n]+)/g);\n if (subheadings && subheadings.length > 0) {\n const firstHeading = subheadings[0].replace('### ', '').trim();\n if (!isMeaninglessCandidate(firstHeading) && firstHeading.length > 5 && firstHeading.length < 60) {\n return sanitizeForFilename(firstHeading);\n }\n }\n\n const boldMatches = workDoneSection.match(/\\*\\*([^*]+)\\*\\*/g);\n if (boldMatches && boldMatches.length > 0) {\n const firstBold = boldMatches[0].replace(/\\*\\*/g, '').trim();\n if (!isMeaninglessCandidate(firstBold) && firstBold.length > 3 && firstBold.length < 50) {\n return sanitizeForFilename(firstBold);\n }\n }\n\n const numberedItems = workDoneSection.match(/^\\d+\\.\\s+\\*\\*([^*]+)\\*\\*/m);\n if (numberedItems && !isMeaninglessCandidate(numberedItems[1])) {\n return sanitizeForFilename(numberedItems[1]);\n }\n }\n\n if (summary && summary.length > 5 && summary !== 'Session completed.' && !isMeaninglessCandidate(summary)) {\n const cleanSummary = summary\n .replace(/[^\\w\\s-]/g, ' ')\n .trim()\n .split(/\\s+/)\n .slice(0, 5)\n .join(' ');\n if (cleanSummary.length > 3 && !isMeaninglessCandidate(cleanSummary)) {\n return sanitizeForFilename(cleanSummary);\n }\n }\n\n return '';\n}\n\n/**\n * Rename a session note with a meaningful name.\n * Always uses \"NNNN - YYYY-MM-DD - Description.md\" format.\n * Returns the new path, or original path if rename fails.\n */\nexport function renameSessionNote(notePath: string, meaningfulName: string): string {\n if (!meaningfulName || !existsSync(notePath)) return notePath;\n\n const dir = join(notePath, '..');\n const oldFilename = basename(notePath);\n\n const correctMatch = oldFilename.match(/^(\\d{3,4}) - (\\d{4}-\\d{2}-\\d{2}) - .*\\.md$/);\n const legacyMatch = oldFilename.match(/^(\\d{3,4})_(\\d{4}-\\d{2}-\\d{2})_.*\\.md$/);\n const match = correctMatch || legacyMatch;\n if (!match) return notePath;\n\n const [, noteNumber, date] = match;\n\n const titleCaseName = meaningfulName\n .split(/[\\s_-]+/)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n .trim();\n\n const paddedNumber = noteNumber.padStart(4, '0');\n const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;\n const newPath = join(dir, newFilename);\n\n if (newFilename === oldFilename) return notePath;\n\n try {\n renameSync(notePath, newPath);\n console.error(`Renamed note: ${oldFilename} → ${newFilename}`);\n return newPath;\n } catch (error) {\n console.error(`Could not rename note: ${error}`);\n return notePath;\n }\n}\n\n/** Update the session note's H1 title and rename the file. */\nexport function updateSessionNoteTitle(notePath: string, newTitle: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n content = content.replace(/^# Session \\d+:.*$/m, (match) => {\n const sessionNum = match.match(/Session (\\d+)/)?.[1] || '';\n return `# Session ${sessionNum}: ${newTitle}`;\n });\n writeFileSync(notePath, content);\n renameSessionNote(notePath, sanitizeForFilename(newTitle));\n}\n\n/**\n * Finalize session note — mark as complete, add summary, rename with meaningful name.\n * IDEMPOTENT: subsequent calls are no-ops if already finalized.\n * Returns the final path (may be renamed).\n */\nexport function finalizeSessionNote(notePath: string, summary: string): string {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return notePath;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n if (content.includes('**Status:** Completed')) {\n console.error(`Note already finalized: ${basename(notePath)}`);\n return notePath;\n }\n\n content = content.replace('**Status:** In Progress', '**Status:** Completed');\n\n if (!content.includes('**Completed:**')) {\n const completionTime = new Date().toISOString();\n content = content.replace(\n '---\\n\\n## Work Done',\n `**Completed:** ${completionTime}\\n\\n---\\n\\n## Work Done`\n );\n }\n\n const nextStepsMatch = content.match(/## Next Steps\\n\\n(<!-- .*? -->)/);\n if (nextStepsMatch) {\n content = content.replace(\n nextStepsMatch[0],\n `## Next Steps\\n\\n${summary || 'Session completed.'}`\n );\n }\n\n writeFileSync(notePath, content);\n console.error(`Session note finalized: ${basename(notePath)}`);\n\n const meaningfulName = extractMeaningfulName(content, summary);\n if (meaningfulName) {\n return renameSessionNote(notePath, meaningfulName);\n }\n\n return notePath;\n}\n","/**\n * Shared \"## Continue\" checkpoint logic for a project's TODO.md.\n *\n * WHY THIS MODULE EXISTS\n * ----------------------\n * Before this, `pause.ts` and `handover.ts` each carried their own copy of\n * findProjectTodo / stripContinueSection / block-builder. Both wrote the same\n * fixed four-line block, and both stripped any existing ## Continue section\n * unconditionally. The consequence was that a rich, model-authored checkpoint\n * could never survive:\n *\n * 1. `pai pause` had no way to accept a body, so the model printed its\n * checkpoint to the terminal and it was lost.\n * 2. Even if a body had been written by hand, the session-stop hook runs\n * `pai session handover` on every clean exit, which regenerated the\n * generic block and erased it.\n *\n * So there are two jobs here:\n *\n * - AUTHORED writes (`pai pause --body-file`) carry the model's markdown\n * verbatim, wrapped in explicit start/end markers.\n * - AUTO writes (hooks) must never destroy an authored checkpoint belonging\n * to the *same* session. They may replace a stale one left by an earlier\n * session, otherwise TODO.md would show a checkpoint that no longer\n * describes where the work stands.\n *\n * PARSING\n * -------\n * A rich body can legitimately contain `---` rules and `##` headings, which the\n * old heuristic scanner treated as section terminators. Authored blocks are\n * therefore delimited by an explicit HTML-comment pair; the legacy heuristic is\n * kept only as a fallback for blocks written before this change.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n readdirSync,\n renameSync,\n mkdirSync,\n realpathSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Locations searched for a project TODO.md, in priority order. */\nexport const TODO_LOCATIONS = [\n \"Notes/TODO.md\",\n \".claude/Notes/TODO.md\",\n \"tasks/todo.md\",\n \"TODO.md\",\n];\n\nexport const MARKER_OPEN = \"<!-- pai:checkpoint\";\nexport const MARKER_CLOSE = \"<!-- /pai:checkpoint -->\";\n\n/**\n * Markers for a handover that has been superseded but not thrown away.\n *\n * Deliberately NOT `pai:checkpoint`: `locateContinue` scans for that marker and\n * for the `## Continue` heading, so an archived block written verbatim would be\n * found as the live section and rewritten in place of it. Archived entries are\n * inert by construction — different marker, no heading.\n */\nexport const ARCHIVE_OPEN = \"<!-- pai:archived-handover\";\nexport const ARCHIVE_CLOSE = \"<!-- /pai:archived-handover -->\";\nexport const ARCHIVE_HEADING = \"## Previous handovers\";\n\n/**\n * How many superseded handovers TODO.md keeps.\n *\n * Enough that a handover survives a run of sessions that each end without\n * pausing, which is the sequence that destroyed one on 2026-08-04. Not\n * unbounded: TODO.md is read by a human at the top of every session, and a file\n * that grows without limit stops being read, which is its own kind of loss.\n */\nexport const ARCHIVE_LIMIT = 5;\n\nexport const CONTINUE_HEADING = \"## Continue\";\n\n/** Who wrote the checkpoint currently in TODO.md. */\nexport type Authorship = \"model\" | \"auto\";\n\n/** Result of applying a checkpoint. */\nexport type ApplyAction = \"written\" | \"preserved\" | \"failed\";\n\n// ---------------------------------------------------------------------------\n// TODO.md discovery\n// ---------------------------------------------------------------------------\n\nexport function findProjectTodo(\n rootPath: string\n): { path: string; content: string } | null {\n for (const rel of TODO_LOCATIONS) {\n const full = join(rootPath, rel);\n if (existsSync(full)) {\n try {\n return { path: full, content: readFileSync(full, \"utf8\") };\n } catch {\n // unreadable — try next\n }\n }\n }\n return null;\n}\n\n/**\n * Resolve the TODO.md to write to, creating Notes/ if nothing exists yet.\n * Returns null only when the directory could not be created.\n */\nexport function resolveTodoTarget(\n rootPath: string,\n opts: { create?: boolean } = {}\n): { path: string; content: string } | null {\n const found = findProjectTodo(rootPath);\n if (found) return found;\n\n const notesDir = join(rootPath, \"Notes\");\n if (opts.create !== false) {\n try {\n if (!existsSync(notesDir)) mkdirSync(notesDir, { recursive: true });\n } catch {\n return null;\n }\n }\n return { path: join(notesDir, \"TODO.md\"), content: \"\" };\n}\n\n// ---------------------------------------------------------------------------\n// Marker parsing\n// ---------------------------------------------------------------------------\n\nexport interface CheckpointMeta {\n authored: Authorship;\n /** Session line the checkpoint describes, e.g. \"0003 - 2026-08-01 - Title\". */\n session?: string;\n /** Claude Code session UUID, when known. */\n sessionId?: string;\n ts?: string;\n}\n\n/**\n * Parse a `<!-- pai:checkpoint key=\"value\" ... -->` marker line.\n * Returns null when the line is not a marker.\n */\nexport function parseMarker(line: string): CheckpointMeta | null {\n const trimmed = line.trim();\n if (!trimmed.startsWith(MARKER_OPEN)) return null;\n\n const attrs: Record<string, string> = {};\n for (const m of trimmed.matchAll(/([a-zA-Z][\\w-]*)=\"([^\"]*)\"/g)) {\n attrs[m[1]] = m[2];\n }\n\n return {\n authored: attrs.authored === \"model\" ? \"model\" : \"auto\",\n session: attrs.session || undefined,\n sessionId: attrs[\"session-id\"] || undefined,\n ts: attrs.ts || undefined,\n };\n}\n\nexport interface LocatedContinue {\n /** Index of the \"## Continue\" heading line. */\n startIdx: number;\n /** Exclusive end index, past any trailing `---` separator. */\n endIdx: number;\n meta: CheckpointMeta | null;\n /** Raw lines of the section, heading included. */\n lines: string[];\n}\n\n/**\n * Lines an auto-generated block is made of. Anything else in a section is\n * content somebody put there deliberately.\n *\n * The blockquote marker is optional on every pattern. These lines appear\n * quoted when the block builder emits them as its header, and unquoted when a\n * caller passes the same text as a BODY — which the session-stop hook does: it\n * builds `Working directory: ${cwd}` and hands it over as state. Matching only\n * the quoted form made such a body read as content, which is how a session\n * that did no work at all overwrote a real handover on 2026-08-04. A line that\n * merely restates the generated header is boilerplate wherever it sits.\n */\nconst BOILERPLATE_PATTERNS = [\n /^##\\s+Continue$/,\n /^<!--\\s*\\/?pai:checkpoint/,\n /^>?\\s*\\*\\*Last session:\\*\\*/,\n /^>?\\s*\\*\\*Paused at:\\*\\*/,\n /^>?\\s*Working directory:/,\n /^>?\\s*Resume with:/,\n /^>?\\s*_No checkpoint body was recorded/,\n /^-{3,}$/,\n];\n\n/** A blank line, with or without a blockquote marker. */\nconst BLANK_LINE = /^>?\\s*$/;\n\n/**\n * True when a section contains nothing but generated header lines.\n *\n * This is the guard that stops an auto write from destroying content it did\n * not author. A session that hit the old clobbering bug may have worked around\n * it by hand — writing its state into a subsection underneath the generated\n * header lines, in an unmarked block. Those blocks predate the marker, so\n * authorship cannot be read off them; the only safe signal is whether anything\n * beyond boilerplate is present.\n */\nexport function isBoilerplateOnly(lines: string[]): boolean {\n return lines.every((line) => {\n const t = line.trim();\n return BLANK_LINE.test(t) || BOILERPLATE_PATTERNS.some((re) => re.test(t));\n });\n}\n\n/**\n * True when a checkpoint body says something the generated header does not.\n *\n * \"Has a body\" and \"has something to say\" are not the same question, and the\n * preservation rules care only about the second. An unattended writer that\n * emits a single line restating the working directory has produced a body by\n * any string test and a handover by none.\n */\nexport function hasSubstance(body: string | null | undefined): boolean {\n const text = (body ?? \"\").trim();\n if (!text) return false;\n return !isBoilerplateOnly(text.split(\"\\n\"));\n}\n\n/**\n * Extract the non-boilerplate content of a section, or \"\" if there is none.\n *\n * Interior blank lines are kept. They are not decoration — in Markdown they\n * are what separates a paragraph from the table or list that follows, so\n * dropping them (as this did while it treated every blank line as boilerplate)\n * silently welds a checkpoint body into one unreadable run.\n */\nexport function extractSectionContent(lines: string[]): string {\n const kept: string[] = [];\n for (const line of lines) {\n const t = line.trim();\n if (BOILERPLATE_PATTERNS.some((re) => re.test(t))) continue;\n kept.push(line);\n }\n return trimBlankEdges(collapseBlankRuns(kept)).join(\"\\n\");\n}\n\n/** Drop leading and trailing blank lines, leaving interior spacing alone. */\nfunction trimBlankEdges(lines: string[]): string[] {\n let start = 0;\n let end = lines.length;\n while (start < end && BLANK_LINE.test(lines[start].trim())) start += 1;\n while (end > start && BLANK_LINE.test(lines[end - 1].trim())) end -= 1;\n return lines.slice(start, end);\n}\n\n/**\n * Collapse runs of blank lines to a single blank.\n *\n * Removing a boilerplate line leaves the blank that surrounded it behind, so\n * stripping the header can open a three-line gap in the middle of the body.\n * One blank line is all Markdown needs.\n */\nfunction collapseBlankRuns(lines: string[]): string[] {\n const out: string[] = [];\n let lastWasBlank = false;\n for (const line of lines) {\n const isBlank = BLANK_LINE.test(line.trim());\n if (isBlank && lastWasBlank) continue;\n out.push(line);\n lastWasBlank = isBlank;\n }\n return out;\n}\n\n/**\n * Locate the existing ## Continue section.\n *\n * When the section carries an explicit marker pair, the close marker defines\n * the end — this is what lets a rich body contain `---` and `##` safely.\n * Otherwise the legacy heuristic applies: stop at the first `---` or the next\n * `##` heading.\n */\nexport function locateContinue(content: string): LocatedContinue | null {\n const lines = content.split(\"\\n\");\n const startIdx = lines.findIndex((l) => l.trim() === CONTINUE_HEADING);\n if (startIdx === -1) return null;\n\n // Look for an open marker in the first few lines of the section.\n let meta: CheckpointMeta | null = null;\n let markerIdx = -1;\n for (let i = startIdx + 1; i < Math.min(startIdx + 6, lines.length); i++) {\n const parsed = parseMarker(lines[i]);\n if (parsed) {\n meta = parsed;\n markerIdx = i;\n break;\n }\n // A non-blank, non-marker line means there is no marker for this section.\n if (lines[i].trim() !== \"\") break;\n }\n\n let endIdx = lines.length;\n\n if (markerIdx !== -1) {\n const closeIdx = lines.findIndex(\n (l, i) => i > markerIdx && l.trim() === MARKER_CLOSE\n );\n if (closeIdx !== -1) {\n endIdx = closeIdx + 1;\n } else {\n // Malformed (open without close) — fall back to the heuristic so we do\n // not swallow the rest of the file.\n endIdx = heuristicEnd(lines, startIdx);\n }\n } else {\n endIdx = heuristicEnd(lines, startIdx);\n }\n\n // Consume one trailing `---` separator and the blank lines around it.\n let trailingEnd = endIdx;\n while (trailingEnd < lines.length && lines[trailingEnd].trim() === \"\") {\n trailingEnd += 1;\n }\n if (trailingEnd < lines.length && lines[trailingEnd].trim() === \"---\") {\n trailingEnd += 1;\n } else {\n trailingEnd = endIdx;\n }\n\n return {\n startIdx,\n endIdx: trailingEnd,\n meta,\n lines: lines.slice(startIdx, trailingEnd),\n };\n}\n\n/**\n * Legacy scanner for blocks written before checkpoint markers existed.\n *\n * Terminates on a horizontal rule or the next level-1 or level-2 heading. Note\n * the `(?!#)` — `###` is a *subsection* of `## Continue`, not a terminator. The\n * original scanner stopped at any run of `#`, which meant a `### Restored\n * state` subsection fell outside the section entirely and could not be seen,\n * let alone carried forward.\n *\n * `#{1,2}` rather than `##`, because matching only `##` did not stop at an H1\n * and therefore ATE IT. A TODO.md whose `## Continue` block precedes its\n * `# TODO` title had the title absorbed into the section and destroyed on the\n * next regenerate — reported independently by three sessions on 2026-08-03,\n * one of which lost it three times and recovered from git each time.\n *\n * An H1 is a document-level heading. It can never be part of a `## Continue`\n * section, so treating it as a terminator is not a heuristic improvement but a\n * structural fact.\n */\nfunction heuristicEnd(lines: string[], startIdx: number): number {\n for (let i = startIdx + 1; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (\n trimmed === \"---\" ||\n (/^#{1,2}(?!#)/.test(trimmed) && trimmed !== CONTINUE_HEADING)\n ) {\n return i;\n }\n }\n return lines.length;\n}\n\n/** Remove the ## Continue section, returning the remainder of the document. */\nexport function stripContinue(content: string): string {\n const found = locateContinue(content);\n if (!found) return content;\n\n const lines = content.split(\"\\n\");\n const before = lines.slice(0, found.startIdx);\n const after = lines.slice(found.endIdx);\n while (after.length > 0 && after[0].trim() === \"\") after.shift();\n\n return [...before, ...after].join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Reading a checkpoint back\n// ---------------------------------------------------------------------------\n\nexport interface ContinueCheckpoint {\n meta: CheckpointMeta | null;\n /** The authored body — the section with generated header lines removed. */\n body: string;\n /** The full section, heading included. */\n raw: string;\n}\n\n/**\n * Read the `## Continue` checkpoint out of a TODO.md.\n *\n * Writing a checkpoint is only half of a handover; something has to deliver it\n * to the next session. This is the read side, used by the SessionStart hook.\n *\n * Returns null when there is no section, or when the section holds nothing but\n * generated header lines — a bodyless block carries no information a new\n * session does not already have, and injecting it would only add noise.\n */\nexport function readContinueCheckpoint(\n content: string\n): ContinueCheckpoint | null {\n const found = locateContinue(content);\n if (!found) return null;\n\n const body = found.meta\n ? extractMarkedBody(found.lines)\n : extractSectionContent(found.lines);\n if (!body) return null;\n\n return { meta: found.meta, body, raw: found.lines.join(\"\\n\") };\n}\n\n/**\n * Extract the body of a marker-delimited block by position rather than by\n * pattern.\n *\n * The layout is fixed: open marker, a contiguous run of `>` header lines, the\n * body, then the close marker. Because the boundaries are known exactly, the\n * body comes back byte-for-byte — including any `---` rules or `##` headings\n * of its own, which a pattern-based filter would mistake for boilerplate and\n * delete. Falls back to the pattern filter if the shape is not as expected.\n */\nfunction extractMarkedBody(lines: string[]): string {\n const markerIdx = lines.findIndex((l) => parseMarker(l) !== null);\n if (markerIdx === -1) return extractSectionContent(lines);\n\n const closeIdx = lines.findIndex(\n (l, i) => i > markerIdx && l.trim() === MARKER_CLOSE\n );\n if (closeIdx === -1) return extractSectionContent(lines);\n\n // Skip the blank lines and the `>` header run that follow the open marker.\n let bodyStart = markerIdx + 1;\n while (bodyStart < closeIdx) {\n const t = lines[bodyStart].trim();\n if (BLANK_LINE.test(t) || t.startsWith(\">\")) {\n bodyStart += 1;\n continue;\n }\n break;\n }\n\n return trimBlankEdges(lines.slice(bodyStart, closeIdx)).join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Block construction\n// ---------------------------------------------------------------------------\n\nexport interface BuildOptions {\n authored: Authorship;\n /** Human-readable session line, or \"Unknown session\". */\n sessionLine: string;\n /** Claude Code session UUID — the `claude --resume` handle. */\n sessionId?: string;\n cwd: string;\n /** Model-authored markdown. Omitted for auto blocks. */\n body?: string;\n /** Overridable for deterministic tests. */\n timestamp?: string;\n}\n\nfunction escapeAttr(value: string): string {\n return value.replace(/\"/g, \"'\");\n}\n\nexport function buildContinueBlock(opts: BuildOptions): string {\n const ts = opts.timestamp ?? new Date().toISOString();\n\n const attrs = [\n `authored=\"${opts.authored}\"`,\n `session=\"${escapeAttr(opts.sessionLine)}\"`,\n opts.sessionId ? `session-id=\"${escapeAttr(opts.sessionId)}\"` : null,\n `ts=\"${ts}\"`,\n ]\n .filter(Boolean)\n .join(\" \");\n\n const header = [\n `> **Last session:** ${opts.sessionLine}`,\n `> **Paused at:** ${ts}`,\n \">\",\n `> Working directory: ${opts.cwd}`,\n ];\n\n if (opts.sessionId) {\n header.push(\">\", `> Resume with: \\`claude --resume ${opts.sessionId}\\``);\n }\n\n const body = (opts.body ?? \"\").trim();\n\n const parts = [\n CONTINUE_HEADING,\n \"\",\n `${MARKER_OPEN} ${attrs} -->`,\n \"\",\n ...header,\n ];\n\n if (body) {\n parts.push(\"\", body);\n } else {\n parts.push(\n \">\",\n \"> _No checkpoint body was recorded — see the latest session note._\"\n );\n }\n\n parts.push(\"\", MARKER_CLOSE, \"\", \"---\", \"\");\n\n return parts.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Apply\n// ---------------------------------------------------------------------------\n\nexport interface ApplyOptions extends BuildOptions {\n /** Project root; TODO.md is resolved beneath it. */\n rootPath: string;\n /** Preview only — nothing is written. */\n dryRun?: boolean;\n}\n\nexport interface ApplyResult {\n action: ApplyAction;\n path: string | null;\n block: string;\n /** Set when action === \"preserved\". */\n preservedMeta?: CheckpointMeta;\n /** Set when unattributed content was carried forward into the new block. */\n carriedForward?: boolean;\n /** Set when a superseded model-authored handover was moved to the archive. */\n archived?: boolean;\n error?: string;\n}\n\n/**\n * Write the ## Continue block, honouring the preservation rules.\n *\n * An AUTO write is unattended — it fires from the session-stop and pre-compact\n * hooks — so it operates under one governing rule: **never destroy content it\n * did not author.** Three cases follow from that:\n *\n * 1. An authored checkpoint for the SAME session is left untouched. The hooks\n * fire after the model has already recorded the real state; overwriting it\n * with metadata is the bug this module exists to fix.\n *\n * \"Same session\" is decided by the Claude session UUID whenever both sides\n * know it, and only falls back to the human-readable session line when one\n * of them does not. The line is derived from the session note filename,\n * and `session-stop.sh` *renames and renumbers that file* — via `session\n * slug --apply` and `session cleanup --execute` — before it reaches the\n * handover step. So the key the hook computes at exit is not the key the\n * model wrote seconds earlier, and a filename-keyed comparison mismatches\n * by construction. Observed live on 2026-08-01: notes renumbered twice\n * within a single session. The UUID is the only identifier that holds\n * still.\n * 2. An authored checkpoint from an EARLIER session is stale — TODO.md would\n * otherwise keep pointing at the wrong session — so it is replaced. Its\n * content is not lost: `pai pause` mirrors every authored body into the\n * session note.\n * 3. An UNMARKED block predates the marker, so authorship cannot be read off\n * it. If it is nothing but generated header lines it is replaced. If it\n * carries anything else, that content was put there deliberately — quite\n * possibly as a hand-rolled workaround for the very clobbering this fixes —\n * and is carried forward into the new block rather than dropped.\n *\n * A MODEL write always replaces: the model is authoring the checkpoint, and a\n * newer one supersedes an older one.\n */\n/**\n * Do an existing checkpoint and an incoming write describe the same session?\n *\n * The UUID is authoritative when both sides carry one: it is assigned by Claude\n * Code and never changes for the life of the session. The session line is a\n * derived, mutable label and is only consulted when there is no UUID to compare\n * — a checkpoint written before `--session-id` was threaded through, or an auto\n * write from a caller that was not given one.\n */\nfunction isSameSession(\n meta: CheckpointMeta,\n opts: Pick<ApplyOptions, \"sessionId\" | \"sessionLine\">\n): boolean {\n if (meta.sessionId && opts.sessionId) {\n return meta.sessionId === opts.sessionId;\n }\n return meta.session === opts.sessionLine;\n}\n\n/**\n * How long a model-authored checkpoint is protected from automated overwriting.\n *\n * Sized for the race, not for the session: the gap between a model writing its\n * checkpoint and a hook firing is seconds to minutes, and an hour covers even a\n * slow checkpoint on a loaded machine. Anything genuinely from an earlier\n * session is hours or days old and still falls through to case 2, which is what\n * keeps TODO.md from freezing on a checkpoint nobody will ever replace.\n */\nconst AUTHORED_GRACE_MS = 60 * 60 * 1000;\n\n/** Was this checkpoint written recently enough to still belong to the run in progress? */\nfunction isRecent(meta: CheckpointMeta, now?: string): boolean {\n if (!meta.ts) return false; // no stamp, no claim — case 2 decides as before\n const then = Date.parse(meta.ts);\n if (Number.isNaN(then)) return false;\n const nowMs = now ? Date.parse(now) : Date.now();\n if (Number.isNaN(nowMs)) return false;\n // Guard the future too: a clock skew that puts the stamp ahead of now must\n // not read as \"very old\" and license an overwrite.\n return nowMs - then < AUTHORED_GRACE_MS;\n}\n\n/**\n * Turn a live checkpoint block into an inert archive entry.\n *\n * Two things have to go: the `## Continue` heading and the `pai:checkpoint`\n * marker. Both are what `locateContinue` looks for, so leaving either in place\n * would let a later write treat the archive as the live section — and the next\n * one after that would then archive the archive. Everything else is kept\n * verbatim, because the body is the whole reason this exists.\n */\nfunction toArchiveEntry(lines: string[], meta: CheckpointMeta | null): string[] {\n const out: string[] = [];\n const label = meta?.session ?? \"Unknown session\";\n const stamp = meta?.ts ? ` — checkpointed ${meta.ts}` : \"\";\n out.push(`${ARCHIVE_OPEN} session=\"${label}\"${meta?.ts ? ` ts=\"${meta.ts}\"` : \"\"} -->`);\n out.push(\"\");\n out.push(`### ${label}${stamp}`);\n out.push(\"\");\n for (const line of lines) {\n if (line.trim() === CONTINUE_HEADING) continue;\n if (line.trim().startsWith(MARKER_OPEN)) continue;\n if (line.trim() === MARKER_CLOSE) continue;\n if (line.trim() === \"---\") continue;\n out.push(line);\n }\n while (out.length > 0 && out[out.length - 1].trim() === \"\") out.pop();\n out.push(\"\");\n out.push(ARCHIVE_CLOSE);\n return out;\n}\n\n/**\n * Put a superseded handover into the archive section, newest first.\n *\n * `rest` is the document with the Continue section already stripped. The\n * archive lives immediately below it so a reader meets the current handover\n * first and the previous ones directly after, rather than hunting for them at\n * the bottom of a file that also holds the project's open work.\n */\nfunction archiveInto(rest: string, entry: string[]): string {\n const lines = rest.split(\"\\n\");\n const headingIdx = lines.findIndex((l) => l.trim() === ARCHIVE_HEADING);\n\n if (headingIdx === -1) {\n return [ARCHIVE_HEADING, \"\", ...entry, \"\", \"---\", \"\", rest.trimStart()].join(\"\\n\");\n }\n\n // Insert directly under the heading, then drop whatever falls past the cap.\n const before = lines.slice(0, headingIdx + 1);\n const after = lines.slice(headingIdx + 1);\n while (after.length > 0 && after[0].trim() === \"\") after.shift();\n\n const merged = [...entry, \"\", ...after];\n const kept: string[] = [];\n let seen = 0;\n for (let i = 0; i < merged.length; i++) {\n if (merged[i].trim().startsWith(ARCHIVE_OPEN)) {\n seen += 1;\n if (seen > ARCHIVE_LIMIT) {\n // Everything from here to the end of THIS entry goes; keep scanning so\n // anything after the archive section (other headings, open work) stays.\n while (i < merged.length && merged[i].trim() !== ARCHIVE_CLOSE) i++;\n continue; // skips the close marker too\n }\n }\n kept.push(merged[i]);\n }\n return [...before, \"\", ...kept].join(\"\\n\");\n}\n\nexport function applyContinue(opts: ApplyOptions): ApplyResult {\n const target = resolveTodoTarget(opts.rootPath, { create: !opts.dryRun });\n if (!target) {\n return {\n action: \"failed\",\n path: null,\n block: buildContinueBlock(opts),\n error: \"Could not resolve or create a TODO.md target\",\n };\n }\n\n const existing = locateContinue(target.content);\n let carriedForward = false;\n let effectiveBody = opts.body;\n let archiveEntry: string[] | null = null;\n\n if (opts.authored === \"auto\" && existing) {\n // Case 1 — authored, same session: hands off.\n if (existing.meta?.authored === \"model\" && isSameSession(existing.meta, opts)) {\n return {\n action: \"preserved\",\n path: target.path,\n block: buildContinueBlock(opts),\n preservedMeta: existing.meta,\n };\n }\n\n // Case 1b — the incoming write has nothing to say, and what is already\n // there does. A metadata-only block says \"see the latest session note\", so\n // replacing a real handover with one trades content for a pointer — and the\n // pointer is not always good: observed live on 2026-08-01 in the AIBroker\n // project, where the stop hook's bodyless handover overwrote the autosave's\n // body and named a session note that had never been created, leaving the\n // next session nothing to resume from.\n //\n // Deferring to the session note is only safe when the note demonstrably has\n // the content. This code cannot see that, so it does the one thing that is\n // never wrong: a write with nothing to say does not get to destroy\n // something that does. A stale-but-real handover beats a fresh dead link.\n // Scoped to blocks we marked. An unmarked block falls through to case 3,\n // which salvages its content into the new block rather than freezing the\n // old one — better, because an unmarked block has no session metadata worth\n // preserving and may predate this scheme entirely.\n //\n // The test is SUBSTANCE, not emptiness. It was emptiness until 2026-08-04,\n // when a session that was opened and immediately exited destroyed the\n // previous day's handover for every project it touched. Its stop hook found\n // no work items and no completion message, so it sent the one line it\n // always builds unconditionally — `Working directory: …`, a verbatim copy\n // of a line the header already generates. Non-empty by a string test, so\n // this guard stood down; worthless by any other reading. `hasSubstance`\n // asks the question the comment above always claimed to be asking.\n if (\n existing.meta &&\n !hasSubstance(opts.body) &&\n !isBoilerplateOnly(existing.lines)\n ) {\n return {\n action: \"preserved\",\n path: target.path,\n block: buildContinueBlock(opts),\n preservedMeta: existing.meta ?? undefined,\n };\n }\n\n // Case 2b — a model checkpoint written moments ago is THIS session's,\n // whatever the identity comparison says.\n //\n // Case 2 deliberately lets an auto write replace an authored checkpoint\n // from an EARLIER session, and that policy is right: TODO.md would freeze\n // otherwise, and `pai pause` mirrors authored bodies into the session note.\n // The bug is not the policy, it is that identity DEGRADES and healthy\n // sessions fall into it.\n //\n // `sessionId` is optional on `pai pause`. Without it isSameSession compares\n // a display line containing the note TITLE — and pausing renames the note.\n // So a session writes a checkpoint, its note is renamed, the hook fires\n // seconds later, no longer recognises its own work, and classifies it as a\n // stale earlier session. Three sessions reported exactly this on\n // 2026-08-03 from one `pai pause all`; one lost its checkpoint three times\n // and recovered from git each time. Sessions without a clean repo would\n // never have known it happened.\n //\n // Recency is the tiebreaker that identity cannot supply. A model checkpoint\n // minutes old is not a stale predecessor by any reading, so an automated\n // digest does not get to overwrite it. Older ones still fall through to\n // case 2 and are replaced as before, which is what stops TODO.md freezing\n // and keeps this from nesting carried-forward blocks without end.\n if (\n existing.meta?.authored === \"model\" &&\n isRecent(existing.meta, opts.timestamp) &&\n !isBoilerplateOnly(existing.lines)\n ) {\n return {\n action: \"preserved\",\n path: target.path,\n block: buildContinueBlock(opts),\n preservedMeta: existing.meta,\n };\n }\n\n // Case 2c — an authored handover from an EARLIER session, past the grace\n // window. Case 2 deleted it, on the reasoning that TODO.md must not freeze\n // and that `pai pause` mirrors authored bodies into the session note.\n //\n // The first half is right. The second is an assumption, and on 2026-08-04\n // it was false in the plainest way available: this project keeps no session\n // notes at all (`Notes/*` is gitignored and none exist on disk), so the\n // checkpoint WAS the only copy. It went at 08:36:29Z, to the autosave of a\n // session that had been open for sixteen minutes and had typed one line —\n // destroying, as its first act, the handover it had been started to read.\n //\n // This is also the escape the v0.27.2 guard could not close: that one asks\n // whether the block is RECENT, and a handover written the night before is\n // not. Age was never the question. Nothing here needs the old block gone;\n // it only needs the slot. So the block moves down the file instead, and the\n // freeze argument and the loss argument stop being in tension.\n //\n // Only for `model` blocks with something in them. An auto block is a\n // transcript scrape that regenerates on the next tick, and archiving those\n // would bury the real handovers under mechanical noise.\n if (\n existing.meta?.authored === \"model\" &&\n !isBoilerplateOnly(existing.lines) &&\n !isSameSession(existing.meta, opts)\n ) {\n archiveEntry = toArchiveEntry(existing.lines, existing.meta);\n }\n\n // Case 3 — unmarked block holding content nobody can attribute to us.\n if (!existing.meta && !isBoilerplateOnly(existing.lines)) {\n const salvaged = extractSectionContent(existing.lines);\n if (salvaged) {\n effectiveBody = [\n \"_Carried forward from the previous checkpoint (author unknown — this\",\n \"block predates checkpoint authorship markers):_\",\n \"\",\n salvaged,\n ].join(\"\\n\");\n carriedForward = true;\n }\n }\n }\n\n const block = buildContinueBlock({ ...opts, body: effectiveBody });\n\n if (opts.dryRun) {\n return { action: \"written\", path: target.path, block, carriedForward, archived: !!archiveEntry };\n }\n\n const rest = stripContinue(target.content).trimStart();\n const newContent = block + (archiveEntry ? archiveInto(rest, archiveEntry) : rest);\n const tmpPath = `${target.path}.continue.tmp`;\n\n try {\n writeFileSync(tmpPath, newContent, \"utf8\");\n renameSync(tmpPath, target.path);\n } catch (err) {\n try {\n if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);\n } catch {\n /* ignore */\n }\n return {\n action: \"failed\",\n path: target.path,\n block,\n error: String(err),\n };\n }\n\n return { action: \"written\", path: target.path, block, carriedForward, archived: !!archiveEntry };\n}\n\n// ---------------------------------------------------------------------------\n// Session-note discovery\n//\n// Lifted verbatim from end.ts so pause, end and any future caller share one\n// implementation. end.ts now imports these rather than carrying its own copy.\n// ---------------------------------------------------------------------------\n\n/** ADAPTER_DIR (harness adapter root) — mirrors pai-paths.ts's resolveAdapterDir(),\n * kept self-contained here to avoid importing pai-paths.ts's process.exit(1)\n * validation side effect. PAI_DIR is the deprecated alias, same as there. */\nfunction getPaiDir(): string {\n const envDir = process.env.ADAPTER_DIR || process.env.PAI_DIR;\n if (envDir) {\n try {\n return realpathSync(envDir);\n } catch {\n return envDir;\n }\n }\n return join(homedir(), \".claude\");\n}\n\n/**\n * Find the notes directory for a project — local first, then the central\n * ~/.claude/projects/<encoded>/Notes fallback. Never creates.\n */\nexport function findNotesDir(\n rootPath: string,\n encodedDir: string\n): string | null {\n for (const rel of [\"Notes\", \"notes\", \".claude/Notes\"]) {\n const p = join(rootPath, rel);\n if (existsSync(p)) return p;\n }\n const central = join(getPaiDir(), \"projects\", encodedDir, \"Notes\");\n if (existsSync(central)) return central;\n return null;\n}\n\n/**\n * Find the current (highest-numbered) session note: current month, then the\n * previous month, then a flat notesDir as legacy fallback.\n */\nexport function findLatestNote(notesDir: string): string | null {\n const findIn = (dir: string): string | null => {\n if (!existsSync(dir)) return null;\n let files: string[];\n try {\n files = readdirSync(dir);\n } catch {\n return null;\n }\n const notes = files\n .filter((f) => /^\\d{3,4}[\\s_-].*\\.md$/.test(f))\n .sort((a, b) => {\n const na = parseInt(a.match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n const nb = parseInt(b.match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n return na - nb;\n });\n return notes.length > 0 ? join(dir, notes[notes.length - 1]) : null;\n };\n\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, \"0\");\n\n const current = findIn(join(notesDir, year, month));\n if (current) return current;\n\n const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const py = String(prev.getFullYear());\n const pm = String(prev.getMonth() + 1).padStart(2, \"0\");\n const prevFound = findIn(join(notesDir, py, pm));\n if (prevFound) return prevFound;\n\n return findIn(notesDir);\n}\n\n// ---------------------------------------------------------------------------\n// Session-note append\n// ---------------------------------------------------------------------------\n\n/**\n * Append the checkpoint body to a session note.\n *\n * TODO.md's ## Continue is a single slot that every later checkpoint\n * overwrites. The session note is the durable record, so the body goes to both.\n * Idempotent per timestamp: re-running with the same stamp will not duplicate.\n */\nexport function appendCheckpointToNote(\n notePath: string,\n body: string,\n timestamp?: string\n): { appended: boolean; error?: string } {\n const ts = timestamp ?? new Date().toISOString();\n const heading = `## Pause Checkpoint — ${ts}`;\n\n let existing: string;\n try {\n existing = readFileSync(notePath, \"utf8\");\n } catch (err) {\n return { appended: false, error: String(err) };\n }\n\n if (existing.includes(heading)) return { appended: false };\n\n const block = `\\n\\n---\\n\\n${heading}\\n\\n${body.trim()}\\n`;\n const tmpPath = `${notePath}.checkpoint.tmp`;\n\n try {\n writeFileSync(tmpPath, existing.trimEnd() + block, \"utf8\");\n renameSync(tmpPath, notePath);\n } catch (err) {\n try {\n if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);\n } catch {\n /* ignore */\n }\n return { appended: false, error: String(err) };\n }\n\n return { appended: true };\n}\n\n// ---------------------------------------------------------------------------\n// Body loading\n// ---------------------------------------------------------------------------\n\n/** Read a checkpoint body from a file, or from stdin when path is \"-\". */\nexport function readBodyFile(path: string): string {\n if (path === \"-\") {\n return readFileSync(0, \"utf8\");\n }\n return readFileSync(path, \"utf8\");\n}\n","/**\n * TODO.md management — creation, task updates, checkpoints, and Continue section.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { findTodoPath } from './paths.js';\nimport { applyContinue } from '../../../../session/checkpoint-block.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Task item for TODO.md. */\nexport interface TodoItem {\n content: string;\n completed: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Ensure TODO.md exists. Creates it with default structure if missing.\n * Returns the path to the TODO.md file.\n */\nexport function ensureTodoMd(cwd: string): string {\n const todoPath = findTodoPath(cwd);\n\n if (!existsSync(todoPath)) {\n const parentDir = join(todoPath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n\n const content = `# TODO\n\n## Current Session\n\n- [ ] (Tasks will be tracked here)\n\n## Backlog\n\n- [ ] (Future tasks)\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Created TODO.md: ${todoPath}`);\n }\n\n return todoPath;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Update TODO.md with current session tasks.\n * Preserves the Backlog section and ensures exactly ONE timestamp at the end.\n */\nexport function updateTodoMd(cwd: string, tasks: TodoItem[], sessionSummary?: string): void {\n const todoPath = ensureTodoMd(cwd);\n const content = readFileSync(todoPath, 'utf-8');\n\n const backlogMatch = content.match(/## Backlog[\\s\\S]*?(?=\\n---|\\n\\*Last updated|$)/);\n const backlogSection = backlogMatch\n ? backlogMatch[0].trim()\n : '## Backlog\\n\\n- [ ] (Future tasks)';\n\n const taskLines = tasks.length > 0\n ? tasks.map(t => `- [${t.completed ? 'x' : ' '}] ${t.content}`).join('\\n')\n : '- [ ] (No active tasks)';\n\n const newContent = `# TODO\n\n## Current Session\n\n${taskLines}\n\n${sessionSummary ? `**Session Summary:** ${sessionSummary}\\n\\n` : ''}${backlogSection}\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, newContent);\n console.error(`Updated TODO.md: ${todoPath}`);\n}\n\n/**\n * Add a checkpoint entry to TODO.md (without replacing tasks).\n * Ensures exactly ONE timestamp line at the end.\n */\nexport function addTodoCheckpoint(cwd: string, checkpoint: string): void {\n const todoPath = ensureTodoMd(cwd);\n let content = readFileSync(todoPath, 'utf-8');\n\n // Remove ALL existing timestamp lines and trailing separators\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n\n const checkpointText = `\\n**Checkpoint (${new Date().toISOString()}):** ${checkpoint}\\n\\n`;\n\n const backlogIndex = content.indexOf('## Backlog');\n if (backlogIndex !== -1) {\n content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);\n } else {\n const continueIndex = content.indexOf('## Continue');\n if (continueIndex !== -1) {\n const afterContinue = content.indexOf('\\n---', continueIndex);\n if (afterContinue !== -1) {\n const insertAt = afterContinue + 4;\n content = content.substring(0, insertAt) + '\\n' + checkpointText + content.substring(insertAt);\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n }\n\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${new Date().toISOString()}*\\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Checkpoint added to TODO.md`);\n}\n\n/**\n * The Claude Code session UUID, read off the transcript path.\n *\n * Claude Code names a transcript `<session-uuid>.jsonl`, so the hooks that\n * receive a transcript path are holding the session's identity without knowing\n * it. Both session-end writers had one available and neither used it.\n *\n * Shape-checked rather than trusted: only a UUID is returned, so a renamed,\n * archived or otherwise unexpected filename yields undefined and the caller\n * falls back to the session line rather than writing a `session-id` that\n * identifies nothing. A wrong id is worse than none — it would make two\n * different sessions compare equal.\n */\nexport function sessionIdFromTranscript(transcriptPath: string | undefined): string | undefined {\n if (!transcriptPath) return undefined;\n const base = basename(transcriptPath).replace(/\\.jsonl$/, '');\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(base)\n ? base\n : undefined;\n}\n\n/**\n * Update the ## Continue section at the top of TODO.md.\n *\n * This is the unattended writer: the pre-compact hook and the daemon's\n * work-queue worker both come through here. It used to build its own block and\n * strip the existing section with `/## Continue\\n[\\s\\S]*?\\n---\\n+/` — a\n * non-greedy match to the first `---`, which cuts a rich checkpoint in half at\n * the first horizontal rule inside its body and leaves the remainder orphaned\n * in the document.\n *\n * More importantly it was the writer that actually destroyed model-authored\n * checkpoints: `pai pause` wrote one, then this ran seconds later on session\n * end and replaced it with `Working directory: … Check the latest session note\n * for details.`\n *\n * It now delegates to the shared checkpoint module in \"auto\" mode, which means\n * it inherits the preservation rules rather than reimplementing them. Guarding\n * here rather than at each of the three call sites is deliberate: any future\n * caller inherits the behaviour without knowing it exists.\n */\nexport function updateTodoContinue(\n cwd: string,\n noteFilename: string,\n state: string | null,\n tokenDisplay: string,\n /**\n * The Claude Code session UUID, when the caller knows it.\n *\n * Optional only because a caller may genuinely not have it; supply it\n * whenever you can. Without it `isSameSession` falls back to comparing the\n * note FILENAME, and pausing renames the note — so a session stops\n * recognising its own checkpoint seconds after writing it, and an automated\n * write is then free to replace it as though it belonged to a predecessor.\n * That is how a live session lost a model-authored handover on 2026-08-04.\n */\n sessionId?: string\n): void {\n // Ensure a TODO.md exists so applyContinue writes to the same file the rest\n // of the hooks lib uses.\n ensureTodoMd(cwd);\n\n const result = applyContinue({\n rootPath: cwd,\n authored: 'auto',\n // The hooks identify a session by its note filename; `pai pause` resolves\n // the same string from the registry (and falls back to this filename when\n // the registry has no session row). They must agree, because this string\n // is the key that decides whether an authored checkpoint belongs to the\n // current session and must be preserved.\n sessionLine: noteFilename.replace(/\\.md$/, ''),\n // The UUID is authoritative when present; the line above is the fallback\n // for callers that cannot supply one, and it is mutable by design.\n sessionId,\n cwd,\n body: state?.trim() || undefined,\n });\n\n if (result.action === 'preserved') {\n console.error(\n 'TODO.md ## Continue left intact — authored checkpoint for this session'\n );\n return;\n }\n\n if (result.action === 'failed') {\n console.error(`TODO.md ## Continue update failed: ${result.error}`);\n return;\n }\n\n // Refresh the trailing \"Last updated\" stamp without disturbing the block.\n try {\n const todoPath = result.path!;\n const now = new Date().toISOString();\n let content = readFileSync(todoPath, 'utf-8');\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${now}*\\n`;\n writeFileSync(todoPath, content);\n } catch {\n // Non-fatal — the checkpoint itself is already written.\n }\n\n console.error(\n result.carriedForward\n ? 'TODO.md ## Continue section updated (previous content carried forward)'\n : 'TODO.md ## Continue section updated'\n );\n}\n","/**\n * session-summary-prompt.ts — Prompt template for AI-powered session summaries\n *\n * Produces a prompt that instructs the summarizer model to generate a structured\n * session note from extracted user messages and git commits. The output format\n * matches PAI's existing session note structure (Reconstruct skill format).\n *\n * The prompt also requests a TOPIC line on the first line of output, which the\n * session-summary-worker uses to detect topic shifts and decide whether to\n * create a new note or update the existing one.\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SummaryPromptParams {\n /** Extracted user messages from the JSONL transcript. */\n userMessages: string[];\n /** Git log output (--oneline --stat) for the session period. */\n gitLog: string;\n /** Working directory of the session. */\n cwd: string;\n /** ISO date string for the session. */\n date: string;\n /** Files modified during the session (from tool_use blocks). */\n filesModified?: string[];\n /** Existing session note content (if updating). */\n existingNote?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Prompt builder\n// ---------------------------------------------------------------------------\n\n/**\n * Build the prompt string to send to the summarizer model.\n *\n * Returns a single string suitable for piping to `claude --model <model> --print`.\n */\nexport function buildSessionSummaryPrompt(params: SummaryPromptParams): string {\n const {\n userMessages,\n gitLog,\n cwd,\n date,\n filesModified,\n existingNote,\n } = params;\n\n const userSection = userMessages.length > 0\n ? userMessages.map((m, i) => `[${i + 1}] ${m}`).join(\"\\n\\n\")\n : \"(No user messages extracted)\";\n\n const gitSection = gitLog.trim() || \"(No git commits during this session)\";\n\n const filesSection = filesModified && filesModified.length > 0\n ? filesModified.map(f => `- ${f}`).join(\"\\n\")\n : \"\";\n\n const updateInstruction = existingNote\n ? `\\nAn existing session note is provided below. Merge the new information into it,\npreserving what was already written. Add new work items and update the summary.\nDo NOT duplicate existing content.\n\nEXISTING NOTE:\n${existingNote}\n`\n : \"\";\n\n return `You are summarizing a coding session. Given the user messages and git commits below, write a session note.\n\nProject directory: ${cwd}\nDate: ${date}\n\nFocus on:\n- What problems were encountered and how they were solved\n- Key architectural decisions and their rationale\n- What was built (reference actual files and code patterns)\n- What was left unfinished or needs follow-up\n\nDo NOT include:\n- Mechanical metadata (token counts, checkpoint timestamps)\n- System messages or tool results verbatim\n- Generic descriptions — be specific about what happened\n- Markdown frontmatter or YAML headers\n${updateInstruction}\nFormat your response EXACTLY as follows (no extra text before or after):\n\nTOPIC: [A short topic label, max 60 characters, describing the WORK DONE — not quoting user messages. Format as \"Topic1, Topic2, and Topic3\" if multiple themes. Example: \"Session Summary Worker, Topic Detection\"]\n\n# Session: [Descriptive title summarizing what was ACCOMPLISHED, max 60 characters. Describe the work done, not the user's request. Bad: \"Dark Mode Button Does Nothing\". Good: \"Dark Mode Toggle, Keyboard IPC, and Audio Fix\"]\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n[Organize by theme, not chronologically. Group related work under descriptive bullet points.\nUse checkbox format: - [x] for completed items, - [ ] for incomplete items.\nInclude specific file names, function names, and technical details.]\n\n## Key Decisions\n\n[List important choices made during the session with brief rationale.\nSkip this section entirely if no significant decisions were made.]\n\n## Known Issues\n\n[What was left unfinished, bugs discovered, or follow-up items needed.\nSkip this section entirely if nothing is pending.]\n\n---\n\nUSER MESSAGES:\n${userSection}\n\nGIT COMMITS:\n${gitSection}\n${filesSection ? `\\nFILES MODIFIED:\\n${filesSection}` : \"\"}`;\n}\n","/**\n * triple-extraction-prompt.ts — Prompt template for KG triple extraction.\n *\n * Used by the session-summary-worker to extract structured facts from\n * a completed session summary and store them in the temporal knowledge graph.\n */\n\nexport function buildTripleExtractionPrompt(params: {\n sessionContent: string;\n projectSlug: string;\n gitLog: string;\n}): string {\n return `Extract structured entities and relations from this coding session.\n\nOutput a single JSON object with two arrays: \"entities\" and \"relations\".\n\nEntity types: project | person | concept | tool | file | version | decision | technology | organization\n\nRules:\n- Be SPECIFIC: entity names must be concrete (e.g., \"FSRS\", \"Glidr\", \"the owner\")\n- Use snake_case relation verb phrases (e.g., \"uses_algorithm\", \"decided_to\", \"shipped_version\")\n- Skip opinions, speculation, and \"we should\" statements\n- Skip entities obvious from project metadata unless they have a meaningful relation\n- Maximum 15 relations per session — pick the most important\n- Each entity should have a brief description (1 sentence, what it is in this context)\n- Each relation must reference entity names that appear in the entities array\n\nExample output:\n{\n \"entities\": [\n {\"name\": \"Glidr\", \"type\": \"project\", \"description\": \"Flashcard app using FSRS spaced repetition\"},\n {\"name\": \"FSRS\", \"type\": \"concept\", \"description\": \"Free Spaced Repetition Scheduler algorithm\"},\n {\"name\": \"the owner\", \"type\": \"person\", \"description\": \"Developer of Glidr and Quassl\"},\n {\"name\": \"Quassl\", \"type\": \"project\", \"description\": \"iOS app being rewritten in Flutter\"},\n {\"name\": \"Flutter\", \"type\": \"technology\", \"description\": \"Cross-platform mobile framework\"}\n ],\n \"relations\": [\n {\"source\": \"Glidr\", \"relation\": \"uses_algorithm\", \"target\": \"FSRS\"},\n {\"source\": \"Glidr\", \"relation\": \"shipped_version\", \"target\": \"1.0.5\"},\n {\"source\": \"the owner\", \"relation\": \"decided_to_rewrite\", \"target\": \"Quassl\"},\n {\"source\": \"Quassl\", \"relation\": \"migrating_to\", \"target\": \"Flutter\"}\n ]\n}\n\nPROJECT: ${params.projectSlug}\n\nSESSION CONTENT:\n${params.sessionContent}\n\nGIT COMMITS:\n${params.gitLog}\n\nOutput ONLY the JSON object with no surrounding prose, no code fences, no trailing commas, and no comments.\n\nJSON object (entities + relations):`;\n}\n","/**\n * daemon-llm.ts — provider-routed LLM spawns for daemon-side background calls.\n *\n * Session summaries, context handovers and KG extraction used to spawn a bare\n * `claude --model <tier>` with the ambient environment: on a machine whose\n * only login is a worker provider that spawn either dies (no Anthropic\n * credentials) or bills Anthropic outside the provider abstraction. This\n * module routes those spawns through the same registry and env build the\n * worker runner uses (`buildRunEnv`), resolving the tier alias to the\n * provider's concrete model id. With no usable provider configured it falls\n * back to the historical behaviour: the tier alias itself, ambient env minus\n * the Anthropic API key.\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport {\n defaultWorkersConfig,\n readWorkersSection,\n resolveModelCapability,\n type ModelTier,\n type WorkerProvider,\n type WorkersConfig,\n} from \"./config.js\";\nimport { resolveTarget } from \"./routing.js\";\nimport { ensureNoMcpConfig, workersLogDir } from \"./paths.js\";\nimport { buildRunEnv } from \"./run-env.js\";\nimport { DEFAULT_PROXY_PORT, ensureProxyRunning } from \"./proxy/server.js\";\n\nexport type { ModelTier } from \"./config.js\";\n\n/** Timeout per tier (ms) — a class-appropriate budget, not a per-model one. */\nexport const LLM_TIMEOUT_MS: Record<ModelTier, number> = {\n haiku: 60_000, // 60 seconds\n sonnet: 120_000, // 2 minutes\n opus: 300_000, // 5 minutes — the thorough tier\n};\n\nexport interface LlmSpawnPlan {\n /** --model value: the provider's concrete model id, or the tier alias on\n * the no-provider fallback (the CLI resolves tier aliases itself there). */\n model: string;\n /** Full headless claude args, model flag included. */\n args: string[];\n env: NodeJS.ProcessEnv;\n timeoutMs: number;\n /** Provider name the spawn is routed through; null on the fallback path. */\n provider: string | null;\n}\n\n/** Resolve a tier to the provider's concrete model id: the fast capability\n * for the cheap tier, the default model for the middle and top tiers. */\nfunction tierModel(provider: WorkerProvider, tier: ModelTier): string {\n return resolveModelCapability(provider, tier === \"haiku\" ? \"fast\" : \"default\");\n}\n\n/**\n * Containment every daemon-side LLM spawn carries. These calls are\n * text-in/text-out: the prompt carries the context, the answer is the\n * product, and nothing about summarizing a session justifies a tool. An\n * unrestricted background summarizer was observed on 2026-09-18 (01:33-01:51)\n * rewriting the live config with a schema-shaped dump while the daemon ran;\n * with the tool grant empty and MCP strictly empty, that class of damage is\n * structurally impossible rather than merely unlikely.\n */\nfunction containmentArgs(logDir: string): string[] {\n return [\n \"--strict-mcp-config\", \"--mcp-config\", ensureNoMcpConfig(logDir),\n // the empty grant allows no tool at all — not Read, not Bash, nothing\n \"--allowedTools\", \"\",\n // --tools \"\" drops the built-in tool schemas from the static prefix too:\n // 29,270 tokens with only --allowedTools \"\" vs 14,049 with --tools \"\" as well\n \"--tools\", \"\",\n ];\n}\n\n/** The historical fallback: tier alias as the model id, ambient env with the\n * Anthropic API key stripped (so the CLI uses its interactive login). */\nfunction legacyPlan(tier: ModelTier, config: WorkersConfig | null): LlmSpawnPlan {\n const { ANTHROPIC_API_KEY: _drop, ...env } = process.env;\n return {\n model: tier,\n args: [\n \"--model\", tier,\n ...containmentArgs(workersLogDir(config ?? defaultWorkersConfig())),\n \"-p\", \"--no-session-persistence\",\n ],\n env,\n timeoutMs: LLM_TIMEOUT_MS[tier],\n provider: null,\n };\n}\n\n/**\n * Plan a daemon-side LLM spawn for a tier. Provider resolution failures (no\n * config, workers off, no usable provider) fall back to the tier-alias path;\n * a configured-but-broken provider (unreadable key file) throws, because\n * silently degrading to Anthropic billing is worse than a failed summary.\n */\nexport async function planLlmSpawn(tier: ModelTier, configPath?: string): Promise<LlmSpawnPlan> {\n let provider: WorkerProvider | null = null;\n let providerName: string | null = null;\n let config: WorkersConfig | null = null;\n try {\n const { workers } = readWorkersSection(configPath);\n // the section is kept even when routing is off: its logDir is where the\n // containment config lives, on every path, provider or fallback\n config = workers;\n if (workers.enabled) {\n const target = resolveTarget(workers, workersLogDir(workers), {});\n provider = target.provider;\n providerName = target.providerName;\n }\n } catch {\n // no usable provider configured — the tier-alias fallback keeps the\n // feature alive exactly as it behaved before providers existed. The\n // section (when it parsed) is kept: its logDir still decides where the\n // containment config lives.\n provider = null;\n providerName = null;\n }\n if (!provider || !providerName || !config) return legacyPlan(tier, config);\n\n const logDir = workersLogDir(config);\n let proxyUrl: string | undefined;\n if (provider.protocol === \"openai\") {\n mkdirSync(logDir, { recursive: true });\n proxyUrl = `${await ensureProxyRunning(DEFAULT_PROXY_PORT, logDir)}/${providerName}`;\n }\n\n const model = tierModel(provider, tier);\n return {\n model,\n args: [\n \"--model\", model,\n ...containmentArgs(logDir),\n \"-p\", \"--no-session-persistence\",\n ],\n env: buildRunEnv(provider, true, proxyUrl),\n timeoutMs: LLM_TIMEOUT_MS[tier],\n provider: providerName,\n };\n}\n","/**\n * kg-extraction.ts — Shared KG triple extraction logic.\n *\n * Extracted from session-summary-worker.ts so both the worker and the\n * CLI backfill (`pai kg backfill`) can use the same code path.\n *\n * Provides:\n * - findClaudeBinary() — locate the claude CLI\n * - spawnClaude() — generic prompt -> response runner (provider-routed)\n * - extractAndStoreTriples() — run the extractor prompt and persist triples to Postgres\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nimport { buildTripleExtractionPrompt } from \"../daemon/templates/triple-extraction-prompt.js\";\nimport { planLlmSpawn, type ModelTier } from \"../workers/daemon-llm.js\";\nimport type { StorageBackend } from \"../storage/interface.js\";\n\n// ---------------------------------------------------------------------------\n// Claude CLI binary discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `claude` CLI binary. Checks common installation locations first\n * (launchd PATH is minimal so bare \"claude\" often won't resolve).\n */\nexport function findClaudeBinary(): string | null {\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"claude\"),\n join(homedir(), \".claude\", \"local\", \"claude\"),\n \"/usr/local/bin/claude\",\n \"/opt/homebrew/bin/claude\",\n ];\n\n for (const candidate of candidates) {\n try {\n if (existsSync(candidate)) return candidate;\n } catch { /* skip */ }\n }\n return \"claude\";\n}\n\n/**\n * Spawn the claude CLI with a prompt on stdin and return stdout.\n *\n * Routed through the provider registry (planLlmSpawn): the tier resolves to\n * the configured provider's model id and the env carries its base URL and\n * token. With no provider configured it degrades to the historical behaviour\n * — the tier alias itself, ambient env minus ANTHROPIC_API_KEY.\n */\nexport async function spawnClaude(\n prompt: string,\n tier: ModelTier = \"sonnet\"\n): Promise<string | null> {\n const claudeBin = findClaudeBinary();\n if (!claudeBin) {\n process.stderr.write(\"[kg-extraction] claude CLI not found.\\n\");\n return null;\n }\n\n let plan;\n try {\n plan = await planLlmSpawn(tier);\n } catch (e) {\n process.stderr.write(`[kg-extraction] could not plan ${tier} spawn: ${e}\\n`);\n return null;\n }\n\n const { spawn } = await import(\"node:child_process\");\n\n return new Promise((resolve) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const child = spawn(claudeBin, plan.args, {\n env: plan.env,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => { stdout += chunk.toString(); });\n child.stderr.on(\"data\", (chunk: Buffer) => { stderr += chunk.toString(); });\n\n child.on(\"error\", (err: Error) => {\n if (timer) { clearTimeout(timer); timer = null; }\n process.stderr.write(`[kg-extraction] ${plan.model} spawn error: ${err.message}\\n`);\n resolve(null);\n });\n\n child.on(\"close\", (code: number | null) => {\n if (timer) { clearTimeout(timer); timer = null; }\n if (code !== 0) {\n process.stderr.write(\n `[kg-extraction] ${plan.model} exited ${code}: ${stderr.slice(0, 300)}\\n`\n );\n resolve(null);\n } else {\n resolve(stdout.trim() || null);\n }\n });\n\n timer = setTimeout(() => {\n process.stderr.write(`[kg-extraction] ${plan.model} timed out — killing process.\\n`);\n child.kill(\"SIGTERM\");\n resolve(null);\n }, plan.timeoutMs);\n\n child.stdin.write(prompt);\n child.stdin.end();\n });\n}\n\n// ---------------------------------------------------------------------------\n// Triple extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Slice text down to the outermost JSON structure, dropping any surrounding\n * prose or code-fence artifacts the LLM added around the JSON object/array.\n * Returns the input unchanged if no opening brace/bracket is found.\n */\nexport function extractJsonSpan(text: string): string {\n const firstBrace = text.indexOf(\"{\");\n const firstBracket = text.indexOf(\"[\");\n\n let start = -1;\n let closeChar = \"\";\n if (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) {\n start = firstBrace;\n closeChar = \"}\";\n } else if (firstBracket !== -1) {\n start = firstBracket;\n closeChar = \"]\";\n }\n\n if (start === -1) return text;\n\n const end = text.lastIndexOf(closeChar);\n if (end === -1 || end <= start) return text;\n\n return text.slice(start, end + 1);\n}\n\nexport interface ExtractTriplesParams {\n summaryText: string;\n projectSlug: string;\n projectId: number | null;\n sessionId: string;\n gitLog?: string;\n model?: ModelTier;\n /** Tenant ID for multi-tenant entity scoping (default: \"default\") */\n tenantId?: string;\n}\n\nexport interface ExtractTriplesResult {\n extracted: number;\n added: number;\n superseded: number;\n}\n\n/**\n * Extract structured KG triples from a session summary and store them in\n * Postgres. Idempotent: if a (subject, predicate) pair already has the same\n * object, no new row is added; if the object differs, the old triple is\n * invalidated (valid_to = NOW()) and a new one is inserted.\n *\n * Best-effort: per-triple errors are caught and logged but never thrown.\n * Returns a small stats object so callers can report progress.\n */\nexport async function extractAndStoreTriples(\n backend: StorageBackend,\n params: ExtractTriplesParams\n): Promise<ExtractTriplesResult> {\n const stats: ExtractTriplesResult = { extracted: 0, added: 0, superseded: 0 };\n\n const prompt = buildTripleExtractionPrompt({\n sessionContent: params.summaryText,\n projectSlug: params.projectSlug,\n gitLog: params.gitLog ?? \"\",\n });\n\n const jsonOutput = await spawnClaude(prompt, params.model ?? \"sonnet\");\n if (!jsonOutput) return stats;\n\n // Strip markdown code fences if Claude wrapped the JSON\n let cleaned = jsonOutput\n .replace(/^```json\\s*/m, \"\")\n .replace(/^```\\s*/m, \"\")\n .replace(/\\s*```$/m, \"\")\n .trim();\n\n // Some outputs still carry leading/trailing prose or fences the regexes\n // above miss (e.g. fence not at line start); slice to the outermost\n // JSON structure before parsing.\n cleaned = extractJsonSpan(cleaned);\n\n // Support both legacy array format and new structured format\n type LegacyTriple = { subject: string; predicate: string; object: string };\n type NewRelation = { source: string; relation: string; target: string };\n type NewEntity = { name: string; type: string; description: string };\n type NewFormat = { entities: NewEntity[]; relations: NewRelation[] };\n\n let triples: Array<LegacyTriple>;\n try {\n const parsed = JSON.parse(cleaned);\n\n if (Array.isArray(parsed)) {\n // Legacy format: [{subject, predicate, object}]\n triples = parsed;\n } else if (parsed && typeof parsed === \"object\" && Array.isArray(parsed.relations)) {\n // New structured format: {entities: [...], relations: [...]}\n const newFmt = parsed as NewFormat;\n\n // QW1: Upsert entities into kg_entities\n if (Array.isArray(newFmt.entities)) {\n const tenantId = params.tenantId ?? \"default\";\n for (const entity of newFmt.entities) {\n if (!entity.name) continue;\n try {\n await backend.upsertKgEntity({\n name: entity.name,\n type: entity.type ?? \"unknown\",\n description: entity.description,\n tenantId,\n });\n } catch (entityErr) {\n process.stderr.write(`[kg-extraction] entity upsert error (${entity.name}): ${entityErr}\\n`);\n }\n }\n }\n\n triples = newFmt.relations.map((r: NewRelation) => ({\n subject: r.source,\n predicate: r.relation,\n object: r.target,\n }));\n } else {\n process.stderr.write(`[kg-extraction] Unexpected JSON shape — neither array nor {entities,relations}\\n`);\n return stats;\n }\n } catch (e) {\n const collapse = (s: string) => s.replace(/\\s+/g, \" \").trim();\n const head200 = collapse(jsonOutput.slice(0, 200));\n const tail200 = collapse(jsonOutput.slice(-200));\n process.stderr.write(\n `[kg-extraction] JSON parse failed: ${e}. head200=\"${head200}\" tail200=\"${tail200}\"\\n`\n );\n return stats;\n }\n\n if (!Array.isArray(triples)) return stats;\n stats.extracted = triples.length;\n\n for (const t of triples) {\n if (!t.subject || !t.predicate || !t.object) continue;\n\n try {\n const existing = await backend.queryKgTriples({\n subject: t.subject,\n predicate: t.predicate,\n project_id: params.projectId ?? undefined,\n });\n\n // If an identical (subject, predicate, object) is already valid, skip — idempotent\n const alreadyValid = existing.find((e) => e.object === t.object && !e.valid_to);\n if (alreadyValid) continue;\n\n // Invalidate any superseded triple (same subject+predicate, different object)\n const supersedes = existing.find((e) => e.object !== t.object && !e.valid_to);\n if (supersedes) {\n await backend.invalidateKgTriple(supersedes.id);\n stats.superseded++;\n }\n\n await backend.addKgTriple({\n subject: t.subject,\n predicate: t.predicate,\n object: t.object,\n project_id: params.projectId ?? undefined,\n source_session: params.sessionId,\n confidence: \"EXTRACTED\",\n });\n stats.added++;\n } catch (tripleErr) {\n process.stderr.write(`[kg-extraction] store error (${t.subject}): ${tripleErr}\\n`);\n }\n }\n\n return stats;\n}\n","/**\n * session-summary-worker.ts — AI-powered session note generation\n *\n * Processes `session-summary` work items by:\n * 1. Finding the current session's JSONL transcript\n * 2. Extracting user messages and assistant context\n * 3. Gathering git commits from the session period\n * 4. Spawning Claude (sonnet for compaction, opus for session end) to generate a structured summary\n * 5. Comparing the new topic against the existing note's topic\n * 6. Creating a NEW note if the topic shifted, or updating the existing one\n *\n * Topic detection: the summarizer outputs a TOPIC: line as the first line of\n * its response. This is compared against the existing note's title using word\n * overlap. If overlap is below ~30%, a new note is created.\n *\n * Designed to run inside the daemon's work queue worker. All errors are\n * thrown (not swallowed) so the work queue retry logic handles them.\n */\n\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join, basename, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nimport {\n findNotesDir,\n getCurrentNotePath,\n createSessionNote,\n addWorkToSessionNote,\n renameSessionNote,\n} from \"../hooks/ts/lib/project-utils/index.js\";\n\nimport { buildSessionSummaryPrompt } from \"./templates/session-summary-prompt.js\";\nimport {\n extractAndStoreTriples as kgExtractAndStoreTriples,\n} from \"../memory/kg-extraction.js\";\nimport { getRegistryBackend } from \"../storage/factory.js\";\nimport { storageBackend, daemonConfig } from \"./daemon/state.js\";\nimport { planLlmSpawn, type ModelTier } from \"../workers/daemon-llm.js\";\nimport { paiHomePath, resolvePaiFile, migratePaiFile, type MigrateFileResult } from \"../config/pai-home.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Minimum interval between summaries for the same project (ms). */\nconst SUMMARY_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes\n\n/** Maximum JSONL content to feed to the summarizer (characters). */\n/** Max JSONL chars per model. Opus/Sonnet can handle much more than Haiku. */\nconst MAX_JSONL_CHARS: Record<string, number> = {\n haiku: 50_000,\n sonnet: 200_000,\n opus: 500_000,\n};\n\n/** Maximum user messages to include in the prompt. */\nconst MAX_USER_MESSAGES = 30;\n\n/** File tracking last summary timestamps per project — resolved under\n * PAI_HOME, falling back to the pre-2026-09-19 ~/.config/pai location. */\nfunction oldCooldownFile(): string {\n return join(homedir(), \".config\", \"pai\", \"summary-cooldowns.json\");\n}\n\nfunction cooldownFilePath(): string {\n return resolvePaiFile(paiHomePath(\"summary-cooldowns.json\"), [oldCooldownFile()], \"pai config migrate\");\n}\n\nexport function migrateSummaryCooldowns(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"summary-cooldowns.json\"), [oldCooldownFile()], opts);\n}\n\n/** Claude Code projects directory. */\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SessionSummaryPayload {\n cwd: string;\n sessionId?: string;\n projectSlug?: string;\n transcriptPath?: string;\n /** If true, bypass the cooldown check (e.g. triggered by stop-hook at session end). */\n force?: boolean;\n /** Model tier to use for summarization (resolved to the configured\n * provider's model id). Defaults based on trigger:\n * - forced (mid-session auto-save, every N messages): \"haiku\" — runs often\n * - everything else (manual, reconstruct, batch): \"sonnet\"\n * Session end does not summarise via an LLM at all; it is mechanical. */\n model?: ModelTier;\n}\n\n// ---------------------------------------------------------------------------\n// Cooldown tracking\n// ---------------------------------------------------------------------------\n\nfunction loadCooldowns(): Record<string, number> {\n try {\n const file = cooldownFilePath();\n if (existsSync(file)) {\n return JSON.parse(readFileSync(file, \"utf-8\"));\n }\n } catch { /* ignore */ }\n return {};\n}\n\nfunction saveCooldowns(cooldowns: Record<string, number>): void {\n try {\n const file = paiHomePath(\"summary-cooldowns.json\");\n mkdirSync(dirname(file), { recursive: true });\n writeFileSync(file, JSON.stringify(cooldowns, null, 2), \"utf-8\");\n } catch { /* ignore */ }\n}\n\nfunction isOnCooldown(cwd: string): boolean {\n const cooldowns = loadCooldowns();\n const lastRun = cooldowns[cwd];\n if (!lastRun) return false;\n return Date.now() - lastRun < SUMMARY_COOLDOWN_MS;\n}\n\nfunction markCooldown(cwd: string): void {\n const cooldowns = loadCooldowns();\n cooldowns[cwd] = Date.now();\n // Prune entries older than 24 hours\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n for (const key of Object.keys(cooldowns)) {\n if (cooldowns[key] < cutoff) delete cooldowns[key];\n }\n saveCooldowns(cooldowns);\n}\n\n// ---------------------------------------------------------------------------\n// JSONL discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Encode a cwd path the same way Claude Code does for its project directories.\n * Replaces /, space, dot, and hyphen with -.\n */\nfunction encodeProjectPath(cwd: string): string {\n return cwd.replace(/[\\/\\s.\\-]/g, \"-\");\n}\n\n/**\n * Find the most recently modified JSONL file for the given project.\n *\n * Claude Code stores transcripts in:\n * ~/.claude/projects/<encoded-path>/sessions/*.jsonl\n * ~/.claude/projects/<encoded-path>/<uuid>.jsonl (legacy)\n */\nexport function findLatestJsonl(cwd: string): string | null {\n const encoded = encodeProjectPath(cwd);\n const projectDir = join(CLAUDE_PROJECTS_DIR, encoded);\n\n if (!existsSync(projectDir)) {\n process.stderr.write(\n `[session-summary] No Claude project dir found: ${projectDir}\\n`\n );\n return null;\n }\n\n // Collect all JSONL candidates\n const candidates: Array<{ path: string; mtime: number }> = [];\n\n // Check sessions/ subdirectory first (current layout)\n const sessionsDir = join(projectDir, \"sessions\");\n if (existsSync(sessionsDir)) {\n try {\n for (const f of readdirSync(sessionsDir)) {\n if (!f.endsWith(\".jsonl\")) continue;\n const fullPath = join(sessionsDir, f);\n try {\n const st = statSync(fullPath);\n candidates.push({ path: fullPath, mtime: st.mtimeMs });\n } catch { /* skip */ }\n }\n } catch { /* skip */ }\n }\n\n // Also check top-level for legacy .jsonl files\n try {\n for (const f of readdirSync(projectDir)) {\n if (!f.endsWith(\".jsonl\")) continue;\n const fullPath = join(projectDir, f);\n try {\n const st = statSync(fullPath);\n candidates.push({ path: fullPath, mtime: st.mtimeMs });\n } catch { /* skip */ }\n }\n } catch { /* skip */ }\n\n if (candidates.length === 0) {\n process.stderr.write(\n `[session-summary] No JSONL files found in ${projectDir}\\n`\n );\n return null;\n }\n\n // Sort by modification time descending — pick the most recent\n candidates.sort((a, b) => b.mtime - a.mtime);\n return candidates[0].path;\n}\n\n// ---------------------------------------------------------------------------\n// JSONL content extraction\n// ---------------------------------------------------------------------------\n\ninterface ExtractedContent {\n userMessages: string[];\n filesModified: string[];\n sessionStartTime: string;\n}\n\n/**\n * Parse a JSONL transcript and extract relevant content.\n * Filters noise, truncates to model-appropriate size from the end of the file.\n */\nfunction extractFromJsonl(jsonlPath: string, tier: ModelTier = \"sonnet\"): ExtractedContent {\n const result: ExtractedContent = {\n userMessages: [],\n filesModified: [],\n sessionStartTime: \"\",\n };\n\n let raw: string;\n try {\n raw = readFileSync(jsonlPath, \"utf-8\");\n } catch (e) {\n throw new Error(`Could not read JSONL at ${jsonlPath}: ${e}`);\n }\n\n // Truncate from the start if too large (keep the most recent content)\n const maxChars = MAX_JSONL_CHARS[tier] ?? 200_000;\n if (raw.length > maxChars) {\n const truncPoint = raw.indexOf(\"\\n\", raw.length - maxChars);\n raw = truncPoint >= 0 ? raw.slice(truncPoint + 1) : raw.slice(-MAX_JSONL_CHARS);\n }\n\n const lines = raw.trim().split(\"\\n\");\n const seenMessages = new Set<string>();\n\n for (const line of lines) {\n if (!line.trim()) continue;\n\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n\n // Track earliest timestamp\n if (entry.timestamp && !result.sessionStartTime) {\n result.sessionStartTime = String(entry.timestamp);\n }\n\n // Extract user messages\n if (entry.type === \"user\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n if (msg?.content) {\n const text = contentToText(msg.content);\n if (text && !isNoise(text) && !seenMessages.has(text)) {\n seenMessages.add(text);\n result.userMessages.push(text.slice(0, 500));\n }\n }\n }\n\n // Extract file modifications from assistant tool_use blocks\n if (entry.type === \"assistant\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n if (msg?.content && Array.isArray(msg.content)) {\n for (const block of msg.content as Array<Record<string, unknown>>) {\n if (block.type === \"tool_use\") {\n const name = block.name as string;\n const input = block.input as Record<string, unknown> | undefined;\n if ((name === \"Edit\" || name === \"Write\") && input?.file_path) {\n const fp = String(input.file_path);\n if (!result.filesModified.includes(fp)) {\n result.filesModified.push(fp);\n }\n }\n }\n }\n }\n }\n }\n\n // Limit user messages\n if (result.userMessages.length > MAX_USER_MESSAGES) {\n result.userMessages = result.userMessages.slice(-MAX_USER_MESSAGES);\n }\n\n return result;\n}\n\n/** Convert Claude content (string or content block array) to plain text. */\nexport function contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === \"string\") return c;\n const block = c as Record<string, unknown>;\n if (block?.text) return String(block.text);\n if (block?.content) return String(block.content);\n return \"\";\n })\n .join(\" \")\n .trim();\n }\n return \"\";\n}\n\n/** Filter out noise entries that shouldn't be included in the summary. */\nfunction isNoise(text: string): boolean {\n if (!text || text.length < 3) return true;\n if (text.includes(\"<task-notification>\")) return true;\n if (text.includes(\"[object Object]\")) return true;\n if (text.startsWith(\"<system-reminder>\")) return true;\n if (/^(yes|ok|sure|go|continue|weiter|thanks|thank you)\\.?$/i.test(text.trim())) return true;\n // Skip pure tool result blocks\n if (text.startsWith(\"Tool Result:\") || text.startsWith(\"tool_result\")) return true;\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Git context\n// ---------------------------------------------------------------------------\n\n/**\n * Get git log for the session period.\n * Falls back gracefully if git is not available or the dir is not a repo.\n */\nexport async function getGitContext(cwd: string, sinceTime?: string): Promise<string> {\n let since = \"6 hours ago\";\n if (sinceTime) {\n // sinceTime may be a Unix epoch (seconds as string) or already ISO 8601\n const asNum = Number(sinceTime);\n if (!isNaN(asNum) && asNum > 1_000_000_000) {\n // Unix epoch seconds → ISO 8601 (git accepts this unambiguously)\n since = new Date(asNum * 1000).toISOString();\n } else {\n since = sinceTime;\n }\n }\n\n try {\n const { execFile: execFileCb } = await import(\"node:child_process\");\n const { promisify } = await import(\"node:util\");\n const execFileAsync = promisify(execFileCb);\n\n const { stdout } = await execFileAsync(\n \"git\",\n [\"log\", \"--format=%h %ai %s\", `--since=${since}`, \"--stat\", \"--no-color\"],\n {\n cwd,\n timeout: 10_000,\n env: { ...process.env, GIT_TERMINAL_PROMPT: \"0\" },\n }\n );\n return stdout.trim();\n } catch {\n return \"\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Claude CLI spawning\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `claude` CLI binary.\n * Checks PATH first, then common installation locations.\n */\nfunction findClaudeBinary(): string | null {\n // Check known locations first (launchd PATH is minimal, bare \"claude\" won't resolve)\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"claude\"),\n join(homedir(), \".claude\", \"local\", \"claude\"),\n \"/usr/local/bin/claude\",\n \"/opt/homebrew/bin/claude\",\n ];\n\n for (const candidate of candidates) {\n try {\n if (existsSync(candidate)) return candidate;\n } catch { /* skip */ }\n }\n\n // Last resort: try bare \"claude\" in case PATH has it\n return \"claude\";\n}\n\n/**\n * Spawn a Claude model via the CLI to generate a session summary.\n * Pipes the prompt via stdin. Tier selection (resolved to the configured\n * provider's model id, falling back to the tier alias when no provider is\n * configured — see planLlmSpawn):\n * - opus: session end (best quality for final summary, runs once)\n * - sonnet: auto-compaction (good quality for incremental checkpoints, runs often)\n * - haiku: fallback / budget mode\n * Returns the generated text, or null if spawning fails.\n */\nexport async function spawnSummarizer(prompt: string, tier: ModelTier = \"sonnet\"): Promise<string | null> {\n const claudeBin = findClaudeBinary();\n if (!claudeBin) {\n process.stderr.write(\n \"[session-summary] Claude CLI not found in PATH or common locations.\\n\"\n );\n return null;\n }\n\n let plan;\n try {\n plan = await planLlmSpawn(tier);\n } catch (e) {\n process.stderr.write(`[session-summary] could not plan ${tier} spawn: ${e}\\n`);\n return null;\n }\n\n const { spawn } = await import(\"node:child_process\");\n\n return new Promise((resolve) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const child = spawn(claudeBin, plan.args, {\n env: plan.env,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString();\n });\n\n child.stderr.on(\"data\", (chunk: Buffer) => {\n stderr += chunk.toString();\n });\n\n child.on(\"error\", (err: Error) => {\n if (timer) { clearTimeout(timer); timer = null; }\n process.stderr.write(`[session-summary] ${plan.model} spawn error: ${err.message}\\n`);\n resolve(null);\n });\n\n child.on(\"close\", (code: number | null) => {\n if (timer) { clearTimeout(timer); timer = null; }\n if (code !== 0) {\n process.stderr.write(\n `[session-summary] ${plan.model} exited with code ${code}: ${stderr.slice(0, 300)}\\n`\n );\n resolve(null);\n } else {\n resolve(stdout.trim() || null);\n }\n });\n\n // Timeout protection\n timer = setTimeout(() => {\n process.stderr.write(`[session-summary] ${plan.model} timed out — killing process.\\n`);\n child.kill(\"SIGTERM\");\n resolve(null);\n }, plan.timeoutMs);\n\n // Write prompt to stdin and close\n child.stdin.write(prompt);\n child.stdin.end();\n });\n}\n\n// ---------------------------------------------------------------------------\n// Topic extraction and comparison\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the TOPIC: line from the summarizer output.\n * Returns the topic string, or null if not found.\n */\nfunction extractTopic(summaryText: string): string | null {\n const match = summaryText.match(/^TOPIC:\\s*(.+)$/m);\n if (!match) return null;\n return match[1].trim();\n}\n\n/**\n * Extract the topic from an existing session note.\n * First checks for a <!-- TOPIC: ... --> comment (stored by previous summaries).\n * Falls back to the H1 \"# Session NNNN: Title\" line.\n *\n * The HTML comment is the reliable source because the H1 gets renamed by\n * renameSessionNote, which can add/change words and cause false topic shifts.\n */\nfunction extractExistingNoteTitle(notePath: string): string | null {\n try {\n const content = readFileSync(notePath, \"utf-8\");\n // Prefer stored TOPIC comment (exact match to what the summarizer produced)\n const topicComment = content.match(/<!-- TOPIC:\\s*(.+?)\\s*-->/);\n if (topicComment) return topicComment[1].trim();\n // Fallback to H1\n const match = content.match(/^# Session \\d+:\\s*(.+)$/m);\n if (match) return match[1].trim();\n } catch { /* ignore */ }\n return null;\n}\n\n/**\n * Compute word overlap ratio between two topic strings.\n * Returns a value in [0, 1] — 1.0 means identical word sets.\n *\n * Uses lowercased, normalized words. Stop words and very short words\n * are excluded to avoid false positives on common terms.\n */\nfunction computeTopicOverlap(topicA: string, topicB: string): number {\n const stopWords = new Set([\n \"a\", \"an\", \"the\", \"and\", \"or\", \"but\", \"in\", \"on\", \"at\", \"to\", \"for\",\n \"of\", \"with\", \"by\", \"from\", \"is\", \"was\", \"are\", \"were\", \"be\", \"been\",\n \"being\", \"have\", \"has\", \"had\", \"do\", \"does\", \"did\", \"will\", \"would\",\n \"could\", \"should\", \"may\", \"might\", \"can\", \"shall\", \"this\", \"that\",\n \"these\", \"those\", \"it\", \"its\", \"new\", \"session\", \"work\", \"done\",\n ]);\n\n const normalize = (text: string): Set<string> => {\n const words = text\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 2 && !stopWords.has(w));\n return new Set(words);\n };\n\n const wordsA = normalize(topicA);\n const wordsB = normalize(topicB);\n\n if (wordsA.size === 0 || wordsB.size === 0) return 0;\n\n let intersection = 0;\n for (const w of wordsA) {\n if (wordsB.has(w)) intersection++;\n }\n\n // Jaccard similarity\n const union = new Set([...wordsA, ...wordsB]).size;\n return union > 0 ? intersection / union : 0;\n}\n\n/** Threshold: below this overlap ratio, we consider topics different. */\n// Raised from 0.3 to 0.15 — lower threshold means topics must be MORE different\n// to trigger a split. 0.3 was too aggressive: incremental work on the same project\n// (e.g., \"Flutter Rewrite\" vs \"Fix Transcription in Flutter\") was splitting into\n// separate notes on every compaction.\nconst TOPIC_OVERLAP_THRESHOLD = 0.15;\n\n// ---------------------------------------------------------------------------\n// Session note writing\n// ---------------------------------------------------------------------------\n\n/**\n * Write (or update) the session note with the AI-generated summary.\n *\n * Strategy:\n * - Find the current month's latest note\n * - If it's from today, compare topics:\n * - Same topic (overlap >= 30%) → update existing note\n * - Different topic (overlap < 30%) → create a NEW note\n * - If it's from a different day, create a new note\n */\n/**\n * Return true if the summarizer output has a meaningful body — i.e. content\n * beyond the TOPIC:/title/metadata/horizontal-rule scaffolding.\n *\n * A summary that is only headers (no Work Done items) must NOT create or update\n * a note: doing so births an empty-bodied scaffold that later rename/finalize\n * can strip to a footer-only stub. This is the born-stub failure mode.\n */\nfunction summaryHasContent(summaryText: string): boolean {\n const body = summaryText\n .replace(/^TOPIC:.*$/m, \"\")\n .replace(/^# Session:.*$/m, \"\")\n .replace(/^\\*\\*(Date|Status|Completed):\\*\\*.*$/gm, \"\")\n .replace(/^#{1,6}\\s.*$/gm, \"\") // bare section headers (## Work Done, etc.)\n .replace(/^---$/gm, \"\")\n .replace(/<!--[\\s\\S]*?-->/g, \"\") // template placeholder comments\n .trim();\n return body.length > 0;\n}\n\nfunction writeSessionNote(\n cwd: string,\n summaryText: string,\n filesModified: string[],\n sessionId?: string,\n): string | null {\n // Never create/update a note from a body-less summary — that is the born-stub\n // path (empty scaffold → stripped to a footer-only stub on finalize/rename).\n if (!summaryHasContent(summaryText)) {\n process.stderr.write(\n `[session-summary] Summary has no meaningful body — skipping note write.\\n`\n );\n return null;\n }\n\n const notesInfo = findNotesDir(cwd);\n // Identity first: the note this session already owns. Falling back to the\n // positional lookup only when there is no session id keeps older callers\n // working, but the fallback is what used to create duplicates.\n let notePath = (sessionId && findNoteBySessionId(notesInfo.path, sessionId))\n || getCurrentNotePath(notesInfo.path);\n\n const today = new Date().toISOString().split(\"T\")[0];\n\n // Extract the new topic from the summarizer output\n const newTopic = extractTopic(summaryText);\n\n if (notePath) {\n const noteFilename = basename(notePath);\n // Check if this note is from today\n const dateMatch = noteFilename.match(/(\\d{4}-\\d{2}-\\d{2})/);\n const noteDate = dateMatch ? dateMatch[1] : \"\";\n\n if (noteDate === today) {\n // Check for topic shift — two signals:\n // 1. TOPIC: line from summarizer vs existing note title (word overlap)\n // 2. topic-boundary.json written by topic-detect-worker (project-level shift)\n const existingTitle = extractExistingNoteTitle(notePath);\n let topicShifted = false;\n\n // Signal 1: summarizer topic vs existing note title\n if (newTopic && existingTitle) {\n const overlap = computeTopicOverlap(newTopic, existingTitle);\n process.stderr.write(\n `[session-summary] Topic overlap: ${(overlap * 100).toFixed(1)}%` +\n ` (new=\"${newTopic}\", existing=\"${existingTitle}\")\\n`\n );\n\n if (overlap < TOPIC_OVERLAP_THRESHOLD) {\n topicShifted = true;\n process.stderr.write(\n `[session-summary] Topic shift detected (word overlap) — creating new note.\\n`\n );\n }\n }\n\n // Signal 2: topic boundary marker from topic-detect-worker\n if (!topicShifted) {\n const boundaryPath = join(notesInfo.path, \"topic-boundary.json\");\n if (existsSync(boundaryPath)) {\n try {\n const boundary = JSON.parse(readFileSync(boundaryPath, \"utf-8\"));\n if (boundary.timestamp) {\n const boundaryAge = Date.now() - new Date(boundary.timestamp).getTime();\n // Only honor boundaries from the last 30 minutes\n if (boundaryAge < 30 * 60 * 1000) {\n topicShifted = true;\n process.stderr.write(\n `[session-summary] Topic shift detected (boundary marker) — ` +\n `${boundary.previousProject} → ${boundary.suggestedProject}\\n`\n );\n }\n }\n // Consume the boundary file (one-shot)\n unlinkSync(boundaryPath);\n } catch { /* ignore invalid boundary file */ }\n }\n }\n\n if (topicShifted) {\n // Different topic — create a NEW note (topic-based split)\n notePath = createNoteFromSummary(notesInfo.path, summaryText, sessionId);\n } else {\n // Same topic — update existing note\n updateNoteWithSummary(notePath, summaryText);\n process.stderr.write(\n `[session-summary] Updated existing note: ${noteFilename}\\n`\n );\n }\n } else {\n // Different day — create a new note\n notePath = createNoteFromSummary(notesInfo.path, summaryText, sessionId);\n }\n } else {\n // No note exists — create one\n notePath = createNoteFromSummary(notesInfo.path, summaryText, sessionId);\n }\n\n // Try to rename with a meaningful title from the summary\n if (notePath) {\n const titleMatch = summaryText.match(/^# Session:\\s*(.+)$/m);\n if (titleMatch) {\n const title = titleMatch[1].trim();\n if (title.length > 5 && title.length < 80) {\n const newPath = renameSessionNote(notePath, title);\n if (newPath !== notePath) {\n notePath = newPath;\n }\n }\n }\n }\n\n return notePath;\n}\n\n/**\n * Update an existing session note's Work Done section with AI-generated content.\n */\nfunction updateNoteWithSummary(notePath: string, summaryText: string): void {\n if (!existsSync(notePath)) return;\n\n let content = readFileSync(notePath, \"utf-8\");\n\n // Update the TOPIC comment if present (keeps topic comparison stable across renames)\n const newTopic = extractTopic(summaryText);\n if (newTopic) {\n if (content.includes(\"<!-- TOPIC:\")) {\n content = content.replace(/<!-- TOPIC:.*?-->/, `<!-- TOPIC: ${newTopic} -->`);\n } else {\n // Insert after H1 line\n content = content.replace(/^(# Session .+)$/m, `$1\\n<!-- TOPIC: ${newTopic} -->`);\n }\n }\n\n // Extract the work items from the AI summary\n const workDoneMatch = summaryText.match(\n /## Work Done\\n\\n([\\s\\S]*?)(?=\\n## Key Decisions|\\n## Known Issues|\\n\\*\\*Tags|\\n$)/\n );\n\n if (workDoneMatch) {\n const aiWorkContent = workDoneMatch[1].trim();\n const timestamp = new Date().toISOString().split(\"T\")[1].split(\".\")[0];\n\n // Add as a new subsection under Work Done\n const sectionHeader = `\\n### AI Summary (${timestamp})\\n\\n${aiWorkContent}\\n`;\n\n const nextStepsIdx = content.indexOf(\"## Next Steps\");\n const knownIssuesIdx = content.indexOf(\"## Known Issues\");\n const insertBefore = knownIssuesIdx !== -1 ? knownIssuesIdx :\n nextStepsIdx !== -1 ? nextStepsIdx :\n content.length;\n\n content = content.slice(0, insertBefore) + sectionHeader + \"\\n\" + content.slice(insertBefore);\n }\n\n // Extract and add Key Decisions if present\n const decisionsMatch = summaryText.match(\n /## Key Decisions\\n\\n([\\s\\S]*?)(?=\\n## Known Issues|\\n\\*\\*Tags|\\n$)/\n );\n if (decisionsMatch) {\n const decisions = decisionsMatch[1].trim();\n if (decisions && !content.includes(\"## Key Decisions\")) {\n const nextStepsIdx = content.indexOf(\"## Next Steps\");\n const insertAt = nextStepsIdx !== -1 ? nextStepsIdx : content.length;\n content = content.slice(0, insertAt) + `## Key Decisions\\n\\n${decisions}\\n\\n` + content.slice(insertAt);\n }\n }\n\n // Extract and add Known Issues if present\n const issuesMatch = summaryText.match(\n /## Known Issues\\n\\n([\\s\\S]*?)(?=\\n\\*\\*Tags|\\n$)/\n );\n if (issuesMatch) {\n const issues = issuesMatch[1].trim();\n if (issues && !content.includes(\"## Known Issues\")) {\n const nextStepsIdx = content.indexOf(\"## Next Steps\");\n const insertAt = nextStepsIdx !== -1 ? nextStepsIdx : content.length;\n content = content.slice(0, insertAt) + `## Known Issues\\n\\n${issues}\\n\\n` + content.slice(insertAt);\n }\n }\n\n writeFileSync(notePath, content, \"utf-8\");\n}\n\n/**\n * Create a brand new session note from the AI summary.\n */\n/**\n * Find the note this session already owns, by the marker its first write left.\n *\n * The previous resolver, getCurrentNotePath, returned the numerically highest\n * note in the month directory. That is a position, not an identity, and it is\n * why one session produced many notes: whenever a session's own note was not\n * the current maximum — two sessions in one project, or a number collision, of\n * which one corpus had 110 — the lookup missed and the checkpoint created a\n * new file instead of updating. Measured consequence in that corpus: 303 of\n * 407 notes sat in 31 same-title groups, 48 of them sharing a single title.\n *\n * Matching on the session id makes the answer exact: a session either has a\n * note or it does not, regardless of what else the directory contains.\n */\nfunction findNoteBySessionId(notesDir: string, sessionId: string): string | null {\n if (!sessionId || !existsSync(notesDir)) return null;\n const marker = `<!-- SESSION: ${sessionId} -->`;\n\n const now = new Date();\n const dirs = [0, -1].map((delta) => {\n const d = new Date(now.getFullYear(), now.getMonth() + delta, 1);\n return join(notesDir, String(d.getFullYear()),\n String(d.getMonth() + 1).padStart(2, \"0\"));\n });\n\n for (const dir of dirs) {\n if (!existsSync(dir)) continue;\n for (const f of readdirSync(dir)) {\n if (!f.endsWith(\".md\")) continue;\n const p = join(dir, f);\n try {\n if (readFileSync(p, \"utf-8\").includes(marker)) return p;\n } catch { /* unreadable file is not a match */ }\n }\n }\n return null;\n}\n\nfunction createNoteFromSummary(\n notesDir: string,\n summaryText: string,\n sessionId?: string,\n): string | null {\n try {\n // Create the note with a placeholder title\n const notePath = createSessionNote(notesDir, \"New Session\");\n\n // We will overwrite the entire content with the AI-generated summary, preserving\n // the note number (derived from the filename) and adding the standard footer.\n const noteFilename = basename(notePath);\n const numberMatch = noteFilename.match(/^(\\d+)/);\n const noteNumber = numberMatch ? numberMatch[1] : \"0000\";\n\n // Replace the H1 title from the AI summary with the numbered format\n const titleMatch = summaryText.match(/^# Session:\\s*(.+)$/m);\n const title = titleMatch ? titleMatch[1].trim() : \"New Session\";\n\n const date = new Date().toISOString().split(\"T\")[0];\n\n // Extract topic before stripping it (stored as HTML comment for future comparison)\n const topic = extractTopic(summaryText);\n\n // Build the final note content, merging AI output with the PAI note structure.\n // Strip the TOPIC: line (used for topic detection, not for the note body).\n const aiBody = summaryText\n .replace(/^TOPIC:.*$/m, \"\")\n .replace(/^# Session:.*$/m, \"\")\n .replace(/^\\*\\*Date:\\*\\*.*$/m, \"\")\n .replace(/^\\*\\*Status:\\*\\*.*$/m, \"\")\n .replace(/^---$/m, \"\")\n .trim();\n\n // The session marker is what later checkpoints of THIS session match on to\n // find and update this note rather than creating another one beside it.\n const finalContent = `# Session ${noteNumber}: ${title}\n${topic ? `<!-- TOPIC: ${topic} -->` : \"\"}${sessionId ? `\\n<!-- SESSION: ${sessionId} -->` : \"\"}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n${aiBody}\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n writeFileSync(notePath, finalContent, \"utf-8\");\n process.stderr.write(`[session-summary] Created AI-powered note: ${noteFilename}\\n`);\n return notePath;\n } catch (e) {\n process.stderr.write(`[session-summary] Failed to create note: ${e}\\n`);\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// KG triple extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Look up the integer project_id from the registry backend for a given slug.\n * Returns null if not found.\n */\nasync function lookupProjectId(slug: string): Promise<number | null> {\n try {\n const registry = await getRegistryBackend();\n const project = await registry.getProjectBySlug(slug);\n return project?.id ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Extract structured KG triples from a session summary and store them.\n *\n * This is best-effort: any error is logged but never propagated.\n * Requires Postgres backend — silently no-ops on SQLite.\n */\nasync function extractAndStoreTriples(params: {\n summaryText: string;\n projectSlug: string;\n projectId: number | null;\n sessionId: string;\n gitLog: string;\n model: string;\n}): Promise<void> {\n try {\n // Only works with Postgres backend — KG tables live in Postgres\n if (!storageBackend?.supportsPostgresFeatures) {\n return;\n }\n\n // Check config flag (default: true)\n const cfg = daemonConfig as (typeof daemonConfig & { kg_extraction_enabled?: boolean }) | undefined;\n if (cfg && cfg.kg_extraction_enabled === false) {\n process.stderr.write(\"[session-summary] Triple extraction disabled via kg_extraction_enabled=false.\\n\");\n return;\n }\n\n // storageBackend is already confirmed Postgres above — entity upserts\n // inside kgExtractAndStoreTriples go straight to Postgres, no SQLite touched.\n const result = await kgExtractAndStoreTriples(storageBackend, {\n summaryText: params.summaryText,\n projectSlug: params.projectSlug,\n projectId: params.projectId,\n sessionId: params.sessionId,\n gitLog: params.gitLog,\n model: \"sonnet\",\n });\n\n process.stderr.write(\n `[session-summary] Triple extraction complete: ` +\n `${result.extracted} extracted, ${result.added} added, ${result.superseded} superseded.\\n`\n );\n } catch (err) {\n // Entire extraction is best-effort — never fail the session summary\n process.stderr.write(`[session-summary] Triple extraction failed: ${err}\\n`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Process a `session-summary` work item.\n *\n * This is the main function called by work-queue-worker.ts.\n * Throws on fatal errors (work queue will retry with backoff).\n */\nexport async function handleSessionSummary(payload: SessionSummaryPayload): Promise<void> {\n const { cwd, sessionId, projectSlug, transcriptPath, force } = payload;\n\n if (!cwd) {\n throw new Error(\"session-summary payload missing cwd\");\n }\n\n process.stderr.write(\n `[session-summary] Starting for ${cwd}` +\n `${sessionId ? ` (session=${sessionId})` : \"\"}` +\n `${force ? \" (force=true)\" : \"\"}\\n`\n );\n\n // -------------------------------------------------------------------------\n // Cooldown check — don't summarize too frequently\n // force=true bypasses the cooldown (used by stop-hook at session end so a\n // final summary is always produced regardless of recent PreCompact runs).\n // -------------------------------------------------------------------------\n if (!force && isOnCooldown(cwd)) {\n process.stderr.write(\n \"[session-summary] Skipping — last summary was less than 30 minutes ago.\\n\"\n );\n return;\n }\n\n // -------------------------------------------------------------------------\n // Step 1: Find the JSONL transcript\n // -------------------------------------------------------------------------\n let jsonlPath: string | null = transcriptPath || null;\n\n if (jsonlPath && !existsSync(jsonlPath)) {\n process.stderr.write(\n `[session-summary] Provided transcript path not found: ${jsonlPath}\\n`\n );\n jsonlPath = null;\n }\n\n if (!jsonlPath) {\n jsonlPath = findLatestJsonl(cwd);\n }\n\n if (!jsonlPath) {\n process.stderr.write(\n \"[session-summary] No JSONL transcript found — skipping.\\n\"\n );\n return;\n }\n\n process.stderr.write(`[session-summary] Using transcript: ${jsonlPath}\\n`);\n\n // -------------------------------------------------------------------------\n // Step 2: Extract content from the JSONL\n // -------------------------------------------------------------------------\n // Model selection: opus for session end (force=true), sonnet for auto-compact\n // `force` means \"skip the cooldown\" and nothing else. It used to also select\n // opus, on the assumption that a forced run was the one final session-end\n // summary. It is not: session end is handled mechanically by handleSessionEnd\n // and never reaches here. The only thing that sets force is the stop-hook's\n // mid-session auto-save, which fires every AUTO_SAVE_INTERVAL human messages\n // — so the most expensive model was running over a full transcript every 15\n // messages, in every open session. Measured: 102 opus vs 5 sonnet in 16h.\n //\n // Incremental checkpoints are a summarisation task with a clear input; haiku\n // is sufficient. Callers wanting better can still pass `model` explicitly.\n const selectedModel = payload.model ?? (force ? \"haiku\" : \"sonnet\");\n const extracted = extractFromJsonl(jsonlPath, selectedModel);\n\n if (extracted.userMessages.length === 0) {\n process.stderr.write(\n \"[session-summary] No user messages found in transcript — skipping.\\n\"\n );\n return;\n }\n\n process.stderr.write(\n `[session-summary] Extracted ${extracted.userMessages.length} user messages, ` +\n `${extracted.filesModified.length} modified files.\\n`\n );\n\n // -------------------------------------------------------------------------\n // Step 3: Get git context\n // -------------------------------------------------------------------------\n const gitLog = await getGitContext(cwd, extracted.sessionStartTime);\n\n if (gitLog) {\n process.stderr.write(\n `[session-summary] Got git context (${gitLog.split(\"\\n\").length} lines).\\n`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 4: Build and send prompt to summarizer\n // -------------------------------------------------------------------------\n const today = new Date().toISOString().split(\"T\")[0];\n\n // Check for existing note to merge with\n const notesInfo = findNotesDir(cwd);\n const existingNotePath = getCurrentNotePath(notesInfo.path);\n let existingNote: string | undefined;\n\n if (existingNotePath) {\n const noteFilename = basename(existingNotePath);\n const dateMatch = noteFilename.match(/(\\d{4}-\\d{2}-\\d{2})/);\n if (dateMatch && dateMatch[1] === today) {\n try {\n existingNote = readFileSync(existingNotePath, \"utf-8\");\n } catch { /* ignore */ }\n }\n }\n\n const prompt = buildSessionSummaryPrompt({\n userMessages: extracted.userMessages,\n gitLog,\n cwd,\n date: today,\n filesModified: extracted.filesModified,\n existingNote,\n });\n\n process.stderr.write(\n `[session-summary] Sending ${prompt.length} char prompt to ${selectedModel}...\\n`\n );\n\n const summaryText = await spawnSummarizer(prompt, selectedModel);\n\n if (!summaryText) {\n process.stderr.write(\n `[session-summary] ${selectedModel} did not produce output — falling back to mechanical checkpoint.\\n`\n );\n // Don't throw — this is a soft failure. The existing PreCompact checkpoint\n // is sufficient. Just mark the cooldown so we don't retry too soon.\n markCooldown(cwd);\n return;\n }\n\n process.stderr.write(\n `[session-summary] ${selectedModel} produced ${summaryText.length} char summary.\\n`\n );\n\n // -------------------------------------------------------------------------\n // Step 5: Write the session note\n // -------------------------------------------------------------------------\n const notePath = writeSessionNote(cwd, summaryText, extracted.filesModified, sessionId);\n\n if (notePath) {\n process.stderr.write(\n `[session-summary] Session note written: ${basename(notePath)}\\n`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 6: Best-effort KG triple extraction (Postgres only)\n // -------------------------------------------------------------------------\n const effectiveSlug = projectSlug ?? basename(cwd);\n const projectId = projectSlug ? await lookupProjectId(projectSlug) : null;\n await extractAndStoreTriples({\n summaryText,\n projectSlug: effectiveSlug,\n projectId,\n sessionId: sessionId ?? cwd,\n gitLog,\n model: selectedModel,\n });\n\n // Mark cooldown\n markCooldown(cwd);\n\n process.stderr.write(\"[session-summary] Done.\\n\");\n}\n","/**\n * work-queue.ts — Persistent work queue for the PAI Daemon\n *\n * Provides a durable, file-backed queue that survives daemon restarts.\n * Items are processed sequentially to avoid concurrent writes to the same\n * session note. Failed items are retried with exponential backoff.\n *\n * Queue file: under PAI_HOME (~/.claude/pai/work-queue.json), falling back\n * to the pre-2026-09-19 ~/.config/pai/work-queue.json location.\n * Written atomically (write temp → rename) to prevent corruption.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n renameSync,\n mkdirSync,\n statSync,\n} from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { randomUUID } from \"node:crypto\";\nimport { paiHomePath, resolvePaiFile, migratePaiFile, type MigrateFileResult } from \"../config/pai-home.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type WorkItemType =\n | \"session-end\"\n | \"session-summary\"\n | \"context-handover\"\n | \"note-update\"\n | \"todo-update\"\n | \"topic-detect\"\n | \"registry-scan\";\n\nexport type WorkItemStatus =\n | \"pending\"\n | \"processing\"\n | \"completed\"\n | \"failed\";\n\nexport interface WorkItem {\n id: string;\n type: WorkItemType;\n priority: number; // 1=high, 5=low\n payload: Record<string, unknown>;\n status: WorkItemStatus;\n createdAt: string; // ISO timestamp\n attempts: number;\n maxAttempts: number; // default 3\n nextRetryAt?: string; // ISO timestamp — undefined means ready now\n error?: string; // last error message\n completedAt?: string; // ISO timestamp\n}\n\nexport interface WorkQueueStats {\n pending: number;\n processing: number;\n completed: number;\n failed: number;\n total: number;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nfunction oldQueueFile(): string {\n return join(homedir(), \".config\", \"pai\", \"work-queue.json\");\n}\n\nconst QUEUE_FILE = resolvePaiFile(paiHomePath(\"work-queue.json\"), [oldQueueFile()], \"pai config migrate\");\n\nexport function migrateWorkQueue(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"work-queue.json\"), [oldQueueFile()], opts);\n}\nconst MAX_QUEUE_SIZE = 1000;\nconst MAX_QUEUE_FILE_BYTES = 1024 * 1024; // 1 MB\nconst COMPLETED_TTL_MS = 60 * 60 * 1000; // 1 hour\nconst FAILED_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\n\n/** Backoff delays in ms by attempt number (0-indexed). */\nconst BACKOFF_MS = [\n 5_000, // attempt 1 → wait 5 s\n 30_000, // attempt 2 → wait 30 s\n 300_000, // attempt 3 → wait 5 min\n];\n\n// ---------------------------------------------------------------------------\n// In-memory state\n// ---------------------------------------------------------------------------\n\nlet _queue: WorkItem[] = [];\nlet _dirty = false;\n\n// ---------------------------------------------------------------------------\n// Persistence helpers\n// ---------------------------------------------------------------------------\n\n/** Load queue from disk. Call once at daemon startup. */\nexport function loadQueue(): void {\n if (!existsSync(QUEUE_FILE)) {\n _queue = [];\n return;\n }\n\n try {\n const raw = readFileSync(QUEUE_FILE, \"utf-8\");\n const parsed = JSON.parse(raw) as WorkItem[];\n if (!Array.isArray(parsed)) {\n process.stderr.write(\"[work-queue] Invalid queue file format — starting empty.\\n\");\n _queue = [];\n return;\n }\n\n // On restart, reset any 'processing' items back to 'pending' — they\n // were interrupted mid-flight and need to be retried.\n _queue = parsed.map((item) => {\n if (item.status === \"processing\") {\n return { ...item, status: \"pending\" as WorkItemStatus };\n }\n return item;\n });\n\n const stats = getStats();\n process.stderr.write(\n `[work-queue] Loaded ${_queue.length} items from disk ` +\n `(pending=${stats.pending}, failed=${stats.failed}).\\n`\n );\n } catch (e) {\n process.stderr.write(`[work-queue] Could not load queue file: ${e}\\n`);\n _queue = [];\n }\n}\n\n/** Persist queue to disk atomically. */\nexport function saveQueue(): void {\n const dir = dirname(QUEUE_FILE);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const tmpFile = QUEUE_FILE + \".tmp\";\n try {\n writeFileSync(tmpFile, JSON.stringify(_queue, null, 2), \"utf-8\");\n renameSync(tmpFile, QUEUE_FILE);\n _dirty = false;\n } catch (e) {\n process.stderr.write(`[work-queue] Could not persist queue: ${e}\\n`);\n }\n}\n\n/** Persist only if there are unsaved changes. */\nfunction saveIfDirty(): void {\n if (_dirty) saveQueue();\n}\n\n// ---------------------------------------------------------------------------\n// Queue management\n// ---------------------------------------------------------------------------\n\n/**\n * Enforce the maximum queue size cap.\n * Strategy: first drop oldest completed, then oldest low-priority pending.\n */\nfunction enforceMaxSize(): void {\n if (_queue.length <= MAX_QUEUE_SIZE) return;\n\n const excess = _queue.length - MAX_QUEUE_SIZE;\n\n // Step 1: drop oldest completed items\n const completed = _queue\n .filter((i) => i.status === \"completed\")\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n\n const toDropCompleted = completed.slice(0, excess);\n const dropIds = new Set(toDropCompleted.map((i) => i.id));\n _queue = _queue.filter((i) => !dropIds.has(i.id));\n\n if (_queue.length <= MAX_QUEUE_SIZE) return;\n\n // Step 2: drop oldest low-priority pending items (priority 4-5)\n const remainingExcess = _queue.length - MAX_QUEUE_SIZE;\n const lowPriorityPending = _queue\n .filter((i) => i.status === \"pending\" && i.priority >= 4)\n .sort((a, b) => a.priority - b.priority || a.createdAt.localeCompare(b.createdAt));\n\n const toDropLow = lowPriorityPending.slice(0, remainingExcess);\n const dropLowIds = new Set(toDropLow.map((i) => i.id));\n _queue = _queue.filter((i) => !dropLowIds.has(i.id));\n\n process.stderr.write(\n `[work-queue] Pruned queue to ${_queue.length} items (cap=${MAX_QUEUE_SIZE}).\\n`\n );\n}\n\n/**\n * Add a new work item to the queue.\n * Returns the created WorkItem.\n */\nexport function enqueue(params: {\n type: WorkItemType;\n priority?: number;\n payload: Record<string, unknown>;\n maxAttempts?: number;\n}): WorkItem {\n const item: WorkItem = {\n id: randomUUID(),\n type: params.type,\n priority: params.priority ?? 3,\n payload: params.payload,\n status: \"pending\",\n createdAt: new Date().toISOString(),\n attempts: 0,\n maxAttempts: params.maxAttempts ?? 3,\n };\n\n _queue.push(item);\n enforceMaxSize();\n _dirty = true;\n saveIfDirty();\n\n process.stderr.write(\n `[work-queue] Enqueued ${item.type} (id=${item.id}, priority=${item.priority}).\\n`\n );\n\n return item;\n}\n\n/**\n * Pick the next pending item that is ready to process (respects nextRetryAt).\n * Returns null if no eligible item exists.\n * Highest priority (lowest number) is processed first; ties broken by createdAt.\n */\nexport function dequeue(): WorkItem | null {\n const now = new Date().toISOString();\n\n const eligible = _queue\n .filter((i) => {\n if (i.status !== \"pending\") return false;\n if (i.nextRetryAt && i.nextRetryAt > now) return false;\n return true;\n })\n .sort((a, b) => {\n if (a.priority !== b.priority) return a.priority - b.priority;\n return a.createdAt.localeCompare(b.createdAt);\n });\n\n if (eligible.length === 0) return null;\n\n const item = eligible[0];\n item.status = \"processing\";\n item.attempts += 1;\n _dirty = true;\n saveIfDirty();\n\n return item;\n}\n\n/** Peek at the next eligible pending item without changing its status. */\nexport function peek(): WorkItem | null {\n const now = new Date().toISOString();\n\n return (\n _queue\n .filter((i) => {\n if (i.status !== \"pending\") return false;\n if (i.nextRetryAt && i.nextRetryAt > now) return false;\n return true;\n })\n .sort((a, b) => {\n if (a.priority !== b.priority) return a.priority - b.priority;\n return a.createdAt.localeCompare(b.createdAt);\n })[0] ?? null\n );\n}\n\n/**\n * Mark an item as completed.\n */\nexport function markCompleted(id: string): void {\n const item = _queue.find((i) => i.id === id);\n if (!item) return;\n item.status = \"completed\";\n item.completedAt = new Date().toISOString();\n item.error = undefined;\n _dirty = true;\n saveIfDirty();\n}\n\n/**\n * Mark an item as failed.\n * If attempts < maxAttempts, schedules a retry with exponential backoff.\n * Otherwise, leaves status as 'failed'.\n */\nexport function markFailed(id: string, errorMsg: string): void {\n const item = _queue.find((i) => i.id === id);\n if (!item) return;\n\n item.error = errorMsg;\n\n if (item.attempts < item.maxAttempts) {\n const backoffMs = BACKOFF_MS[item.attempts - 1] ?? BACKOFF_MS[BACKOFF_MS.length - 1];\n item.status = \"pending\";\n item.nextRetryAt = new Date(Date.now() + backoffMs).toISOString();\n process.stderr.write(\n `[work-queue] Item ${id} failed (attempt ${item.attempts}/${item.maxAttempts}), ` +\n `retry in ${backoffMs / 1000}s: ${errorMsg}\\n`\n );\n } else {\n item.status = \"failed\";\n process.stderr.write(\n `[work-queue] Item ${id} exhausted retries (${item.maxAttempts} attempts): ${errorMsg}\\n`\n );\n }\n\n _dirty = true;\n saveIfDirty();\n}\n\n// ---------------------------------------------------------------------------\n// Stats\n// ---------------------------------------------------------------------------\n\nexport function getStats(): WorkQueueStats {\n const stats: WorkQueueStats = {\n pending: 0,\n processing: 0,\n completed: 0,\n failed: 0,\n total: _queue.length,\n };\n for (const item of _queue) {\n stats[item.status as keyof Omit<WorkQueueStats, \"total\">]++;\n }\n return stats;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if any pending or processing item of the given type exists.\n * Used for debouncing work items that are expensive to run concurrently.\n */\nexport function hasPendingOrProcessingOfType(type: WorkItemType): boolean {\n return _queue.some(\n (i) =>\n i.type === type &&\n (i.status === \"pending\" || i.status === \"processing\")\n );\n}\n\n// ---------------------------------------------------------------------------\n// Housekeeping\n// ---------------------------------------------------------------------------\n\n/**\n * Remove completed and permanently-failed items older than their TTL.\n * Also force-cleans all completed items if the queue file exceeds 1 MB.\n */\nexport function cleanup(): void {\n const now = Date.now();\n const before = _queue.length;\n\n // Check file size for force-clean\n let forceCleanCompleted = false;\n try {\n if (existsSync(QUEUE_FILE)) {\n const { size } = statSync(QUEUE_FILE);\n if (size > MAX_QUEUE_FILE_BYTES) {\n forceCleanCompleted = true;\n process.stderr.write(\n `[work-queue] Queue file exceeds 1 MB (${size} bytes) — force-cleaning completed items.\\n`\n );\n }\n }\n } catch {\n // non-fatal\n }\n\n _queue = _queue.filter((item) => {\n if (item.status === \"completed\") {\n if (forceCleanCompleted) return false;\n const completedMs = item.completedAt ? new Date(item.completedAt).getTime() : 0;\n return now - completedMs < COMPLETED_TTL_MS;\n }\n if (item.status === \"failed\") {\n const createdMs = new Date(item.createdAt).getTime();\n return now - createdMs < FAILED_TTL_MS;\n }\n return true;\n });\n\n const removed = before - _queue.length;\n const stats = getStats();\n\n if (removed > 0 || before === 0) {\n process.stderr.write(\n `[work-queue] Cleanup: removed ${removed} items. ` +\n `Queue stats: pending=${stats.pending}, processing=${stats.processing}, ` +\n `completed=${stats.completed}, failed=${stats.failed}.\\n`\n );\n }\n\n _dirty = removed > 0;\n saveIfDirty();\n}\n","/**\n * context-handover-cache.ts — the well-known file where the threshold-\n * triggered pre-compaction handover (see ../../../daemon/context-handover-\n * worker.ts) is written, and where context-compression-hook.ts looks for it\n * at compaction time.\n *\n * Deliberately dependency-light (fs/os/path only). The daemon worker that\n * WRITES this file needs the heavier machinery in session-summary-worker.ts\n * (spawning Claude, git log, etc.), but the PreCompact hook that READS it\n * runs as a short-lived process on every compaction and must not drag in\n * daemon state, database pools, or anything else that module graph carries —\n * this file is the shared seam so neither side has to.\n */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport type HandoverThreshold = \"warmup\" | \"refresh\";\n\nexport interface ContextHandoverCache {\n sessionId: string;\n cwd: string;\n threshold: HandoverThreshold;\n generatedAt: string; // ISO timestamp\n model: string;\n summary: string;\n}\n\nexport function contextHandoverCachePath(sessionId: string): string {\n return join(tmpdir(), `pai-context-handover-${sessionId}.json`);\n}\n\nexport function readContextHandoverCache(sessionId: string): ContextHandoverCache | null {\n const path = contextHandoverCachePath(sessionId);\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf-8\")) as ContextHandoverCache;\n } catch {\n return null;\n }\n}\n\nexport function writeContextHandoverCache(cache: ContextHandoverCache): void {\n writeFileSync(contextHandoverCachePath(cache.sessionId), JSON.stringify(cache, null, 2), \"utf-8\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"context-handover-cache-pkHzmL3c.mjs","names":["CLAUDE_PROJECTS_DIR","resolve","homedir","existsSync","readFileSync","join","join","findNotesDir","basename","existsSync","readdirSync","join","existsSync","readdirSync","basename","readFileSync","existsSync","join","basename","readFileSync","findClaudeBinary","extractAndStoreTriples","findNotesDir","kgExtractAndStoreTriples"],"sources":["../src/registry/moved.ts","../src/cli/commands/registry/utils.ts","../src/cli/commands/registry/scan.ts","../src/hooks/ts/lib/pai-paths.ts","../src/hooks/ts/lib/project-utils/paths.ts","../src/hooks/ts/lib/project-utils/session-notes.ts","../src/session/checkpoint-block.ts","../src/hooks/ts/lib/project-utils/todo.ts","../src/daemon/templates/session-summary-prompt.ts","../src/daemon/templates/triple-extraction-prompt.ts","../src/workers/daemon-llm.ts","../src/memory/kg-extraction.ts","../src/daemon/session-summary-worker.ts","../src/daemon/work-queue.ts","../src/hooks/ts/lib/context-handover-cache.ts"],"sourcesContent":["/**\n * moved.ts — reconnect a project to transcripts that moved out from under it.\n *\n * The registry records an `encoded_dir`: the name Claude Code gave the folder\n * holding a project's transcripts. It is written once, when the project is\n * added, and nothing updates it when the project moves. So a project that has\n * been relocated points at a directory that is empty or gone, and every lookup\n * — checkpoints, handovers, session digests — quietly returns nothing.\n *\n * `resolveTranscriptDir` already re-derives the name from the project's current\n * root path, which fixes the case where the encoding is stale but the path is\n * right. It cannot fix the case where the ENCODING RULE itself does not\n * reproduce the folder name: iCloud paths with `~` in them, emoji segments, or\n * a folder Claude Code created under a path the project no longer has.\n *\n * This module answers that case by asking the transcripts instead of guessing.\n * Every transcript entry records the `cwd` it was written in, so the mapping\n * from a project root to its transcript folder is a fact sitting on disk rather\n * than something to be inferred from a naming convention. Reading it is slower\n * than a string transform and it is right, which is the correct trade for a\n * repair that runs on demand.\n */\n\nimport { existsSync, readdirSync, statSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n/** Where Claude Code keeps per-project transcript folders. */\nexport function claudeProjectsDir(): string {\n return join(homedir(), \".claude\", \"projects\");\n}\n\n/**\n * Every transcript file for one project folder — live and archived.\n *\n * The archived half matters: `session-stop` moves all but the newest transcript\n * into `sessions/`, so a folder whose work is finished has an empty top level\n * and everything underneath. Counting only the top level reports a busy project\n * as unused, which is how an earlier audit of this same problem overstated the\n * breakage by a factor of three.\n */\nexport function transcriptFiles(projectDir: string): string[] {\n const out: string[] = [];\n for (const dir of [projectDir, join(projectDir, \"sessions\")]) {\n if (!existsSync(dir)) continue;\n try {\n for (const entry of readdirSync(dir)) {\n if (entry.endsWith(\".jsonl\")) out.push(join(dir, entry));\n }\n } catch {\n /* unreadable — treat as empty rather than failing the scan */\n }\n }\n return out;\n}\n\n/**\n * The working directory a transcript was recorded in.\n *\n * Reads only the head of the file. `cwd` is present on the first entry and does\n * not change within a session, so scanning further would cost time to learn\n * nothing. Returns null rather than throwing: a truncated or half-written\n * transcript is normal for a session that is still running.\n */\nexport function cwdOfTranscript(file: string, maxBytes = 64 * 1024): string | null {\n let head: string;\n try {\n const buf = readFileSync(file);\n head = buf.subarray(0, maxBytes).toString(\"utf-8\");\n } catch {\n return null;\n }\n\n for (const line of head.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const cwd = (JSON.parse(line) as { cwd?: string }).cwd;\n if (cwd) return cwd;\n } catch {\n // A partial final line is expected when the head is cut mid-entry.\n continue;\n }\n }\n return null;\n}\n\nexport interface TranscriptFolder {\n /** Folder name under ~/.claude/projects. */\n name: string;\n /** Number of transcripts, live plus archived. */\n count: number;\n /** Newest transcript mtime, for choosing between candidates. */\n newest: number;\n /**\n * How many sampled transcripts in this folder began in the cwd being looked\n * up, and how many began somewhere else.\n *\n * One folder routinely holds transcripts for more than one directory: a\n * session started in a subdirectory can land in the parent's folder. So a\n * project appearing in a folder is not evidence the folder is ITS folder.\n * Measured 2026-08-02 — `apps/youdrill` held 5 transcripts for itself and 3\n * for `apps/youdrill/app`, and reconnecting `app` there would have attached a\n * subdirectory to its parent's history.\n */\n matching: number;\n total: number;\n}\n\n/**\n * Map every working directory seen on disk to the folders that hold its\n * transcripts.\n *\n * A directory can legitimately map to more than one folder — Claude Code's\n * encoding is lossy, so two different paths can collide, and a project moved\n * and moved back leaves both. Callers pick; this reports.\n */\nexport function scanTranscriptFolders(\n projectsDir = claudeProjectsDir(),\n sampleFilesPerFolder = 8\n): Map<string, TranscriptFolder[]> {\n const byCwd = new Map<string, TranscriptFolder[]>();\n if (!existsSync(projectsDir)) return byCwd;\n\n let entries: string[];\n try {\n entries = readdirSync(projectsDir);\n } catch {\n return byCwd;\n }\n\n for (const name of entries) {\n const dir = join(projectsDir, name);\n try {\n if (!statSync(dir).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const files = transcriptFiles(dir);\n if (files.length === 0) continue;\n\n // Newest first: a moved project's most recent transcripts carry its current\n // path, and older ones may still carry the old one.\n const mtimes = new Map<string, number>();\n for (const f of files) {\n try {\n mtimes.set(f, statSync(f).mtimeMs);\n } catch {\n mtimes.set(f, 0);\n }\n }\n const ordered = files.sort((a, b) => (mtimes.get(b) ?? 0) - (mtimes.get(a) ?? 0));\n const newest = mtimes.get(ordered[0]) ?? 0;\n\n // Count how many sampled transcripts began in each cwd, so a caller can\n // tell \"this folder is that project's\" from \"that project appears in this\n // folder\" — which are very different claims.\n const tally = new Map<string, number>();\n let sampled = 0;\n for (const f of ordered.slice(0, sampleFilesPerFolder)) {\n const cwd = cwdOfTranscript(f);\n if (!cwd) continue;\n sampled++;\n tally.set(cwd, (tally.get(cwd) ?? 0) + 1);\n }\n\n for (const [cwd, matching] of tally) {\n const record: TranscriptFolder = {\n name,\n count: files.length,\n newest,\n matching,\n total: sampled,\n };\n const list = byCwd.get(cwd);\n if (list) list.push(record);\n else byCwd.set(cwd, [record]);\n }\n }\n\n return byCwd;\n}\n\nexport interface MovedProject {\n id: number;\n slug: string;\n rootPath: string;\n /** What the registry currently claims. Null when it has never been set. */\n storedDir: string | null;\n /** The folder that actually holds this project's transcripts. */\n correctDir: string;\n transcripts: number;\n /** Sessions the registry believes exist — how much is currently unreachable. */\n sessions: number;\n}\n\nexport interface RegistryProjectRow {\n id: number;\n slug: string;\n root_path: string;\n encoded_dir: string | null;\n sessions: number;\n}\n\n/**\n * Which projects point at the wrong transcript folder, and where they belong.\n *\n * A project is only reported when its stored folder yields nothing AND its root\n * path is recorded as a `cwd` somewhere else. Both halves matter: without the\n * first this would rewrite entries that work, and without the second it would\n * have nothing better to offer than the value already there.\n *\n * `resolvesNow` is supplied by the caller so this module does not have to\n * duplicate the resolver's fallback logic — a project the shipped resolver\n * already handles is not broken and must not be \"repaired\".\n */\nexport function findMovedProjects(\n rows: RegistryProjectRow[],\n byCwd: Map<string, TranscriptFolder[]>,\n resolvesNow: (row: RegistryProjectRow) => boolean\n): MovedProject[] {\n const out: MovedProject[] = [];\n\n for (const row of rows) {\n if (resolvesNow(row)) continue;\n\n const candidates = byCwd.get(row.root_path);\n if (!candidates || candidates.length === 0) continue;\n\n // Only a folder this project DOMINATES. A folder where most transcripts\n // began somewhere else belongs to that somewhere else, and attaching a\n // subdirectory to its parent's folder would hand it a history that is\n // mostly not its own — then the no-session-id fallback, which takes the\n // newest transcript in the folder, would read a sibling's work as this\n // project's. A missed repair costs nothing; a wrong one is silent and\n // wrong in the direction of confidently reporting someone else's data.\n const owned = candidates.filter((c) => c.matching * 2 > c.total);\n if (owned.length === 0) continue;\n\n // Most transcripts wins, newest breaks a tie: the folder with the most\n // history is the one worth reconnecting to, and recency separates a live\n // folder from an abandoned duplicate.\n const best = [...owned].sort((a, b) => b.count - a.count || b.newest - a.newest)[0];\n\n if (best.name === row.encoded_dir) continue;\n\n out.push({\n id: row.id,\n slug: row.slug,\n rootPath: row.root_path,\n storedDir: row.encoded_dir,\n correctDir: best.name,\n transcripts: best.count,\n sessions: row.sessions,\n });\n }\n\n // Most sessions first: those are the projects where the breakage costs most.\n return out.sort((a, b) => b.sessions - a.sessions || b.transcripts - a.transcripts);\n}\n","/** Shared registry helpers for registry command operations. */\n\nimport type { RegistryBackend } from \"../../../storage/registry-interface.js\";\nimport { now } from \"../../utils.js\";\nimport { basename, join } from \"node:path\";\nimport { transcriptFiles, claudeProjectsDir } from \"../../../registry/moved.js\";\n\n/**\n * Would writing this encoded_dir replace a working one with a broken one?\n *\n * `encodeDir` is lossy: `~` and emoji do not survive it, so a value derived\n * from a project's path can name a folder Claude Code never created. Every\n * caller here derives its value that way, and this function is the single\n * point they all pass through.\n *\n * `pai registry reconnect` repairs those by reading the cwd recorded inside the\n * transcripts. Without this check the next scan silently reverts the repair —\n * and it reverts it in the most confusing possible way, because a read taken\n * straight after the repair still shows it. Observed twice on 2026-08-02:\n * three projects reconnected, verified, and rediscovered as broken within the\n * hour, with the repair command reporting success both times.\n *\n * Permits the write when the new value resolves to transcripts, or when the\n * existing one resolves to nothing — i.e. whenever it cannot make things worse.\n */\nasync function worthWriting(backend: RegistryBackend, projectId: number, encodedDir: string): Promise<boolean> {\n if (transcriptFiles(join(claudeProjectsDir(), encodedDir)).length > 0) return true;\n\n const project = await backend.getProjectById(projectId);\n const current = project?.encoded_dir;\n if (!current) return true;\n\n return transcriptFiles(join(claudeProjectsDir(), current)).length === 0;\n}\n\n/**\n * Upsert a project row. Returns { id, isNew }.\n *\n * Matching priority:\n * 1. root_path — most reliable; handles slug collisions\n * 2. encoded_dir — Claude project dirs are canonical\n * 3. Insert with suffix-deduplication on slug collision\n *\n * display_name is set to basename(rootPath) on INSERT so that the unified\n * listing always shows a human-readable name rather than the kebab-case slug.\n */\nexport async function upsertProject(\n backend: RegistryBackend,\n slug: string,\n rootPath: string,\n encodedDir: string\n): Promise<{ id: number; isNew: boolean }> {\n const ts = now();\n\n const byPath = await backend.getProjectByRootPath(rootPath);\n\n if (byPath) {\n const encodedOwner = await backend.getProjectByEncodedDir(encodedDir);\n\n if (\n (!encodedOwner || encodedOwner.id === byPath.id) &&\n (await worthWriting(backend, byPath.id, encodedDir))\n ) {\n await backend.updateProjectPath(byPath.id, { encodedDir }, ts);\n }\n return { id: byPath.id, isNew: false };\n }\n\n const byEncoded = await backend.getProjectByEncodedDir(encodedDir);\n\n if (byEncoded) {\n const pathOwner = await backend.getProjectByRootPath(rootPath);\n\n if (!pathOwner || pathOwner.id === byEncoded.id) {\n await backend.updateProjectPath(byEncoded.id, { rootPath }, ts);\n }\n return { id: byEncoded.id, isNew: false };\n }\n\n // Use basename(rootPath) as the human display name. If rootPath is empty or\n // just \"/\" fall back to the slug so we always have something non-empty.\n const displayName = basename(rootPath) || slug;\n\n const result = await backend.createProjectWithSlugRetry({\n baseSlug: slug,\n displayName,\n rootPath,\n encodedDir,\n createdAt: ts,\n updatedAt: ts,\n });\n\n return { id: result.id, isNew: result.created };\n}\n\n/** Upsert a session note. Returns true if newly inserted. */\nexport async function upsertSession(\n backend: RegistryBackend,\n projectId: number,\n number: number,\n date: string,\n slug: string,\n title: string,\n filename: string\n): Promise<boolean> {\n return backend.upsertSessionIfAbsent({\n projectId,\n number,\n date,\n slug,\n title,\n filename,\n createdAt: now(),\n });\n}\n","/** Registry scan command: walk ~/.claude/projects/ and populate the registry. */\n\nimport { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { realpathSync } from \"node:fs\";\nimport { join, basename, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { ok, warn, err, dim, bold } from \"../../utils.js\";\nimport { encodeDir } from \"../../utils.js\";\nimport { decodeEncodedDir, slugify, parseSessionFilename, buildEncodedDirMap } from \"../../../registry/migrate.js\";\nimport { ensurePaiMarker, discoverPaiMarkers } from \"../../../registry/pai-marker.js\";\nimport { transcriptFiles, claudeProjectsDir } from \"../../../registry/moved.js\";\nimport { upsertProject, upsertSession } from \"./utils.js\";\nimport { paiHomePath, resolvePaiFile, migratePaiFile, type MigrateFileResult } from \"../../../config/pai-home.js\";\nimport { getRegistryBackend } from \"../../../storage/factory.js\";\nimport type { RegistryBackend } from \"../../../storage/registry-interface.js\";\n\n// ---------------------------------------------------------------------------\n// clc session.json fallback map\n// ---------------------------------------------------------------------------\n\n/**\n * Build a reverse map from encoded-dir → real path using the clc session\n * registry (~/.claude/session.json).\n *\n * The clc registry stores { sessions: [{ directory, ... }, ...] }. For each\n * entry with a resolvable directory, we encode the realpath and add it to\n * the map. This lets the scanner recover project paths that are not in\n * Claude's session-registry.json but were registered by clc.\n */\nfunction buildClcDirMap(): Map<string, string> {\n const map = new Map<string, string>();\n const sessionFile = join(homedir(), \".claude\", \"session.json\");\n if (!existsSync(sessionFile)) return map;\n\n try {\n const raw = readFileSync(sessionFile, \"utf8\");\n const parsed = JSON.parse(raw) as { sessions?: Array<{ directory?: string }> };\n for (const entry of parsed.sessions ?? []) {\n const dir = entry.directory;\n if (!dir) continue;\n try {\n const real = realpathSync(dir);\n if (!existsSync(real)) continue;\n const encoded = encodeDir(real);\n map.set(encoded, real);\n } catch {\n // Unresolvable path — skip\n }\n }\n } catch {\n // Unparseable — return empty map\n }\n return map;\n}\n\n// ---------------------------------------------------------------------------\n// Config helpers\n// ---------------------------------------------------------------------------\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\n/** Old registry-scan config path: ~/.pai/config.json (pre-2026-09-19). Named\n * \"config.json\" there, but that name is taken at PAI_HOME by the daemon\n * config, so the new location is registry-scan.json instead. */\nfunction oldScanConfigFile(): string {\n return join(homedir(), \".pai\", \"config.json\");\n}\n\nfunction scanConfigFilePath(): string {\n return resolvePaiFile(paiHomePath(\"registry-scan.json\"), [oldScanConfigFile()], \"pai config migrate --registry-scan\");\n}\n\nexport function migrateScanConfig(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"registry-scan.json\"), [oldScanConfigFile()], opts);\n}\n\ninterface PaiConfig {\n scan_dirs: string[];\n}\n\nexport function loadScanConfig(): PaiConfig {\n const file = scanConfigFilePath();\n if (!existsSync(file)) return { scan_dirs: [] };\n try {\n return JSON.parse(readFileSync(file, \"utf8\")) as PaiConfig;\n } catch {\n return { scan_dirs: [] };\n }\n}\n\nexport function saveScanConfig(config: PaiConfig): void {\n const file = paiHomePath(\"registry-scan.json\");\n mkdirSync(paiHomePath(), { recursive: true });\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\", \"utf8\");\n}\n\n/**\n * Resolve a path to its canonical form, falling back to the input.\n *\n * Every path that becomes a `projects.root_path` must go through this. That\n * column is UNIQUE, so two spellings of one directory create two projects and\n * split session history between them. The spellings are not hypothetical: a\n * configured scan_dir of `~/dev/ai` (where `~/dev` is a symlink) makes every\n * project under it register under the symlinked path, while a session started\n * from the resolved path registers a rival row.\n */\nexport function canonicalPath(p: string): string {\n try {\n return realpathSync(p);\n } catch {\n return p;\n }\n}\n\nexport function resolveHome(p: string): string {\n if (p.startsWith(\"~/\")) return join(homedir(), p.slice(2));\n return resolve(p);\n}\n\n// ---------------------------------------------------------------------------\n// File discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively find all .md files in a directory, including YYYY/MM subdirectories.\n * Returns filenames (basename only).\n */\nexport function findNoteFiles(dir: string): string[] {\n const results: string[] = [];\n if (!existsSync(dir)) return results;\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isFile() && entry.name.endsWith(\".md\")) {\n results.push(entry.name);\n } else if (entry.isDirectory() && /^\\d{4}$/.test(entry.name)) {\n const yearDir = join(dir, entry.name);\n for (const monthEntry of readdirSync(yearDir, { withFileTypes: true })) {\n if (monthEntry.isDirectory() && /^\\d{2}$/.test(monthEntry.name)) {\n const monthDir = join(yearDir, monthEntry.name);\n for (const noteEntry of readdirSync(monthDir, { withFileTypes: true })) {\n if (noteEntry.isFile() && noteEntry.name.endsWith(\".md\")) {\n results.push(noteEntry.name);\n }\n }\n }\n }\n }\n }\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Scan result type\n// ---------------------------------------------------------------------------\n\nexport interface ScanResult {\n projectsScanned: number;\n projectsNew: number;\n projectsUpdated: number;\n sessionsScanned: number;\n sessionsNew: number;\n skipped: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Core scan logic\n// ---------------------------------------------------------------------------\n\nexport async function performScan(): Promise<ScanResult> {\n const backend = await getRegistryBackend();\n const result: ScanResult = {\n projectsScanned: 0,\n projectsNew: 0,\n projectsUpdated: 0,\n sessionsScanned: 0,\n sessionsNew: 0,\n skipped: [],\n };\n\n if (!existsSync(CLAUDE_PROJECTS_DIR)) {\n throw new Error(`Claude projects directory not found: ${CLAUDE_PROJECTS_DIR}`);\n }\n\n const entries = readdirSync(CLAUDE_PROJECTS_DIR).filter((name) => {\n const full = join(CLAUDE_PROJECTS_DIR, name);\n return statSync(full).isDirectory();\n });\n\n const lookupMap = buildEncodedDirMap();\n const clcMap = buildClcDirMap();\n\n for (const encodedDir of entries) {\n let rootPath = decodeEncodedDir(encodedDir, lookupMap);\n\n // If the decoded path doesn't exist, try the clc session.json fallback map.\n // The clc map is keyed by encodeDir(realpathSync(directory)), so paths that\n // were registered by clc but not in session-registry.json can be recovered.\n if (!existsSync(rootPath) && clcMap.has(encodedDir)) {\n const clcPath = clcMap.get(encodedDir)!;\n if (existsSync(clcPath)) {\n rootPath = clcPath;\n }\n }\n\n if (!existsSync(rootPath)) {\n result.skipped.push(`${encodedDir} (decoded: ${rootPath} — path not found on disk)`);\n result.projectsScanned++;\n continue;\n }\n\n // Canonicalize before upserting.\n //\n // ~/.claude/projects/ holds one encoded entry per path *spelling*, so a\n // directory reachable through a symlinked prefix (~/dev -> Cloud/Development)\n // appears twice. Decoding each to its literal path registers two projects\n // for one directory, and session history then splits between them —\n // whichever spelling the shell used gets the session.\n //\n // upsertProject looks up by root_path first, so resolving here makes the\n // second spelling land on the row the first one created instead of\n // creating a rival. Both encoded_dir values stay valid for locating\n // central notes; only the project identity is unified.\n rootPath = canonicalPath(rootPath);\n\n const slug = slugify(basename(rootPath) || encodedDir);\n const { id, isNew } = await upsertProject(backend, slug, rootPath, encodedDir);\n\n result.projectsScanned++;\n if (isNew) result.projectsNew++;\n else result.projectsUpdated++;\n\n try {\n ensurePaiMarker(rootPath, slug);\n } catch {\n // Non-fatal\n }\n\n const claudeNotesDir = join(CLAUDE_PROJECTS_DIR, encodedDir, \"Notes\");\n\n if (existsSync(claudeNotesDir)) {\n const rootNotesDir = join(rootPath, \"Notes\");\n if (claudeNotesDir !== rootNotesDir) {\n await backend.updateProjectClaudeNotesDir(id, claudeNotesDir, Date.now());\n }\n }\n\n if (!existsSync(claudeNotesDir)) continue;\n\n const noteFiles = findNoteFiles(claudeNotesDir);\n\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n result.sessionsScanned++;\n const isNewSession = await upsertSession(backend, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename);\n if (isNewSession) result.sessionsNew++;\n }\n }\n\n // Phase 2: Scan project-root Notes/ for all registered active projects\n {\n const activeProjects = await backend.listProjects({ status: \"active\" });\n\n for (const project of activeProjects) {\n const notesDir = join(project.root_path, \"Notes\");\n if (!existsSync(notesDir)) continue;\n\n let files: string[];\n try {\n files = findNoteFiles(notesDir);\n } catch {\n continue;\n }\n\n for (const filename of files) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n result.sessionsScanned++;\n const isNewSession = await upsertSession(backend, project.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename);\n if (isNewSession) result.sessionsNew++;\n }\n }\n }\n\n // Phase 3: Scan extra directories from config\n const config = loadScanConfig();\n if (config.scan_dirs.length) {\n for (const rawDir of config.scan_dirs) {\n const scanDir = resolveHome(rawDir);\n if (!existsSync(scanDir)) {\n result.skipped.push(`${rawDir} (configured scan_dir not found)`);\n continue;\n }\n\n const children = readdirSync(scanDir).filter((name) => {\n if (name.startsWith(\".\")) return false;\n const full = join(scanDir, name);\n try { return statSync(full).isDirectory(); } catch { return false; }\n });\n\n for (const child of children) {\n // Canonicalize: a scan_dir of \"~/dev/ai\" (symlinked prefix) would\n // otherwise register every project under the symlinked spelling and\n // duplicate anything already registered under the resolved one.\n const childPath = canonicalPath(join(scanDir, child));\n const childSlug = slugify(child);\n const childEncoded = encodeDir(childPath);\n\n const existing = await backend.getProjectByRootPath(childPath);\n\n if (existing) {\n result.projectsScanned++;\n result.projectsUpdated++;\n\n try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n const notesDir = join(childPath, \"Notes\");\n if (existsSync(notesDir)) {\n const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n result.sessionsScanned++;\n if (await upsertSession(backend, existing.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n result.sessionsNew++;\n }\n }\n }\n continue;\n }\n\n const { id, isNew } = await upsertProject(backend, childSlug, childPath, childEncoded);\n result.projectsScanned++;\n if (isNew) result.projectsNew++;\n else result.projectsUpdated++;\n\n try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n const notesDir = join(childPath, \"Notes\");\n if (existsSync(notesDir)) {\n const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n result.sessionsScanned++;\n if (await upsertSession(backend, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n result.sessionsNew++;\n }\n }\n }\n }\n }\n }\n\n // Phase 4: Discover PAI.md markers in scan_dirs\n if (config.scan_dirs.length) {\n const resolvedScanDirs = config.scan_dirs.map(resolveHome).filter(existsSync);\n const markers = discoverPaiMarkers(resolvedScanDirs);\n\n for (const marker of markers) {\n const registeredRow = await backend.getProjectBySlug(marker.slug);\n\n if (!registeredRow) continue;\n\n // The marker was found by walking a scan_dir, so its projectRoot carries\n // whatever spelling that dir used. Canonicalize before comparing —\n // otherwise this \"repair\" step rewrites a correct root_path back to the\n // symlinked form on every scan, undoing any deduplication.\n const markerRoot = canonicalPath(marker.projectRoot);\n\n if (registeredRow.root_path !== markerRoot) {\n const newEncoded = encodeDir(markerRoot);\n const now4 = Date.now();\n\n const encodedOwner = await backend.getProjectByEncodedDir(newEncoded);\n const pathOwner = await backend.getProjectByRootPath(markerRoot);\n\n const encodedSafe = !encodedOwner || encodedOwner.id === registeredRow.id;\n const pathSafe = !pathOwner || pathOwner.id === registeredRow.id;\n\n // Never trade a working encoded_dir for one that resolves to nothing.\n //\n // encodeDir is lossy — `~` and emoji do not survive it — so re-deriving\n // from the path can produce a folder name Claude Code never created.\n // `pai registry reconnect` repairs exactly those by reading the cwd out\n // of the transcripts, and this scan runs every 30 minutes from the\n // daemon: without this check it silently reverts that repair, and the\n // repair looks like it worked because a fresh read right afterwards\n // still shows it. Observed on 2026-08-02 — three projects reconnected,\n // reverted, and rediscovered as broken within the hour.\n const derivedResolves = transcriptFiles(join(claudeProjectsDir(), newEncoded)).length > 0;\n const currentResolves =\n Boolean(registeredRow.encoded_dir) &&\n transcriptFiles(join(claudeProjectsDir(), registeredRow.encoded_dir!)).length > 0;\n const encodedWorthWriting = derivedResolves || !currentResolves;\n\n if (encodedSafe && pathSafe && encodedWorthWriting) {\n await backend.updateProjectPath(registeredRow.id, { rootPath: markerRoot, encodedDir: newEncoded }, now4);\n } else if (pathSafe) {\n await backend.updateProjectPath(registeredRow.id, { rootPath: markerRoot }, now4);\n }\n }\n }\n }\n\n // Phase 5: Backfill display_name for rows where it still equals the slug.\n // This converts legacy entries created before the basename-based naming was\n // introduced: \"jobs-beta\" → \"Jobs Beta\" (from basename of root_path).\n {\n const all = await backend.listProjects();\n const stale = all.filter((p) => p.display_name === p.slug && p.root_path);\n\n for (const row of stale) {\n const name = basename(row.root_path);\n if (name && name !== row.slug) {\n await backend.updateProjectDisplayName(row.id, name, Date.now());\n }\n }\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// cmdScan\n// ---------------------------------------------------------------------------\n\n/**\n * Run the registry scan CLI command.\n *\n * @param opts.quick When true, skips verbose output (same scan, less noise).\n * The underlying scan is always incremental via upsert —\n * this flag exists for hook/daemon-triggered invocations that\n * want minimal log output.\n */\nexport async function cmdScan(opts: { quick?: boolean } = {}): Promise<void> {\n const config = loadScanConfig();\n if (!opts.quick) {\n console.log(dim(\"Scanning ~/.claude/projects/ ...\"));\n if (config.scan_dirs.length) {\n console.log(dim(`Scanning ${config.scan_dirs.length} extra dir(s): ${config.scan_dirs.join(\", \")}`));\n }\n console.log(dim(\"Scanning project-root Notes/ directories ...\"));\n }\n\n let result: ScanResult;\n try {\n result = await performScan();\n } catch (e) {\n console.error(err(String(e)));\n process.exitCode = 1;\n return;\n }\n\n if (!opts.quick) {\n console.log(\n ok(`Scanned ${bold(String(result.projectsScanned))} projects, ${bold(String(result.sessionsScanned))} session notes.`)\n );\n console.log(dim(` Projects: ${result.projectsNew} new, ${result.projectsUpdated} updated`));\n console.log(dim(` Sessions: ${result.sessionsNew} new`));\n\n if (result.skipped.length) {\n console.log();\n console.log(warn(` ${result.skipped.length} project(s) skipped (path not found on disk):`));\n for (const s of result.skipped.slice(0, 10)) {\n console.log(dim(` ${s}`));\n }\n if (result.skipped.length > 10) {\n console.log(dim(` ... and ${result.skipped.length - 10} more`));\n }\n }\n } else {\n // Quick mode: single compact line (stderr so it doesn't pollute pipe output)\n process.stderr.write(\n `[registry-scan] ${result.projectsScanned} projects, ${result.sessionsScanned} sessions ` +\n `(${result.projectsNew}+${result.sessionsNew} new).\\n`\n );\n }\n}\n","/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n *\n * Two different things live here, and they must not be confused:\n *\n * - ADAPTER_DIR (~/.claude by default) is the Claude Code harness adapter —\n * fixed, hardcoded paths the harness itself loads from (Hooks/, skills/,\n * agents/, commands/, settings.json, statusline-command.sh,\n * tab-color-command.sh). It is harness-specific: a future harness would\n * need its own adapter directory with its own conventions.\n * - PAI_HOME (~/.claude/pai by default — see ../../../config/pai-home.ts) is\n * where PAI's own state lives: everything hooks WRITE (History/,\n * agent-sessions.json, session-routing.json, ...) resolves there, with a\n * fallback to the pre-2026-09-19 ADAPTER_DIR location and a one-time\n * stderr notice, exactly like every other PAI_HOME file.\n *\n * ALSO loads .env file from ADAPTER_DIR so all hooks get environment\n * variables without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { ADAPTER_DIR, HOOKS_DIR, SKILLS_DIR, historyDir } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\nimport {\n paiHomePath,\n resolvePaiFile,\n migratePaiFile,\n migratePaiDir,\n type MigrateFileResult,\n type MigrateDirResult,\n} from '../../../config/pai-home.js';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE ADAPTER_DIR resolution so .env can set ADAPTER_DIR/PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.ADAPTER_DIR || process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\nfunction defaultAdapterDir(): string {\n return resolve(homedir(), '.claude');\n}\n\n/**\n * Never print when stdout/stderr is a data channel, not a human: hook\n * bundles and worker-status-line.mjs set PAI_QUIET_NOTICES=1 via their\n * esbuild banner (scripts/build-hooks.mjs), and a `pai worker run\n * --output-format json` invocation is detected directly off argv since its\n * env can't be set before this module's static imports resolve.\n */\nfunction suppressDeprecationNotices(): boolean {\n if (process.env.PAI_QUIET_NOTICES === '1') return true;\n const idx = process.argv.indexOf('--output-format');\n return idx !== -1 && process.argv[idx + 1] === 'json';\n}\n\n/**\n * At most once per process — a `globalThis` flag rather than a module-level\n * `let` because hooks and the CLI can end up with more than one instance of\n * this module loaded into the same process (separate bundles), each with its\n * own module scope; only a property on the shared global survives that.\n */\nfunction warnPaiDirDeprecatedOnce(message: string): void {\n const g = globalThis as typeof globalThis & { __paiDirNoticePrinted?: boolean };\n if (g.__paiDirNoticePrinted || suppressDeprecationNotices()) return;\n g.__paiDirNoticePrinted = true;\n process.stderr.write(`pai: ${message}\\n`);\n}\n\n/**\n * Smart ADAPTER_DIR detection with fallback\n * Priority:\n * 1. ADAPTER_DIR environment variable (if set) — warns once if PAI_DIR is\n * ALSO set and resolves to a different path (naming both).\n * 2. PAI_DIR environment variable (deprecated alias, one release) — warns\n * once only when it differs from the default adapter root; PAI_DIR set to\n * the same value as the default is the operator's current, correct\n * setting and stays silent.\n * 3. ~/.claude (standard location)\n */\nfunction resolveAdapterDir(): string {\n if (process.env.ADAPTER_DIR) {\n const adapterDir = resolve(process.env.ADAPTER_DIR);\n if (process.env.PAI_DIR) {\n const paiDir = resolve(process.env.PAI_DIR);\n if (paiDir !== adapterDir) {\n warnPaiDirDeprecatedOnce(\n `ADAPTER_DIR (${adapterDir}) and PAI_DIR (${paiDir}) are both set and differ — ADAPTER_DIR wins. PAI_DIR still works for one release.`\n );\n }\n }\n return adapterDir;\n }\n if (process.env.PAI_DIR) {\n const paiDir = resolve(process.env.PAI_DIR);\n if (paiDir !== defaultAdapterDir()) {\n warnPaiDirDeprecatedOnce(\n 'PAI_DIR is deprecated — use ADAPTER_DIR for the harness adapter root (~/.claude). PAI_DIR still works for one release.'\n );\n }\n return paiDir;\n }\n return defaultAdapterDir();\n}\n\n/** The Claude Code harness adapter root — NOT PAI's state home. See module doc above. */\nexport const ADAPTER_DIR = resolveAdapterDir();\n\n/** @deprecated alias for ADAPTER_DIR, kept for one release. Prefer ADAPTER_DIR. */\nexport const PAI_DIR = ADAPTER_DIR;\n\n/**\n * Adapter directories — fixed paths the Claude Code harness itself loads\n * from. These stay under ADAPTER_DIR; they are not PAI state.\n */\nexport const HOOKS_DIR = join(ADAPTER_DIR, 'Hooks');\nexport const SKILLS_DIR = join(ADAPTER_DIR, 'skills');\nexport const AGENTS_DIR = join(ADAPTER_DIR, 'agents');\nexport const COMMANDS_DIR = join(ADAPTER_DIR, 'commands');\n\n// ---------------------------------------------------------------------------\n// PAI state written by hooks — resolves under PAI_HOME, falling back to the\n// pre-2026-09-19 ADAPTER_DIR location (one-time stderr notice) until\n// `pai config migrate --history` moves it. Actively written on every hook\n// event across every session, so unlike most PAI_HOME files the live move is\n// deliberately NOT automatic — see `pai config migrate --history`.\n// ---------------------------------------------------------------------------\n\nfunction oldHistoryDir(): string {\n return join(ADAPTER_DIR, 'History');\n}\n\n/** Read/write location for hook-captured history: PAI_HOME/History if\n * present, else the old ADAPTER_DIR/History (one-time stderr notice). */\nexport function historyDir(): string {\n return resolvePaiFile(paiHomePath('History'), [oldHistoryDir()], 'pai config migrate --history');\n}\n\nexport function migrateHistoryDir(opts: { dryRun?: boolean } = {}): MigrateDirResult {\n return migratePaiDir(paiHomePath('History'), [oldHistoryDir()], opts);\n}\n\nfunction oldAgentSessionsPath(): string {\n return join(ADAPTER_DIR, 'agent-sessions.json');\n}\n\nexport function agentSessionsPath(): string {\n return resolvePaiFile(paiHomePath('agent-sessions.json'), [oldAgentSessionsPath()], 'pai config migrate --history');\n}\n\nexport function migrateAgentSessions(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath('agent-sessions.json'), [oldAgentSessionsPath()], opts);\n}\n\nfunction oldSecurityEventsPath(): string {\n return join(ADAPTER_DIR, 'history', 'security', 'security-events.jsonl');\n}\n\nexport function securityEventsPath(): string {\n return resolvePaiFile(\n paiHomePath('History', 'security', 'security-events.jsonl'),\n [oldSecurityEventsPath()],\n 'pai config migrate --history'\n );\n}\n\nexport function migrateSecurityEvents(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath('History', 'security', 'security-events.jsonl'), [oldSecurityEventsPath()], opts);\n}\n\nfunction oldSessionRoutingPath(): string {\n return join(ADAPTER_DIR, 'session-routing.json');\n}\n\nexport function sessionRoutingPath(): string {\n return resolvePaiFile(paiHomePath('session-routing.json'), [oldSessionRoutingPath()], 'pai config migrate --history');\n}\n\nexport function migrateSessionRouting(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath('session-routing.json'), [oldSessionRoutingPath()], opts);\n}\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(historyDir(), subdir, `${year}-${month}`, filename);\n}\n","/**\n * Path utilities — encoding, Notes/Sessions directory discovery and creation.\n */\n\nimport { existsSync, mkdirSync, readdirSync, linkSync, copyFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { ADAPTER_DIR, PAI_DIR } from '../pai-paths.js';\n\n// Re-export for consumers. PROJECTS_DIR is a harness-adjacent data dir, not\n// PAI_HOME state — out of scope for the PAI_DIR→PAI_HOME fold (see pai-paths.ts).\nexport { ADAPTER_DIR, PAI_DIR };\nexport const PROJECTS_DIR = join(ADAPTER_DIR, 'projects');\n\n/**\n * Directories known to be automated health-check / probe sessions.\n * Hooks should exit early for these to avoid registry clutter and wasted work.\n */\nconst PROBE_CWD_PATTERNS = [\n '/CodexBar/ClaudeProbe',\n '/ClaudeProbe',\n];\n\n/**\n * Check if the current working directory belongs to a probe/health-check session.\n * Returns true if hooks should skip this session entirely.\n */\nexport function isProbeSession(cwd?: string): boolean {\n const dir = cwd || process.cwd();\n return PROBE_CWD_PATTERNS.some(pattern => dir.includes(pattern));\n}\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with -\n * - Replace space with -\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-')\n .replace(/\\./g, '-')\n .replace(/ /g, '-');\n}\n\n/** Get the project directory for a given working directory. */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/** Get the Notes directory for a project (central location). */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory — checks local first, falls back to central.\n * Does NOT create the directory.\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n const localPaths = [\n join(cwd, 'Notes'),\n join(cwd, 'notes'),\n join(cwd, '.claude', 'Notes'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return { path, isLocal: true };\n }\n }\n\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/** Get the sessions/ directory for a project (stores .jsonl transcripts). */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/** Get the sessions/ directory from a project directory path. */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n// ---------------------------------------------------------------------------\n// Directory creation helpers\n// ---------------------------------------------------------------------------\n\n/** Ensure the Notes directory exists for a project. @deprecated Use ensureNotesDirSmart() */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\n return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists → use it (don't create anything new)\n * - If no local Notes/ → ensure central exists and use that\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n if (found.isLocal) return found;\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n return found;\n}\n\n/** Ensure the sessions/ directory exists for a project. */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/** Ensure the sessions/ directory exists (from project dir path). */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/**\n * Publish every project-root .jsonl into sessions/ as well, WITHOUT removing it.\n *\n * This used to renameSync, and that is how PAI destroyed its users' sessions.\n *\n * `claude --resume <uuid>` finds a transcript only at the project root. Move it\n * into sessions/ and the session becomes permanently unresumable — measured\n * 2026-08-04: `claude --resume b3462801` (867 KB of real work, sessions/ only)\n * answers \"No conversation found with session ID\", while a top-level id is found\n * fine. Nothing warned; the id still looked valid everywhere PAI displayed it.\n *\n * The damage was not occasional. This ran from a UserPromptSubmit hook excluding\n * only the CURRENT session, so every prompt anyone typed unresumed every other\n * session in the project. One PAI project measured 1 transcript at top level\n * against 52 underneath. Among the casualties was 046bb712 — the exact id PAI's\n * own handover tells the user to resume.\n *\n * A hardlink satisfies both sides, which is why this is a two-line fix rather\n * than a redesign: the archive genuinely has consumers that read sessions/\n * (session-summary-worker, registry/moved, session/autosave), and `--resume`\n * needs the root path. One inode, two names, no copy, no window where the file\n * is missing from either place.\n *\n * Never unlink the source. Tidying up another tool's store was the whole\n * mistake; a stale duplicate is free, a lost session is not. Every caller of\n * `transcriptFiles()` was checked before choosing this — they all test emptiness\n * (`.length > 0`), never count, so the duplicate cannot skew a project's stats.\n *\n * `excludeFile` keeps a hook from archiving the transcript it is itself watching\n * being written. It is not a \"finished sessions only\" guard and must not be read\n * as one: it excludes exactly one file, the caller's own, so with two sessions\n * live in one project each still archives the other mid-turn. A consumer that\n * needs \"finished only\" has to enforce that itself — this function cannot know\n * what is live.\n *\n * Returns the number of files newly archived.\n */\nexport function archiveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) return 0;\n\n const files = readdirSync(projectDir);\n let archivedCount = 0;\n\n for (const file of files) {\n if (!file.endsWith('.jsonl') || file === excludeFile) continue;\n\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n\n // Already archived — including by an earlier rename, before this was a\n // hardlink. Those are the sessions that need restoring to the root, which is\n // a separate repair and not this function's job.\n if (existsSync(destPath)) continue;\n\n try {\n linkSync(sourcePath, destPath);\n if (!silent) console.error(`Archived ${file} → sessions/ (still resumable)`);\n archivedCount++;\n } catch (error) {\n // Cross-device (EXDEV) is the realistic failure: sessions/ on another\n // volume. Copy instead, and still leave the original alone.\n try {\n copyFileSync(sourcePath, destPath);\n if (!silent) console.error(`Copied ${file} → sessions/ (hardlink unavailable)`);\n archivedCount++;\n } catch {\n if (!silent) console.error(`Could not archive ${file}: ${error}`);\n }\n }\n }\n\n return archivedCount;\n}\n\n/**\n * @deprecated Renamed to `archiveSessionFilesToSessionsDir`, which is what it\n * now does. Kept so an out-of-tree caller fails loudly at the type level rather\n * than silently keeping the old destructive name for a non-destructive action.\n */\nexport const moveSessionFilesToSessionsDir = archiveSessionFilesToSessionsDir;\n\n// ---------------------------------------------------------------------------\n// CLAUDE.md / TODO.md discovery\n// ---------------------------------------------------------------------------\n\n/** Find TODO.md — check local first, fallback to central. */\nexport function findTodoPath(cwd: string): string {\n const localPaths = [\n join(cwd, 'TODO.md'),\n join(cwd, 'notes', 'TODO.md'),\n join(cwd, 'Notes', 'TODO.md'),\n join(cwd, '.claude', 'TODO.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) return path;\n }\n\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/** Find CLAUDE.md — returns the FIRST found path. */\nexport function findClaudeMdPath(cwd: string): string | null {\n const paths = findAllClaudeMdPaths(cwd);\n return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations in priority order.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n const localPaths = [\n join(cwd, '.claude', 'CLAUDE.md'),\n join(cwd, 'CLAUDE.md'),\n join(cwd, 'Notes', 'CLAUDE.md'),\n join(cwd, 'notes', 'CLAUDE.md'),\n join(cwd, 'Prompts', 'CLAUDE.md'),\n join(cwd, 'prompts', 'CLAUDE.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) foundPaths.push(path);\n }\n\n return foundPaths;\n}\n","/**\n * Session note creation, editing, checkpointing, renaming, and finalization.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Get or create the YYYY/MM subdirectory for the current month inside notesDir. */\nfunction getMonthDir(notesDir: string): string {\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const monthDir = join(notesDir, year, month);\n if (!existsSync(monthDir)) {\n mkdirSync(monthDir, { recursive: true });\n }\n return monthDir;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Get the next note number (4-digit format: 0001, 0002, etc.).\n * Numbers are scoped per YYYY/MM directory.\n */\nexport function getNextNoteNumber(notesDir: string): string {\n const monthDir = getMonthDir(notesDir);\n\n const files = readdirSync(monthDir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-]/))\n .sort();\n\n if (files.length === 0) return '0001';\n\n let maxNumber = 0;\n for (const file of files) {\n const digitMatch = file.match(/^(\\d+)/);\n if (digitMatch) {\n const num = parseInt(digitMatch[1], 10);\n if (num > maxNumber) maxNumber = num;\n }\n }\n\n return String(maxNumber + 1).padStart(4, '0');\n}\n\n/**\n * Get the current (latest) note file path, or null if none exists.\n * Searches current month → previous month → flat notesDir (legacy).\n */\nexport function getCurrentNotePath(notesDir: string): string | null {\n if (!existsSync(notesDir)) return null;\n\n const findLatestIn = (dir: string): string | null => {\n if (!existsSync(dir)) return null;\n const files = readdirSync(dir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-].*\\.md$/))\n .sort((a, b) => {\n const numA = parseInt(a.match(/^(\\d+)/)?.[1] || '0', 10);\n const numB = parseInt(b.match(/^(\\d+)/)?.[1] || '0', 10);\n return numA - numB;\n });\n if (files.length === 0) return null;\n return join(dir, files[files.length - 1]);\n };\n\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const currentMonthDir = join(notesDir, year, month);\n const found = findLatestIn(currentMonthDir);\n if (found) return found;\n\n const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const prevYear = String(prevDate.getFullYear());\n const prevMonth = String(prevDate.getMonth() + 1).padStart(2, '0');\n const prevMonthDir = join(notesDir, prevYear, prevMonth);\n const prevFound = findLatestIn(prevMonthDir);\n if (prevFound) return prevFound;\n\n return findLatestIn(notesDir);\n}\n\n/**\n * Create a new session note.\n * Format: \"NNNN - YYYY-MM-DD - New Session.md\" filed into YYYY/MM subdirectory.\n * Claude MUST rename at session end with a meaningful description.\n */\nexport function createSessionNote(notesDir: string, description: string): string {\n const noteNumber = getNextNoteNumber(notesDir);\n const date = new Date().toISOString().split('T')[0];\n const monthDir = getMonthDir(notesDir);\n const filename = `${noteNumber} - ${date} - New Session.md`;\n const filepath = join(monthDir, filename);\n\n const content = `# Session ${noteNumber}: ${description}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n<!-- PAI will add completed work here during session -->\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n writeFileSync(filepath, content);\n console.error(`Created session note: ${filename}`);\n\n return filepath;\n}\n\n/** Append a checkpoint to the current session note. */\nexport function appendCheckpoint(notePath: string, checkpoint: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found, recreating: ${notePath}`);\n try {\n const parentDir = join(notePath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n const noteFilename = basename(notePath);\n const numberMatch = noteFilename.match(/^(\\d+)/);\n const noteNumber = numberMatch ? numberMatch[1] : '0000';\n const date = new Date().toISOString().split('T')[0];\n const content = `# Session ${noteNumber}: Recovered\\n\\n**Date:** ${date}\\n**Status:** In Progress\\n\\n---\\n\\n## Work Done\\n\\n<!-- PAI will add completed work here during session -->\\n\\n---\\n\\n## Next Steps\\n\\n<!-- To be filled at session end -->\\n\\n---\\n\\n**Tags:** #Session\\n`;\n writeFileSync(notePath, content);\n console.error(`Recreated session note: ${noteFilename}`);\n } catch (err) {\n console.error(`Failed to recreate note: ${err}`);\n return;\n }\n }\n\n const content = readFileSync(notePath, 'utf-8');\n const timestamp = new Date().toISOString();\n const checkpointText = `\\n### Checkpoint ${timestamp}\\n\\n${checkpoint}\\n`;\n\n const nextStepsIndex = content.indexOf('## Next Steps');\n const newContent = nextStepsIndex !== -1\n ? content.substring(0, nextStepsIndex) + checkpointText + content.substring(nextStepsIndex)\n : content + checkpointText;\n\n writeFileSync(notePath, newContent);\n console.error(`Checkpoint added to: ${basename(notePath)}`);\n}\n\n/** Work item for session notes. */\nexport interface WorkItem {\n title: string;\n details?: string[];\n completed?: boolean;\n}\n\n/** Add work items to the \"Work Done\" section of a session note. */\nexport function addWorkToSessionNote(notePath: string, workItems: WorkItem[], sectionTitle?: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n let workText = '';\n if (sectionTitle) workText += `\\n### ${sectionTitle}\\n\\n`;\n\n for (const item of workItems) {\n const checkbox = item.completed !== false ? '[x]' : '[ ]';\n workText += `- ${checkbox} **${item.title}**\\n`;\n if (item.details && item.details.length > 0) {\n for (const detail of item.details) {\n workText += ` - ${detail}\\n`;\n }\n }\n }\n\n const workDoneMatch = content.match(/## Work Done\\n\\n(<!-- .*? -->)?/);\n if (workDoneMatch) {\n const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;\n content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);\n } else {\n const nextStepsIndex = content.indexOf('## Next Steps');\n if (nextStepsIndex !== -1) {\n content = content.substring(0, nextStepsIndex) + workText + '\\n' + content.substring(nextStepsIndex);\n }\n }\n\n writeFileSync(notePath, content);\n console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);\n}\n\n/**\n * Check if a candidate title is meaningless / garbage.\n * Public wrapper around the internal filter for use by other hooks.\n */\nexport function isMeaningfulTitle(text: string): boolean {\n return !isMeaninglessCandidate(text);\n}\n\n/** Sanitize a string for use in a filename. */\nexport function sanitizeForFilename(str: string): string {\n return str\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .substring(0, 50);\n}\n\n/**\n * Return true if the candidate string should be rejected as a meaningful name.\n * Rejects file paths, shebangs, timestamps, system noise, XML tags, hashes, etc.\n */\nfunction isMeaninglessCandidate(text: string): boolean {\n const t = text.trim();\n if (!t) return true;\n if (t.length < 5) return true; // too short to be meaningful\n if (t.startsWith('/') || t.startsWith('~')) return true; // file path\n if (t.startsWith('#!')) return true; // shebang\n if (t.includes('[object Object]')) return true; // serialization artifact\n if (/^\\d{4}-\\d{2}-\\d{2}(T[\\d:.Z+-]+)?$/.test(t)) return true; // ISO timestamp\n if (/^\\d{1,2}:\\d{2}(:\\d{2})?(\\s*(AM|PM))?$/i.test(t)) return true; // time-only\n if (/^<[a-z-]+[\\s/>]/i.test(t)) return true; // XML/HTML tags (<task-notification>, etc.)\n if (/^[0-9a-f]{10,}$/i.test(t)) return true; // hex hash strings\n if (/^Exit code \\d+/i.test(t)) return true; // exit code messages\n if (/^Error:/i.test(t)) return true; // error messages\n if (/^This session is being continued/i.test(t)) return true; // continuation boilerplate\n if (/^\\(Bash completed/i.test(t)) return true; // bash output noise\n if (/^Task Notification$/i.test(t)) return true; // literal \"Task Notification\"\n if (/^New Session$/i.test(t)) return true; // placeholder title\n if (/^Recovered Session$/i.test(t)) return true; // placeholder title\n if (/^Continued Session$/i.test(t)) return true; // placeholder title\n if (/^Untitled Session$/i.test(t)) return true; // placeholder title\n if (/^Context Compression$/i.test(t)) return true; // compression artifact\n if (/^[A-Fa-f0-9]{8,}\\s+Output$/i.test(t)) return true; // hash + \"Output\" pattern\n return false;\n}\n\n/**\n * Extract a meaningful name from session note content and summary.\n * Looks at Work Done section headers, bold text, and summary.\n */\nexport function extractMeaningfulName(noteContent: string, summary: string): string {\n const workDoneMatch = noteContent.match(/## Work Done\\n\\n([\\s\\S]*?)(?=\\n---|\\n## Next)/);\n\n if (workDoneMatch) {\n const workDoneSection = workDoneMatch[1];\n\n const subheadings = workDoneSection.match(/### ([^\\n]+)/g);\n if (subheadings && subheadings.length > 0) {\n const firstHeading = subheadings[0].replace('### ', '').trim();\n if (!isMeaninglessCandidate(firstHeading) && firstHeading.length > 5 && firstHeading.length < 60) {\n return sanitizeForFilename(firstHeading);\n }\n }\n\n const boldMatches = workDoneSection.match(/\\*\\*([^*]+)\\*\\*/g);\n if (boldMatches && boldMatches.length > 0) {\n const firstBold = boldMatches[0].replace(/\\*\\*/g, '').trim();\n if (!isMeaninglessCandidate(firstBold) && firstBold.length > 3 && firstBold.length < 50) {\n return sanitizeForFilename(firstBold);\n }\n }\n\n const numberedItems = workDoneSection.match(/^\\d+\\.\\s+\\*\\*([^*]+)\\*\\*/m);\n if (numberedItems && !isMeaninglessCandidate(numberedItems[1])) {\n return sanitizeForFilename(numberedItems[1]);\n }\n }\n\n if (summary && summary.length > 5 && summary !== 'Session completed.' && !isMeaninglessCandidate(summary)) {\n const cleanSummary = summary\n .replace(/[^\\w\\s-]/g, ' ')\n .trim()\n .split(/\\s+/)\n .slice(0, 5)\n .join(' ');\n if (cleanSummary.length > 3 && !isMeaninglessCandidate(cleanSummary)) {\n return sanitizeForFilename(cleanSummary);\n }\n }\n\n return '';\n}\n\n/**\n * Rename a session note with a meaningful name.\n * Always uses \"NNNN - YYYY-MM-DD - Description.md\" format.\n * Returns the new path, or original path if rename fails.\n */\nexport function renameSessionNote(notePath: string, meaningfulName: string): string {\n if (!meaningfulName || !existsSync(notePath)) return notePath;\n\n const dir = join(notePath, '..');\n const oldFilename = basename(notePath);\n\n const correctMatch = oldFilename.match(/^(\\d{3,4}) - (\\d{4}-\\d{2}-\\d{2}) - .*\\.md$/);\n const legacyMatch = oldFilename.match(/^(\\d{3,4})_(\\d{4}-\\d{2}-\\d{2})_.*\\.md$/);\n const match = correctMatch || legacyMatch;\n if (!match) return notePath;\n\n const [, noteNumber, date] = match;\n\n const titleCaseName = meaningfulName\n .split(/[\\s_-]+/)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n .trim();\n\n const paddedNumber = noteNumber.padStart(4, '0');\n const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;\n const newPath = join(dir, newFilename);\n\n if (newFilename === oldFilename) return notePath;\n\n try {\n renameSync(notePath, newPath);\n console.error(`Renamed note: ${oldFilename} → ${newFilename}`);\n return newPath;\n } catch (error) {\n console.error(`Could not rename note: ${error}`);\n return notePath;\n }\n}\n\n/** Update the session note's H1 title and rename the file. */\nexport function updateSessionNoteTitle(notePath: string, newTitle: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n content = content.replace(/^# Session \\d+:.*$/m, (match) => {\n const sessionNum = match.match(/Session (\\d+)/)?.[1] || '';\n return `# Session ${sessionNum}: ${newTitle}`;\n });\n writeFileSync(notePath, content);\n renameSessionNote(notePath, sanitizeForFilename(newTitle));\n}\n\n/**\n * Finalize session note — mark as complete, add summary, rename with meaningful name.\n * IDEMPOTENT: subsequent calls are no-ops if already finalized.\n * Returns the final path (may be renamed).\n */\nexport function finalizeSessionNote(notePath: string, summary: string): string {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return notePath;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n if (content.includes('**Status:** Completed')) {\n console.error(`Note already finalized: ${basename(notePath)}`);\n return notePath;\n }\n\n content = content.replace('**Status:** In Progress', '**Status:** Completed');\n\n if (!content.includes('**Completed:**')) {\n const completionTime = new Date().toISOString();\n content = content.replace(\n '---\\n\\n## Work Done',\n `**Completed:** ${completionTime}\\n\\n---\\n\\n## Work Done`\n );\n }\n\n const nextStepsMatch = content.match(/## Next Steps\\n\\n(<!-- .*? -->)/);\n if (nextStepsMatch) {\n content = content.replace(\n nextStepsMatch[0],\n `## Next Steps\\n\\n${summary || 'Session completed.'}`\n );\n }\n\n writeFileSync(notePath, content);\n console.error(`Session note finalized: ${basename(notePath)}`);\n\n const meaningfulName = extractMeaningfulName(content, summary);\n if (meaningfulName) {\n return renameSessionNote(notePath, meaningfulName);\n }\n\n return notePath;\n}\n","/**\n * Shared \"## Continue\" checkpoint logic for a project's TODO.md.\n *\n * WHY THIS MODULE EXISTS\n * ----------------------\n * Before this, `pause.ts` and `handover.ts` each carried their own copy of\n * findProjectTodo / stripContinueSection / block-builder. Both wrote the same\n * fixed four-line block, and both stripped any existing ## Continue section\n * unconditionally. The consequence was that a rich, model-authored checkpoint\n * could never survive:\n *\n * 1. `pai pause` had no way to accept a body, so the model printed its\n * checkpoint to the terminal and it was lost.\n * 2. Even if a body had been written by hand, the session-stop hook runs\n * `pai session handover` on every clean exit, which regenerated the\n * generic block and erased it.\n *\n * So there are two jobs here:\n *\n * - AUTHORED writes (`pai pause --body-file`) carry the model's markdown\n * verbatim, wrapped in explicit start/end markers.\n * - AUTO writes (hooks) must never destroy an authored checkpoint belonging\n * to the *same* session. They may replace a stale one left by an earlier\n * session, otherwise TODO.md would show a checkpoint that no longer\n * describes where the work stands.\n *\n * PARSING\n * -------\n * A rich body can legitimately contain `---` rules and `##` headings, which the\n * old heuristic scanner treated as section terminators. Authored blocks are\n * therefore delimited by an explicit HTML-comment pair; the legacy heuristic is\n * kept only as a fallback for blocks written before this change.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n readdirSync,\n renameSync,\n mkdirSync,\n realpathSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Locations searched for a project TODO.md, in priority order. */\nexport const TODO_LOCATIONS = [\n \"Notes/TODO.md\",\n \".claude/Notes/TODO.md\",\n \"tasks/todo.md\",\n \"TODO.md\",\n];\n\nexport const MARKER_OPEN = \"<!-- pai:checkpoint\";\nexport const MARKER_CLOSE = \"<!-- /pai:checkpoint -->\";\n\n/**\n * Markers for a handover that has been superseded but not thrown away.\n *\n * Deliberately NOT `pai:checkpoint`: `locateContinue` scans for that marker and\n * for the `## Continue` heading, so an archived block written verbatim would be\n * found as the live section and rewritten in place of it. Archived entries are\n * inert by construction — different marker, no heading.\n */\nexport const ARCHIVE_OPEN = \"<!-- pai:archived-handover\";\nexport const ARCHIVE_CLOSE = \"<!-- /pai:archived-handover -->\";\nexport const ARCHIVE_HEADING = \"## Previous handovers\";\n\n/**\n * How many superseded handovers TODO.md keeps.\n *\n * Enough that a handover survives a run of sessions that each end without\n * pausing, which is the sequence that destroyed one on 2026-08-04. Not\n * unbounded: TODO.md is read by a human at the top of every session, and a file\n * that grows without limit stops being read, which is its own kind of loss.\n */\nexport const ARCHIVE_LIMIT = 5;\n\nexport const CONTINUE_HEADING = \"## Continue\";\n\n/** Who wrote the checkpoint currently in TODO.md. */\nexport type Authorship = \"model\" | \"auto\";\n\n/** Result of applying a checkpoint. */\nexport type ApplyAction = \"written\" | \"preserved\" | \"failed\";\n\n// ---------------------------------------------------------------------------\n// TODO.md discovery\n// ---------------------------------------------------------------------------\n\nexport function findProjectTodo(\n rootPath: string\n): { path: string; content: string } | null {\n for (const rel of TODO_LOCATIONS) {\n const full = join(rootPath, rel);\n if (existsSync(full)) {\n try {\n return { path: full, content: readFileSync(full, \"utf8\") };\n } catch {\n // unreadable — try next\n }\n }\n }\n return null;\n}\n\n/**\n * Resolve the TODO.md to write to, creating Notes/ if nothing exists yet.\n * Returns null only when the directory could not be created.\n */\nexport function resolveTodoTarget(\n rootPath: string,\n opts: { create?: boolean } = {}\n): { path: string; content: string } | null {\n const found = findProjectTodo(rootPath);\n if (found) return found;\n\n const notesDir = join(rootPath, \"Notes\");\n if (opts.create !== false) {\n try {\n if (!existsSync(notesDir)) mkdirSync(notesDir, { recursive: true });\n } catch {\n return null;\n }\n }\n return { path: join(notesDir, \"TODO.md\"), content: \"\" };\n}\n\n// ---------------------------------------------------------------------------\n// Marker parsing\n// ---------------------------------------------------------------------------\n\nexport interface CheckpointMeta {\n authored: Authorship;\n /** Session line the checkpoint describes, e.g. \"0003 - 2026-08-01 - Title\". */\n session?: string;\n /** Claude Code session UUID, when known. */\n sessionId?: string;\n ts?: string;\n}\n\n/**\n * Parse a `<!-- pai:checkpoint key=\"value\" ... -->` marker line.\n * Returns null when the line is not a marker.\n */\nexport function parseMarker(line: string): CheckpointMeta | null {\n const trimmed = line.trim();\n if (!trimmed.startsWith(MARKER_OPEN)) return null;\n\n const attrs: Record<string, string> = {};\n for (const m of trimmed.matchAll(/([a-zA-Z][\\w-]*)=\"([^\"]*)\"/g)) {\n attrs[m[1]] = m[2];\n }\n\n return {\n authored: attrs.authored === \"model\" ? \"model\" : \"auto\",\n session: attrs.session || undefined,\n sessionId: attrs[\"session-id\"] || undefined,\n ts: attrs.ts || undefined,\n };\n}\n\nexport interface LocatedContinue {\n /** Index of the \"## Continue\" heading line. */\n startIdx: number;\n /** Exclusive end index, past any trailing `---` separator. */\n endIdx: number;\n meta: CheckpointMeta | null;\n /** Raw lines of the section, heading included. */\n lines: string[];\n}\n\n/**\n * Lines an auto-generated block is made of. Anything else in a section is\n * content somebody put there deliberately.\n *\n * The blockquote marker is optional on every pattern. These lines appear\n * quoted when the block builder emits them as its header, and unquoted when a\n * caller passes the same text as a BODY — which the session-stop hook does: it\n * builds `Working directory: ${cwd}` and hands it over as state. Matching only\n * the quoted form made such a body read as content, which is how a session\n * that did no work at all overwrote a real handover on 2026-08-04. A line that\n * merely restates the generated header is boilerplate wherever it sits.\n */\nconst BOILERPLATE_PATTERNS = [\n /^##\\s+Continue$/,\n /^<!--\\s*\\/?pai:checkpoint/,\n /^>?\\s*\\*\\*Last session:\\*\\*/,\n /^>?\\s*\\*\\*Paused at:\\*\\*/,\n /^>?\\s*Working directory:/,\n /^>?\\s*Resume with:/,\n /^>?\\s*_No checkpoint body was recorded/,\n /^-{3,}$/,\n];\n\n/** A blank line, with or without a blockquote marker. */\nconst BLANK_LINE = /^>?\\s*$/;\n\n/**\n * True when a section contains nothing but generated header lines.\n *\n * This is the guard that stops an auto write from destroying content it did\n * not author. A session that hit the old clobbering bug may have worked around\n * it by hand — writing its state into a subsection underneath the generated\n * header lines, in an unmarked block. Those blocks predate the marker, so\n * authorship cannot be read off them; the only safe signal is whether anything\n * beyond boilerplate is present.\n */\nexport function isBoilerplateOnly(lines: string[]): boolean {\n return lines.every((line) => {\n const t = line.trim();\n return BLANK_LINE.test(t) || BOILERPLATE_PATTERNS.some((re) => re.test(t));\n });\n}\n\n/**\n * True when a checkpoint body says something the generated header does not.\n *\n * \"Has a body\" and \"has something to say\" are not the same question, and the\n * preservation rules care only about the second. An unattended writer that\n * emits a single line restating the working directory has produced a body by\n * any string test and a handover by none.\n */\nexport function hasSubstance(body: string | null | undefined): boolean {\n const text = (body ?? \"\").trim();\n if (!text) return false;\n return !isBoilerplateOnly(text.split(\"\\n\"));\n}\n\n/**\n * Extract the non-boilerplate content of a section, or \"\" if there is none.\n *\n * Interior blank lines are kept. They are not decoration — in Markdown they\n * are what separates a paragraph from the table or list that follows, so\n * dropping them (as this did while it treated every blank line as boilerplate)\n * silently welds a checkpoint body into one unreadable run.\n */\nexport function extractSectionContent(lines: string[]): string {\n const kept: string[] = [];\n for (const line of lines) {\n const t = line.trim();\n if (BOILERPLATE_PATTERNS.some((re) => re.test(t))) continue;\n kept.push(line);\n }\n return trimBlankEdges(collapseBlankRuns(kept)).join(\"\\n\");\n}\n\n/** Drop leading and trailing blank lines, leaving interior spacing alone. */\nfunction trimBlankEdges(lines: string[]): string[] {\n let start = 0;\n let end = lines.length;\n while (start < end && BLANK_LINE.test(lines[start].trim())) start += 1;\n while (end > start && BLANK_LINE.test(lines[end - 1].trim())) end -= 1;\n return lines.slice(start, end);\n}\n\n/**\n * Collapse runs of blank lines to a single blank.\n *\n * Removing a boilerplate line leaves the blank that surrounded it behind, so\n * stripping the header can open a three-line gap in the middle of the body.\n * One blank line is all Markdown needs.\n */\nfunction collapseBlankRuns(lines: string[]): string[] {\n const out: string[] = [];\n let lastWasBlank = false;\n for (const line of lines) {\n const isBlank = BLANK_LINE.test(line.trim());\n if (isBlank && lastWasBlank) continue;\n out.push(line);\n lastWasBlank = isBlank;\n }\n return out;\n}\n\n/**\n * Locate the existing ## Continue section.\n *\n * When the section carries an explicit marker pair, the close marker defines\n * the end — this is what lets a rich body contain `---` and `##` safely.\n * Otherwise the legacy heuristic applies: stop at the first `---` or the next\n * `##` heading.\n */\nexport function locateContinue(content: string): LocatedContinue | null {\n const lines = content.split(\"\\n\");\n const startIdx = lines.findIndex((l) => l.trim() === CONTINUE_HEADING);\n if (startIdx === -1) return null;\n\n // Look for an open marker in the first few lines of the section.\n let meta: CheckpointMeta | null = null;\n let markerIdx = -1;\n for (let i = startIdx + 1; i < Math.min(startIdx + 6, lines.length); i++) {\n const parsed = parseMarker(lines[i]);\n if (parsed) {\n meta = parsed;\n markerIdx = i;\n break;\n }\n // A non-blank, non-marker line means there is no marker for this section.\n if (lines[i].trim() !== \"\") break;\n }\n\n let endIdx = lines.length;\n\n if (markerIdx !== -1) {\n const closeIdx = lines.findIndex(\n (l, i) => i > markerIdx && l.trim() === MARKER_CLOSE\n );\n if (closeIdx !== -1) {\n endIdx = closeIdx + 1;\n } else {\n // Malformed (open without close) — fall back to the heuristic so we do\n // not swallow the rest of the file.\n endIdx = heuristicEnd(lines, startIdx);\n }\n } else {\n endIdx = heuristicEnd(lines, startIdx);\n }\n\n // Consume one trailing `---` separator and the blank lines around it.\n let trailingEnd = endIdx;\n while (trailingEnd < lines.length && lines[trailingEnd].trim() === \"\") {\n trailingEnd += 1;\n }\n if (trailingEnd < lines.length && lines[trailingEnd].trim() === \"---\") {\n trailingEnd += 1;\n } else {\n trailingEnd = endIdx;\n }\n\n return {\n startIdx,\n endIdx: trailingEnd,\n meta,\n lines: lines.slice(startIdx, trailingEnd),\n };\n}\n\n/**\n * Legacy scanner for blocks written before checkpoint markers existed.\n *\n * Terminates on a horizontal rule or the next level-1 or level-2 heading. Note\n * the `(?!#)` — `###` is a *subsection* of `## Continue`, not a terminator. The\n * original scanner stopped at any run of `#`, which meant a `### Restored\n * state` subsection fell outside the section entirely and could not be seen,\n * let alone carried forward.\n *\n * `#{1,2}` rather than `##`, because matching only `##` did not stop at an H1\n * and therefore ATE IT. A TODO.md whose `## Continue` block precedes its\n * `# TODO` title had the title absorbed into the section and destroyed on the\n * next regenerate — reported independently by three sessions on 2026-08-03,\n * one of which lost it three times and recovered from git each time.\n *\n * An H1 is a document-level heading. It can never be part of a `## Continue`\n * section, so treating it as a terminator is not a heuristic improvement but a\n * structural fact.\n */\nfunction heuristicEnd(lines: string[], startIdx: number): number {\n for (let i = startIdx + 1; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (\n trimmed === \"---\" ||\n (/^#{1,2}(?!#)/.test(trimmed) && trimmed !== CONTINUE_HEADING)\n ) {\n return i;\n }\n }\n return lines.length;\n}\n\n/** Remove the ## Continue section, returning the remainder of the document. */\nexport function stripContinue(content: string): string {\n const found = locateContinue(content);\n if (!found) return content;\n\n const lines = content.split(\"\\n\");\n const before = lines.slice(0, found.startIdx);\n const after = lines.slice(found.endIdx);\n while (after.length > 0 && after[0].trim() === \"\") after.shift();\n\n return [...before, ...after].join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Reading a checkpoint back\n// ---------------------------------------------------------------------------\n\nexport interface ContinueCheckpoint {\n meta: CheckpointMeta | null;\n /** The authored body — the section with generated header lines removed. */\n body: string;\n /** The full section, heading included. */\n raw: string;\n}\n\n/**\n * Read the `## Continue` checkpoint out of a TODO.md.\n *\n * Writing a checkpoint is only half of a handover; something has to deliver it\n * to the next session. This is the read side, used by the SessionStart hook.\n *\n * Returns null when there is no section, or when the section holds nothing but\n * generated header lines — a bodyless block carries no information a new\n * session does not already have, and injecting it would only add noise.\n */\nexport function readContinueCheckpoint(\n content: string\n): ContinueCheckpoint | null {\n const found = locateContinue(content);\n if (!found) return null;\n\n const body = found.meta\n ? extractMarkedBody(found.lines)\n : extractSectionContent(found.lines);\n if (!body) return null;\n\n return { meta: found.meta, body, raw: found.lines.join(\"\\n\") };\n}\n\n/**\n * Extract the body of a marker-delimited block by position rather than by\n * pattern.\n *\n * The layout is fixed: open marker, a contiguous run of `>` header lines, the\n * body, then the close marker. Because the boundaries are known exactly, the\n * body comes back byte-for-byte — including any `---` rules or `##` headings\n * of its own, which a pattern-based filter would mistake for boilerplate and\n * delete. Falls back to the pattern filter if the shape is not as expected.\n */\nfunction extractMarkedBody(lines: string[]): string {\n const markerIdx = lines.findIndex((l) => parseMarker(l) !== null);\n if (markerIdx === -1) return extractSectionContent(lines);\n\n const closeIdx = lines.findIndex(\n (l, i) => i > markerIdx && l.trim() === MARKER_CLOSE\n );\n if (closeIdx === -1) return extractSectionContent(lines);\n\n // Skip the blank lines and the `>` header run that follow the open marker.\n let bodyStart = markerIdx + 1;\n while (bodyStart < closeIdx) {\n const t = lines[bodyStart].trim();\n if (BLANK_LINE.test(t) || t.startsWith(\">\")) {\n bodyStart += 1;\n continue;\n }\n break;\n }\n\n return trimBlankEdges(lines.slice(bodyStart, closeIdx)).join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Block construction\n// ---------------------------------------------------------------------------\n\nexport interface BuildOptions {\n authored: Authorship;\n /** Human-readable session line, or \"Unknown session\". */\n sessionLine: string;\n /** Claude Code session UUID — the `claude --resume` handle. */\n sessionId?: string;\n cwd: string;\n /** Model-authored markdown. Omitted for auto blocks. */\n body?: string;\n /** Overridable for deterministic tests. */\n timestamp?: string;\n}\n\nfunction escapeAttr(value: string): string {\n return value.replace(/\"/g, \"'\");\n}\n\nexport function buildContinueBlock(opts: BuildOptions): string {\n const ts = opts.timestamp ?? new Date().toISOString();\n\n const attrs = [\n `authored=\"${opts.authored}\"`,\n `session=\"${escapeAttr(opts.sessionLine)}\"`,\n opts.sessionId ? `session-id=\"${escapeAttr(opts.sessionId)}\"` : null,\n `ts=\"${ts}\"`,\n ]\n .filter(Boolean)\n .join(\" \");\n\n const header = [\n `> **Last session:** ${opts.sessionLine}`,\n `> **Paused at:** ${ts}`,\n \">\",\n `> Working directory: ${opts.cwd}`,\n ];\n\n if (opts.sessionId) {\n header.push(\">\", `> Resume with: \\`claude --resume ${opts.sessionId}\\``);\n }\n\n const body = (opts.body ?? \"\").trim();\n\n const parts = [\n CONTINUE_HEADING,\n \"\",\n `${MARKER_OPEN} ${attrs} -->`,\n \"\",\n ...header,\n ];\n\n if (body) {\n parts.push(\"\", body);\n } else {\n parts.push(\n \">\",\n \"> _No checkpoint body was recorded — see the latest session note._\"\n );\n }\n\n parts.push(\"\", MARKER_CLOSE, \"\", \"---\", \"\");\n\n return parts.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Apply\n// ---------------------------------------------------------------------------\n\nexport interface ApplyOptions extends BuildOptions {\n /** Project root; TODO.md is resolved beneath it. */\n rootPath: string;\n /** Preview only — nothing is written. */\n dryRun?: boolean;\n}\n\nexport interface ApplyResult {\n action: ApplyAction;\n path: string | null;\n block: string;\n /** Set when action === \"preserved\". */\n preservedMeta?: CheckpointMeta;\n /** Set when unattributed content was carried forward into the new block. */\n carriedForward?: boolean;\n /** Set when a superseded model-authored handover was moved to the archive. */\n archived?: boolean;\n error?: string;\n}\n\n/**\n * Write the ## Continue block, honouring the preservation rules.\n *\n * An AUTO write is unattended — it fires from the session-stop and pre-compact\n * hooks — so it operates under one governing rule: **never destroy content it\n * did not author.** Three cases follow from that:\n *\n * 1. An authored checkpoint for the SAME session is left untouched. The hooks\n * fire after the model has already recorded the real state; overwriting it\n * with metadata is the bug this module exists to fix.\n *\n * \"Same session\" is decided by the Claude session UUID whenever both sides\n * know it, and only falls back to the human-readable session line when one\n * of them does not. The line is derived from the session note filename,\n * and `session-stop.sh` *renames and renumbers that file* — via `session\n * slug --apply` and `session cleanup --execute` — before it reaches the\n * handover step. So the key the hook computes at exit is not the key the\n * model wrote seconds earlier, and a filename-keyed comparison mismatches\n * by construction. Observed live on 2026-08-01: notes renumbered twice\n * within a single session. The UUID is the only identifier that holds\n * still.\n * 2. An authored checkpoint from an EARLIER session is stale — TODO.md would\n * otherwise keep pointing at the wrong session — so it is replaced. Its\n * content is not lost: `pai pause` mirrors every authored body into the\n * session note.\n * 3. An UNMARKED block predates the marker, so authorship cannot be read off\n * it. If it is nothing but generated header lines it is replaced. If it\n * carries anything else, that content was put there deliberately — quite\n * possibly as a hand-rolled workaround for the very clobbering this fixes —\n * and is carried forward into the new block rather than dropped.\n *\n * A MODEL write always replaces: the model is authoring the checkpoint, and a\n * newer one supersedes an older one.\n */\n/**\n * Do an existing checkpoint and an incoming write describe the same session?\n *\n * The UUID is authoritative when both sides carry one: it is assigned by Claude\n * Code and never changes for the life of the session. The session line is a\n * derived, mutable label and is only consulted when there is no UUID to compare\n * — a checkpoint written before `--session-id` was threaded through, or an auto\n * write from a caller that was not given one.\n */\nfunction isSameSession(\n meta: CheckpointMeta,\n opts: Pick<ApplyOptions, \"sessionId\" | \"sessionLine\">\n): boolean {\n if (meta.sessionId && opts.sessionId) {\n return meta.sessionId === opts.sessionId;\n }\n return meta.session === opts.sessionLine;\n}\n\n/**\n * How long a model-authored checkpoint is protected from automated overwriting.\n *\n * Sized for the race, not for the session: the gap between a model writing its\n * checkpoint and a hook firing is seconds to minutes, and an hour covers even a\n * slow checkpoint on a loaded machine. Anything genuinely from an earlier\n * session is hours or days old and still falls through to case 2, which is what\n * keeps TODO.md from freezing on a checkpoint nobody will ever replace.\n */\nconst AUTHORED_GRACE_MS = 60 * 60 * 1000;\n\n/** Was this checkpoint written recently enough to still belong to the run in progress? */\nfunction isRecent(meta: CheckpointMeta, now?: string): boolean {\n if (!meta.ts) return false; // no stamp, no claim — case 2 decides as before\n const then = Date.parse(meta.ts);\n if (Number.isNaN(then)) return false;\n const nowMs = now ? Date.parse(now) : Date.now();\n if (Number.isNaN(nowMs)) return false;\n // Guard the future too: a clock skew that puts the stamp ahead of now must\n // not read as \"very old\" and license an overwrite.\n return nowMs - then < AUTHORED_GRACE_MS;\n}\n\n/**\n * Turn a live checkpoint block into an inert archive entry.\n *\n * Two things have to go: the `## Continue` heading and the `pai:checkpoint`\n * marker. Both are what `locateContinue` looks for, so leaving either in place\n * would let a later write treat the archive as the live section — and the next\n * one after that would then archive the archive. Everything else is kept\n * verbatim, because the body is the whole reason this exists.\n */\nfunction toArchiveEntry(lines: string[], meta: CheckpointMeta | null): string[] {\n const out: string[] = [];\n const label = meta?.session ?? \"Unknown session\";\n const stamp = meta?.ts ? ` — checkpointed ${meta.ts}` : \"\";\n out.push(`${ARCHIVE_OPEN} session=\"${label}\"${meta?.ts ? ` ts=\"${meta.ts}\"` : \"\"} -->`);\n out.push(\"\");\n out.push(`### ${label}${stamp}`);\n out.push(\"\");\n for (const line of lines) {\n if (line.trim() === CONTINUE_HEADING) continue;\n if (line.trim().startsWith(MARKER_OPEN)) continue;\n if (line.trim() === MARKER_CLOSE) continue;\n if (line.trim() === \"---\") continue;\n out.push(line);\n }\n while (out.length > 0 && out[out.length - 1].trim() === \"\") out.pop();\n out.push(\"\");\n out.push(ARCHIVE_CLOSE);\n return out;\n}\n\n/**\n * Put a superseded handover into the archive section, newest first.\n *\n * `rest` is the document with the Continue section already stripped. The\n * archive lives immediately below it so a reader meets the current handover\n * first and the previous ones directly after, rather than hunting for them at\n * the bottom of a file that also holds the project's open work.\n */\nfunction archiveInto(rest: string, entry: string[]): string {\n const lines = rest.split(\"\\n\");\n const headingIdx = lines.findIndex((l) => l.trim() === ARCHIVE_HEADING);\n\n if (headingIdx === -1) {\n return [ARCHIVE_HEADING, \"\", ...entry, \"\", \"---\", \"\", rest.trimStart()].join(\"\\n\");\n }\n\n // Insert directly under the heading, then drop whatever falls past the cap.\n const before = lines.slice(0, headingIdx + 1);\n const after = lines.slice(headingIdx + 1);\n while (after.length > 0 && after[0].trim() === \"\") after.shift();\n\n const merged = [...entry, \"\", ...after];\n const kept: string[] = [];\n let seen = 0;\n for (let i = 0; i < merged.length; i++) {\n if (merged[i].trim().startsWith(ARCHIVE_OPEN)) {\n seen += 1;\n if (seen > ARCHIVE_LIMIT) {\n // Everything from here to the end of THIS entry goes; keep scanning so\n // anything after the archive section (other headings, open work) stays.\n while (i < merged.length && merged[i].trim() !== ARCHIVE_CLOSE) i++;\n continue; // skips the close marker too\n }\n }\n kept.push(merged[i]);\n }\n return [...before, \"\", ...kept].join(\"\\n\");\n}\n\nexport function applyContinue(opts: ApplyOptions): ApplyResult {\n const target = resolveTodoTarget(opts.rootPath, { create: !opts.dryRun });\n if (!target) {\n return {\n action: \"failed\",\n path: null,\n block: buildContinueBlock(opts),\n error: \"Could not resolve or create a TODO.md target\",\n };\n }\n\n const existing = locateContinue(target.content);\n let carriedForward = false;\n let effectiveBody = opts.body;\n let archiveEntry: string[] | null = null;\n\n if (opts.authored === \"auto\" && existing) {\n // Case 1 — authored, same session: hands off.\n if (existing.meta?.authored === \"model\" && isSameSession(existing.meta, opts)) {\n return {\n action: \"preserved\",\n path: target.path,\n block: buildContinueBlock(opts),\n preservedMeta: existing.meta,\n };\n }\n\n // Case 1b — the incoming write has nothing to say, and what is already\n // there does. A metadata-only block says \"see the latest session note\", so\n // replacing a real handover with one trades content for a pointer — and the\n // pointer is not always good: observed live on 2026-08-01 in the AIBroker\n // project, where the stop hook's bodyless handover overwrote the autosave's\n // body and named a session note that had never been created, leaving the\n // next session nothing to resume from.\n //\n // Deferring to the session note is only safe when the note demonstrably has\n // the content. This code cannot see that, so it does the one thing that is\n // never wrong: a write with nothing to say does not get to destroy\n // something that does. A stale-but-real handover beats a fresh dead link.\n // Scoped to blocks we marked. An unmarked block falls through to case 3,\n // which salvages its content into the new block rather than freezing the\n // old one — better, because an unmarked block has no session metadata worth\n // preserving and may predate this scheme entirely.\n //\n // The test is SUBSTANCE, not emptiness. It was emptiness until 2026-08-04,\n // when a session that was opened and immediately exited destroyed the\n // previous day's handover for every project it touched. Its stop hook found\n // no work items and no completion message, so it sent the one line it\n // always builds unconditionally — `Working directory: …`, a verbatim copy\n // of a line the header already generates. Non-empty by a string test, so\n // this guard stood down; worthless by any other reading. `hasSubstance`\n // asks the question the comment above always claimed to be asking.\n if (\n existing.meta &&\n !hasSubstance(opts.body) &&\n !isBoilerplateOnly(existing.lines)\n ) {\n return {\n action: \"preserved\",\n path: target.path,\n block: buildContinueBlock(opts),\n preservedMeta: existing.meta ?? undefined,\n };\n }\n\n // Case 2b — a model checkpoint written moments ago is THIS session's,\n // whatever the identity comparison says.\n //\n // Case 2 deliberately lets an auto write replace an authored checkpoint\n // from an EARLIER session, and that policy is right: TODO.md would freeze\n // otherwise, and `pai pause` mirrors authored bodies into the session note.\n // The bug is not the policy, it is that identity DEGRADES and healthy\n // sessions fall into it.\n //\n // `sessionId` is optional on `pai pause`. Without it isSameSession compares\n // a display line containing the note TITLE — and pausing renames the note.\n // So a session writes a checkpoint, its note is renamed, the hook fires\n // seconds later, no longer recognises its own work, and classifies it as a\n // stale earlier session. Three sessions reported exactly this on\n // 2026-08-03 from one `pai pause all`; one lost its checkpoint three times\n // and recovered from git each time. Sessions without a clean repo would\n // never have known it happened.\n //\n // Recency is the tiebreaker that identity cannot supply. A model checkpoint\n // minutes old is not a stale predecessor by any reading, so an automated\n // digest does not get to overwrite it. Older ones still fall through to\n // case 2 and are replaced as before, which is what stops TODO.md freezing\n // and keeps this from nesting carried-forward blocks without end.\n if (\n existing.meta?.authored === \"model\" &&\n isRecent(existing.meta, opts.timestamp) &&\n !isBoilerplateOnly(existing.lines)\n ) {\n return {\n action: \"preserved\",\n path: target.path,\n block: buildContinueBlock(opts),\n preservedMeta: existing.meta,\n };\n }\n\n // Case 2c — an authored handover from an EARLIER session, past the grace\n // window. Case 2 deleted it, on the reasoning that TODO.md must not freeze\n // and that `pai pause` mirrors authored bodies into the session note.\n //\n // The first half is right. The second is an assumption, and on 2026-08-04\n // it was false in the plainest way available: this project keeps no session\n // notes at all (`Notes/*` is gitignored and none exist on disk), so the\n // checkpoint WAS the only copy. It went at 08:36:29Z, to the autosave of a\n // session that had been open for sixteen minutes and had typed one line —\n // destroying, as its first act, the handover it had been started to read.\n //\n // This is also the escape the v0.27.2 guard could not close: that one asks\n // whether the block is RECENT, and a handover written the night before is\n // not. Age was never the question. Nothing here needs the old block gone;\n // it only needs the slot. So the block moves down the file instead, and the\n // freeze argument and the loss argument stop being in tension.\n //\n // Only for `model` blocks with something in them. An auto block is a\n // transcript scrape that regenerates on the next tick, and archiving those\n // would bury the real handovers under mechanical noise.\n if (\n existing.meta?.authored === \"model\" &&\n !isBoilerplateOnly(existing.lines) &&\n !isSameSession(existing.meta, opts)\n ) {\n archiveEntry = toArchiveEntry(existing.lines, existing.meta);\n }\n\n // Case 3 — unmarked block holding content nobody can attribute to us.\n if (!existing.meta && !isBoilerplateOnly(existing.lines)) {\n const salvaged = extractSectionContent(existing.lines);\n if (salvaged) {\n effectiveBody = [\n \"_Carried forward from the previous checkpoint (author unknown — this\",\n \"block predates checkpoint authorship markers):_\",\n \"\",\n salvaged,\n ].join(\"\\n\");\n carriedForward = true;\n }\n }\n }\n\n const block = buildContinueBlock({ ...opts, body: effectiveBody });\n\n if (opts.dryRun) {\n return { action: \"written\", path: target.path, block, carriedForward, archived: !!archiveEntry };\n }\n\n const rest = stripContinue(target.content).trimStart();\n const newContent = block + (archiveEntry ? archiveInto(rest, archiveEntry) : rest);\n const tmpPath = `${target.path}.continue.tmp`;\n\n try {\n writeFileSync(tmpPath, newContent, \"utf8\");\n renameSync(tmpPath, target.path);\n } catch (err) {\n try {\n if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);\n } catch {\n /* ignore */\n }\n return {\n action: \"failed\",\n path: target.path,\n block,\n error: String(err),\n };\n }\n\n return { action: \"written\", path: target.path, block, carriedForward, archived: !!archiveEntry };\n}\n\n// ---------------------------------------------------------------------------\n// Session-note discovery\n//\n// Lifted verbatim from end.ts so pause, end and any future caller share one\n// implementation. end.ts now imports these rather than carrying its own copy.\n// ---------------------------------------------------------------------------\n\n/** ADAPTER_DIR (harness adapter root) — mirrors pai-paths.ts's resolveAdapterDir(),\n * kept self-contained here to avoid importing pai-paths.ts's process.exit(1)\n * validation side effect. PAI_DIR is the deprecated alias, same as there. */\nfunction getPaiDir(): string {\n const envDir = process.env.ADAPTER_DIR || process.env.PAI_DIR;\n if (envDir) {\n try {\n return realpathSync(envDir);\n } catch {\n return envDir;\n }\n }\n return join(homedir(), \".claude\");\n}\n\n/**\n * Find the notes directory for a project — local first, then the central\n * ~/.claude/projects/<encoded>/Notes fallback. Never creates.\n */\nexport function findNotesDir(\n rootPath: string,\n encodedDir: string\n): string | null {\n for (const rel of [\"Notes\", \"notes\", \".claude/Notes\"]) {\n const p = join(rootPath, rel);\n if (existsSync(p)) return p;\n }\n const central = join(getPaiDir(), \"projects\", encodedDir, \"Notes\");\n if (existsSync(central)) return central;\n return null;\n}\n\n/**\n * Find the current (highest-numbered) session note: current month, then the\n * previous month, then a flat notesDir as legacy fallback.\n */\nexport function findLatestNote(notesDir: string): string | null {\n const findIn = (dir: string): string | null => {\n if (!existsSync(dir)) return null;\n let files: string[];\n try {\n files = readdirSync(dir);\n } catch {\n return null;\n }\n const notes = files\n .filter((f) => /^\\d{3,4}[\\s_-].*\\.md$/.test(f))\n .sort((a, b) => {\n const na = parseInt(a.match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n const nb = parseInt(b.match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n return na - nb;\n });\n return notes.length > 0 ? join(dir, notes[notes.length - 1]) : null;\n };\n\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, \"0\");\n\n const current = findIn(join(notesDir, year, month));\n if (current) return current;\n\n const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const py = String(prev.getFullYear());\n const pm = String(prev.getMonth() + 1).padStart(2, \"0\");\n const prevFound = findIn(join(notesDir, py, pm));\n if (prevFound) return prevFound;\n\n return findIn(notesDir);\n}\n\n// ---------------------------------------------------------------------------\n// Session-note append\n// ---------------------------------------------------------------------------\n\n/**\n * Append the checkpoint body to a session note.\n *\n * TODO.md's ## Continue is a single slot that every later checkpoint\n * overwrites. The session note is the durable record, so the body goes to both.\n * Idempotent per timestamp: re-running with the same stamp will not duplicate.\n */\nexport function appendCheckpointToNote(\n notePath: string,\n body: string,\n timestamp?: string\n): { appended: boolean; error?: string } {\n const ts = timestamp ?? new Date().toISOString();\n const heading = `## Pause Checkpoint — ${ts}`;\n\n let existing: string;\n try {\n existing = readFileSync(notePath, \"utf8\");\n } catch (err) {\n return { appended: false, error: String(err) };\n }\n\n if (existing.includes(heading)) return { appended: false };\n\n const block = `\\n\\n---\\n\\n${heading}\\n\\n${body.trim()}\\n`;\n const tmpPath = `${notePath}.checkpoint.tmp`;\n\n try {\n writeFileSync(tmpPath, existing.trimEnd() + block, \"utf8\");\n renameSync(tmpPath, notePath);\n } catch (err) {\n try {\n if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);\n } catch {\n /* ignore */\n }\n return { appended: false, error: String(err) };\n }\n\n return { appended: true };\n}\n\n// ---------------------------------------------------------------------------\n// Body loading\n// ---------------------------------------------------------------------------\n\n/** Read a checkpoint body from a file, or from stdin when path is \"-\". */\nexport function readBodyFile(path: string): string {\n if (path === \"-\") {\n return readFileSync(0, \"utf8\");\n }\n return readFileSync(path, \"utf8\");\n}\n","/**\n * TODO.md management — creation, task updates, checkpoints, and Continue section.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { findTodoPath } from './paths.js';\nimport { applyContinue } from '../../../../session/checkpoint-block.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Task item for TODO.md. */\nexport interface TodoItem {\n content: string;\n completed: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Ensure TODO.md exists. Creates it with default structure if missing.\n * Returns the path to the TODO.md file.\n */\nexport function ensureTodoMd(cwd: string): string {\n const todoPath = findTodoPath(cwd);\n\n if (!existsSync(todoPath)) {\n const parentDir = join(todoPath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n\n const content = `# TODO\n\n## Current Session\n\n- [ ] (Tasks will be tracked here)\n\n## Backlog\n\n- [ ] (Future tasks)\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Created TODO.md: ${todoPath}`);\n }\n\n return todoPath;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Update TODO.md with current session tasks.\n * Preserves the Backlog section and ensures exactly ONE timestamp at the end.\n */\nexport function updateTodoMd(cwd: string, tasks: TodoItem[], sessionSummary?: string): void {\n const todoPath = ensureTodoMd(cwd);\n const content = readFileSync(todoPath, 'utf-8');\n\n const backlogMatch = content.match(/## Backlog[\\s\\S]*?(?=\\n---|\\n\\*Last updated|$)/);\n const backlogSection = backlogMatch\n ? backlogMatch[0].trim()\n : '## Backlog\\n\\n- [ ] (Future tasks)';\n\n const taskLines = tasks.length > 0\n ? tasks.map(t => `- [${t.completed ? 'x' : ' '}] ${t.content}`).join('\\n')\n : '- [ ] (No active tasks)';\n\n const newContent = `# TODO\n\n## Current Session\n\n${taskLines}\n\n${sessionSummary ? `**Session Summary:** ${sessionSummary}\\n\\n` : ''}${backlogSection}\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, newContent);\n console.error(`Updated TODO.md: ${todoPath}`);\n}\n\n/**\n * Add a checkpoint entry to TODO.md (without replacing tasks).\n * Ensures exactly ONE timestamp line at the end.\n */\nexport function addTodoCheckpoint(cwd: string, checkpoint: string): void {\n const todoPath = ensureTodoMd(cwd);\n let content = readFileSync(todoPath, 'utf-8');\n\n // Remove ALL existing timestamp lines and trailing separators\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n\n const checkpointText = `\\n**Checkpoint (${new Date().toISOString()}):** ${checkpoint}\\n\\n`;\n\n const backlogIndex = content.indexOf('## Backlog');\n if (backlogIndex !== -1) {\n content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);\n } else {\n const continueIndex = content.indexOf('## Continue');\n if (continueIndex !== -1) {\n const afterContinue = content.indexOf('\\n---', continueIndex);\n if (afterContinue !== -1) {\n const insertAt = afterContinue + 4;\n content = content.substring(0, insertAt) + '\\n' + checkpointText + content.substring(insertAt);\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n }\n\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${new Date().toISOString()}*\\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Checkpoint added to TODO.md`);\n}\n\n/**\n * The Claude Code session UUID, read off the transcript path.\n *\n * Claude Code names a transcript `<session-uuid>.jsonl`, so the hooks that\n * receive a transcript path are holding the session's identity without knowing\n * it. Both session-end writers had one available and neither used it.\n *\n * Shape-checked rather than trusted: only a UUID is returned, so a renamed,\n * archived or otherwise unexpected filename yields undefined and the caller\n * falls back to the session line rather than writing a `session-id` that\n * identifies nothing. A wrong id is worse than none — it would make two\n * different sessions compare equal.\n */\nexport function sessionIdFromTranscript(transcriptPath: string | undefined): string | undefined {\n if (!transcriptPath) return undefined;\n const base = basename(transcriptPath).replace(/\\.jsonl$/, '');\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(base)\n ? base\n : undefined;\n}\n\n/**\n * Update the ## Continue section at the top of TODO.md.\n *\n * This is the unattended writer: the pre-compact hook and the daemon's\n * work-queue worker both come through here. It used to build its own block and\n * strip the existing section with `/## Continue\\n[\\s\\S]*?\\n---\\n+/` — a\n * non-greedy match to the first `---`, which cuts a rich checkpoint in half at\n * the first horizontal rule inside its body and leaves the remainder orphaned\n * in the document.\n *\n * More importantly it was the writer that actually destroyed model-authored\n * checkpoints: `pai pause` wrote one, then this ran seconds later on session\n * end and replaced it with `Working directory: … Check the latest session note\n * for details.`\n *\n * It now delegates to the shared checkpoint module in \"auto\" mode, which means\n * it inherits the preservation rules rather than reimplementing them. Guarding\n * here rather than at each of the three call sites is deliberate: any future\n * caller inherits the behaviour without knowing it exists.\n */\nexport function updateTodoContinue(\n cwd: string,\n noteFilename: string,\n state: string | null,\n tokenDisplay: string,\n /**\n * The Claude Code session UUID, when the caller knows it.\n *\n * Optional only because a caller may genuinely not have it; supply it\n * whenever you can. Without it `isSameSession` falls back to comparing the\n * note FILENAME, and pausing renames the note — so a session stops\n * recognising its own checkpoint seconds after writing it, and an automated\n * write is then free to replace it as though it belonged to a predecessor.\n * That is how a live session lost a model-authored handover on 2026-08-04.\n */\n sessionId?: string\n): void {\n // Ensure a TODO.md exists so applyContinue writes to the same file the rest\n // of the hooks lib uses.\n ensureTodoMd(cwd);\n\n const result = applyContinue({\n rootPath: cwd,\n authored: 'auto',\n // The hooks identify a session by its note filename; `pai pause` resolves\n // the same string from the registry (and falls back to this filename when\n // the registry has no session row). They must agree, because this string\n // is the key that decides whether an authored checkpoint belongs to the\n // current session and must be preserved.\n sessionLine: noteFilename.replace(/\\.md$/, ''),\n // The UUID is authoritative when present; the line above is the fallback\n // for callers that cannot supply one, and it is mutable by design.\n sessionId,\n cwd,\n body: state?.trim() || undefined,\n });\n\n if (result.action === 'preserved') {\n console.error(\n 'TODO.md ## Continue left intact — authored checkpoint for this session'\n );\n return;\n }\n\n if (result.action === 'failed') {\n console.error(`TODO.md ## Continue update failed: ${result.error}`);\n return;\n }\n\n // Refresh the trailing \"Last updated\" stamp without disturbing the block.\n try {\n const todoPath = result.path!;\n const now = new Date().toISOString();\n let content = readFileSync(todoPath, 'utf-8');\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${now}*\\n`;\n writeFileSync(todoPath, content);\n } catch {\n // Non-fatal — the checkpoint itself is already written.\n }\n\n console.error(\n result.carriedForward\n ? 'TODO.md ## Continue section updated (previous content carried forward)'\n : 'TODO.md ## Continue section updated'\n );\n}\n","/**\n * session-summary-prompt.ts — Prompt template for AI-powered session summaries\n *\n * Produces a prompt that instructs the summarizer model to generate a structured\n * session note from extracted user messages and git commits. The output format\n * matches PAI's existing session note structure (Reconstruct skill format).\n *\n * The prompt also requests a TOPIC line on the first line of output, which the\n * session-summary-worker uses to detect topic shifts and decide whether to\n * create a new note or update the existing one.\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SummaryPromptParams {\n /** Extracted user messages from the JSONL transcript. */\n userMessages: string[];\n /** Git log output (--oneline --stat) for the session period. */\n gitLog: string;\n /** Working directory of the session. */\n cwd: string;\n /** ISO date string for the session. */\n date: string;\n /** Files modified during the session (from tool_use blocks). */\n filesModified?: string[];\n /** Existing session note content (if updating). */\n existingNote?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Prompt builder\n// ---------------------------------------------------------------------------\n\n/**\n * Build the prompt string to send to the summarizer model.\n *\n * Returns a single string suitable for piping to `claude --model <model> --print`.\n */\nexport function buildSessionSummaryPrompt(params: SummaryPromptParams): string {\n const {\n userMessages,\n gitLog,\n cwd,\n date,\n filesModified,\n existingNote,\n } = params;\n\n const userSection = userMessages.length > 0\n ? userMessages.map((m, i) => `[${i + 1}] ${m}`).join(\"\\n\\n\")\n : \"(No user messages extracted)\";\n\n const gitSection = gitLog.trim() || \"(No git commits during this session)\";\n\n const filesSection = filesModified && filesModified.length > 0\n ? filesModified.map(f => `- ${f}`).join(\"\\n\")\n : \"\";\n\n const updateInstruction = existingNote\n ? `\\nAn existing session note is provided below. Merge the new information into it,\npreserving what was already written. Add new work items and update the summary.\nDo NOT duplicate existing content.\n\nEXISTING NOTE:\n${existingNote}\n`\n : \"\";\n\n return `You are summarizing a coding session. Given the user messages and git commits below, write a session note.\n\nProject directory: ${cwd}\nDate: ${date}\n\nFocus on:\n- What problems were encountered and how they were solved\n- Key architectural decisions and their rationale\n- What was built (reference actual files and code patterns)\n- What was left unfinished or needs follow-up\n\nDo NOT include:\n- Mechanical metadata (token counts, checkpoint timestamps)\n- System messages or tool results verbatim\n- Generic descriptions — be specific about what happened\n- Markdown frontmatter or YAML headers\n${updateInstruction}\nFormat your response EXACTLY as follows (no extra text before or after):\n\nTOPIC: [A short topic label, max 60 characters, describing the WORK DONE — not quoting user messages. Format as \"Topic1, Topic2, and Topic3\" if multiple themes. Example: \"Session Summary Worker, Topic Detection\"]\n\n# Session: [Descriptive title summarizing what was ACCOMPLISHED, max 60 characters. Describe the work done, not the user's request. Bad: \"Dark Mode Button Does Nothing\". Good: \"Dark Mode Toggle, Keyboard IPC, and Audio Fix\"]\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n[Organize by theme, not chronologically. Group related work under descriptive bullet points.\nUse checkbox format: - [x] for completed items, - [ ] for incomplete items.\nInclude specific file names, function names, and technical details.]\n\n## Key Decisions\n\n[List important choices made during the session with brief rationale.\nSkip this section entirely if no significant decisions were made.]\n\n## Known Issues\n\n[What was left unfinished, bugs discovered, or follow-up items needed.\nSkip this section entirely if nothing is pending.]\n\n---\n\nUSER MESSAGES:\n${userSection}\n\nGIT COMMITS:\n${gitSection}\n${filesSection ? `\\nFILES MODIFIED:\\n${filesSection}` : \"\"}`;\n}\n","/**\n * triple-extraction-prompt.ts — Prompt template for KG triple extraction.\n *\n * Used by the session-summary-worker to extract structured facts from\n * a completed session summary and store them in the temporal knowledge graph.\n */\n\nexport function buildTripleExtractionPrompt(params: {\n sessionContent: string;\n projectSlug: string;\n gitLog: string;\n}): string {\n return `Extract structured entities and relations from this coding session.\n\nOutput a single JSON object with two arrays: \"entities\" and \"relations\".\n\nEntity types: project | person | concept | tool | file | version | decision | technology | organization\n\nRules:\n- Be SPECIFIC: entity names must be concrete (e.g., \"FSRS\", \"Glidr\", \"the owner\")\n- Use snake_case relation verb phrases (e.g., \"uses_algorithm\", \"decided_to\", \"shipped_version\")\n- Skip opinions, speculation, and \"we should\" statements\n- Skip entities obvious from project metadata unless they have a meaningful relation\n- Maximum 15 relations per session — pick the most important\n- Each entity should have a brief description (1 sentence, what it is in this context)\n- Each relation must reference entity names that appear in the entities array\n\nExample output:\n{\n \"entities\": [\n {\"name\": \"Glidr\", \"type\": \"project\", \"description\": \"Flashcard app using FSRS spaced repetition\"},\n {\"name\": \"FSRS\", \"type\": \"concept\", \"description\": \"Free Spaced Repetition Scheduler algorithm\"},\n {\"name\": \"the owner\", \"type\": \"person\", \"description\": \"Developer of Glidr and Quassl\"},\n {\"name\": \"Quassl\", \"type\": \"project\", \"description\": \"iOS app being rewritten in Flutter\"},\n {\"name\": \"Flutter\", \"type\": \"technology\", \"description\": \"Cross-platform mobile framework\"}\n ],\n \"relations\": [\n {\"source\": \"Glidr\", \"relation\": \"uses_algorithm\", \"target\": \"FSRS\"},\n {\"source\": \"Glidr\", \"relation\": \"shipped_version\", \"target\": \"1.0.5\"},\n {\"source\": \"the owner\", \"relation\": \"decided_to_rewrite\", \"target\": \"Quassl\"},\n {\"source\": \"Quassl\", \"relation\": \"migrating_to\", \"target\": \"Flutter\"}\n ]\n}\n\nPROJECT: ${params.projectSlug}\n\nSESSION CONTENT:\n${params.sessionContent}\n\nGIT COMMITS:\n${params.gitLog}\n\nOutput ONLY the JSON object with no surrounding prose, no code fences, no trailing commas, and no comments.\n\nJSON object (entities + relations):`;\n}\n","/**\n * daemon-llm.ts — provider-routed LLM spawns for daemon-side background calls.\n *\n * Session summaries, context handovers and KG extraction used to spawn a bare\n * `claude --model <tier>` with the ambient environment: on a machine whose\n * only login is a worker provider that spawn either dies (no Anthropic\n * credentials) or bills Anthropic outside the provider abstraction. This\n * module routes those spawns through the same registry and env build the\n * worker runner uses (`buildRunEnv`), resolving the tier alias to the\n * provider's concrete model id. With no usable provider configured it falls\n * back to the historical behaviour: the tier alias itself, ambient env minus\n * the Anthropic API key.\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport {\n defaultWorkersConfig,\n readWorkersSection,\n resolveModelCapability,\n type ModelTier,\n type WorkerProvider,\n type WorkersConfig,\n} from \"./config.js\";\nimport { resolveTarget } from \"./routing.js\";\nimport { ensureNoMcpConfig, workersLogDir } from \"./paths.js\";\nimport { buildRunEnv } from \"./run-env.js\";\nimport { DEFAULT_PROXY_PORT, ensureProxyRunning } from \"./proxy/server.js\";\n\nexport type { ModelTier } from \"./config.js\";\n\n/** Timeout per tier (ms) — a class-appropriate budget, not a per-model one. */\nexport const LLM_TIMEOUT_MS: Record<ModelTier, number> = {\n haiku: 60_000, // 60 seconds\n sonnet: 120_000, // 2 minutes\n opus: 300_000, // 5 minutes — the thorough tier\n};\n\nexport interface LlmSpawnPlan {\n /** --model value: the provider's concrete model id, or the tier alias on\n * the no-provider fallback (the CLI resolves tier aliases itself there). */\n model: string;\n /** Full headless claude args, model flag included. */\n args: string[];\n env: NodeJS.ProcessEnv;\n timeoutMs: number;\n /** Provider name the spawn is routed through; null on the fallback path. */\n provider: string | null;\n}\n\n/** Resolve a tier to the provider's concrete model id: the fast capability\n * for the cheap tier, the default model for the middle and top tiers. */\nfunction tierModel(provider: WorkerProvider, tier: ModelTier): string {\n return resolveModelCapability(provider, tier === \"haiku\" ? \"fast\" : \"default\");\n}\n\n/**\n * Containment every daemon-side LLM spawn carries. These calls are\n * text-in/text-out: the prompt carries the context, the answer is the\n * product, and nothing about summarizing a session justifies a tool. An\n * unrestricted background summarizer was observed on 2026-09-18 (01:33-01:51)\n * rewriting the live config with a schema-shaped dump while the daemon ran;\n * with the tool grant empty and MCP strictly empty, that class of damage is\n * structurally impossible rather than merely unlikely.\n */\nfunction containmentArgs(logDir: string): string[] {\n return [\n \"--strict-mcp-config\", \"--mcp-config\", ensureNoMcpConfig(logDir),\n // the empty grant allows no tool at all — not Read, not Bash, nothing\n \"--allowedTools\", \"\",\n // --tools \"\" drops the built-in tool schemas from the static prefix too:\n // 29,270 tokens with only --allowedTools \"\" vs 14,049 with --tools \"\" as well\n \"--tools\", \"\",\n ];\n}\n\n/** The historical fallback: tier alias as the model id, ambient env with the\n * Anthropic API key stripped (so the CLI uses its interactive login). */\nfunction legacyPlan(tier: ModelTier, config: WorkersConfig | null): LlmSpawnPlan {\n const { ANTHROPIC_API_KEY: _drop, ...env } = process.env;\n return {\n model: tier,\n args: [\n \"--model\", tier,\n ...containmentArgs(workersLogDir(config ?? defaultWorkersConfig())),\n \"-p\", \"--no-session-persistence\",\n ],\n env,\n timeoutMs: LLM_TIMEOUT_MS[tier],\n provider: null,\n };\n}\n\n/**\n * Plan a daemon-side LLM spawn for a tier. Provider resolution failures (no\n * config, workers off, no usable provider) fall back to the tier-alias path;\n * a configured-but-broken provider (unreadable key file) throws, because\n * silently degrading to Anthropic billing is worse than a failed summary.\n */\nexport async function planLlmSpawn(tier: ModelTier, configPath?: string): Promise<LlmSpawnPlan> {\n let provider: WorkerProvider | null = null;\n let providerName: string | null = null;\n let config: WorkersConfig | null = null;\n try {\n const { workers } = readWorkersSection(configPath);\n // the section is kept even when routing is off: its logDir is where the\n // containment config lives, on every path, provider or fallback\n config = workers;\n if (workers.enabled) {\n const target = resolveTarget(workers, workersLogDir(workers), {});\n provider = target.provider;\n providerName = target.providerName;\n }\n } catch {\n // no usable provider configured — the tier-alias fallback keeps the\n // feature alive exactly as it behaved before providers existed. The\n // section (when it parsed) is kept: its logDir still decides where the\n // containment config lives.\n provider = null;\n providerName = null;\n }\n if (!provider || !providerName || !config) return legacyPlan(tier, config);\n\n const logDir = workersLogDir(config);\n let proxyUrl: string | undefined;\n if (provider.protocol === \"openai\") {\n mkdirSync(logDir, { recursive: true });\n proxyUrl = `${await ensureProxyRunning(DEFAULT_PROXY_PORT, logDir)}/${providerName}`;\n }\n\n const model = tierModel(provider, tier);\n return {\n model,\n args: [\n \"--model\", model,\n ...containmentArgs(logDir),\n \"-p\", \"--no-session-persistence\",\n ],\n env: buildRunEnv(provider, true, proxyUrl),\n timeoutMs: LLM_TIMEOUT_MS[tier],\n provider: providerName,\n };\n}\n","/**\n * kg-extraction.ts — Shared KG triple extraction logic.\n *\n * Extracted from session-summary-worker.ts so both the worker and the\n * CLI backfill (`pai kg backfill`) can use the same code path.\n *\n * Provides:\n * - findClaudeBinary() — locate the claude CLI\n * - spawnClaude() — generic prompt -> response runner (provider-routed)\n * - extractAndStoreTriples() — run the extractor prompt and persist triples to Postgres\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nimport { buildTripleExtractionPrompt } from \"../daemon/templates/triple-extraction-prompt.js\";\nimport { planLlmSpawn, type ModelTier } from \"../workers/daemon-llm.js\";\nimport type { StorageBackend } from \"../storage/interface.js\";\n\n// ---------------------------------------------------------------------------\n// Claude CLI binary discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `claude` CLI binary. Checks common installation locations first\n * (launchd PATH is minimal so bare \"claude\" often won't resolve).\n */\nexport function findClaudeBinary(): string | null {\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"claude\"),\n join(homedir(), \".claude\", \"local\", \"claude\"),\n \"/usr/local/bin/claude\",\n \"/opt/homebrew/bin/claude\",\n ];\n\n for (const candidate of candidates) {\n try {\n if (existsSync(candidate)) return candidate;\n } catch { /* skip */ }\n }\n return \"claude\";\n}\n\n/**\n * Spawn the claude CLI with a prompt on stdin and return stdout.\n *\n * Routed through the provider registry (planLlmSpawn): the tier resolves to\n * the configured provider's model id and the env carries its base URL and\n * token. With no provider configured it degrades to the historical behaviour\n * — the tier alias itself, ambient env minus ANTHROPIC_API_KEY.\n */\nexport async function spawnClaude(\n prompt: string,\n tier: ModelTier = \"sonnet\"\n): Promise<string | null> {\n const claudeBin = findClaudeBinary();\n if (!claudeBin) {\n process.stderr.write(\"[kg-extraction] claude CLI not found.\\n\");\n return null;\n }\n\n let plan;\n try {\n plan = await planLlmSpawn(tier);\n } catch (e) {\n process.stderr.write(`[kg-extraction] could not plan ${tier} spawn: ${e}\\n`);\n return null;\n }\n\n const { spawn } = await import(\"node:child_process\");\n\n return new Promise((resolve) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const child = spawn(claudeBin, plan.args, {\n env: plan.env,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => { stdout += chunk.toString(); });\n child.stderr.on(\"data\", (chunk: Buffer) => { stderr += chunk.toString(); });\n\n child.on(\"error\", (err: Error) => {\n if (timer) { clearTimeout(timer); timer = null; }\n process.stderr.write(`[kg-extraction] ${plan.model} spawn error: ${err.message}\\n`);\n resolve(null);\n });\n\n child.on(\"close\", (code: number | null) => {\n if (timer) { clearTimeout(timer); timer = null; }\n if (code !== 0) {\n process.stderr.write(\n `[kg-extraction] ${plan.model} exited ${code}: ${stderr.slice(0, 300)}\\n`\n );\n resolve(null);\n } else {\n resolve(stdout.trim() || null);\n }\n });\n\n timer = setTimeout(() => {\n process.stderr.write(`[kg-extraction] ${plan.model} timed out — killing process.\\n`);\n child.kill(\"SIGTERM\");\n resolve(null);\n }, plan.timeoutMs);\n\n child.stdin.write(prompt);\n child.stdin.end();\n });\n}\n\n// ---------------------------------------------------------------------------\n// Triple extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Slice text down to the outermost JSON structure, dropping any surrounding\n * prose or code-fence artifacts the LLM added around the JSON object/array.\n * Returns the input unchanged if no opening brace/bracket is found.\n */\nexport function extractJsonSpan(text: string): string {\n const firstBrace = text.indexOf(\"{\");\n const firstBracket = text.indexOf(\"[\");\n\n let start = -1;\n let closeChar = \"\";\n if (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) {\n start = firstBrace;\n closeChar = \"}\";\n } else if (firstBracket !== -1) {\n start = firstBracket;\n closeChar = \"]\";\n }\n\n if (start === -1) return text;\n\n const end = text.lastIndexOf(closeChar);\n if (end === -1 || end <= start) return text;\n\n return text.slice(start, end + 1);\n}\n\nexport interface ExtractTriplesParams {\n summaryText: string;\n projectSlug: string;\n projectId: number | null;\n sessionId: string;\n gitLog?: string;\n model?: ModelTier;\n /** Tenant ID for multi-tenant entity scoping (default: \"default\") */\n tenantId?: string;\n}\n\nexport interface ExtractTriplesResult {\n extracted: number;\n added: number;\n superseded: number;\n}\n\n/**\n * Extract structured KG triples from a session summary and store them in\n * Postgres. Idempotent: if a (subject, predicate) pair already has the same\n * object, no new row is added; if the object differs, the old triple is\n * invalidated (valid_to = NOW()) and a new one is inserted.\n *\n * Best-effort: per-triple errors are caught and logged but never thrown.\n * Returns a small stats object so callers can report progress.\n */\nexport async function extractAndStoreTriples(\n backend: StorageBackend,\n params: ExtractTriplesParams\n): Promise<ExtractTriplesResult> {\n const stats: ExtractTriplesResult = { extracted: 0, added: 0, superseded: 0 };\n\n const prompt = buildTripleExtractionPrompt({\n sessionContent: params.summaryText,\n projectSlug: params.projectSlug,\n gitLog: params.gitLog ?? \"\",\n });\n\n const jsonOutput = await spawnClaude(prompt, params.model ?? \"sonnet\");\n if (!jsonOutput) return stats;\n\n // Strip markdown code fences if Claude wrapped the JSON\n let cleaned = jsonOutput\n .replace(/^```json\\s*/m, \"\")\n .replace(/^```\\s*/m, \"\")\n .replace(/\\s*```$/m, \"\")\n .trim();\n\n // Some outputs still carry leading/trailing prose or fences the regexes\n // above miss (e.g. fence not at line start); slice to the outermost\n // JSON structure before parsing.\n cleaned = extractJsonSpan(cleaned);\n\n // Support both legacy array format and new structured format\n type LegacyTriple = { subject: string; predicate: string; object: string };\n type NewRelation = { source: string; relation: string; target: string };\n type NewEntity = { name: string; type: string; description: string };\n type NewFormat = { entities: NewEntity[]; relations: NewRelation[] };\n\n let triples: Array<LegacyTriple>;\n try {\n const parsed = JSON.parse(cleaned);\n\n if (Array.isArray(parsed)) {\n // Legacy format: [{subject, predicate, object}]\n triples = parsed;\n } else if (parsed && typeof parsed === \"object\" && Array.isArray(parsed.relations)) {\n // New structured format: {entities: [...], relations: [...]}\n const newFmt = parsed as NewFormat;\n\n // QW1: Upsert entities into kg_entities\n if (Array.isArray(newFmt.entities)) {\n const tenantId = params.tenantId ?? \"default\";\n for (const entity of newFmt.entities) {\n if (!entity.name) continue;\n try {\n await backend.upsertKgEntity({\n name: entity.name,\n type: entity.type ?? \"unknown\",\n description: entity.description,\n tenantId,\n });\n } catch (entityErr) {\n process.stderr.write(`[kg-extraction] entity upsert error (${entity.name}): ${entityErr}\\n`);\n }\n }\n }\n\n triples = newFmt.relations.map((r: NewRelation) => ({\n subject: r.source,\n predicate: r.relation,\n object: r.target,\n }));\n } else {\n process.stderr.write(`[kg-extraction] Unexpected JSON shape — neither array nor {entities,relations}\\n`);\n return stats;\n }\n } catch (e) {\n const collapse = (s: string) => s.replace(/\\s+/g, \" \").trim();\n const head200 = collapse(jsonOutput.slice(0, 200));\n const tail200 = collapse(jsonOutput.slice(-200));\n process.stderr.write(\n `[kg-extraction] JSON parse failed: ${e}. head200=\"${head200}\" tail200=\"${tail200}\"\\n`\n );\n return stats;\n }\n\n if (!Array.isArray(triples)) return stats;\n stats.extracted = triples.length;\n\n for (const t of triples) {\n if (!t.subject || !t.predicate || !t.object) continue;\n\n try {\n const existing = await backend.queryKgTriples({\n subject: t.subject,\n predicate: t.predicate,\n project_id: params.projectId ?? undefined,\n });\n\n // If an identical (subject, predicate, object) is already valid, skip — idempotent\n const alreadyValid = existing.find((e) => e.object === t.object && !e.valid_to);\n if (alreadyValid) continue;\n\n // Invalidate any superseded triple (same subject+predicate, different object)\n const supersedes = existing.find((e) => e.object !== t.object && !e.valid_to);\n if (supersedes) {\n await backend.invalidateKgTriple(supersedes.id);\n stats.superseded++;\n }\n\n await backend.addKgTriple({\n subject: t.subject,\n predicate: t.predicate,\n object: t.object,\n project_id: params.projectId ?? undefined,\n source_session: params.sessionId,\n confidence: \"EXTRACTED\",\n });\n stats.added++;\n } catch (tripleErr) {\n process.stderr.write(`[kg-extraction] store error (${t.subject}): ${tripleErr}\\n`);\n }\n }\n\n return stats;\n}\n","/**\n * session-summary-worker.ts — AI-powered session note generation\n *\n * Processes `session-summary` work items by:\n * 1. Finding the current session's JSONL transcript\n * 2. Extracting user messages and assistant context\n * 3. Gathering git commits from the session period\n * 4. Spawning Claude (sonnet for compaction, opus for session end) to generate a structured summary\n * 5. Comparing the new topic against the existing note's topic\n * 6. Creating a NEW note if the topic shifted, or updating the existing one\n *\n * Topic detection: the summarizer outputs a TOPIC: line as the first line of\n * its response. This is compared against the existing note's title using word\n * overlap. If overlap is below ~30%, a new note is created.\n *\n * Designed to run inside the daemon's work queue worker. All errors are\n * thrown (not swallowed) so the work queue retry logic handles them.\n */\n\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join, basename, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nimport {\n findNotesDir,\n getCurrentNotePath,\n createSessionNote,\n addWorkToSessionNote,\n renameSessionNote,\n} from \"../hooks/ts/lib/project-utils/index.js\";\n\nimport { buildSessionSummaryPrompt } from \"./templates/session-summary-prompt.js\";\nimport {\n extractAndStoreTriples as kgExtractAndStoreTriples,\n} from \"../memory/kg-extraction.js\";\nimport { getRegistryBackend } from \"../storage/factory.js\";\nimport { storageBackend, daemonConfig } from \"./daemon/state.js\";\nimport { planLlmSpawn, type ModelTier } from \"../workers/daemon-llm.js\";\nimport { paiHomePath, resolvePaiFile, migratePaiFile, type MigrateFileResult } from \"../config/pai-home.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Minimum interval between summaries for the same project (ms). */\nconst SUMMARY_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes\n\n/** Maximum JSONL content to feed to the summarizer (characters). */\n/** Max JSONL chars per model. Opus/Sonnet can handle much more than Haiku. */\nconst MAX_JSONL_CHARS: Record<string, number> = {\n haiku: 50_000,\n sonnet: 200_000,\n opus: 500_000,\n};\n\n/** Maximum user messages to include in the prompt. */\nconst MAX_USER_MESSAGES = 30;\n\n/** File tracking last summary timestamps per project — resolved under\n * PAI_HOME, falling back to the pre-2026-09-19 ~/.config/pai location. */\nfunction oldCooldownFile(): string {\n return join(homedir(), \".config\", \"pai\", \"summary-cooldowns.json\");\n}\n\nfunction cooldownFilePath(): string {\n return resolvePaiFile(paiHomePath(\"summary-cooldowns.json\"), [oldCooldownFile()], \"pai config migrate\");\n}\n\nexport function migrateSummaryCooldowns(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"summary-cooldowns.json\"), [oldCooldownFile()], opts);\n}\n\n/** Claude Code projects directory. */\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SessionSummaryPayload {\n cwd: string;\n sessionId?: string;\n projectSlug?: string;\n transcriptPath?: string;\n /** If true, bypass the cooldown check (e.g. triggered by stop-hook at session end). */\n force?: boolean;\n /** Model tier to use for summarization (resolved to the configured\n * provider's model id). Defaults based on trigger:\n * - forced (mid-session auto-save, every N messages): \"haiku\" — runs often\n * - everything else (manual, reconstruct, batch): \"sonnet\"\n * Session end does not summarise via an LLM at all; it is mechanical. */\n model?: ModelTier;\n}\n\n// ---------------------------------------------------------------------------\n// Cooldown tracking\n// ---------------------------------------------------------------------------\n\nfunction loadCooldowns(): Record<string, number> {\n try {\n const file = cooldownFilePath();\n if (existsSync(file)) {\n return JSON.parse(readFileSync(file, \"utf-8\"));\n }\n } catch { /* ignore */ }\n return {};\n}\n\nfunction saveCooldowns(cooldowns: Record<string, number>): void {\n try {\n const file = paiHomePath(\"summary-cooldowns.json\");\n mkdirSync(dirname(file), { recursive: true });\n writeFileSync(file, JSON.stringify(cooldowns, null, 2), \"utf-8\");\n } catch { /* ignore */ }\n}\n\nfunction isOnCooldown(cwd: string): boolean {\n const cooldowns = loadCooldowns();\n const lastRun = cooldowns[cwd];\n if (!lastRun) return false;\n return Date.now() - lastRun < SUMMARY_COOLDOWN_MS;\n}\n\nfunction markCooldown(cwd: string): void {\n const cooldowns = loadCooldowns();\n cooldowns[cwd] = Date.now();\n // Prune entries older than 24 hours\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n for (const key of Object.keys(cooldowns)) {\n if (cooldowns[key] < cutoff) delete cooldowns[key];\n }\n saveCooldowns(cooldowns);\n}\n\n// ---------------------------------------------------------------------------\n// JSONL discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Encode a cwd path the same way Claude Code does for its project directories.\n * Replaces /, space, dot, and hyphen with -.\n */\nfunction encodeProjectPath(cwd: string): string {\n return cwd.replace(/[\\/\\s.\\-]/g, \"-\");\n}\n\n/**\n * Find the most recently modified JSONL file for the given project.\n *\n * Claude Code stores transcripts in:\n * ~/.claude/projects/<encoded-path>/sessions/*.jsonl\n * ~/.claude/projects/<encoded-path>/<uuid>.jsonl (legacy)\n */\nexport function findLatestJsonl(cwd: string): string | null {\n const encoded = encodeProjectPath(cwd);\n const projectDir = join(CLAUDE_PROJECTS_DIR, encoded);\n\n if (!existsSync(projectDir)) {\n process.stderr.write(\n `[session-summary] No Claude project dir found: ${projectDir}\\n`\n );\n return null;\n }\n\n // Collect all JSONL candidates\n const candidates: Array<{ path: string; mtime: number }> = [];\n\n // Check sessions/ subdirectory first (current layout)\n const sessionsDir = join(projectDir, \"sessions\");\n if (existsSync(sessionsDir)) {\n try {\n for (const f of readdirSync(sessionsDir)) {\n if (!f.endsWith(\".jsonl\")) continue;\n const fullPath = join(sessionsDir, f);\n try {\n const st = statSync(fullPath);\n candidates.push({ path: fullPath, mtime: st.mtimeMs });\n } catch { /* skip */ }\n }\n } catch { /* skip */ }\n }\n\n // Also check top-level for legacy .jsonl files\n try {\n for (const f of readdirSync(projectDir)) {\n if (!f.endsWith(\".jsonl\")) continue;\n const fullPath = join(projectDir, f);\n try {\n const st = statSync(fullPath);\n candidates.push({ path: fullPath, mtime: st.mtimeMs });\n } catch { /* skip */ }\n }\n } catch { /* skip */ }\n\n if (candidates.length === 0) {\n process.stderr.write(\n `[session-summary] No JSONL files found in ${projectDir}\\n`\n );\n return null;\n }\n\n // Sort by modification time descending — pick the most recent\n candidates.sort((a, b) => b.mtime - a.mtime);\n return candidates[0].path;\n}\n\n// ---------------------------------------------------------------------------\n// JSONL content extraction\n// ---------------------------------------------------------------------------\n\ninterface ExtractedContent {\n userMessages: string[];\n filesModified: string[];\n sessionStartTime: string;\n}\n\n/**\n * Parse a JSONL transcript and extract relevant content.\n * Filters noise, truncates to model-appropriate size from the end of the file.\n */\nfunction extractFromJsonl(jsonlPath: string, tier: ModelTier = \"sonnet\"): ExtractedContent {\n const result: ExtractedContent = {\n userMessages: [],\n filesModified: [],\n sessionStartTime: \"\",\n };\n\n let raw: string;\n try {\n raw = readFileSync(jsonlPath, \"utf-8\");\n } catch (e) {\n throw new Error(`Could not read JSONL at ${jsonlPath}: ${e}`);\n }\n\n // Truncate from the start if too large (keep the most recent content)\n const maxChars = MAX_JSONL_CHARS[tier] ?? 200_000;\n if (raw.length > maxChars) {\n const truncPoint = raw.indexOf(\"\\n\", raw.length - maxChars);\n raw = truncPoint >= 0 ? raw.slice(truncPoint + 1) : raw.slice(-MAX_JSONL_CHARS);\n }\n\n const lines = raw.trim().split(\"\\n\");\n const seenMessages = new Set<string>();\n\n for (const line of lines) {\n if (!line.trim()) continue;\n\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n\n // Track earliest timestamp\n if (entry.timestamp && !result.sessionStartTime) {\n result.sessionStartTime = String(entry.timestamp);\n }\n\n // Extract user messages\n if (entry.type === \"user\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n if (msg?.content) {\n const text = contentToText(msg.content);\n if (text && !isNoise(text) && !seenMessages.has(text)) {\n seenMessages.add(text);\n result.userMessages.push(text.slice(0, 500));\n }\n }\n }\n\n // Extract file modifications from assistant tool_use blocks\n if (entry.type === \"assistant\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n if (msg?.content && Array.isArray(msg.content)) {\n for (const block of msg.content as Array<Record<string, unknown>>) {\n if (block.type === \"tool_use\") {\n const name = block.name as string;\n const input = block.input as Record<string, unknown> | undefined;\n if ((name === \"Edit\" || name === \"Write\") && input?.file_path) {\n const fp = String(input.file_path);\n if (!result.filesModified.includes(fp)) {\n result.filesModified.push(fp);\n }\n }\n }\n }\n }\n }\n }\n\n // Limit user messages\n if (result.userMessages.length > MAX_USER_MESSAGES) {\n result.userMessages = result.userMessages.slice(-MAX_USER_MESSAGES);\n }\n\n return result;\n}\n\n/** Convert Claude content (string or content block array) to plain text. */\nexport function contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === \"string\") return c;\n const block = c as Record<string, unknown>;\n if (block?.text) return String(block.text);\n if (block?.content) return String(block.content);\n return \"\";\n })\n .join(\" \")\n .trim();\n }\n return \"\";\n}\n\n/** Filter out noise entries that shouldn't be included in the summary. */\nfunction isNoise(text: string): boolean {\n if (!text || text.length < 3) return true;\n if (text.includes(\"<task-notification>\")) return true;\n if (text.includes(\"[object Object]\")) return true;\n if (text.startsWith(\"<system-reminder>\")) return true;\n if (/^(yes|ok|sure|go|continue|weiter|thanks|thank you)\\.?$/i.test(text.trim())) return true;\n // Skip pure tool result blocks\n if (text.startsWith(\"Tool Result:\") || text.startsWith(\"tool_result\")) return true;\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Git context\n// ---------------------------------------------------------------------------\n\n/**\n * Get git log for the session period.\n * Falls back gracefully if git is not available or the dir is not a repo.\n */\nexport async function getGitContext(cwd: string, sinceTime?: string): Promise<string> {\n let since = \"6 hours ago\";\n if (sinceTime) {\n // sinceTime may be a Unix epoch (seconds as string) or already ISO 8601\n const asNum = Number(sinceTime);\n if (!isNaN(asNum) && asNum > 1_000_000_000) {\n // Unix epoch seconds → ISO 8601 (git accepts this unambiguously)\n since = new Date(asNum * 1000).toISOString();\n } else {\n since = sinceTime;\n }\n }\n\n try {\n const { execFile: execFileCb } = await import(\"node:child_process\");\n const { promisify } = await import(\"node:util\");\n const execFileAsync = promisify(execFileCb);\n\n const { stdout } = await execFileAsync(\n \"git\",\n [\"log\", \"--format=%h %ai %s\", `--since=${since}`, \"--stat\", \"--no-color\"],\n {\n cwd,\n timeout: 10_000,\n env: { ...process.env, GIT_TERMINAL_PROMPT: \"0\" },\n }\n );\n return stdout.trim();\n } catch {\n return \"\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Claude CLI spawning\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `claude` CLI binary.\n * Checks PATH first, then common installation locations.\n */\nfunction findClaudeBinary(): string | null {\n // Check known locations first (launchd PATH is minimal, bare \"claude\" won't resolve)\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"claude\"),\n join(homedir(), \".claude\", \"local\", \"claude\"),\n \"/usr/local/bin/claude\",\n \"/opt/homebrew/bin/claude\",\n ];\n\n for (const candidate of candidates) {\n try {\n if (existsSync(candidate)) return candidate;\n } catch { /* skip */ }\n }\n\n // Last resort: try bare \"claude\" in case PATH has it\n return \"claude\";\n}\n\n/**\n * Spawn a Claude model via the CLI to generate a session summary.\n * Pipes the prompt via stdin. Tier selection (resolved to the configured\n * provider's model id, falling back to the tier alias when no provider is\n * configured — see planLlmSpawn):\n * - opus: session end (best quality for final summary, runs once)\n * - sonnet: auto-compaction (good quality for incremental checkpoints, runs often)\n * - haiku: fallback / budget mode\n * Returns the generated text, or null if spawning fails.\n */\nexport async function spawnSummarizer(prompt: string, tier: ModelTier = \"sonnet\"): Promise<string | null> {\n const claudeBin = findClaudeBinary();\n if (!claudeBin) {\n process.stderr.write(\n \"[session-summary] Claude CLI not found in PATH or common locations.\\n\"\n );\n return null;\n }\n\n let plan;\n try {\n plan = await planLlmSpawn(tier);\n } catch (e) {\n process.stderr.write(`[session-summary] could not plan ${tier} spawn: ${e}\\n`);\n return null;\n }\n\n const { spawn } = await import(\"node:child_process\");\n\n return new Promise((resolve) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const child = spawn(claudeBin, plan.args, {\n env: plan.env,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString();\n });\n\n child.stderr.on(\"data\", (chunk: Buffer) => {\n stderr += chunk.toString();\n });\n\n child.on(\"error\", (err: Error) => {\n if (timer) { clearTimeout(timer); timer = null; }\n process.stderr.write(`[session-summary] ${plan.model} spawn error: ${err.message}\\n`);\n resolve(null);\n });\n\n child.on(\"close\", (code: number | null) => {\n if (timer) { clearTimeout(timer); timer = null; }\n if (code !== 0) {\n process.stderr.write(\n `[session-summary] ${plan.model} exited with code ${code}: ${stderr.slice(0, 300)}\\n`\n );\n resolve(null);\n } else {\n resolve(stdout.trim() || null);\n }\n });\n\n // Timeout protection\n timer = setTimeout(() => {\n process.stderr.write(`[session-summary] ${plan.model} timed out — killing process.\\n`);\n child.kill(\"SIGTERM\");\n resolve(null);\n }, plan.timeoutMs);\n\n // Write prompt to stdin and close\n child.stdin.write(prompt);\n child.stdin.end();\n });\n}\n\n// ---------------------------------------------------------------------------\n// Topic extraction and comparison\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the TOPIC: line from the summarizer output.\n * Returns the topic string, or null if not found.\n */\nfunction extractTopic(summaryText: string): string | null {\n const match = summaryText.match(/^TOPIC:\\s*(.+)$/m);\n if (!match) return null;\n return match[1].trim();\n}\n\n/**\n * Extract the topic from an existing session note.\n * First checks for a <!-- TOPIC: ... --> comment (stored by previous summaries).\n * Falls back to the H1 \"# Session NNNN: Title\" line.\n *\n * The HTML comment is the reliable source because the H1 gets renamed by\n * renameSessionNote, which can add/change words and cause false topic shifts.\n */\nfunction extractExistingNoteTitle(notePath: string): string | null {\n try {\n const content = readFileSync(notePath, \"utf-8\");\n // Prefer stored TOPIC comment (exact match to what the summarizer produced)\n const topicComment = content.match(/<!-- TOPIC:\\s*(.+?)\\s*-->/);\n if (topicComment) return topicComment[1].trim();\n // Fallback to H1\n const match = content.match(/^# Session \\d+:\\s*(.+)$/m);\n if (match) return match[1].trim();\n } catch { /* ignore */ }\n return null;\n}\n\n/**\n * Compute word overlap ratio between two topic strings.\n * Returns a value in [0, 1] — 1.0 means identical word sets.\n *\n * Uses lowercased, normalized words. Stop words and very short words\n * are excluded to avoid false positives on common terms.\n */\nfunction computeTopicOverlap(topicA: string, topicB: string): number {\n const stopWords = new Set([\n \"a\", \"an\", \"the\", \"and\", \"or\", \"but\", \"in\", \"on\", \"at\", \"to\", \"for\",\n \"of\", \"with\", \"by\", \"from\", \"is\", \"was\", \"are\", \"were\", \"be\", \"been\",\n \"being\", \"have\", \"has\", \"had\", \"do\", \"does\", \"did\", \"will\", \"would\",\n \"could\", \"should\", \"may\", \"might\", \"can\", \"shall\", \"this\", \"that\",\n \"these\", \"those\", \"it\", \"its\", \"new\", \"session\", \"work\", \"done\",\n ]);\n\n const normalize = (text: string): Set<string> => {\n const words = text\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 2 && !stopWords.has(w));\n return new Set(words);\n };\n\n const wordsA = normalize(topicA);\n const wordsB = normalize(topicB);\n\n if (wordsA.size === 0 || wordsB.size === 0) return 0;\n\n let intersection = 0;\n for (const w of wordsA) {\n if (wordsB.has(w)) intersection++;\n }\n\n // Jaccard similarity\n const union = new Set([...wordsA, ...wordsB]).size;\n return union > 0 ? intersection / union : 0;\n}\n\n/** Threshold: below this overlap ratio, we consider topics different. */\n// Raised from 0.3 to 0.15 — lower threshold means topics must be MORE different\n// to trigger a split. 0.3 was too aggressive: incremental work on the same project\n// (e.g., \"Flutter Rewrite\" vs \"Fix Transcription in Flutter\") was splitting into\n// separate notes on every compaction.\nconst TOPIC_OVERLAP_THRESHOLD = 0.15;\n\n// ---------------------------------------------------------------------------\n// Session note writing\n// ---------------------------------------------------------------------------\n\n/**\n * Write (or update) the session note with the AI-generated summary.\n *\n * Strategy:\n * - Find the current month's latest note\n * - If it's from today, compare topics:\n * - Same topic (overlap >= 30%) → update existing note\n * - Different topic (overlap < 30%) → create a NEW note\n * - If it's from a different day, create a new note\n */\n/**\n * Return true if the summarizer output has a meaningful body — i.e. content\n * beyond the TOPIC:/title/metadata/horizontal-rule scaffolding.\n *\n * A summary that is only headers (no Work Done items) must NOT create or update\n * a note: doing so births an empty-bodied scaffold that later rename/finalize\n * can strip to a footer-only stub. This is the born-stub failure mode.\n */\nfunction summaryHasContent(summaryText: string): boolean {\n const body = summaryText\n .replace(/^TOPIC:.*$/m, \"\")\n .replace(/^# Session:.*$/m, \"\")\n .replace(/^\\*\\*(Date|Status|Completed):\\*\\*.*$/gm, \"\")\n .replace(/^#{1,6}\\s.*$/gm, \"\") // bare section headers (## Work Done, etc.)\n .replace(/^---$/gm, \"\")\n .replace(/<!--[\\s\\S]*?-->/g, \"\") // template placeholder comments\n .trim();\n return body.length > 0;\n}\n\nfunction writeSessionNote(\n cwd: string,\n summaryText: string,\n filesModified: string[],\n sessionId?: string,\n): string | null {\n // Never create/update a note from a body-less summary — that is the born-stub\n // path (empty scaffold → stripped to a footer-only stub on finalize/rename).\n if (!summaryHasContent(summaryText)) {\n process.stderr.write(\n `[session-summary] Summary has no meaningful body — skipping note write.\\n`\n );\n return null;\n }\n\n const notesInfo = findNotesDir(cwd);\n // Identity first: the note this session already owns. Falling back to the\n // positional lookup only when there is no session id keeps older callers\n // working, but the fallback is what used to create duplicates.\n let notePath = (sessionId && findNoteBySessionId(notesInfo.path, sessionId))\n || getCurrentNotePath(notesInfo.path);\n\n const today = new Date().toISOString().split(\"T\")[0];\n\n // Extract the new topic from the summarizer output\n const newTopic = extractTopic(summaryText);\n\n if (notePath) {\n const noteFilename = basename(notePath);\n // Check if this note is from today\n const dateMatch = noteFilename.match(/(\\d{4}-\\d{2}-\\d{2})/);\n const noteDate = dateMatch ? dateMatch[1] : \"\";\n\n if (noteDate === today) {\n // Check for topic shift — two signals:\n // 1. TOPIC: line from summarizer vs existing note title (word overlap)\n // 2. topic-boundary.json written by topic-detect-worker (project-level shift)\n const existingTitle = extractExistingNoteTitle(notePath);\n let topicShifted = false;\n\n // Signal 1: summarizer topic vs existing note title\n if (newTopic && existingTitle) {\n const overlap = computeTopicOverlap(newTopic, existingTitle);\n process.stderr.write(\n `[session-summary] Topic overlap: ${(overlap * 100).toFixed(1)}%` +\n ` (new=\"${newTopic}\", existing=\"${existingTitle}\")\\n`\n );\n\n if (overlap < TOPIC_OVERLAP_THRESHOLD) {\n topicShifted = true;\n process.stderr.write(\n `[session-summary] Topic shift detected (word overlap) — creating new note.\\n`\n );\n }\n }\n\n // Signal 2: topic boundary marker from topic-detect-worker\n if (!topicShifted) {\n const boundaryPath = join(notesInfo.path, \"topic-boundary.json\");\n if (existsSync(boundaryPath)) {\n try {\n const boundary = JSON.parse(readFileSync(boundaryPath, \"utf-8\"));\n if (boundary.timestamp) {\n const boundaryAge = Date.now() - new Date(boundary.timestamp).getTime();\n // Only honor boundaries from the last 30 minutes\n if (boundaryAge < 30 * 60 * 1000) {\n topicShifted = true;\n process.stderr.write(\n `[session-summary] Topic shift detected (boundary marker) — ` +\n `${boundary.previousProject} → ${boundary.suggestedProject}\\n`\n );\n }\n }\n // Consume the boundary file (one-shot)\n unlinkSync(boundaryPath);\n } catch { /* ignore invalid boundary file */ }\n }\n }\n\n if (topicShifted) {\n // Different topic — create a NEW note (topic-based split)\n notePath = createNoteFromSummary(notesInfo.path, summaryText, sessionId);\n } else {\n // Same topic — update existing note\n updateNoteWithSummary(notePath, summaryText);\n process.stderr.write(\n `[session-summary] Updated existing note: ${noteFilename}\\n`\n );\n }\n } else {\n // Different day — create a new note\n notePath = createNoteFromSummary(notesInfo.path, summaryText, sessionId);\n }\n } else {\n // No note exists — create one\n notePath = createNoteFromSummary(notesInfo.path, summaryText, sessionId);\n }\n\n // Try to rename with a meaningful title from the summary\n if (notePath) {\n const titleMatch = summaryText.match(/^# Session:\\s*(.+)$/m);\n if (titleMatch) {\n const title = titleMatch[1].trim();\n if (title.length > 5 && title.length < 80) {\n const newPath = renameSessionNote(notePath, title);\n if (newPath !== notePath) {\n notePath = newPath;\n }\n }\n }\n }\n\n return notePath;\n}\n\n/**\n * Update an existing session note's Work Done section with AI-generated content.\n */\nfunction updateNoteWithSummary(notePath: string, summaryText: string): void {\n if (!existsSync(notePath)) return;\n\n let content = readFileSync(notePath, \"utf-8\");\n\n // Update the TOPIC comment if present (keeps topic comparison stable across renames)\n const newTopic = extractTopic(summaryText);\n if (newTopic) {\n if (content.includes(\"<!-- TOPIC:\")) {\n content = content.replace(/<!-- TOPIC:.*?-->/, `<!-- TOPIC: ${newTopic} -->`);\n } else {\n // Insert after H1 line\n content = content.replace(/^(# Session .+)$/m, `$1\\n<!-- TOPIC: ${newTopic} -->`);\n }\n }\n\n // Extract the work items from the AI summary\n const workDoneMatch = summaryText.match(\n /## Work Done\\n\\n([\\s\\S]*?)(?=\\n## Key Decisions|\\n## Known Issues|\\n\\*\\*Tags|\\n$)/\n );\n\n if (workDoneMatch) {\n const aiWorkContent = workDoneMatch[1].trim();\n const timestamp = new Date().toISOString().split(\"T\")[1].split(\".\")[0];\n\n // Add as a new subsection under Work Done\n const sectionHeader = `\\n### AI Summary (${timestamp})\\n\\n${aiWorkContent}\\n`;\n\n const nextStepsIdx = content.indexOf(\"## Next Steps\");\n const knownIssuesIdx = content.indexOf(\"## Known Issues\");\n const insertBefore = knownIssuesIdx !== -1 ? knownIssuesIdx :\n nextStepsIdx !== -1 ? nextStepsIdx :\n content.length;\n\n content = content.slice(0, insertBefore) + sectionHeader + \"\\n\" + content.slice(insertBefore);\n }\n\n // Extract and add Key Decisions if present\n const decisionsMatch = summaryText.match(\n /## Key Decisions\\n\\n([\\s\\S]*?)(?=\\n## Known Issues|\\n\\*\\*Tags|\\n$)/\n );\n if (decisionsMatch) {\n const decisions = decisionsMatch[1].trim();\n if (decisions && !content.includes(\"## Key Decisions\")) {\n const nextStepsIdx = content.indexOf(\"## Next Steps\");\n const insertAt = nextStepsIdx !== -1 ? nextStepsIdx : content.length;\n content = content.slice(0, insertAt) + `## Key Decisions\\n\\n${decisions}\\n\\n` + content.slice(insertAt);\n }\n }\n\n // Extract and add Known Issues if present\n const issuesMatch = summaryText.match(\n /## Known Issues\\n\\n([\\s\\S]*?)(?=\\n\\*\\*Tags|\\n$)/\n );\n if (issuesMatch) {\n const issues = issuesMatch[1].trim();\n if (issues && !content.includes(\"## Known Issues\")) {\n const nextStepsIdx = content.indexOf(\"## Next Steps\");\n const insertAt = nextStepsIdx !== -1 ? nextStepsIdx : content.length;\n content = content.slice(0, insertAt) + `## Known Issues\\n\\n${issues}\\n\\n` + content.slice(insertAt);\n }\n }\n\n writeFileSync(notePath, content, \"utf-8\");\n}\n\n/**\n * Create a brand new session note from the AI summary.\n */\n/**\n * Find the note this session already owns, by the marker its first write left.\n *\n * The previous resolver, getCurrentNotePath, returned the numerically highest\n * note in the month directory. That is a position, not an identity, and it is\n * why one session produced many notes: whenever a session's own note was not\n * the current maximum — two sessions in one project, or a number collision, of\n * which one corpus had 110 — the lookup missed and the checkpoint created a\n * new file instead of updating. Measured consequence in that corpus: 303 of\n * 407 notes sat in 31 same-title groups, 48 of them sharing a single title.\n *\n * Matching on the session id makes the answer exact: a session either has a\n * note or it does not, regardless of what else the directory contains.\n */\nfunction findNoteBySessionId(notesDir: string, sessionId: string): string | null {\n if (!sessionId || !existsSync(notesDir)) return null;\n const marker = `<!-- SESSION: ${sessionId} -->`;\n\n const now = new Date();\n const dirs = [0, -1].map((delta) => {\n const d = new Date(now.getFullYear(), now.getMonth() + delta, 1);\n return join(notesDir, String(d.getFullYear()),\n String(d.getMonth() + 1).padStart(2, \"0\"));\n });\n\n for (const dir of dirs) {\n if (!existsSync(dir)) continue;\n for (const f of readdirSync(dir)) {\n if (!f.endsWith(\".md\")) continue;\n const p = join(dir, f);\n try {\n if (readFileSync(p, \"utf-8\").includes(marker)) return p;\n } catch { /* unreadable file is not a match */ }\n }\n }\n return null;\n}\n\nfunction createNoteFromSummary(\n notesDir: string,\n summaryText: string,\n sessionId?: string,\n): string | null {\n try {\n // Create the note with a placeholder title\n const notePath = createSessionNote(notesDir, \"New Session\");\n\n // We will overwrite the entire content with the AI-generated summary, preserving\n // the note number (derived from the filename) and adding the standard footer.\n const noteFilename = basename(notePath);\n const numberMatch = noteFilename.match(/^(\\d+)/);\n const noteNumber = numberMatch ? numberMatch[1] : \"0000\";\n\n // Replace the H1 title from the AI summary with the numbered format\n const titleMatch = summaryText.match(/^# Session:\\s*(.+)$/m);\n const title = titleMatch ? titleMatch[1].trim() : \"New Session\";\n\n const date = new Date().toISOString().split(\"T\")[0];\n\n // Extract topic before stripping it (stored as HTML comment for future comparison)\n const topic = extractTopic(summaryText);\n\n // Build the final note content, merging AI output with the PAI note structure.\n // Strip the TOPIC: line (used for topic detection, not for the note body).\n const aiBody = summaryText\n .replace(/^TOPIC:.*$/m, \"\")\n .replace(/^# Session:.*$/m, \"\")\n .replace(/^\\*\\*Date:\\*\\*.*$/m, \"\")\n .replace(/^\\*\\*Status:\\*\\*.*$/m, \"\")\n .replace(/^---$/m, \"\")\n .trim();\n\n // The session marker is what later checkpoints of THIS session match on to\n // find and update this note rather than creating another one beside it.\n const finalContent = `# Session ${noteNumber}: ${title}\n${topic ? `<!-- TOPIC: ${topic} -->` : \"\"}${sessionId ? `\\n<!-- SESSION: ${sessionId} -->` : \"\"}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n${aiBody}\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n writeFileSync(notePath, finalContent, \"utf-8\");\n process.stderr.write(`[session-summary] Created AI-powered note: ${noteFilename}\\n`);\n return notePath;\n } catch (e) {\n process.stderr.write(`[session-summary] Failed to create note: ${e}\\n`);\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// KG triple extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Look up the integer project_id from the registry backend for a given slug.\n * Returns null if not found.\n */\nasync function lookupProjectId(slug: string): Promise<number | null> {\n try {\n const registry = await getRegistryBackend();\n const project = await registry.getProjectBySlug(slug);\n return project?.id ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Extract structured KG triples from a session summary and store them.\n *\n * This is best-effort: any error is logged but never propagated.\n * Requires Postgres backend — silently no-ops on SQLite.\n */\nasync function extractAndStoreTriples(params: {\n summaryText: string;\n projectSlug: string;\n projectId: number | null;\n sessionId: string;\n gitLog: string;\n model: string;\n}): Promise<void> {\n try {\n // Only works with Postgres backend — KG tables live in Postgres\n if (!storageBackend?.supportsPostgresFeatures) {\n return;\n }\n\n // Check config flag (default: true)\n const cfg = daemonConfig as (typeof daemonConfig & { kg_extraction_enabled?: boolean }) | undefined;\n if (cfg && cfg.kg_extraction_enabled === false) {\n process.stderr.write(\"[session-summary] Triple extraction disabled via kg_extraction_enabled=false.\\n\");\n return;\n }\n\n // storageBackend is already confirmed Postgres above — entity upserts\n // inside kgExtractAndStoreTriples go straight to Postgres, no SQLite touched.\n const result = await kgExtractAndStoreTriples(storageBackend, {\n summaryText: params.summaryText,\n projectSlug: params.projectSlug,\n projectId: params.projectId,\n sessionId: params.sessionId,\n gitLog: params.gitLog,\n model: \"sonnet\",\n });\n\n process.stderr.write(\n `[session-summary] Triple extraction complete: ` +\n `${result.extracted} extracted, ${result.added} added, ${result.superseded} superseded.\\n`\n );\n } catch (err) {\n // Entire extraction is best-effort — never fail the session summary\n process.stderr.write(`[session-summary] Triple extraction failed: ${err}\\n`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Process a `session-summary` work item.\n *\n * This is the main function called by work-queue-worker.ts.\n * Throws on fatal errors (work queue will retry with backoff).\n */\nexport async function handleSessionSummary(payload: SessionSummaryPayload): Promise<void> {\n const { cwd, sessionId, projectSlug, transcriptPath, force } = payload;\n\n if (!cwd) {\n throw new Error(\"session-summary payload missing cwd\");\n }\n\n process.stderr.write(\n `[session-summary] Starting for ${cwd}` +\n `${sessionId ? ` (session=${sessionId})` : \"\"}` +\n `${force ? \" (force=true)\" : \"\"}\\n`\n );\n\n // -------------------------------------------------------------------------\n // Cooldown check — don't summarize too frequently\n // force=true bypasses the cooldown (used by stop-hook at session end so a\n // final summary is always produced regardless of recent PreCompact runs).\n // -------------------------------------------------------------------------\n if (!force && isOnCooldown(cwd)) {\n process.stderr.write(\n \"[session-summary] Skipping — last summary was less than 30 minutes ago.\\n\"\n );\n return;\n }\n\n // -------------------------------------------------------------------------\n // Step 1: Find the JSONL transcript\n // -------------------------------------------------------------------------\n let jsonlPath: string | null = transcriptPath || null;\n\n if (jsonlPath && !existsSync(jsonlPath)) {\n process.stderr.write(\n `[session-summary] Provided transcript path not found: ${jsonlPath}\\n`\n );\n jsonlPath = null;\n }\n\n if (!jsonlPath) {\n jsonlPath = findLatestJsonl(cwd);\n }\n\n if (!jsonlPath) {\n process.stderr.write(\n \"[session-summary] No JSONL transcript found — skipping.\\n\"\n );\n return;\n }\n\n process.stderr.write(`[session-summary] Using transcript: ${jsonlPath}\\n`);\n\n // -------------------------------------------------------------------------\n // Step 2: Extract content from the JSONL\n // -------------------------------------------------------------------------\n // Model selection: opus for session end (force=true), sonnet for auto-compact\n // `force` means \"skip the cooldown\" and nothing else. It used to also select\n // opus, on the assumption that a forced run was the one final session-end\n // summary. It is not: session end is handled mechanically by handleSessionEnd\n // and never reaches here. The only thing that sets force is the stop-hook's\n // mid-session auto-save, which fires every AUTO_SAVE_INTERVAL human messages\n // — so the most expensive model was running over a full transcript every 15\n // messages, in every open session. Measured: 102 opus vs 5 sonnet in 16h.\n //\n // Incremental checkpoints are a summarisation task with a clear input; haiku\n // is sufficient. Callers wanting better can still pass `model` explicitly.\n const selectedModel = payload.model ?? (force ? \"haiku\" : \"sonnet\");\n const extracted = extractFromJsonl(jsonlPath, selectedModel);\n\n if (extracted.userMessages.length === 0) {\n process.stderr.write(\n \"[session-summary] No user messages found in transcript — skipping.\\n\"\n );\n return;\n }\n\n process.stderr.write(\n `[session-summary] Extracted ${extracted.userMessages.length} user messages, ` +\n `${extracted.filesModified.length} modified files.\\n`\n );\n\n // -------------------------------------------------------------------------\n // Step 3: Get git context\n // -------------------------------------------------------------------------\n const gitLog = await getGitContext(cwd, extracted.sessionStartTime);\n\n if (gitLog) {\n process.stderr.write(\n `[session-summary] Got git context (${gitLog.split(\"\\n\").length} lines).\\n`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 4: Build and send prompt to summarizer\n // -------------------------------------------------------------------------\n const today = new Date().toISOString().split(\"T\")[0];\n\n // Check for existing note to merge with\n const notesInfo = findNotesDir(cwd);\n const existingNotePath = getCurrentNotePath(notesInfo.path);\n let existingNote: string | undefined;\n\n if (existingNotePath) {\n const noteFilename = basename(existingNotePath);\n const dateMatch = noteFilename.match(/(\\d{4}-\\d{2}-\\d{2})/);\n if (dateMatch && dateMatch[1] === today) {\n try {\n existingNote = readFileSync(existingNotePath, \"utf-8\");\n } catch { /* ignore */ }\n }\n }\n\n const prompt = buildSessionSummaryPrompt({\n userMessages: extracted.userMessages,\n gitLog,\n cwd,\n date: today,\n filesModified: extracted.filesModified,\n existingNote,\n });\n\n process.stderr.write(\n `[session-summary] Sending ${prompt.length} char prompt to ${selectedModel}...\\n`\n );\n\n const summaryText = await spawnSummarizer(prompt, selectedModel);\n\n if (!summaryText) {\n process.stderr.write(\n `[session-summary] ${selectedModel} did not produce output — falling back to mechanical checkpoint.\\n`\n );\n // Don't throw — this is a soft failure. The existing PreCompact checkpoint\n // is sufficient. Just mark the cooldown so we don't retry too soon.\n markCooldown(cwd);\n return;\n }\n\n process.stderr.write(\n `[session-summary] ${selectedModel} produced ${summaryText.length} char summary.\\n`\n );\n\n // -------------------------------------------------------------------------\n // Step 5: Write the session note\n // -------------------------------------------------------------------------\n const notePath = writeSessionNote(cwd, summaryText, extracted.filesModified, sessionId);\n\n if (notePath) {\n process.stderr.write(\n `[session-summary] Session note written: ${basename(notePath)}\\n`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 6: Best-effort KG triple extraction (Postgres only)\n // -------------------------------------------------------------------------\n const effectiveSlug = projectSlug ?? basename(cwd);\n const projectId = projectSlug ? await lookupProjectId(projectSlug) : null;\n await extractAndStoreTriples({\n summaryText,\n projectSlug: effectiveSlug,\n projectId,\n sessionId: sessionId ?? cwd,\n gitLog,\n model: selectedModel,\n });\n\n // Mark cooldown\n markCooldown(cwd);\n\n process.stderr.write(\"[session-summary] Done.\\n\");\n}\n","/**\n * work-queue.ts — Persistent work queue for the PAI Daemon\n *\n * Provides a durable, file-backed queue that survives daemon restarts.\n * Items are processed sequentially to avoid concurrent writes to the same\n * session note. Failed items are retried with exponential backoff.\n *\n * Queue file: under PAI_HOME (~/.claude/pai/work-queue.json), falling back\n * to the pre-2026-09-19 ~/.config/pai/work-queue.json location.\n * Written atomically (write temp → rename) to prevent corruption.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n renameSync,\n mkdirSync,\n statSync,\n} from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { randomUUID } from \"node:crypto\";\nimport { paiHomePath, resolvePaiFile, migratePaiFile, type MigrateFileResult } from \"../config/pai-home.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type WorkItemType =\n | \"session-end\"\n | \"session-summary\"\n | \"context-handover\"\n | \"note-update\"\n | \"todo-update\"\n | \"topic-detect\"\n | \"registry-scan\";\n\nexport type WorkItemStatus =\n | \"pending\"\n | \"processing\"\n | \"completed\"\n | \"failed\";\n\nexport interface WorkItem {\n id: string;\n type: WorkItemType;\n priority: number; // 1=high, 5=low\n payload: Record<string, unknown>;\n status: WorkItemStatus;\n createdAt: string; // ISO timestamp\n attempts: number;\n maxAttempts: number; // default 3\n nextRetryAt?: string; // ISO timestamp — undefined means ready now\n error?: string; // last error message\n completedAt?: string; // ISO timestamp\n}\n\nexport interface WorkQueueStats {\n pending: number;\n processing: number;\n completed: number;\n failed: number;\n total: number;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nfunction oldQueueFile(): string {\n return join(homedir(), \".config\", \"pai\", \"work-queue.json\");\n}\n\nconst QUEUE_FILE = resolvePaiFile(paiHomePath(\"work-queue.json\"), [oldQueueFile()], \"pai config migrate\");\n\nexport function migrateWorkQueue(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"work-queue.json\"), [oldQueueFile()], opts);\n}\nconst MAX_QUEUE_SIZE = 1000;\nconst MAX_QUEUE_FILE_BYTES = 1024 * 1024; // 1 MB\nconst COMPLETED_TTL_MS = 60 * 60 * 1000; // 1 hour\nconst FAILED_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\n\n/** Backoff delays in ms by attempt number (0-indexed). */\nconst BACKOFF_MS = [\n 5_000, // attempt 1 → wait 5 s\n 30_000, // attempt 2 → wait 30 s\n 300_000, // attempt 3 → wait 5 min\n];\n\n// ---------------------------------------------------------------------------\n// In-memory state\n// ---------------------------------------------------------------------------\n\nlet _queue: WorkItem[] = [];\nlet _dirty = false;\n\n// ---------------------------------------------------------------------------\n// Persistence helpers\n// ---------------------------------------------------------------------------\n\n/** Load queue from disk. Call once at daemon startup. */\nexport function loadQueue(): void {\n if (!existsSync(QUEUE_FILE)) {\n _queue = [];\n return;\n }\n\n try {\n const raw = readFileSync(QUEUE_FILE, \"utf-8\");\n const parsed = JSON.parse(raw) as WorkItem[];\n if (!Array.isArray(parsed)) {\n process.stderr.write(\"[work-queue] Invalid queue file format — starting empty.\\n\");\n _queue = [];\n return;\n }\n\n // On restart, reset any 'processing' items back to 'pending' — they\n // were interrupted mid-flight and need to be retried.\n _queue = parsed.map((item) => {\n if (item.status === \"processing\") {\n return { ...item, status: \"pending\" as WorkItemStatus };\n }\n return item;\n });\n\n const stats = getStats();\n process.stderr.write(\n `[work-queue] Loaded ${_queue.length} items from disk ` +\n `(pending=${stats.pending}, failed=${stats.failed}).\\n`\n );\n } catch (e) {\n process.stderr.write(`[work-queue] Could not load queue file: ${e}\\n`);\n _queue = [];\n }\n}\n\n/** Persist queue to disk atomically. */\nexport function saveQueue(): void {\n const dir = dirname(QUEUE_FILE);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const tmpFile = QUEUE_FILE + \".tmp\";\n try {\n writeFileSync(tmpFile, JSON.stringify(_queue, null, 2), \"utf-8\");\n renameSync(tmpFile, QUEUE_FILE);\n _dirty = false;\n } catch (e) {\n process.stderr.write(`[work-queue] Could not persist queue: ${e}\\n`);\n }\n}\n\n/** Persist only if there are unsaved changes. */\nfunction saveIfDirty(): void {\n if (_dirty) saveQueue();\n}\n\n// ---------------------------------------------------------------------------\n// Queue management\n// ---------------------------------------------------------------------------\n\n/**\n * Enforce the maximum queue size cap.\n * Strategy: first drop oldest completed, then oldest low-priority pending.\n */\nfunction enforceMaxSize(): void {\n if (_queue.length <= MAX_QUEUE_SIZE) return;\n\n const excess = _queue.length - MAX_QUEUE_SIZE;\n\n // Step 1: drop oldest completed items\n const completed = _queue\n .filter((i) => i.status === \"completed\")\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n\n const toDropCompleted = completed.slice(0, excess);\n const dropIds = new Set(toDropCompleted.map((i) => i.id));\n _queue = _queue.filter((i) => !dropIds.has(i.id));\n\n if (_queue.length <= MAX_QUEUE_SIZE) return;\n\n // Step 2: drop oldest low-priority pending items (priority 4-5)\n const remainingExcess = _queue.length - MAX_QUEUE_SIZE;\n const lowPriorityPending = _queue\n .filter((i) => i.status === \"pending\" && i.priority >= 4)\n .sort((a, b) => a.priority - b.priority || a.createdAt.localeCompare(b.createdAt));\n\n const toDropLow = lowPriorityPending.slice(0, remainingExcess);\n const dropLowIds = new Set(toDropLow.map((i) => i.id));\n _queue = _queue.filter((i) => !dropLowIds.has(i.id));\n\n process.stderr.write(\n `[work-queue] Pruned queue to ${_queue.length} items (cap=${MAX_QUEUE_SIZE}).\\n`\n );\n}\n\n/**\n * Add a new work item to the queue.\n * Returns the created WorkItem.\n */\nexport function enqueue(params: {\n type: WorkItemType;\n priority?: number;\n payload: Record<string, unknown>;\n maxAttempts?: number;\n}): WorkItem {\n const item: WorkItem = {\n id: randomUUID(),\n type: params.type,\n priority: params.priority ?? 3,\n payload: params.payload,\n status: \"pending\",\n createdAt: new Date().toISOString(),\n attempts: 0,\n maxAttempts: params.maxAttempts ?? 3,\n };\n\n _queue.push(item);\n enforceMaxSize();\n _dirty = true;\n saveIfDirty();\n\n process.stderr.write(\n `[work-queue] Enqueued ${item.type} (id=${item.id}, priority=${item.priority}).\\n`\n );\n\n return item;\n}\n\n/**\n * Pick the next pending item that is ready to process (respects nextRetryAt).\n * Returns null if no eligible item exists.\n * Highest priority (lowest number) is processed first; ties broken by createdAt.\n */\nexport function dequeue(): WorkItem | null {\n const now = new Date().toISOString();\n\n const eligible = _queue\n .filter((i) => {\n if (i.status !== \"pending\") return false;\n if (i.nextRetryAt && i.nextRetryAt > now) return false;\n return true;\n })\n .sort((a, b) => {\n if (a.priority !== b.priority) return a.priority - b.priority;\n return a.createdAt.localeCompare(b.createdAt);\n });\n\n if (eligible.length === 0) return null;\n\n const item = eligible[0];\n item.status = \"processing\";\n item.attempts += 1;\n _dirty = true;\n saveIfDirty();\n\n return item;\n}\n\n/** Peek at the next eligible pending item without changing its status. */\nexport function peek(): WorkItem | null {\n const now = new Date().toISOString();\n\n return (\n _queue\n .filter((i) => {\n if (i.status !== \"pending\") return false;\n if (i.nextRetryAt && i.nextRetryAt > now) return false;\n return true;\n })\n .sort((a, b) => {\n if (a.priority !== b.priority) return a.priority - b.priority;\n return a.createdAt.localeCompare(b.createdAt);\n })[0] ?? null\n );\n}\n\n/**\n * Mark an item as completed.\n */\nexport function markCompleted(id: string): void {\n const item = _queue.find((i) => i.id === id);\n if (!item) return;\n item.status = \"completed\";\n item.completedAt = new Date().toISOString();\n item.error = undefined;\n _dirty = true;\n saveIfDirty();\n}\n\n/**\n * Mark an item as failed.\n * If attempts < maxAttempts, schedules a retry with exponential backoff.\n * Otherwise, leaves status as 'failed'.\n */\nexport function markFailed(id: string, errorMsg: string): void {\n const item = _queue.find((i) => i.id === id);\n if (!item) return;\n\n item.error = errorMsg;\n\n if (item.attempts < item.maxAttempts) {\n const backoffMs = BACKOFF_MS[item.attempts - 1] ?? BACKOFF_MS[BACKOFF_MS.length - 1];\n item.status = \"pending\";\n item.nextRetryAt = new Date(Date.now() + backoffMs).toISOString();\n process.stderr.write(\n `[work-queue] Item ${id} failed (attempt ${item.attempts}/${item.maxAttempts}), ` +\n `retry in ${backoffMs / 1000}s: ${errorMsg}\\n`\n );\n } else {\n item.status = \"failed\";\n process.stderr.write(\n `[work-queue] Item ${id} exhausted retries (${item.maxAttempts} attempts): ${errorMsg}\\n`\n );\n }\n\n _dirty = true;\n saveIfDirty();\n}\n\n// ---------------------------------------------------------------------------\n// Stats\n// ---------------------------------------------------------------------------\n\nexport function getStats(): WorkQueueStats {\n const stats: WorkQueueStats = {\n pending: 0,\n processing: 0,\n completed: 0,\n failed: 0,\n total: _queue.length,\n };\n for (const item of _queue) {\n stats[item.status as keyof Omit<WorkQueueStats, \"total\">]++;\n }\n return stats;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if any pending or processing item of the given type exists.\n * Used for debouncing work items that are expensive to run concurrently.\n */\nexport function hasPendingOrProcessingOfType(type: WorkItemType): boolean {\n return _queue.some(\n (i) =>\n i.type === type &&\n (i.status === \"pending\" || i.status === \"processing\")\n );\n}\n\n// ---------------------------------------------------------------------------\n// Housekeeping\n// ---------------------------------------------------------------------------\n\n/**\n * Remove completed and permanently-failed items older than their TTL.\n * Also force-cleans all completed items if the queue file exceeds 1 MB.\n */\nexport function cleanup(): void {\n const now = Date.now();\n const before = _queue.length;\n\n // Check file size for force-clean\n let forceCleanCompleted = false;\n try {\n if (existsSync(QUEUE_FILE)) {\n const { size } = statSync(QUEUE_FILE);\n if (size > MAX_QUEUE_FILE_BYTES) {\n forceCleanCompleted = true;\n process.stderr.write(\n `[work-queue] Queue file exceeds 1 MB (${size} bytes) — force-cleaning completed items.\\n`\n );\n }\n }\n } catch {\n // non-fatal\n }\n\n _queue = _queue.filter((item) => {\n if (item.status === \"completed\") {\n if (forceCleanCompleted) return false;\n const completedMs = item.completedAt ? new Date(item.completedAt).getTime() : 0;\n return now - completedMs < COMPLETED_TTL_MS;\n }\n if (item.status === \"failed\") {\n const createdMs = new Date(item.createdAt).getTime();\n return now - createdMs < FAILED_TTL_MS;\n }\n return true;\n });\n\n const removed = before - _queue.length;\n const stats = getStats();\n\n if (removed > 0 || before === 0) {\n process.stderr.write(\n `[work-queue] Cleanup: removed ${removed} items. ` +\n `Queue stats: pending=${stats.pending}, processing=${stats.processing}, ` +\n `completed=${stats.completed}, failed=${stats.failed}.\\n`\n );\n }\n\n _dirty = removed > 0;\n saveIfDirty();\n}\n","/**\n * context-handover-cache.ts — the well-known file where the threshold-\n * triggered pre-compaction handover (see ../../../daemon/context-handover-\n * worker.ts) is written, and where context-compression-hook.ts looks for it\n * at compaction time.\n *\n * Deliberately dependency-light (fs/os/path only). The daemon worker that\n * WRITES this file needs the heavier machinery in session-summary-worker.ts\n * (spawning Claude, git log, etc.), but the PreCompact hook that READS it\n * runs as a short-lived process on every compaction and must not drag in\n * daemon state, database pools, or anything else that module graph carries —\n * this file is the shared seam so neither side has to.\n */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport type HandoverThreshold = \"warmup\" | \"refresh\";\n\nexport interface ContextHandoverCache {\n sessionId: string;\n cwd: string;\n threshold: HandoverThreshold;\n generatedAt: string; // ISO timestamp\n model: string;\n summary: string;\n}\n\nexport function contextHandoverCachePath(sessionId: string): string {\n return join(tmpdir(), `pai-context-handover-${sessionId}.json`);\n}\n\nexport function readContextHandoverCache(sessionId: string): ContextHandoverCache | null {\n const path = contextHandoverCachePath(sessionId);\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf-8\")) as ContextHandoverCache;\n } catch {\n return null;\n }\n}\n\nexport function writeContextHandoverCache(cache: ContextHandoverCache): void {\n writeFileSync(contextHandoverCachePath(cache.sessionId), JSON.stringify(cache, null, 2), \"utf-8\");\n}\n"],"mappings":"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"}
|