{"version":3,"file":"context-handover-cache-yOuK9RIk.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 database helpers for registry command operations. */\n\nimport type { Database } from \"better-sqlite3\";\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 */\nfunction worthWriting(db: Database, projectId: number, encodedDir: string): boolean {\n  if (transcriptFiles(join(claudeProjectsDir(), encodedDir)).length > 0) return true;\n\n  const row = db\n    .prepare(\"SELECT encoded_dir FROM projects WHERE id = ?\")\n    .get(projectId) as { encoded_dir: string | null } | undefined;\n\n  const current = row?.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 function upsertProject(\n  db: Database,\n  slug: string,\n  rootPath: string,\n  encodedDir: string\n): { id: number; isNew: boolean } {\n  const ts = now();\n\n  const byPath = db\n    .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n    .get(rootPath) as { id: number } | undefined;\n\n  if (byPath) {\n    const encodedOwner = db\n      .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n      .get(encodedDir) as { id: number } | undefined;\n\n    if (\n      (!encodedOwner || encodedOwner.id === byPath.id) &&\n      worthWriting(db, byPath.id, encodedDir)\n    ) {\n      db.prepare(\n        \"UPDATE projects SET encoded_dir = ?, updated_at = ? WHERE id = ?\"\n      ).run(encodedDir, ts, byPath.id);\n    }\n    return { id: byPath.id, isNew: false };\n  }\n\n  const byEncoded = db\n    .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n    .get(encodedDir) as { id: number } | undefined;\n\n  if (byEncoded) {\n    const pathOwner = db\n      .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n      .get(rootPath) as { id: number } | undefined;\n\n    if (!pathOwner || pathOwner.id === byEncoded.id) {\n      db.prepare(\n        \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n      ).run(rootPath, ts, byEncoded.id);\n    }\n    return { id: byEncoded.id, isNew: false };\n  }\n\n  // Insert — deduplicate slug with numeric suffix if needed.\n  let finalSlug = slug;\n  let attempt = 0;\n  while (true) {\n    const conflict = db\n      .prepare(\"SELECT id FROM projects WHERE slug = ?\")\n      .get(finalSlug) as { id: number } | undefined;\n    if (!conflict) break;\n    attempt++;\n    finalSlug = `${slug}-${attempt}`;\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) || finalSlug;\n\n  const result = db\n    .prepare(\n      `INSERT OR IGNORE INTO projects\n         (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n       VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`\n    )\n    .run(finalSlug, displayName, rootPath, encodedDir, ts, ts);\n\n  if (result.changes === 0) {\n    const fallback =\n      (db.prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\").get(encodedDir) as { id: number } | undefined) ??\n      (db.prepare(\"SELECT id FROM projects WHERE root_path = ?\").get(rootPath) as { id: number } | undefined);\n\n    if (fallback) {\n      return { id: fallback.id, isNew: false };\n    }\n\n    throw new Error(\n      `upsertProject: INSERT OR IGNORE was suppressed but no matching row found ` +\n      `for root_path=${rootPath} encoded_dir=${encodedDir}`\n    );\n  }\n\n  return { id: result.lastInsertRowid as number, isNew: true };\n}\n\n/** Upsert a session note. Returns true if newly inserted. */\nexport function upsertSession(\n  db: Database,\n  projectId: number,\n  number: number,\n  date: string,\n  slug: string,\n  title: string,\n  filename: string\n): boolean {\n  const existing = db\n    .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND number = ?\")\n    .get(projectId, number);\n\n  if (existing) return false;\n\n  const ts = now();\n  db.prepare(\n    `INSERT INTO sessions\n       (project_id, number, date, slug, title, filename, status, created_at)\n     VALUES (?, ?, ?, ?, ?, ?, 'completed', ?)`\n  ).run(projectId, number, date, slug, title, filename, ts);\n\n  return true;\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 type { Database } from \"better-sqlite3\";\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 function performScan(db: Database): ScanResult {\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 } = upsertProject(db, 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        db.prepare(\n          \"UPDATE projects SET claude_notes_dir = ?, updated_at = ? WHERE id = ?\"\n        ).run(claudeNotesDir, Date.now(), id);\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 = upsertSession(db, 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 = db\n      .prepare(\"SELECT id, slug, root_path FROM projects WHERE status = 'active'\")\n      .all() as { id: number; slug: string; root_path: string }[];\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 = upsertSession(db, 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 = db\n          .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n          .get(childPath) as { id: number } | undefined;\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 (upsertSession(db, 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 } = upsertProject(db, 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 (upsertSession(db, 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 = db\n        .prepare(\"SELECT id, root_path, slug, encoded_dir FROM projects WHERE slug = ?\")\n        .get(marker.slug) as\n        | { id: number; root_path: string; slug: string; encoded_dir: string | null }\n        | undefined;\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 = db\n          .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n          .get(newEncoded) as { id: number } | undefined;\n        const pathOwner = db\n          .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n          .get(markerRoot) as { id: number } | undefined;\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          db.prepare(\n            \"UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?\"\n          ).run(markerRoot, newEncoded, now4, registeredRow.id);\n        } else if (pathSafe) {\n          db.prepare(\n            \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n          ).run(markerRoot, now4, registeredRow.id);\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 stale = db\n      .prepare(\"SELECT id, slug, root_path FROM projects WHERE display_name = slug AND root_path IS NOT NULL AND root_path != ''\")\n      .all() as { id: number; slug: string; root_path: string }[];\n\n    for (const row of stale) {\n      const name = basename(row.root_path);\n      if (name && name !== row.slug) {\n        db.prepare(\"UPDATE projects SET display_name = ?, updated_at = ? WHERE id = ?\")\n          .run(name, Date.now(), row.id);\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 function cmdScan(db: Database, opts: { quick?: boolean } = {}): 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 = performScan(db);\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 * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n  if (!existsSync(ADAPTER_DIR)) {\n    console.error(`ADAPTER_DIR does not exist: ${ADAPTER_DIR}`);\n    console.error(`   Expected ~/.claude or set ADAPTER_DIR environment variable`);\n    process.exit(1);\n  }\n\n  if (!existsSync(HOOKS_DIR)) {\n    console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n    console.error(`   Your ADAPTER_DIR may be misconfigured`);\n    console.error(`   Current ADAPTER_DIR: ${ADAPTER_DIR}`);\n    process.exit(1);\n  }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\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\";\nimport type { Pool } from \"pg\";\nimport type { Database } from \"better-sqlite3\";\n\nimport { buildTripleExtractionPrompt } from \"../daemon/templates/triple-extraction-prompt.js\";\nimport { kgAdd, kgQuery, kgInvalidate } from \"./kg.js\";\nimport { upsertKgEntity } from \"./kg-entity.js\";\nimport { planLlmSpawn, type ModelTier } from \"../workers/daemon-llm.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  /** Optional federation SQLite db — when provided, entities are upserted into kg_entities (QW1) */\n  federationDb?: Database;\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  pool: Pool,\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 federation SQLite kg_entities table when db is available\n      if (params.federationDb && 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            upsertKgEntity(params.federationDb, {\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 kgQuery(pool, {\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 kgInvalidate(pool, supersedes.id);\n        stats.superseded++;\n      }\n\n      await kgAdd(pool, {\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 { openFederation } from \"../memory/db.js\";\nimport { registryDb, 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/** Narrow cast for backends that expose a Postgres pool. */\ninterface BackendWithPool {\n  getPool?(): import(\"pg\").Pool;\n}\n\n/**\n * Look up the integer project_id from the registry DB for a given slug.\n * Returns null if not found or registryDb is not yet initialized.\n */\nfunction lookupProjectId(slug: string): number | null {\n  try {\n    if (!registryDb) return null;\n    const row = (registryDb as import(\"better-sqlite3\").Database)\n      .prepare(\"SELECT id FROM projects WHERE slug = ? LIMIT 1\")\n      .get(slug) as { id: number } | undefined;\n    return row?.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 || storageBackend.backendType !== \"postgres\") {\n      return;\n    }\n\n    const pool = (storageBackend as BackendWithPool).getPool?.();\n    if (!pool) {\n      process.stderr.write(\"[session-summary] Triple extraction: no pool available.\\n\");\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    const federationDb = openFederation();\n    let result;\n    try {\n      result = await kgExtractAndStoreTriples(pool, {\n        summaryText: params.summaryText,\n        projectSlug: params.projectSlug,\n        projectId: params.projectId,\n        sessionId: params.sessionId,\n        gitLog: params.gitLog,\n        model: \"sonnet\",\n        federationDb,\n      });\n    } finally {\n      federationDb.close();\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 ? 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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"}