{"version":3,"file":"main-resolver-BmZPCmgj.mjs","names":["shortenProject"],"sources":["../src/cli/lib/session-scan.ts","../src/cli/lib/exit-dir.ts","../src/cli/lib/launch.ts","../src/cli/lib/history-search.ts","../src/cli/lib/dedup-sessions.ts","../src/cli/commands/main-resolver.ts"],"sourcesContent":["/**\n * Shared scanner for Claude Code sessions stored in ~/.claude/projects/ .\n *\n * Storage layout (Claude Code v2.1.143+):\n *\n *   <project>/<uuid>.jsonl          — top-level: metadata, system snapshots, hook events.\n *                                     REQUIRED for claude --resume to work.\n *   <project>/sessions/<uuid>.jsonl — full transcript: user/assistant/attachment lines.\n *                                     3000+ files, almost none resumable on their own.\n *\n * Resumability rule (empirically verified):\n *   A session is resumable iff the TOP-LEVEL jsonl exists AND contains at least one\n *   line of type \"system\". Sessions that only have a sessions/ counterpart cannot be\n *   resumed by Claude Code regardless of how much transcript content they have.\n *\n * Stale-UUID problem (fixed in this version):\n *   The clc session.json registry stores ONE uuid per named session — the uuid Claude Code\n *   had when the user last named the session. If the user resumes and the session gets a\n *   new uuid (or they Ctrl+C and start fresh), the registry entry still points to the OLD\n *   uuid. The scanner now resolves names to the MOST RECENT top-level jsonl in the project\n *   directory, not the clc-cached uuid.\n *\n * Resolution strategy:\n *   1. Walk top-level <project>/<uuid>.jsonl files (Pass 1).\n *   2. For each, attach the clc name if the uuid matches (exact hit).\n *   3. After Pass 1, for every clc registry entry:\n *      a. If the cached uuid was found → already handled.\n *      b. Find the encodedDir for this entry's directory.\n *      c. Check ALL sessions in that encodedDir (already in our Pass-1 results).\n *      d. Pick the MOST RECENT resumable session in that dir and attach the name to it.\n *         If no resumable session, fall through to transcript-only pass.\n *   4. This means the displayed uuid for \"Jobs Alpha\" is always today's active session,\n *      not the stale cached one.\n *\n * Used by: pai sessions, pai resume\n */\n\nimport {\n  readdirSync,\n  statSync,\n  readFileSync,\n  existsSync,\n  realpathSync,\n  mkdirSync,\n  writeFileSync,\n  renameSync,\n} from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { Database } from \"better-sqlite3\";\nimport { smartDecodeDir } from \"../utils.js\";\nimport { paiHomePath, resolvePaiFile, migratePaiFile, type MigrateFileResult } from \"../../config/pai-home.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Display status for a session:\n *   resumable       — top-level jsonl exists with ≥1 system line (claude --resume works)\n *   stub            — in clc registry, top-level jsonl exists but 0 system lines (Ctrl+C exit)\n *   transcript-only — in clc registry but no top-level jsonl at all\n *   orphan          — not in clc registry, only sessions/ transcript exists\n */\nexport type SessionStatus = \"resumable\" | \"stub\" | \"transcript-only\" | \"orphan\";\n\nexport interface ScannedSession {\n  /** Full UUID from the filename. */\n  uuid: string;\n  /** Short 8-char prefix for display. */\n  shortId: string;\n  /** Encoded project directory name (e.g. \"-Users-foo-myproject\"). */\n  encodedDir: string;\n  /** Decoded project root path (via smartDecodeDir). */\n  decodedPath: string;\n\n  // Top-level jsonl — the resumability source\n  /** Full path to the top-level <project>/<uuid>.jsonl (may not exist). */\n  topLevelPath: string;\n  /** Number of \"system\" lines in the top-level jsonl. */\n  topLevelSystemLines: number;\n  /** Size of the top-level jsonl in bytes. */\n  topLevelSize: number;\n  /**\n   * True iff the top-level jsonl has at least one \"system\" line.\n   * This is the canonical resumability gate — matches Claude Code's --resume behaviour.\n   */\n  resumable: boolean;\n  /** Categorised display status. */\n  sessionStatus: SessionStatus;\n\n  // Sessions/ transcript data (best-effort, may not exist)\n  /** Full path to sessions/<uuid>.jsonl, if present. */\n  sessionJsonlPath?: string;\n  /** Number of \"user\" type lines in the transcript. */\n  userLines: number;\n  /** Last user message text (truncated to 80 chars). */\n  lastUserPrompt: string;\n  /** Total line count in the transcript file. */\n  msgCount: number;\n  /** Auto-generated title from the last ai-title line in the transcript. */\n  aiTitle?: string;\n\n  /** mtime in epoch ms — top-level file mtime (primary), transcript mtime (fallback). */\n  mtime: number;\n\n  /**\n   * Human-readable friendly name (for display and /Name restoration).\n   * Priority: clc session.json name → ai-title → project dir basename.\n   * NEVER slugified — preserves original spaces and capitalisation.\n   */\n  friendlyName?: string;\n  /**\n   * Literal directory from the clc session registry for this UUID.\n   * May contain symlinks and emoji (e.g. /Users/foo/dev/... or .../🧠 Vault/...).\n   * When present, realpathSync(clcDirectory) gives the correct cwd for --resume.\n   */\n  clcDirectory?: string;\n  /** Project root path from PAI registry (authoritative cwd for resume). */\n  registryRootPath?: string;\n}\n\n// ---------------------------------------------------------------------------\n// clc session registry (~/.claude/session.json)\n// ---------------------------------------------------------------------------\n\nconst CLC_SESSIONS_FILE = join(homedir(), \".claude\", \"session.json\");\n\ninterface ClcEntry {\n  name?: string;\n  resume?: string;\n  session?: string;\n  directory?: string;\n}\n\ninterface ClcRegistry {\n  sessions?: ClcEntry[];\n}\n\nexport interface ClcInfo {\n  name: string;\n  /** Literal directory from clc registry (may contain symlinks, emoji, spaces). */\n  directory?: string;\n}\n\n/** Load clc's session registry → uuid → ClcInfo map. Verbatim, never slugified. */\nfunction buildClcInfoMap(): Map<string, ClcInfo> {\n  const map = new Map<string, ClcInfo>();\n  try {\n    const raw = readFileSync(CLC_SESSIONS_FILE, \"utf8\");\n    const data = JSON.parse(raw) as ClcRegistry;\n    for (const entry of data.sessions ?? []) {\n      const name = entry.name?.trim();\n      const uuid = (entry.resume ?? entry.session ?? \"\").trim();\n      if (name && uuid) {\n        map.set(uuid, { name, directory: entry.directory?.trim() || undefined });\n      }\n    }\n  } catch {\n    // Missing or malformed — not fatal\n  }\n  return map;\n}\n\n// ---------------------------------------------------------------------------\n// PAI registry\n// ---------------------------------------------------------------------------\n\ninterface RegistryProject {\n  root_path: string;\n  encoded_dir: string;\n}\n\n/** Build encoded_dir → root_path map from PAI registry (authoritative cwd). */\nfunction buildRegistryRootPathMap(db: Database): Map<string, string> {\n  try {\n    const rows = db\n      .prepare(\n        `SELECT root_path, encoded_dir FROM projects\n         WHERE encoded_dir IS NOT NULL AND encoded_dir != ''`\n      )\n      .all() as RegistryProject[];\n    const map = new Map<string, string>();\n    for (const row of rows) {\n      map.set(row.encoded_dir, row.root_path);\n    }\n    return map;\n  } catch {\n    return new Map();\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Parse cache\n// ---------------------------------------------------------------------------\n\n/**\n * Parsing every jsonl under ~/.claude/projects is the scan's whole cost:\n * thousands of files and multiple gigabytes on a long-lived machine, re-read\n * on every `pai` listing just to render twenty rows (measured 2026-09-18:\n * 10.8s of a 13.4s `pai project list`). A parse result is a pure function of\n * the file's bytes, and (path, mtime, size) is enough to know the bytes have\n * not changed since the last parse — the same freshness check make and rsync\n * use. So parse results are cached on disk and only changed files re-read.\n */\nfunction oldScanCacheFile(): string {\n  return join(homedir(), \".config\", \"pai\", \"session-scan-cache.json\");\n}\n\nexport function scanCacheFilePath(): string {\n  return resolvePaiFile(paiHomePath(\"session-scan-cache.json\"), [oldScanCacheFile()], \"pai config migrate\");\n}\n\nexport function migrateSessionScanCache(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n  return migratePaiFile(paiHomePath(\"session-scan-cache.json\"), [oldScanCacheFile()], opts);\n}\n\ninterface ScanCacheFile {\n  v: 1;\n  top: Record<string, { m: number; s: number; info: TopLevelInfo }>;\n  tr: Record<string, { m: number; s: number; info: TranscriptInfo }>;\n}\n\nlet scanCache: ScanCacheFile | null = null;\nlet scanCacheDirty = false;\n/**\n * Paths parsed this run. scanSessions walks every file, so a flush after it\n * may prune entries it did not touch — deleted sessions stop accumulating.\n */\nconst touchedTop = new Set<string>();\nconst touchedTr = new Set<string>();\n\nfunction loadScanCache(): ScanCacheFile {\n  if (scanCache) return scanCache;\n  try {\n    const raw = JSON.parse(readFileSync(scanCacheFilePath(), \"utf8\")) as ScanCacheFile;\n    if (raw && raw.v === 1 && typeof raw.top === \"object\" && typeof raw.tr === \"object\") {\n      scanCache = raw;\n      return scanCache;\n    }\n  } catch {\n    // Missing, unreadable or corrupt — start over. The cache is an\n    // optimization; its absence costs one full parse, never correctness.\n  }\n  scanCache = { v: 1, top: {}, tr: {} };\n  return scanCache;\n}\n\n/**\n * Persist the parse cache, atomically (tmp + rename) so a concurrent `pai`\n * never reads a half-written file.\n *\n * `prune` drops entries for files not touched this run. Only scanSessions\n * may prune, because only it walks every file — the UUID-prefix fallback\n * parses a handful of paths and must not flatten the cache.\n */\nfunction flushScanCache(prune: boolean): void {\n  if (!scanCache || !scanCacheDirty) return;\n  try {\n    const out: ScanCacheFile = prune\n      ? {\n          v: 1,\n          top: Object.fromEntries(\n            Object.entries(scanCache.top).filter(([p]) => touchedTop.has(p))\n          ),\n          tr: Object.fromEntries(\n            Object.entries(scanCache.tr).filter(([p]) => touchedTr.has(p))\n          ),\n        }\n      : scanCache;\n    const target = paiHomePath(\"session-scan-cache.json\");\n    mkdirSync(paiHomePath(), { recursive: true });\n    const tmp = target + \".tmp\";\n    writeFileSync(tmp, JSON.stringify(out));\n    renameSync(tmp, target);\n  } catch {\n    // Unwritable cache location — next run parses cold, which is exactly\n    // what the no-cache behaviour was.\n  }\n  scanCacheDirty = false;\n}\n\n// ---------------------------------------------------------------------------\n// Top-level jsonl parser\n// ---------------------------------------------------------------------------\n\ninterface TopLevelInfo {\n  systemLines: number;\n  size: number;\n  mtime: number;\n}\n\nfunction parseTopLevel(filePath: string): TopLevelInfo {\n  let st: { size: number; mtimeMs: number };\n  try {\n    st = statSync(filePath);\n  } catch {\n    return { systemLines: 0, size: 0, mtime: 0 };\n  }\n  const hit = loadScanCache().top[filePath];\n  if (hit && hit.m === st.mtimeMs && hit.s === st.size) {\n    touchedTop.add(filePath);\n    return hit.info;\n  }\n  const info = parseTopLevelFile(filePath, st);\n  loadScanCache().top[filePath] = { m: st.mtimeMs, s: st.size, info };\n  touchedTop.add(filePath);\n  scanCacheDirty = true;\n  return info;\n}\n\nfunction parseTopLevelFile(\n  filePath: string,\n  st: { size: number; mtimeMs: number }\n): TopLevelInfo {\n  let systemLines = 0;\n  try {\n    const content = readFileSync(filePath, \"utf8\");\n    for (const line of content.split(\"\\n\")) {\n      const t = line.trim();\n      if (!t) continue;\n      if (t.includes('\"type\":\"system\"') || t.includes('\"type\": \"system\"')) {\n        systemLines++;\n      }\n    }\n  } catch {\n    /* unreadable — size and mtime are still reported, as before */\n  }\n  return { systemLines, size: st.size, mtime: st.mtimeMs };\n}\n\n// ---------------------------------------------------------------------------\n// Sessions/ transcript parser\n// ---------------------------------------------------------------------------\n\ninterface TranscriptInfo {\n  userLines: number;\n  lastUserPrompt: string;\n  msgCount: number;\n  aiTitle?: string;\n  mtime: number;\n}\n\nfunction parseTranscript(filePath: string): TranscriptInfo {\n  let mtime = 0;\n  try {\n    const st = statSync(filePath);\n    mtime = st.mtimeMs;\n    const hit = loadScanCache().tr[filePath];\n    if (hit && hit.m === st.mtimeMs && hit.s === st.size) {\n      touchedTr.add(filePath);\n      return hit.info;\n    }\n    const info = parseTranscriptFile(filePath, st.mtimeMs);\n    loadScanCache().tr[filePath] = { m: st.mtimeMs, s: st.size, info };\n    touchedTr.add(filePath);\n    scanCacheDirty = true;\n    return info;\n  } catch {\n    return { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n  }\n}\n\nfunction parseTranscriptFile(filePath: string, mtime: number): TranscriptInfo {\n  let userLines = 0;\n  let lastUserPrompt = \"\";\n  let msgCount = 0;\n  let aiTitle: string | undefined;\n\n  let content: string;\n  try {\n    content = readFileSync(filePath, \"utf8\");\n  } catch {\n    return { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime };\n  }\n\n  for (const line of content.split(\"\\n\")) {\n    const t = line.trim();\n    if (!t) continue;\n    msgCount++;\n\n    const hasUser = t.includes('\"type\":\"user\"') || t.includes('\"type\": \"user\"');\n    const hasTitle =\n      t.includes('\"type\":\"ai-title\"') || t.includes('\"type\": \"ai-title\"');\n    if (!hasUser && !hasTitle) continue;\n\n    let parsed: Record<string, unknown>;\n    try {\n      parsed = JSON.parse(t) as Record<string, unknown>;\n    } catch {\n      continue;\n    }\n\n    if (parsed.type === \"ai-title\") {\n      const v = parsed.title ?? parsed.name;\n      if (typeof v === \"string\" && v.trim()) aiTitle = v.trim();\n      continue;\n    }\n\n    if (parsed.type !== \"user\") continue;\n    userLines++;\n\n    const msg = parsed.message as Record<string, unknown> | undefined;\n    if (!msg) continue;\n    const c = msg.content;\n    let text = \"\";\n    if (typeof c === \"string\") {\n      text = c;\n    } else if (Array.isArray(c) && c.length > 0) {\n      const first = c[0] as Record<string, unknown>;\n      if (typeof first.text === \"string\") text = first.text;\n    }\n    if (text) lastUserPrompt = text.slice(0, 80).replace(/\\n/g, \" \");\n  }\n\n  return { userLines, lastUserPrompt, msgCount, aiTitle, mtime };\n}\n\n// ---------------------------------------------------------------------------\n// Main scanner\n// ---------------------------------------------------------------------------\n\nexport interface ScanOptions {\n  /** Maximum results to return after sorting (default: 200). */\n  limit?: number;\n  /**\n   * Filter mode:\n   *   \"named\"   (default) — resumable OR in clc registry (named sessions the user cares about)\n   *   \"all\"     — everything including unnamed orphans\n   *   \"resumable\" — only sessions claude --resume accepts (top-level + system lines)\n   */\n  filter?: \"named\" | \"all\" | \"resumable\";\n  /**\n   * @deprecated Use filter instead. When true → filter:\"resumable\", false → filter:\"named\".\n   * Kept for backwards compatibility with callers that predate the filter field.\n   */\n  resumableOnly?: boolean;\n}\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\n// UUID format: 8-4-4-4-12 hex digits\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Compute the effective filter mode, resolving the legacy resumableOnly flag.\n */\nfunction resolveFilter(opts: ScanOptions): \"named\" | \"all\" | \"resumable\" {\n  if (opts.filter) return opts.filter;\n  if (opts.resumableOnly === true) return \"resumable\";\n  if (opts.resumableOnly === false) return \"all\";\n  return \"named\"; // default\n}\n\n/**\n * Scan ~/.claude/projects/ for all Claude Code sessions.\n *\n * Pass 1: walk top-level <project>/<uuid>.jsonl files (the resumability source).\n * Pass 1b: walk <project>/sessions/<uuid>.jsonl — where the stop hook moves a\n *         session's transcript once it ends. These are resumable too, and\n *         skipping them made every cleanly-stopped session unfindable by name.\n * Pass 2: handle clc registry entries whose cached UUID was not found in Pass 1.\n *         For each such entry, scan the entry's project dir for the FRESHEST session\n *         and attach the registry name to it. This fixes the stale-UUID bug where\n *         clc's session.json points to an old uuid after a fresh start.\n * Pass 3: any remaining clc entries with truly no top-level jsonl → transcript-only.\n *\n * Filter modes:\n *   \"named\"     — resumable + registry-known stubs + transcript-only (default)\n *   \"all\"       — all of the above + unnamed orphans from sessions/ subdirs\n *   \"resumable\" — only sessions claude --resume accepts\n *\n * Returns results sorted by mtime descending.\n */\nexport function scanSessions(\n  db: Database,\n  opts: ScanOptions = {}\n): ScannedSession[] {\n  const limit = opts.limit ?? 200;\n  const filterMode = resolveFilter(opts);\n\n  if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];\n\n  const clcInfoMap = buildClcInfoMap();\n  const rootPathMap = buildRegistryRootPathMap(db);\n  const results: ScannedSession[] = [];\n  // Track which UUIDs we've already added (from the jsonl walk)\n  const seenUuids = new Set<string>();\n  // Track which clc names we've already attached to a session\n  const attachedClcUuids = new Set<string>();\n\n  let encodedDirs: string[];\n  try {\n    encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);\n  } catch {\n    return [];\n  }\n\n  // Build a map: encodedDir → all top-level sessions found in Pass 1\n  // This is used in Pass 2 to find the freshest session for a clc registry entry.\n  const sessionsByEncodedDir = new Map<string, ScannedSession[]>();\n\n  // ---- Pass 1: walk top-level <project>/<uuid>.jsonl files ----\n  for (const encodedDir of encodedDirs) {\n    const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);\n    try {\n      if (!statSync(projectDir).isDirectory()) continue;\n    } catch {\n      continue;\n    }\n\n    let files: string[];\n    try {\n      files = readdirSync(projectDir);\n    } catch {\n      continue;\n    }\n\n    const decodedPath =\n      smartDecodeDir(encodedDir) ?? encodedDir.replace(/-/g, \"/\");\n    const registryRootPath = rootPathMap.get(encodedDir);\n    const projectBasename = basename(decodedPath);\n\n    for (const file of files) {\n      if (!file.endsWith(\".jsonl\")) continue;\n      const uuid = file.slice(0, -6);\n      if (!UUID_RE.test(uuid)) continue;\n\n      const topLevelPath = join(projectDir, file);\n      const topInfo = parseTopLevel(topLevelPath);\n      const resumable = topInfo.systemLines > 0;\n      const clcInfo = clcInfoMap.get(uuid);\n      const inRegistry = !!clcInfo;\n\n      // Determine status\n      const sessionStatus: SessionStatus = resumable\n        ? \"resumable\"\n        : inRegistry\n          ? \"stub\"\n          : \"orphan\";\n\n      // Apply filter — but include ALL in this pass so Pass 2 can use them\n      // We'll filter again when deciding what to add to `results`.\n      const passesFilter =\n        filterMode === \"resumable\"\n          ? resumable\n          : filterMode === \"named\"\n            ? resumable || inRegistry\n            : true;\n\n      // Join with sessions/ transcript (best-effort)\n      const sessionJsonlPath = join(projectDir, \"sessions\", `${uuid}.jsonl`);\n      const hasTranscript = existsSync(sessionJsonlPath);\n      const transcript: TranscriptInfo = hasTranscript\n        ? parseTranscript(sessionJsonlPath)\n        : { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n\n      // Use top-level mtime as primary\n      const mtime = topInfo.mtime || transcript.mtime;\n\n      // Name resolution: clc registry (verbatim) → ai-title → project basename\n      const friendlyName =\n        clcInfo?.name ?? transcript.aiTitle ?? projectBasename ?? undefined;\n\n      const session: ScannedSession = {\n        uuid,\n        shortId: uuid.slice(0, 8),\n        encodedDir,\n        decodedPath,\n        topLevelPath,\n        topLevelSystemLines: topInfo.systemLines,\n        topLevelSize: topInfo.size,\n        resumable,\n        sessionStatus,\n        sessionJsonlPath: hasTranscript ? sessionJsonlPath : undefined,\n        userLines: transcript.userLines,\n        lastUserPrompt: transcript.lastUserPrompt,\n        msgCount: transcript.msgCount,\n        aiTitle: transcript.aiTitle,\n        mtime,\n        friendlyName,\n        clcDirectory: clcInfo?.directory,\n        registryRootPath,\n      };\n\n      // Track in per-dir map for Pass 2 lookups (all sessions, regardless of filter)\n      if (!sessionsByEncodedDir.has(encodedDir)) {\n        sessionsByEncodedDir.set(encodedDir, []);\n      }\n      sessionsByEncodedDir.get(encodedDir)!.push(session);\n\n      seenUuids.add(uuid);\n      if (inRegistry) attachedClcUuids.add(uuid);\n\n      if (passesFilter) {\n        results.push(session);\n      }\n    }\n\n    // ---- Pass 1b: finished sessions, which live only in sessions/ ----\n    //\n    // The stop hook MOVES a session's transcript from <project>/<uuid>.jsonl to\n    // <project>/sessions/<uuid>.jsonl. Pass 1 walks the top level only, so a\n    // session becomes invisible to this scan the moment it is cleanly stopped —\n    // and name resolution is built on this scan. That is why `pai Paperfull`\n    // stopped finding Paperfull: three resumable transcripts sat in sessions/\n    // while the scan reported zero sessions for the project, so `pai <name>`\n    // fell through to a prompt-history search over old messages.\n    //\n    // These are reported resumable, which needs justifying, because\n    // `claude --resume` does NOT read sessions/ — it reads the top-level path\n    // and only that. What makes the label true is that nothing resumes without\n    // going through probeResume first, and probeResume links the transcript back\n    // to the top level before answering (lib/launch.ts, restoreTopLevel). PAI's\n    // SessionStart hook displaced these files in the first place; it now links\n    // rather than moves, so this is a claim about repairable state, not a hope.\n    //\n    // Do not weaken that chain without demoting these to transcript-only. An\n    // earlier revision asserted plain \"claude --resume accepts these\", which is\n    // false, and it fed unresumable ids to launch with full confidence.\n    const sessionsDir = join(projectDir, \"sessions\");\n    let finished: string[] = [];\n    try {\n      finished = readdirSync(sessionsDir);\n    } catch {\n      finished = []; // no sessions/ subdir — nothing was ever stopped here\n    }\n\n    for (const file of finished) {\n      if (!file.endsWith(\".jsonl\")) continue;\n      const uuid = file.slice(0, -6);\n      if (!UUID_RE.test(uuid)) continue;\n      if (seenUuids.has(uuid)) continue; // Pass 1 already has it\n\n      const sessionJsonlPath = join(sessionsDir, file);\n      const transcript = parseTranscript(sessionJsonlPath);\n      const clcInfo = clcInfoMap.get(uuid);\n\n      const session: ScannedSession = {\n        uuid,\n        shortId: uuid.slice(0, 8),\n        encodedDir,\n        decodedPath,\n        // No top-level file — that is the whole point of this pass.\n        topLevelPath: \"\",\n        topLevelSystemLines: 0,\n        topLevelSize: 0,\n        resumable: true,\n        sessionStatus: \"resumable\",\n        sessionJsonlPath,\n        userLines: transcript.userLines,\n        lastUserPrompt: transcript.lastUserPrompt,\n        msgCount: transcript.msgCount,\n        aiTitle: transcript.aiTitle,\n        mtime: transcript.mtime,\n        friendlyName:\n          clcInfo?.name ?? transcript.aiTitle ?? projectBasename ?? undefined,\n        clcDirectory: clcInfo?.directory,\n        registryRootPath,\n      };\n\n      if (!sessionsByEncodedDir.has(encodedDir)) {\n        sessionsByEncodedDir.set(encodedDir, []);\n      }\n      sessionsByEncodedDir.get(encodedDir)!.push(session);\n\n      seenUuids.add(uuid);\n      if (clcInfo) attachedClcUuids.add(uuid);\n\n      results.push(session); // resumable — passes every filter mode\n    }\n  }\n\n  // ---- Pass 2: clc registry entries whose cached UUID wasn't found as a top-level file ----\n  // For each such entry, find the project's encodedDir and look for the FRESHEST\n  // resumable session there. Attach the clc name to it.\n  // This fixes the stale-UUID bug: clc may store a uuid that's no longer valid,\n  // but the project dir has a new active session the user is actually in.\n  if (filterMode !== \"resumable\") {\n    for (const [cachedUuid, clcInfo] of clcInfoMap) {\n      if (attachedClcUuids.has(cachedUuid)) continue; // already named in Pass 1\n\n      // Derive the encodedDir from clcInfo.directory\n      let foundEncodedDir: string | undefined;\n      if (clcInfo.directory) {\n        const real = realpathSyncSafe(clcInfo.directory);\n        if (real) {\n          const encoded = encodeProjectDir(real);\n          if (existsSync(join(CLAUDE_PROJECTS_DIR, encoded))) {\n            foundEncodedDir = encoded;\n          }\n        }\n      }\n\n      // Fallback: check if the cached uuid exists as a sessions/ transcript somewhere\n      if (!foundEncodedDir) {\n        for (const encodedDir of encodedDirs) {\n          if (existsSync(join(CLAUDE_PROJECTS_DIR, encodedDir, \"sessions\", `${cachedUuid}.jsonl`))) {\n            foundEncodedDir = encodedDir;\n            break;\n          }\n        }\n      }\n\n      if (foundEncodedDir) {\n        const dirSessions = sessionsByEncodedDir.get(foundEncodedDir) ?? [];\n\n        // Find the freshest RESUMABLE session in this dir that doesn't yet have a name\n        const freshestResumable = dirSessions\n          .filter((s) => s.resumable && !s.friendlyName)\n          .sort((a, b) => b.mtime - a.mtime)[0];\n\n        if (freshestResumable) {\n          // Attach the clc name to this fresher session\n          freshestResumable.friendlyName = clcInfo.name;\n          freshestResumable.clcDirectory =\n            freshestResumable.clcDirectory ?? clcInfo.directory;\n          freshestResumable.sessionStatus = \"resumable\";\n          attachedClcUuids.add(freshestResumable.uuid);\n\n          // Add to results if not already there (it might have been filtered out as orphan)\n          if (!seenUuids.has(freshestResumable.uuid) || !results.includes(freshestResumable)) {\n            // It was added to sessionsByEncodedDir but might have been filtered from results\n            if (!results.includes(freshestResumable)) {\n              results.push(freshestResumable);\n            }\n          }\n          continue; // Done with this clc entry\n        }\n\n        // No unnamed resumable session found. The project might have resumable sessions\n        // that already have a name (different name). Or only stubs/orphans.\n        // Fall through to transcript-only handling below.\n      }\n\n      // ---- Pass 3: truly no top-level jsonl → transcript-only ----\n      if (!foundEncodedDir) {\n        // We couldn't find the project dir at all\n        const encodedDir = \"\";\n        const decodedPath = clcInfo.directory ?? cachedUuid;\n        const registryRootPath = undefined;\n\n        const transcript: TranscriptInfo = { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n        const mtime = transcript.mtime;\n\n        seenUuids.add(cachedUuid);\n        results.push({\n          uuid: cachedUuid,\n          shortId: cachedUuid.slice(0, 8),\n          encodedDir,\n          decodedPath,\n          topLevelPath: \"\",\n          topLevelSystemLines: 0,\n          topLevelSize: 0,\n          resumable: false,\n          sessionStatus: \"transcript-only\",\n          sessionJsonlPath: undefined,\n          userLines: 0,\n          lastUserPrompt: \"\",\n          msgCount: 0,\n          mtime,\n          friendlyName: clcInfo.name,\n          clcDirectory: clcInfo.directory,\n          registryRootPath,\n        });\n        continue;\n      }\n\n      // Project dir was found but no unnamed resumable session.\n      // Check sessions/ for the cached uuid transcript.\n      const foundTranscriptPath = existsSync(\n        join(CLAUDE_PROJECTS_DIR, foundEncodedDir, \"sessions\", `${cachedUuid}.jsonl`)\n      )\n        ? join(CLAUDE_PROJECTS_DIR, foundEncodedDir, \"sessions\", `${cachedUuid}.jsonl`)\n        : undefined;\n\n      const decodedPath =\n        clcInfo.directory ??\n        (smartDecodeDir(foundEncodedDir) ?? foundEncodedDir.replace(/-/g, \"/\"));\n      const registryRootPath = rootPathMap.get(foundEncodedDir);\n      const topLevelPath = join(\n        CLAUDE_PROJECTS_DIR,\n        foundEncodedDir,\n        `${cachedUuid}.jsonl`\n      );\n\n      const transcript: TranscriptInfo = foundTranscriptPath\n        ? parseTranscript(foundTranscriptPath)\n        : { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n\n      seenUuids.add(cachedUuid);\n      results.push({\n        uuid: cachedUuid,\n        shortId: cachedUuid.slice(0, 8),\n        encodedDir: foundEncodedDir,\n        decodedPath,\n        topLevelPath,\n        topLevelSystemLines: 0,\n        topLevelSize: 0,\n        resumable: false,\n        sessionStatus: \"transcript-only\",\n        sessionJsonlPath: foundTranscriptPath,\n        userLines: transcript.userLines,\n        lastUserPrompt: transcript.lastUserPrompt,\n        msgCount: transcript.msgCount,\n        aiTitle: transcript.aiTitle,\n        mtime: transcript.mtime,\n        friendlyName: clcInfo.name,\n        clcDirectory: clcInfo.directory,\n        registryRootPath,\n      });\n    }\n  }\n\n  results.sort((a, b) => b.mtime - a.mtime);\n  // This walk covered every file, so this is the one flush allowed to prune.\n  flushScanCache(true);\n  return results.slice(0, limit);\n}\n\n// ---------------------------------------------------------------------------\n// Path encoding helpers\n// ---------------------------------------------------------------------------\n\n/** Claude Code's project-dir encoding: replace /  .  -  (space) with - */\nfunction encodeProjectDir(realPath: string): string {\n  // Leading slash becomes leading -; all / . - and space → -\n  return realPath.replace(/[/.\\- ]/g, \"-\");\n}\n\n/** realpathSync that returns null instead of throwing */\nfunction realpathSyncSafe(p: string): string | null {\n  try {\n    return realpathSync(p);\n  } catch {\n    return null;\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Age formatter\n// ---------------------------------------------------------------------------\n\nexport function fmtAge(mtime: number): string {\n  const diffMs = Date.now() - mtime;\n  const diffMin = Math.floor(diffMs / 60_000);\n  const diffHr = Math.floor(diffMs / 3_600_000);\n  const diffDay = Math.floor(diffMs / 86_400_000);\n\n  if (diffMin < 60) return `${diffMin}m`;\n  if (diffHr < 24) return `${diffHr}h`;\n  if (diffDay < 30) return `${diffDay}d`;\n  return `${Math.floor(diffDay / 30)}mo`;\n}\n\n// ---------------------------------------------------------------------------\n// Resolve a name/id/prefix to a session entry\n// ---------------------------------------------------------------------------\n\nexport interface ResolvedSession {\n  session: ScannedSession;\n  /** The friendly name to use verbatim in the /Name command. Never slugified. */\n  friendlyName?: string;\n}\n\n/**\n * Filesystem-level UUID scan: walk ALL ~/.claude/projects/<encoded-dir>/<uuid>.jsonl\n * looking for top-level files whose UUID starts with `prefix`.\n *\n * Returns a ScannedSession-like object (minimal fields) for each match, or an empty\n * array if nothing is found. This is used as a fallback when the regular catalog\n * (limited to named/recent sessions) doesn't contain the requested UUID.\n *\n * Complexity: O(number of project directories + files per dir). Typically <200 dirs\n * with a handful of top-level jsonl each — fast enough for an interactive CLI.\n */\nfunction scanFilesystemForUuidPrefix(prefix: string): ScannedSession[] {\n  if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];\n\n  const prefixLower = prefix.toLowerCase();\n  const matches: ScannedSession[] = [];\n\n  let encodedDirs: string[];\n  try {\n    encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);\n  } catch {\n    return [];\n  }\n\n  for (const encodedDir of encodedDirs) {\n    const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);\n    try {\n      if (!statSync(projectDir).isDirectory()) continue;\n    } catch {\n      continue;\n    }\n\n    let files: string[];\n    try {\n      files = readdirSync(projectDir);\n    } catch {\n      continue;\n    }\n\n    for (const file of files) {\n      if (!file.endsWith(\".jsonl\")) continue;\n      const uuid = file.slice(0, -6);\n      if (!UUID_RE.test(uuid)) continue;\n      if (!uuid.toLowerCase().startsWith(prefixLower)) continue;\n\n      const topLevelPath = join(projectDir, file);\n      const topInfo = parseTopLevel(topLevelPath);\n      const resumable = topInfo.systemLines > 0;\n      const decodedPath = smartDecodeDir(encodedDir) ?? encodedDir.replace(/-/g, \"/\");\n\n      // Check sessions/ transcript for prompt and title\n      const sessionJsonlPath = join(projectDir, \"sessions\", `${uuid}.jsonl`);\n      const hasTranscript = existsSync(sessionJsonlPath);\n      const transcript: TranscriptInfo = hasTranscript\n        ? parseTranscript(sessionJsonlPath)\n        : { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n\n      matches.push({\n        uuid,\n        shortId: uuid.slice(0, 8),\n        encodedDir,\n        decodedPath,\n        topLevelPath,\n        topLevelSystemLines: topInfo.systemLines,\n        topLevelSize: topInfo.size,\n        resumable,\n        sessionStatus: resumable ? \"resumable\" : \"stub\",\n        sessionJsonlPath: hasTranscript ? sessionJsonlPath : undefined,\n        userLines: transcript.userLines,\n        lastUserPrompt: transcript.lastUserPrompt,\n        msgCount: transcript.msgCount,\n        aiTitle: transcript.aiTitle,\n        mtime: topInfo.mtime || transcript.mtime,\n        friendlyName: transcript.aiTitle ?? basename(decodedPath),\n        clcDirectory: undefined,\n        registryRootPath: undefined,\n      });\n    }\n  }\n\n  return matches.sort((a, b) => b.mtime - a.mtime);\n}\n\n/**\n * Resolve a name-or-id-or-prefix to a single ScannedSession.\n *\n * Comparisons are case-insensitive; stored casing is preserved in output.\n *\n * Priority:\n *  1. Exact case-insensitive match on friendlyName\n *  2. Partial case-insensitive match (contains)\n *  3. UUID prefix match against the in-memory catalog\n *  4. UUID prefix match against the full filesystem (fallback for any session)\n */\nexport function resolveSessionByNameOrId(\n  sessions: ScannedSession[],\n  query: string\n): ResolvedSession {\n  const qLower = query.toLowerCase().trim();\n\n  // 1. Exact match (case-insensitive, preserve stored casing)\n  const byExact = sessions.filter(\n    (s) => s.friendlyName && s.friendlyName.toLowerCase() === qLower\n  );\n  if (byExact.length >= 1) {\n    return { session: byExact[0], friendlyName: byExact[0].friendlyName };\n  }\n\n  // 2. Partial match\n  const byPartial = sessions.filter(\n    (s) => s.friendlyName && s.friendlyName.toLowerCase().includes(qLower)\n  );\n  if (byPartial.length === 1) {\n    return { session: byPartial[0], friendlyName: byPartial[0].friendlyName };\n  }\n  if (byPartial.length > 1) {\n    const candidates = byPartial\n      .slice(0, 5)\n      .map(\n        (s, i) =>\n          `  ${i + 1}. ${s.shortId}  ${s.friendlyName ?? s.decodedPath}  (${fmtAge(s.mtime)} ago)`\n      )\n      .join(\"\\n\");\n    throw new Error(\n      `Ambiguous name \"${query}\" — ${byPartial.length} matches:\\n${candidates}\\n\\nBe more specific or use a UUID prefix.`\n    );\n  }\n\n  // 3. UUID prefix against catalog\n  const byUuid = sessions.filter((s) => s.uuid.startsWith(qLower));\n  if (byUuid.length === 1) {\n    return { session: byUuid[0], friendlyName: byUuid[0].friendlyName };\n  }\n  if (byUuid.length > 1) {\n    const candidates = byUuid\n      .slice(0, 5)\n      .map(\n        (s, i) =>\n          `  ${i + 1}. ${s.shortId}  ${s.friendlyName ?? s.decodedPath}  (${fmtAge(s.mtime)} ago)`\n      )\n      .join(\"\\n\");\n    throw new Error(\n      `UUID prefix \"${query}\" is ambiguous — ${byUuid.length} matches:\\n${candidates}\\n\\nProvide more characters.`\n    );\n  }\n\n  // 4. Filesystem fallback — scan ALL ~/.claude/projects/*/<uuid>.jsonl files\n  //    This handles any session UUID, even if not in the named/recent catalog.\n  //    Only attempted when the query looks like a UUID prefix (hex + dashes).\n  const UUID_PREFIX_RE = /^[0-9a-f-]{4,36}$/i;\n  if (UUID_PREFIX_RE.test(qLower)) {\n    const fsSessions = scanFilesystemForUuidPrefix(qLower);\n    if (fsSessions.length === 1) {\n      return { session: fsSessions[0], friendlyName: fsSessions[0].friendlyName };\n    }\n    if (fsSessions.length > 1) {\n      const candidates = fsSessions\n        .slice(0, 5)\n        .map(\n          (s, i) =>\n            `  ${i + 1}. ${s.shortId}  ${s.friendlyName ?? s.decodedPath}  (${fmtAge(s.mtime)} ago)`\n        )\n        .join(\"\\n\");\n      throw new Error(\n        `UUID prefix \"${query}\" is ambiguous — ${fsSessions.length} matches:\\n${candidates}\\n\\nProvide more characters.`\n      );\n    }\n  }\n\n  throw new Error(\n    `No session found matching \"${query}\".\\n\\nRun: pai sessions          to list sessions.\\nRun: pai sessions --all    to include transcript-only sessions.\\nRun: pai find <words>      to search prompt history.`\n  );\n}\n","/**\n * Print the working directory of a `claude` session that pai launched, right\n * after it exits. This lets the user cd back into the same directory and\n * restart claude to continue — Claude Code's own `--resume` hint is unreliable\n * for this workflow.\n *\n * Why here and not a SessionEnd hook: a hook inside Claude Code is cancelled\n * during teardown (anthropics/claude-code#41577) and races the terminal\n * restore, so it can't reliably print anything. pai spawns the `claude` binary\n * directly (no shell), so the `claude()` shell wrapper never sees pai-launched\n * sessions either. Printing here — after spawnSync has returned and claude has\n * fully exited — is the one place that reliably reaches the terminal.\n */\nexport function printExitDir(dir: string): void {\n  process.stdout.write(\n    `\\n\\x1b[2m📂 Working directory:\\x1b[0m ${dir}\\n` +\n      `\\x1b[2m   cd \"${dir}\"\\x1b[0m\\n`,\n  );\n}\n","/**\n * launch.ts — Launch Claude Code in a directory, in the CURRENT terminal.\n *\n * Shared by the interactive picker (pick.ts). Deliberately does NOT switch\n * iTerm tabs (aibroker_switch): switching jumps the user to a different — and\n * sometimes wrong — terminal, which is confusing. Picking a place should start\n * a session right here, in the chosen directory.\n *\n * Behaviour:\n *   engine           → fresh launches follow the workers config: routing on\n *                      starts an interactive `pai worker run` (the glm-shim\n *                      shape) instead of claude. `engine` in opts forces one;\n *                      resume always stays claude.\n *   resume-or-fresh  → if a resumable UUID is given, probe it; on success\n *                      `claude --resume`, otherwise fall back to a fresh session\n *                      in the same dir. With no UUID, start fresh.\n *\n * The `claude` child inherits the tty (stdio: \"inherit\"), so the session runs\n * in the terminal that launched `pai`. On exit we print the working directory.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport {\n  realpathSync,\n  existsSync,\n  linkSync,\n  copyFileSync,\n  openSync,\n  readSync,\n  closeSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport chalk from \"chalk\";\nimport { err } from \"../utils.js\";\nimport { readWorkersSection, type WorkersConfig } from \"../../workers/config.js\";\nimport { printExitDir } from \"./exit-dir.js\";\n\nexport interface ProbeResult {\n  ok: boolean;\n  reason?: string;\n}\n\n/**\n * Put a transcript back where `claude --resume` looks for it.\n *\n * `claude --resume <uuid>` reads ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl\n * and ONLY that path. A copy under `sessions/` is invisible to it — measured\n * 2026-08-04, both directions:\n *\n *   b3462801  867 KB, sessions/ only  → \"No conversation found with session ID\"\n *   a9ecdc1c  top level               → found\n *\n * Location is one of two factors, and this function addresses that one. PAI\n * displaced these files itself, from FOUR movers — a SessionStart hook, a\n * UserPromptSubmit hook, the stop hook, and the work-queue worker — of which\n * the UserPromptSubmit one did most of the damage, because it ran on every\n * prompt of every session and excluded only the caller's own transcript. All\n * four now hardlink (project-utils/paths.ts). So this restores a file PAI\n * displaced rather than inventing a layout Claude Code does not use.\n *\n * The other factor is content, and no amount of relinking helps there — see\n * `hasConversation`.\n *\n * A hard link is preferred over a copy: same inode, no second megabyte on disk,\n * and the archive under `sessions/` keeps working for everything that reads it.\n * Returns whether the top-level path exists afterwards.\n */\nexport function restoreTopLevel(uuid: string, dir: string): boolean {\n  const topLevel = join(dir, `${uuid}.jsonl`);\n  if (existsSync(topLevel)) return true;\n\n  const archived = join(dir, \"sessions\", `${uuid}.jsonl`);\n  if (!existsSync(archived)) return false;\n\n  try {\n    linkSync(archived, topLevel);\n    return true;\n  } catch {\n    try {\n      copyFileSync(archived, topLevel);\n      return true;\n    } catch {\n      return false;\n    }\n  }\n}\n\n/**\n * Does this transcript hold an exchange, or only session metadata?\n *\n * The second reason `claude --resume` says \"No conversation found\": the file is\n * there and readable and still holds no conversation. Measured 2026-08-04 on\n * 046bb712 — 537 bytes of last-prompt, custom-title, agent-name, mode and\n * permission-mode, no user line, no assistant line. It was restored to the top\n * level, verified same-inode, and still refused. It was never resumable, and\n * relinking cannot make it so. 30 of PAI's own 50 displaced transcripts are\n * this shape, one of them 745 KB — size proves nothing, because hook context\n * attachments are large.\n *\n * This scans the WHOLE file, and a bounded head will not do. It is tempting —\n * \"a real session's first assistant line lands within the first few KB\" — and\n * it is false. Measured on b3462801, a session `claude --resume` accepts:\n *\n *   file size            866953\n *   length of LINE 1     762977      <- one hook context attachment\n *   first \"type\":\"user\"     766830\n *\n * The first exchange sits past 766 KB because line 1 is a single enormous\n * attachment blob. Any head shorter than that reports a 867 KB working session\n * as an empty stub, which is the exact false negative this function exists to\n * prevent — and it is the shape that produced today's whole incident.\n *\n * Chunked so that a large transcript costs a scan rather than a resident copy,\n * with an overlap so the marker cannot hide across a chunk boundary. Reading a\n * few MB to answer a question about resumability is cheap; being wrong is not.\n *\n * Unreadable counts as real. Claiming a session is empty when it cannot be\n * inspected would talk the caller out of a resume that might have worked.\n */\nconst CONVERSATION_MARKERS = ['\"type\":\"assistant\"', '\"type\": \"assistant\"', '\"type\":\"user\"', '\"type\": \"user\"'];\nconst OVERLAP = 32; // > longest marker, so none can straddle two chunks\n\nexport function hasConversation(path: string, chunkBytes = 1 << 20): boolean {\n  let fd: number | undefined;\n  try {\n    fd = openSync(path, \"r\");\n    const buf = Buffer.alloc(chunkBytes);\n    let carry = \"\";\n    let pos = 0;\n\n    for (;;) {\n      const read = readSync(fd, buf, 0, chunkBytes, pos);\n      if (read <= 0) return false;\n      pos += read;\n\n      const text = carry + buf.subarray(0, read).toString(\"utf8\");\n      if (CONVERSATION_MARKERS.some((m) => text.includes(m))) return true;\n      carry = text.slice(-OVERLAP);\n    }\n  } catch {\n    return true;\n  } finally {\n    if (fd !== undefined) {\n      try {\n        closeSync(fd);\n      } catch {\n        /* nothing useful to do */\n      }\n    }\n  }\n}\n\n/**\n * Can this session be resumed — and if the only thing standing in the way is\n * where its transcript sits, put it back so that the answer is yes.\n *\n * Returns true/false when it can tell, and null when it cannot — the caller\n * must treat null as \"ask something else\", never as \"no\". A path this function\n * fails to recognise would otherwise silently veto a perfectly good resume.\n *\n * Claude Code stores transcripts at ~/.claude/projects/<encoded-cwd>/, where the\n * encoding replaces every non-alphanumeric character with `-`.\n */\nfunction transcriptOnDisk(\n  uuid: string,\n  cwd: string,\n  home = homedir()\n): true | \"missing\" | \"stub\" | null {\n  try {\n    const encoded = cwd.replace(/[^a-zA-Z0-9]/g, \"-\");\n    const dir = join(home, \".claude\", \"projects\", encoded);\n    if (!existsSync(dir)) return null; // unknown layout — not evidence of absence\n    if (!restoreTopLevel(uuid, dir)) return \"missing\";\n    // Present is not the same as resumable. A metadata stub survives every\n    // location check and still fails at `claude --resume`, and this probe is the\n    // only thing standing between that and a caller that exits on the failure\n    // instead of falling back to a fresh session.\n    return hasConversation(join(dir, `${uuid}.jsonl`)) ? true : \"stub\";\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Probe whether a session UUID is resumable from `cwd`.\n *\n * The filesystem is asked first, and usually answers.\n *\n * This used to run `claude --resume <uuid> --print --output-format=json \"_\"`\n * with a 5s timeout, which is not a probe: it resumes the session AND sends a\n * prompt to the model, then waits for a complete JSON reply. That costs a model\n * round-trip per probe, and 5s is not enough time for one — for a LARGE session\n * least of all, because the transcript has to be loaded first.\n *\n * So the check failed precisely for the sessions most worth resuming, and the\n * caller's fallback quietly started a fresh session in their place. Observed\n * 2026-08-04: `pai Paperfull` reported `spawn error: spawnSync claude ETIMEDOUT`\n * for fb76a6c3 and started over, while\n * `~/.claude/projects/…-Paperfull/sessions/fb76a6c3-….jsonl` sat on disk the\n * whole time.\n *\n * A transcript on disk is what resumable MEANS, and reading a directory entry is\n * free. The spawn remains only for the case the filesystem cannot answer, and\n * now gets a timeout that a real answer can fit inside.\n *\n * \"On disk\" had to be tightened once. It first accepted a transcript sitting\n * only under `sessions/`, which claude --resume does not read — so the probe\n * swapped a 5s false negative for a confident false positive, and the caller\n * spawned a resume that died with \"No conversation found\" instead of falling\n * back to a fresh session. `restoreTopLevel` is what makes the permissive\n * reading true rather than merely optimistic.\n */\nexport function probeResume(uuid: string, cwd: string, home?: string): ProbeResult {\n  const onDisk = transcriptOnDisk(uuid, cwd, home);\n  if (onDisk === true) return { ok: true };\n  if (onDisk === \"missing\") {\n    return { ok: false, reason: \"No transcript on disk for this UUID\" };\n  }\n  if (onDisk === \"stub\") {\n    // Say which of the two it is. \"No conversation found\" from claude tells the\n    // user nothing about whether a repair might help; this does.\n    return { ok: false, reason: \"Transcript holds only session metadata — no conversation to resume\" };\n  }\n\n  const result = spawnSync(\n    \"claude\",\n    [\"--resume\", uuid, \"--print\", \"--output-format=json\", \"_\"],\n    { cwd, timeout: 30_000, env: process.env, stdio: [\"ignore\", \"ignore\", \"pipe\"] }\n  );\n\n  if (result.error) return { ok: false, reason: `spawn error: ${result.error.message}` };\n\n  const stderr = result.stderr?.toString(\"utf8\") ?? \"\";\n  if (\n    stderr.toLowerCase().includes(\"no conversation found\") ||\n    stderr.toLowerCase().includes(\"session not found\")\n  ) {\n    return { ok: false, reason: \"No conversation found for this UUID\" };\n  }\n  if (result.status !== 0) {\n    return {\n      ok: false,\n      reason: `claude exited ${result.status ?? \"signal\"}${stderr ? `: ${stderr.slice(0, 120).trim()}` : \"\"}`,\n    };\n  }\n  return { ok: true };\n}\n\nexport interface LaunchOpts {\n  /** If set, try to resume this session before falling back to fresh. */\n  resumableUuid?: string;\n  /** Skip the resume probe and start a brand-new session in the dir. */\n  forceFresh?: boolean;\n  /** Print what would happen, then return without launching. */\n  dryRun?: boolean;\n  /** \"auto\" follows the workers config; \"worker\"/\"claude\" force one engine. */\n  engine?: \"auto\" | \"worker\" | \"claude\";\n}\n\n/** Which engine a launch runs on, and whether it resumes. */\nexport interface LaunchRoute {\n  engine: \"worker\" | \"claude\";\n  resume: boolean;\n}\n\n/**\n * Decide the engine for a launch. \"auto\" mirrors the gate the Agent-routing\n * hook uses, so picker and hook never disagree about whether routing is on.\n * An explicit engine wins outright — the picker w/a keys rely on that.\n *\n * Resume stays claude even under a provider: `pai worker run` always starts\n * fresh, so a claude transcript has nothing to resume on. The provider\n * default therefore applies to fresh launches only — a wanted resume pins\n * the engine to claude unless engine \"worker\" is forced, which drops the\n * resume outright.\n */\nexport function resolveLaunchRoute(\n  workers: Pick<WorkersConfig, \"enabled\" | \"active\" | \"providers\">,\n  opts: LaunchOpts\n): LaunchRoute {\n  const routingOn =\n    workers.enabled &&\n    workers.active !== null &&\n    Object.keys(workers.providers).length > 0;\n  const wantsResume = !opts.forceFresh && !!opts.resumableUuid;\n  const engine: \"worker\" | \"claude\" =\n    opts.engine === \"worker\" ||\n    (opts.engine !== \"claude\" && routingOn && !wantsResume)\n      ? \"worker\"\n      : \"claude\";\n  return { engine, resume: engine === \"claude\" && wantsResume };\n}\n\n/**\n * Interactive worker run, same shape as the ~/.local/bin/glm shim: a label,\n * the directory, and no prompt — the run names itself from the label/dir.\n */\nexport function workerRunArgv(label: string, cwd: string): string[] {\n  return [\"worker\", \"run\", \"--label\", label, \"--cwd\", cwd];\n}\n\n/**\n * Launch `claude` in `dir` in the current terminal. `name` is used for both the\n * Claude session label (--name) and the /Name slash command (tab/statusline).\n * Never returns on the live path — it exits the process after claude exits.\n */\nexport function launchInDir(dir: string, name: string, opts: LaunchOpts = {}): void {\n  let cwd: string;\n  try {\n    cwd = realpathSync(dir);\n  } catch {\n    console.error(\n      err(\n        `Directory does not exist or cannot be resolved:\\n  ${dir}\\n` +\n          `  The folder may have moved or been deleted.`\n      )\n    );\n    process.exitCode = 1;\n    return;\n  }\n\n  const promptArg = `/Name ${name}\\ngo`;\n\n  // A broken workers config must degrade to claude, never crash the picker.\n  let workers: Pick<WorkersConfig, \"enabled\" | \"active\" | \"providers\"> = {\n    enabled: false,\n    active: null,\n    providers: {},\n  };\n  try {\n    workers = readWorkersSection().workers;\n  } catch {\n    /* routing stays off for this launch */\n  }\n  const route = resolveLaunchRoute(workers, opts);\n  const wantResume = route.resume;\n\n  if (opts.dryRun) {\n    if (route.engine === \"worker\") {\n      console.log(\"\\n\" + chalk.bold(\"Dry run — would exec (WORKER path):\") + \"\\n\");\n      console.log(`  cwd:  ${chalk.cyan(cwd)}`);\n      console.log(`  argv: pai worker run --label \"${name}\" --cwd ${cwd}`);\n      console.log();\n      return;\n    }\n    if (wantResume) {\n      console.log(\"\\n\" + chalk.bold(\"Dry run — would probe then exec (RESUME path):\") + \"\\n\");\n      console.log(`  cwd:      ${chalk.cyan(cwd)}`);\n      console.log(`  probe:    transcript on disk for ${opts.resumableUuid!.slice(0, 8)}?`);\n      console.log(`  argv:     claude --resume ${opts.resumableUuid} --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n      console.log(`  fallback: claude --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n    } else {\n      console.log(\"\\n\" + chalk.bold(\"Dry run — would exec (FRESH path):\") + \"\\n\");\n      console.log(`  cwd:  ${chalk.cyan(cwd)}`);\n      console.log(`  argv: claude --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n    }\n    console.log();\n    return;\n  }\n\n  // Worker path: interactive `pai worker run`, fresh by design — no resume.\n  if (route.engine === \"worker\") {\n    const result = spawnSync(\"pai\", workerRunArgv(name, cwd), {\n      cwd,\n      stdio: \"inherit\",\n      env: process.env,\n    });\n    if (result.error) {\n      console.error(err(`Failed to launch pai worker run: ${result.error.message}`));\n      process.exitCode = 1;\n      return;\n    }\n    printExitDir(cwd);\n    process.exitCode = result.status ?? 0;\n    return;\n  }\n\n  const fresh = () => {\n    const result = spawnSync(\"claude\", [\"--name\", name, promptArg], {\n      cwd,\n      stdio: \"inherit\",\n      env: process.env,\n    });\n    if (result.error) {\n      console.error(err(`Failed to launch claude: ${result.error.message}`));\n      process.exitCode = 1;\n      return;\n    }\n    printExitDir(cwd);\n    process.exitCode = result.status ?? 0;\n  };\n\n  if (wantResume) {\n    const probe = probeResume(opts.resumableUuid!, cwd);\n    if (probe.ok) {\n      const result = spawnSync(\n        \"claude\",\n        [\"--resume\", opts.resumableUuid!, \"--name\", name, promptArg],\n        { cwd, stdio: \"inherit\", env: process.env }\n      );\n      if (result.error) {\n        console.error(err(`Failed to launch claude: ${result.error.message}`));\n        process.exitCode = 1;\n        return;\n      }\n      printExitDir(cwd);\n      process.exitCode = result.status ?? 0;\n      return;\n    }\n    process.stderr.write(\n      chalk.yellow(\n        `\\n  Resume failed for ${opts.resumableUuid!.slice(0, 8)}: ${probe.reason ?? \"unknown error\"}\\n` +\n          `  Starting fresh session in same directory.\\n\\n`\n      )\n    );\n    fresh();\n    return;\n  }\n\n  fresh();\n}\n","/**\n * history-search.ts\n *\n * Content-based search across ~/.claude/history.jsonl.\n * Streams the file (avoids loading 26k+ lines into memory), groups matching\n * lines by sessionId, and returns results sorted by most-recent match.\n *\n * Used by: pai <query> (main resolver) and pai find (compat alias)\n */\n\nimport { existsSync, createReadStream } from \"node:fs\";\nimport { createInterface } from \"node:readline\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface HistoryEntry {\n  display?: string;\n  timestamp?: number;\n  project?: string;\n  sessionId?: string;\n  pastedContents?: unknown;\n}\n\nexport interface SessionMatch {\n  sessionId: string | null;\n  /** Most-recent timestamp among matching lines */\n  lastMatchTs: number;\n  /** Most-recent matching prompt text */\n  lastMatchDisplay: string;\n  /** Project path (from the most-recent match) */\n  project: string;\n  /** Number of matching lines in this session */\n  matchCount: number;\n}\n\n// ---------------------------------------------------------------------------\n// History file location\n// ---------------------------------------------------------------------------\n\nexport const HISTORY_FILE = join(homedir(), \".claude\", \"history.jsonl\");\n\n// ---------------------------------------------------------------------------\n// Core search function (streaming)\n// ---------------------------------------------------------------------------\n\n/**\n * Search ~/.claude/history.jsonl for prompts matching the query (case-insensitive\n * substring). Results are grouped by sessionId and sorted by most-recent match.\n *\n * Entries without a sessionId (old Claude Code versions) are excluded from\n * results since they can't be resumed.\n */\nexport async function searchHistory(\n  query: string,\n  maxResults: number\n): Promise<SessionMatch[]> {\n  if (!existsSync(HISTORY_FILE)) return [];\n\n  const queryLower = query.toLowerCase();\n  const bySession = new Map<string, SessionMatch>();\n\n  const rl = createInterface({\n    input: createReadStream(HISTORY_FILE, { encoding: \"utf8\" }),\n    crlfDelay: Infinity,\n  });\n\n  for await (const line of rl) {\n    const t = line.trim();\n    if (!t) continue;\n\n    let entry: HistoryEntry;\n    try {\n      entry = JSON.parse(t) as HistoryEntry;\n    } catch {\n      continue;\n    }\n\n    const display = entry.display ?? \"\";\n    if (!display.toLowerCase().includes(queryLower)) continue;\n\n    const ts = entry.timestamp ?? 0;\n    const project = entry.project ?? \"\";\n    const sessionId = entry.sessionId ?? null;\n\n    // Skip entries with no sessionId — can't resume them\n    if (!sessionId) continue;\n\n    const existing = bySession.get(sessionId);\n    if (!existing) {\n      bySession.set(sessionId, {\n        sessionId,\n        lastMatchTs: ts,\n        lastMatchDisplay: display,\n        project,\n        matchCount: 1,\n      });\n    } else {\n      existing.matchCount++;\n      if (ts > existing.lastMatchTs) {\n        existing.lastMatchTs = ts;\n        existing.lastMatchDisplay = display;\n        existing.project = project;\n      }\n    }\n  }\n\n  // Sort by most-recent match descending\n  const sorted = [...bySession.values()].sort(\n    (a, b) => b.lastMatchTs - a.lastMatchTs\n  );\n\n  return sorted.slice(0, maxResults);\n}\n","/**\n * dedup-sessions.ts — Shared dedup catalog for `pai` and listing commands.\n *\n * Both `pai` (no args) and any listing command must show IDENTICAL output.\n * This module is the single source of truth for:\n *   - Name normalization  (strips spinner prefixes and \" (node)\" suffixes)\n *   - Dedup grouping      (one row per normalized name)\n *   - Priority ranking    (live > resumable > transcript-only > stub > project > orphan)\n *   - Registered projects (shown as status \"idle\" when no live/disk session matches)\n *   - Filtering           (hide 0-session / cold / archived projects by default)\n *   - Table rendering     (shared columns and formatting)\n *\n * Default visibility rules (overridden by showAll=true):\n *   SHOW: live sessions (always)\n *   SHOW: resumable sessions (always)\n *   SHOW: registered projects with session_count > 0 AND last_active within 90 days\n *         OR session_count >= 3 (i.e. well-used projects regardless of age)\n *   HIDE: projects with session_count = 0 (never used)\n *   HIDE: projects with last_active > 90 days AND session_count < 3 (cold)\n *   HIDE: archived projects (status != 'active')\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport chalk from \"chalk\";\nimport { renderTable, dim, header, warn } from \"../utils.js\";\nimport { fmtAge, type ScannedSession } from \"./session-scan.js\";\nimport { type AiBrokerSessionMeta } from \"./aibroker-client.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type UnifiedStatus =\n  | \"live\"\n  | \"resumable\"\n  | \"transcript-only\"\n  | \"stub\"\n  | \"project\"\n  | \"orphan\";\n\nexport const STATUS_PRIORITY: Record<UnifiedStatus, number> = {\n  live: 0,\n  resumable: 1,\n  \"transcript-only\": 2,\n  stub: 3,\n  project: 4,\n  orphan: 5,\n};\n\nexport interface UnifiedSession {\n  /** Clean display name (normalized — no spinners, no \"(node)\") */\n  name: string;\n  /** Slug from the project registry (for slug-based matching in `pai <name>`) */\n  slug?: string;\n  status: UnifiedStatus;\n  /** Only present for live sessions */\n  liveSessionId?: string;\n  /** Only present for disk sessions */\n  diskSession?: ScannedSession;\n  lastActivity: number;\n  project: string;\n  lastPrompt: string;\n  /** For project-source entries: number of historical sessions ever */\n  sessionCount?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Name normalization\n// ---------------------------------------------------------------------------\n\n/**\n * Strip Claude Code spinner characters, decoration prefixes, and \" (node)\" suffixes\n * from a session name so that \"✳ Chenarlier (node)\" and \"Chenarlier\" group together.\n *\n * Handles:\n *   - Unicode braille spinner frames: ⠐ ⠂ ⠁ ⠈ ⠘ ⠙ ⠚ ⠛ ⠗ ⠝\n *   - Decoration characters: ✻ ✳ ✲ ✼ ✦ * ★ ☆ • ●\n *   - Any leading non-letter/digit characters followed by whitespace\n *   - Trailing \" (node)\" suffix (Claude Code appends this on some platforms)\n */\nexport function normalizeName(s: string): string {\n  return s\n    // Strip leading spinner / decoration characters (braille + symbols)\n    .replace(/^[⠀-⣿✀-➿✻✳✲✼✦*★☆•●⠐⠂⠁⠈⠘⠙⠚⠛⠗⠝]+\\s*/u, \"\")\n    // Strip any remaining leading non-alphanumeric prefix before whitespace\n    .replace(/^[^a-zA-Z0-9À-ÿ　-鿿]+\\s*/u, \"\")\n    // Strip trailing \" (node)\" suffix\n    .replace(/\\s*\\(node\\)\\s*$/i, \"\")\n    .trim();\n}\n\n/**\n * Normalize a slug (kebab-case) to a human-readable form for matching.\n * \"jobs-beta\" → \"jobs beta\"\n */\nfunction slugToWords(slug: string): string {\n  return slug.replace(/-/g, \" \");\n}\n\n// ---------------------------------------------------------------------------\n// Dedup builder\n// ---------------------------------------------------------------------------\n\nexport interface RegisteredProject {\n  slug: string;\n  display_name: string;\n  root_path: string;\n  session_count: number;\n  last_active: number | null;\n  status: string;\n}\n\n/**\n * Merge live (AIBroker) + disk (session-scan) + registry (projects DB) sessions\n * into a deduped catalog.\n *\n * Algorithm:\n *   1. Normalize names before grouping\n *   2. Group by normalized name (case-insensitive)\n *   3. Within each group, rank by STATUS_PRIORITY (live wins)\n *   4. Within same priority, keep the most recent (highest mtime)\n *   5. Sort output: by priority, then by lastActivity desc\n *   6. Filter idle project entries (default) or show all (showAll=true)\n *\n * @param showAll  When true, include cold / zero-session / archived projects.\n */\n/**\n * Which of two sessions of equal status should represent a name.\n *\n * Recency alone was the rule, and recency is exactly the wrong instinct here.\n * A resume that FAILS still creates a session file, and that file is newer than\n * the transcript it failed to open. So the tie-break reliably handed back the\n * artefact of a bug in preference to the work.\n *\n * Measured on Paperfull, 2026-08-04:\n *\n *   b3462801  867 KB  32 lines  3 user  15:41:57   <- the actual work\n *   7fdbb9a8   82 KB  25 lines  2 user  15:41:59   <- empty, Ctrl-C'd, WON by 2s\n *   a9ecdc1c   82 KB  25 lines  2 user  15:49:40   <- a second failed resume\n *\n * `pai Paperfull` therefore resolved to 7fdbb9a8 — 82 KB of hook context in\n * `attachment` lines, with no assistant turn anywhere in it — while 867 KB of\n * real work sat one second older and unchosen.\n *\n * WHAT THIS DOES NOT FIX, measured before claiming otherwise: the reported\n * failure was `No conversation found with session ID: 7fdbb9a8`, and that error\n * is about WHERE the transcript is, not what is in it. Both transcripts live\n * only in sessions/, and `claude --resume b3462801` — the good one, the one this\n * change now selects — fails with the identical message. Verified directly, and\n * again on a second project. So this stops dedup preferring the artefact of a\n * bug, which is worth doing on its own, and `pai <Name>` still cannot resume a\n * cleanly-stopped session until the sessions/ relocation is addressed. That part\n * is tracked in Notes/TODO.md and belongs to session-scan/the stop hook.\n *\n * Conversation volume comes first because an empty artefact is small BY\n * CONSTRUCTION — nobody spoke into it — whereas an artefact can be newer by\n * mere milliseconds. Recency only decides between transcripts that recorded the\n * same amount, where it is a reasonable guess again.\n *\n * HONEST LIMIT: the exact signal is \"did the assistant ever reply\", and this is\n * not that — it is a proxy that happens to separate the measured case by one\n * user line and seven total lines, which is a thin margin. The real predicate\n * belongs in `parseTranscript` (session-scan.ts) as an `assistantLines` count,\n * which is free there since it already reads every line of the file. That file\n * was mid-edit in another session when this landed, so this stays out of it.\n * Move to the exact signal when it exists; the tests below should keep passing\n * unchanged, since they assert the outcome and not the proxy.\n */\nfunction winsTiebreak(candidate: UnifiedSession, incumbent: UnifiedSession): boolean {\n  const c = candidate.diskSession;\n  const i = incumbent.diskSession;\n\n  // A live entry has no transcript to weigh. It also never reaches here except\n  // against another live entry, where recency is all there is.\n  if (c && i) {\n    if (c.userLines !== i.userLines) return c.userLines > i.userLines;\n    if (c.msgCount !== i.msgCount) return c.msgCount > i.msgCount;\n  }\n\n  return candidate.lastActivity > incumbent.lastActivity;\n}\n\nexport function buildDeduped(\n  liveSessions: AiBrokerSessionMeta[],\n  diskSessions: ScannedSession[],\n  registeredProjects?: RegisteredProject[],\n  showAll = false\n): UnifiedSession[] {\n  const byName = new Map<string, UnifiedSession>();\n\n  // Process live Claude sessions\n  for (const s of liveSessions) {\n    if (s.kind === \"shell\") continue;\n    // NOT the iTerm tab title.\n    //\n    // `s.name` is whatever iTerm is calling the tab right now, which tracks the\n    // foreground process and the window title escape codes. Using it as a\n    // session IDENTITY means any tab that happens to be titled like a project\n    // becomes a \"live session\" of that name — and because live entries win over\n    // project entries in the merge below, `pai <Project>` then SWITCHES to that\n    // tab instead of launching the project. Reported 2026-08-04: `pai PAI`\n    // printing \"Switched to live session: pai\" and opening nothing.\n    //\n    // A session's identity is the name PAI gave it. A session that has none is\n    // listed by its id and matches nothing by name, which is the safe answer:\n    // failing to find it launches a fresh one, whereas matching it wrongly\n    // hands the user someone else's terminal.\n    const raw = s.paiName ?? s.sessionId.slice(0, 8);\n    const normalized = normalizeName(raw);\n    const key = normalized.toLowerCase();\n\n    const entry: UnifiedSession = {\n      name: normalized,\n      status: \"live\",\n      liveSessionId: s.sessionId,\n      lastActivity: Date.now(),\n      project: \"\",\n      lastPrompt: s.lastPrompt ?? \"\",\n    };\n\n    const existing = byName.get(key);\n    if (!existing || STATUS_PRIORITY[\"live\"] < STATUS_PRIORITY[existing.status]) {\n      byName.set(key, entry);\n    }\n  }\n\n  // Process disk sessions\n  for (const s of diskSessions) {\n    const raw = s.friendlyName ?? s.shortId;\n    const normalized = normalizeName(raw);\n    const key = normalized.toLowerCase();\n\n    let status: UnifiedStatus;\n    if (s.resumable) {\n      status = \"resumable\";\n    } else if (s.sessionStatus === \"transcript-only\") {\n      status = \"transcript-only\";\n    } else if (s.sessionStatus === \"stub\") {\n      status = \"stub\";\n    } else {\n      status = \"orphan\";\n    }\n\n    const entry: UnifiedSession = {\n      name: normalized,\n      status,\n      diskSession: s,\n      lastActivity: s.mtime,\n      project: s.decodedPath ?? \"\",\n      lastPrompt: s.lastUserPrompt ?? \"\",\n    };\n\n    const existing = byName.get(key);\n    if (!existing) {\n      byName.set(key, entry);\n    } else {\n      const ep = STATUS_PRIORITY[existing.status];\n      const np = STATUS_PRIORITY[status];\n      if (np < ep || (np === ep && winsTiebreak(entry, existing))) {\n        byName.set(key, entry);\n      }\n    }\n  }\n\n  // Constants for idle-project filtering\n  const COLD_DAYS = 90;\n  const COLD_THRESHOLD_MS = COLD_DAYS * 24 * 60 * 60 * 1000;\n  const now = Date.now();\n\n  // Process registered projects — add any not already covered by live/disk entries\n  if (registeredProjects) {\n    for (const p of registeredProjects) {\n      // Skip archived projects unless showAll\n      if (!showAll && p.status !== \"active\") continue;\n\n      const displayName = p.display_name ?? p.slug;\n      const normalized = normalizeName(displayName);\n      const key = normalized.toLowerCase();\n\n      // Also check if slug matches (via slug→words form)\n      const slugKey = slugToWords(p.slug).toLowerCase();\n\n      if (!byName.has(key) && !byName.has(slugKey)) {\n        // Apply idle-project filter unless showAll:\n        //   - Hide projects with 0 sessions (never used)\n        //   - Hide projects cold AND low usage: last_active > 90 days AND sessions < 3\n        if (!showAll) {\n          if (p.session_count === 0) continue;\n          if (p.session_count < 3) {\n            const lastActive = p.last_active ?? 0;\n            if (now - lastActive > COLD_THRESHOLD_MS) continue;\n          }\n        }\n\n        byName.set(key, {\n          name: normalized,\n          slug: p.slug,\n          status: \"project\",\n          lastActivity: p.last_active ?? 0,\n          project: p.root_path,\n          lastPrompt: \"\",\n          sessionCount: p.session_count,\n        });\n      } else {\n        // Already have an entry — enrich it with the slug for matching\n        const existing = byName.get(key) ?? byName.get(slugKey);\n        if (existing && !existing.slug) {\n          existing.slug = p.slug;\n        }\n        if (existing && existing.sessionCount === undefined) {\n          existing.sessionCount = p.session_count;\n        }\n      }\n    }\n  }\n\n  // Sort: by priority first, then by lastActivity desc\n  const entries = Array.from(byName.values());\n  entries.sort((a, b) => {\n    const pa = STATUS_PRIORITY[a.status];\n    const pb = STATUS_PRIORITY[b.status];\n    if (pa !== pb) return pa - pb;\n    return b.lastActivity - a.lastActivity;\n  });\n\n  return entries;\n}\n\n// ---------------------------------------------------------------------------\n// Status formatter\n// ---------------------------------------------------------------------------\n\nexport function fmtUnifiedStatus(s: UnifiedStatus): string {\n  switch (s) {\n    case \"live\":             return chalk.green(\"live\");\n    case \"resumable\":        return chalk.cyan(\"resumable\");\n    case \"transcript-only\":  return chalk.dim(\"transcript\");\n    case \"stub\":             return chalk.dim(\"stub\");\n    case \"project\":          return chalk.dim(\"idle\");\n    case \"orphan\":           return chalk.dim(\"orphan\");\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Shared table renderer\n// ---------------------------------------------------------------------------\n\nfunction shortenProject(p: string, maxLen = 28): string {\n  if (!p || p.length <= maxLen) return p || dim(\"—\");\n  return \"…\" + p.slice(-(maxLen - 1));\n}\n\n/**\n * Render the deduped session catalog to stdout.\n *\n * @param entries   Output of buildDeduped()\n * @param maxRows   Maximum rows to display. undefined = no limit (for --all).\n */\nexport function renderDedupedSessions(entries: UnifiedSession[], maxRows?: number): void {\n  if (entries.length === 0) {\n    console.log(warn(\"No sessions found. Start Claude Code in a project directory first.\"));\n    return;\n  }\n\n  console.log(\"\\n\" + header(\"Sessions\") + \"\\n\");\n\n  const visible = maxRows !== undefined ? entries.slice(0, maxRows) : entries;\n  const tableHeaders = [\"#\", \"name\", \"status\", \"age\", \"project\", \"last prompt\"];\n  const tableRows = visible.map((entry, i) => {\n    const age =\n      entry.status === \"live\"\n        ? chalk.green(\"now\")\n        : entry.lastActivity > 0\n          ? dim(fmtAge(entry.lastActivity))\n          : dim(\"—\");\n    const snippet = entry.lastPrompt.replace(/\\n+/g, \" \").trim().slice(0, 36);\n    const projectPath =\n      entry.diskSession\n        ? entry.diskSession.friendlyName ?? entry.diskSession.decodedPath\n        : entry.project;\n    const project = dim(shortenProject(projectPath, 28));\n    return [\n      dim(String(i + 1)),\n      chalk.white(entry.name),\n      fmtUnifiedStatus(entry.status),\n      age,\n      project,\n      chalk.dim(snippet ? `\"${snippet}\"` : \"—\"),\n    ];\n  });\n\n  console.log(renderTable(tableHeaders, tableRows));\n\n  if (maxRows !== undefined && entries.length > maxRows) {\n    console.log(dim(`  … ${entries.length - maxRows} more — use --all to show everything`));\n  }\n\n  console.log();\n  console.log(\n    dim(\"  Switch/resume/start: \") +\n    chalk.white(\"pai <name>\") +\n    dim(\"  or  \") +\n    chalk.white(\"pai <uuid-prefix>\")\n  );\n  console.log();\n}\n","/**\n * main-resolver.ts\n *\n * `pai [<query>]`  —  topic-first session discovery and launcher (v0.11.1)\n *\n * Decision tree:\n *   1. No arg          → show deduped session listing (one row per name)\n *   2. UUID prefix     → universal filesystem scan; auto-launch the match\n *   3. Any string:\n *      a. Live match (by normalized paiName) → aibroker_switch → iTerm tab to front. Done.\n *      b. Resumable match                    → probe + claude --resume <uuid>\n *      c. Transcript/stub match              → fresh claude in same project dir\n *      d. No name match                      → free-text history search → picker\n *\n * Dedup + name normalization logic: src/cli/lib/dedup-sessions.ts (shared with listing).\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { createInterface } from \"node:readline\";\nimport { existsSync } from \"node:fs\";\nimport { realpathSync } from \"node:fs\";\nimport chalk from \"chalk\";\nimport { err, dim, warn, ok, header, renderTable } from \"../utils.js\";\nimport {\n  scanSessions,\n  fmtAge,\n  type ScannedSession,\n} from \"../lib/session-scan.js\";\nimport { searchHistory, HISTORY_FILE, type SessionMatch } from \"../lib/history-search.js\";\nimport { fetchLiveSessions, fetchLiveSessionsWithPrompts, switchToSession } from \"../lib/aibroker-client.js\";\nimport { basename } from \"node:path\";\nimport { launchInDir } from \"../lib/launch.js\";\nimport {\n  buildDeduped,\n  normalizeName,\n  STATUS_PRIORITY,\n  fmtUnifiedStatus,\n  renderDedupedSessions,\n  type UnifiedSession,\n  type RegisteredProject,\n} from \"../lib/dedup-sessions.js\";\n\n// ---------------------------------------------------------------------------\n// Launch session (disk-based)\n// ---------------------------------------------------------------------------\n\n/**\n * Which directory should we open for this session?\n *\n * A session carries up to three ideas of where it lives, they disagree in\n * practice, and the FIRST is the least trustworthy. `clcDirectory` and\n * `decodedPath` are recorded per session and go stale the moment a project\n * directory is renamed; `registryRootPath` is kept current by the registry.\n *\n * This used to take the preferred value and, if it did not resolve, print an\n * error and exit — so a live project became unopenable by its own name. The\n * session had been started when its directory had a different name, so the\n * stale per-session value pointed at a path that no longer existed while the\n * registry held the right one, untried.\n *\n * The order is unchanged, because the per-session value IS more specific when\n * valid — it can name a subdirectory the session actually ran in. What changed\n * is that a candidate which fails no longer ends the attempt.\n *\n * realpath, not resolve: a directory reached through a symlinked parent has\n * several valid spellings, and comparing or storing the unresolved one is how\n * the same directory ends up with two identities.\n *\n * Returns the resolved directory plus every candidate tried, so a total failure\n * can report what it looked at instead of naming one path and leaving the user\n * to guess whether the others were even considered.\n */\nexport function resolveSessionDir(session: {\n  clcDirectory?: string;\n  registryRootPath?: string;\n  decodedPath?: string;\n}): { dir: string | undefined; tried: string[] } {\n  const tried = [\n    session.clcDirectory,\n    session.registryRootPath,\n    session.decodedPath,\n  ].filter((d): d is string => typeof d === \"string\" && d.length > 0);\n\n  for (const candidate of tried) {\n    try {\n      return { dir: realpathSync(candidate), tried };\n    } catch {\n      // Stale, renamed or deleted — try the next one.\n    }\n  }\n  return { dir: undefined, tried };\n}\n\nfunction launchSession(\n  session: ScannedSession,\n  allSessions: ScannedSession[],\n  dryRun: boolean\n): boolean {\n  let resumableUuid: string | undefined;\n\n  if (session.resumable) {\n    resumableUuid = session.uuid;\n  } else if (session.encodedDir) {\n    const sameProject = allSessions.filter(\n      (s) => s.encodedDir === session.encodedDir && s.resumable\n    );\n    sameProject.sort((a, b) => b.mtime - a.mtime);\n    if (sameProject.length > 0) {\n      resumableUuid = sameProject[0].uuid;\n    }\n  }\n\n  const { dir: projectDir } = resolveSessionDir(session);\n\n  // A dead session must not end the whole attempt.\n  //\n  // This used to print an error and process.exit(1). But the caller has more\n  // candidates after this one — most importantly the registered PROJECT, whose\n  // path the registry keeps current — and `openMatch` already knows how to open\n  // a directory directly. Exiting here meant one stale transcript made a live\n  // project unreachable by name: the user asks for a name they can see in the\n  // listing, and gets a dead path instead of a session in the directory that\n  // plainly exists.\n  //\n  // Returning false lets resolution continue. If nothing at all can be opened,\n  // the caller reports that once, with everything it tried.\n  if (projectDir === undefined) return false;\n\n  // A session is named for a human to recognise, so a UUID prefix is never an\n  // acceptable answer. `friendlyName` is undefined for the sessions synthesized\n  // from a prompt-history match, and picking one of those used to open a tab\n  // called \"7fdbb9a8\" — for a project plainly called Paperfull, sitting right\n  // there in the path we already resolved. The directory is the better fallback\n  // in every case, so shortId is now only reached for a path with no basename.\n  const name = session.friendlyName ?? (basename(projectDir) || session.shortId);\n\n  // Hand the launch to the shared router, never spawn claude here. This used to\n  // spawn `claude` directly in every branch — resume, resume-fallback and fresh —\n  // so `pai <name>` ran on Anthropic credentials even with a worker provider\n  // active. launchInDir owns the routing: fresh launches follow the workers\n  // config (provider when routing is on), a wanted resume stays claude because\n  // its transcript is claude-format. Duplicating that dance here is also how\n  // probeResume ended up fixed in one copy and not the others.\n  launchInDir(projectDir, name, { resumableUuid, dryRun });\n  return true;\n}\n\n// ---------------------------------------------------------------------------\n// History match → ScannedSession bridge\n// ---------------------------------------------------------------------------\n\nfunction matchToSession(\n  match: SessionMatch,\n  allSessions: ScannedSession[]\n): ScannedSession | null {\n  if (!match.sessionId) return null;\n\n  const catalogMatch = allSessions.find((s) => s.uuid === match.sessionId);\n  if (catalogMatch) return catalogMatch;\n\n  if (!match.project) return null;\n\n  return {\n    uuid: match.sessionId,\n    shortId: match.sessionId.slice(0, 8),\n    encodedDir: \"\",\n    decodedPath: match.project,\n    topLevelPath: \"\",\n    topLevelSystemLines: 0,\n    topLevelSize: 0,\n    resumable: false,\n    sessionStatus: \"transcript-only\",\n    sessionJsonlPath: undefined,\n    userLines: 0,\n    lastUserPrompt: match.lastMatchDisplay.slice(0, 80),\n    msgCount: 0,\n    mtime: match.lastMatchTs,\n    friendlyName: undefined,\n    clcDirectory: undefined,\n    registryRootPath: match.project,\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Formatters\n// ---------------------------------------------------------------------------\n\nfunction fmtTs(ts: number): string {\n  const d = new Date(ts);\n  const pad = (n: number) => String(n).padStart(2, \"0\");\n  return (\n    `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +\n    `${pad(d.getHours())}:${pad(d.getMinutes())}`\n  );\n}\n\nfunction shortenProject(p: string, maxLen = 44): string {\n  if (!p || p.length <= maxLen) return p || dim(\"—\");\n  return \"…\" + p.slice(-(maxLen - 1));\n}\n\n// ---------------------------------------------------------------------------\n// Interactive picker prompt\n// ---------------------------------------------------------------------------\n\nasync function askForChoice(max: number): Promise<number | null> {\n  return new Promise((resolve) => {\n    const rl = createInterface({ input: process.stdin, output: process.stdout });\n    rl.question(\n      dim(`\\n  Enter # to launch (1-${max}), or press Enter to cancel: `),\n      (answer) => {\n        rl.close();\n        const n = parseInt(answer.trim(), 10);\n        if (!isNaN(n) && n >= 1 && n <= max) {\n          resolve(n);\n        } else {\n          resolve(null);\n        }\n      }\n    );\n  });\n}\n\n// ---------------------------------------------------------------------------\n// Shared switch helper (live session → iTerm tab)\n// ---------------------------------------------------------------------------\n\nasync function doSwitch(\n  entry: UnifiedSession,\n  dryRun: boolean\n): Promise<boolean> {\n  if (!entry.liveSessionId) return false;\n  if (dryRun) {\n    console.log(\"\\n\" + chalk.bold(\"Dry run — would switch iTerm tab:\") + \"\\n\");\n    console.log(`  target: ${entry.name} (${entry.liveSessionId.slice(0, 8)})`);\n    console.log(`  action: aibroker_switch + osascript iTerm activate`);\n    console.log();\n    return true;\n  }\n  const result = await switchToSession(entry.liveSessionId);\n  if (result.ok) {\n    // Name AND id. \"Switched to live session: pai\" was reported three times on\n    // 2026-08-04 by a user watching nothing happen, and the message gave no way\n    // to tell WHICH session had been revealed — so there was nothing to check\n    // against the tabs actually on screen. The id makes the claim falsifiable.\n    console.log(\n      ok(\n        `Switched to live session: ${chalk.white(entry.name)} ` +\n          chalk.dim(`(${entry.liveSessionId.slice(0, 8)})`),\n      ),\n    );\n    return true;\n  }\n  console.error(warn(`Could not switch via AIBroker: ${result.error ?? \"unknown error\"}`));\n  return false;\n}\n\n/**\n * Act on a matched catalog entry: switch to it, resume it, or start it.\n *\n * The third case is the one that was missing. A registered project whose\n * sessions have all ended matches by name perfectly well, but carries no\n * `diskSession`, and the old code simply ran off the end of both the exact and\n * the partial branch — past the picker, into a free-text search of prompt\n * history. So `pai Paperfull` answered a request to open a project with a list\n * of five old conversations that merely mentioned the word, while the project's\n * own directory was sitting in `entry.project` the whole time.\n *\n * Returns false only when there is genuinely nothing to open, which is the one\n * case where falling through to a history search is the right thing to do.\n */\nasync function openMatch(\n  entry: UnifiedSession,\n  allSessions: ScannedSession[],\n  dryRun: boolean\n): Promise<boolean> {\n  if (entry.status === \"live\") {\n    if (await doSwitch(entry, dryRun)) return true;\n    // Switch failed — fall through and try to open it from disk instead.\n  }\n  // A transcript whose directory is gone is not an answer. Fall through to the\n  // registered project below, whose path the registry keeps current — that is\n  // the difference between \"your project moved\" and \"cannot open anything\".\n  if (entry.diskSession && launchSession(entry.diskSession, allSessions, dryRun)) {\n    return true;\n  }\n  if (entry.project && existsSync(entry.project)) {\n    launchInDir(entry.project, entry.name, { dryRun });\n    return true;\n  }\n  return false;\n}\n\n// ---------------------------------------------------------------------------\n// Main command\n// ---------------------------------------------------------------------------\n\nexport interface MainResolverOpts {\n  auto?: boolean;    // -y / --auto: pick #1 without prompting\n  dryRun?: boolean;  // --dry-run: show what would happen\n  n?: string;        // --n <count>: max candidates for history search\n  all?: boolean;     // --all: show cold / 0-session / archived projects too\n}\n\nfunction getRegisteredProjects(db: Database, all = false): RegisteredProject[] {\n  try {\n    const statusClause = all ? \"\" : \"WHERE p.status = 'active'\";\n    return db\n      .prepare(`\n        SELECT\n          p.slug,\n          p.display_name,\n          p.root_path,\n          p.status,\n          COUNT(s.id) AS session_count,\n          MAX(s.created_at) AS last_active\n        FROM projects p\n        LEFT JOIN sessions s ON s.project_id = p.id\n        ${statusClause}\n        GROUP BY p.id\n        ORDER BY last_active DESC NULLS LAST, p.updated_at DESC\n      `)\n      .all() as RegisteredProject[];\n  } catch {\n    return [];\n  }\n}\n\nexport async function cmdMain(\n  db: Database,\n  query: string | undefined,\n  pickN: number | undefined,\n  opts: MainResolverOpts\n): Promise<void> {\n  const maxResults = parseInt(opts.n ?? \"20\", 10);\n  const showAll = opts.all ?? false;\n  // Live sessions: metadata-only fetch (1s) — last-prompt fetch is too slow\n  // (~8s for AppleScript per-session scrollback). For live entries, last-prompt\n  // stays empty; user can `pai <name>` to switch into the tab and see context.\n  const livePromise = !query\n    ? fetchLiveSessions().catch(() => [] as Awaited<ReturnType<typeof fetchLiveSessions>>)\n    : Promise.resolve([]);\n  const allSessions = scanSessions(db, { limit: 500, filter: \"named\" });\n  const registeredProjects = getRegisteredProjects(db, showAll);\n\n  // -----------------------------------------------------------------------\n  // Case 1: No query → deduped session listing (shared renderer)\n  // -----------------------------------------------------------------------\n  if (!query) {\n    const liveSessions = await livePromise;\n    const deduped = buildDeduped(liveSessions, allSessions, registeredProjects, showAll);\n    renderDedupedSessions(deduped, showAll ? undefined : maxResults);\n    return;\n  }\n\n  // -----------------------------------------------------------------------\n  // Case 2: UUID prefix (8+ hex chars) → universal filesystem scan\n  // -----------------------------------------------------------------------\n  const UUID_PREFIX_RE = /^[0-9a-f-]{8,36}$/i;\n  if (UUID_PREFIX_RE.test(query)) {\n    const byUuid = allSessions.filter((s) => s.uuid.startsWith(query.toLowerCase()));\n    if (byUuid.length === 1) {\n      launchSession(byUuid[0], allSessions, opts.dryRun ?? false);\n      return;\n    }\n    if (byUuid.length > 1) {\n      console.error(err(`UUID prefix \"${query}\" is ambiguous — ${byUuid.length} catalog matches.`));\n      process.exitCode = 1;\n      return;\n    }\n    // Fall through to name match / history search\n  }\n\n  // -----------------------------------------------------------------------\n  // Case 3: Name match against deduped catalog (normalized + slug matching)\n  // -----------------------------------------------------------------------\n  {\n    let liveSessions: Awaited<ReturnType<typeof fetchLiveSessions>> = [];\n    try {\n      liveSessions = await fetchLiveSessions();\n    } catch {\n      // AIBroker not running\n    }\n\n    const deduped = buildDeduped(liveSessions, allSessions, registeredProjects, showAll);\n    // Normalize the query the same way we normalize session names\n    // Also support slug form: \"jobs-beta\" → \"jobs beta\" for matching\n    const qNorm = normalizeName(query).toLowerCase();\n    const qSlug = query.toLowerCase().replace(/\\s+/g, \"-\"); // words → slug form for slug lookup\n\n    // Match helper: checks normalized display name AND slug\n    const nameMatches = (e: UnifiedSession, q: string) =>\n      e.name.toLowerCase() === q ||\n      (e.slug !== undefined && e.slug.toLowerCase() === qSlug);\n    const nameIncludes = (e: UnifiedSession, q: string) =>\n      e.name.toLowerCase().includes(q) ||\n      (e.slug !== undefined && e.slug.toLowerCase().includes(qSlug));\n\n    // Among equally-good matches, the most recently active one is what the user\n    // means. This used `deduped.find()`, which took whichever entry happened to\n    // come first — so a two-month-old note whose title matched EXACTLY beat the\n    // live project the user was actually working in, whose sessions carried an\n    // extra word in their names. Typing the project's own name opened its oldest\n    // session.\n    const newestFirst = (a: UnifiedSession, b: UnifiedSession) =>\n      b.lastActivity - a.lastActivity;\n\n    // Exact normalized-name match first (display_name or slug), newest wins.\n    //\n    // Try EVERY exact match, not just the best one. They are ranked by recency,\n    // but the top-ranked entry can be unopenable — a transcript pointing at a\n    // directory that has since been renamed — while a lower-ranked entry for the\n    // same name is the live project sitting right there. Stopping at the first\n    // candidate turned \"this name is ambiguous\" into \"this name is broken\".\n    const exactMatches = deduped.filter((e) => nameMatches(e, qNorm)).sort(newestFirst);\n    for (const match of exactMatches) {\n      if (await openMatch(match, allSessions, opts.dryRun ?? false)) return;\n    }\n\n    // Partial normalized-name match (display_name or slug)\n    let partialMatches = deduped.filter((e) => nameIncludes(e, qNorm));\n\n    // Substring matching misses the common case where every query word is\n    // present but not adjacent: \"foo baz\" is not a substring of \"foo bar baz\",\n    // so a project whose directory gained a word became unreachable by the name\n    // its owner thinks of it by. Fall back to requiring every query WORD to\n    // appear somewhere in the name or slug. That stays narrow — a sibling\n    // project sharing only the first word still will not match, because it\n    // lacks the second.\n    if (partialMatches.length === 0) {\n      const qWords = qNorm.split(/\\s+/).filter(Boolean);\n      if (qWords.length > 1) {\n        partialMatches = deduped.filter((e) => {\n          const haystack = `${e.name} ${e.slug ?? \"\"}`.toLowerCase();\n          return qWords.every((w) => haystack.includes(w));\n        });\n      }\n    }\n\n    partialMatches.sort(newestFirst);\n\n    if (partialMatches.length === 1) {\n      if (await openMatch(partialMatches[0], allSessions, opts.dryRun ?? false)) return;\n    }\n\n    if (partialMatches.length > 1) {\n      // Multiple matches — show picker\n      console.log(\"\\n\" + header(`Sessions matching \"${query}\"`) + \"\\n\");\n      const headers = [\"#\", \"name\", \"status\", \"age\", \"project\"];\n      const rows = partialMatches.slice(0, maxResults).map((entry, i) => {\n        const age =\n          entry.status === \"live\"\n            ? chalk.green(\"now\")\n            : dim(fmtAge(entry.lastActivity));\n        const project = entry.diskSession\n          ? dim(shortenProject(entry.diskSession.decodedPath, 36))\n          : dim(\"—\");\n        return [\n          dim(String(i + 1)),\n          chalk.white(entry.name),\n          fmtUnifiedStatus(entry.status),\n          age,\n          project,\n        ];\n      });\n      console.log(renderTable(headers, rows));\n      console.log();\n\n      const pickMatch = async (match: UnifiedSession) => {\n        if (await openMatch(match, allSessions, opts.dryRun ?? false)) return;\n        console.error(err(`Nothing to open for \"${match.name}\" — no live session, no transcript, no directory.`));\n        process.exitCode = 1;\n      };\n\n      if (pickN !== undefined) {\n        const idx = pickN - 1;\n        if (idx >= 0 && idx < partialMatches.length) {\n          await pickMatch(partialMatches[idx]);\n          return;\n        }\n        console.error(err(`Invalid choice: ${pickN}`));\n        process.exitCode = 1;\n        return;\n      }\n\n      if (opts.auto) {\n        await pickMatch(partialMatches[0]);\n        return;\n      }\n\n      const choice = await askForChoice(Math.min(partialMatches.length, maxResults));\n      if (choice !== null) {\n        await pickMatch(partialMatches[choice - 1]);\n      }\n      return;\n    }\n  }\n\n  // -----------------------------------------------------------------------\n  // Case 3b: Registered project by name → open its directory\n  // -----------------------------------------------------------------------\n  // If a project is registered under this name and its directory is there, that\n  // is the answer, full stop. Naming a project should start a session in it.\n  //\n  // The deduped catalog cannot be relied on for this: it keeps ONE entry per\n  // project and prefers a transcript over the registry row, so when the\n  // transcript's recorded directory has gone stale the surviving entry carries a\n  // dead path and the live registry value is never consulted. The catalog is\n  // built for listing, where showing the session is right; it is the wrong\n  // source for \"where do I open this\".\n  //\n  // So ask the registry directly, before falling back to scraping prompt\n  // history — which is a search feature, not a way to open a known project.\n  {\n    const q = query.toLowerCase().replace(/[-_]+/g, \" \").replace(/\\s+/g, \" \").trim();\n    const byName = registeredProjects.filter((p) => {\n      const name = (p.display_name ?? \"\").toLowerCase().replace(/[-_]+/g, \" \").trim();\n      const slug = (p.slug ?? \"\").toLowerCase().replace(/[-_]+/g, \" \").trim();\n      return name === q || slug === q;\n    });\n    for (const p of byName) {\n      if (p.root_path && existsSync(p.root_path)) {\n        launchInDir(p.root_path, p.display_name ?? query, { dryRun: opts.dryRun ?? false });\n        return;\n      }\n    }\n  }\n\n  // -----------------------------------------------------------------------\n  // Case 4: Free-text history search\n  // -----------------------------------------------------------------------\n  if (!existsSync(HISTORY_FILE)) {\n    console.error(err(\"~/.claude/history.jsonl not found.\"));\n    console.error(dim(\"  No prompt history available for search.\"));\n    console.error(dim(\"  Try: pai  (no args) to see all sessions.\"));\n    process.exitCode = 1;\n    return;\n  }\n\n  process.stderr.write(dim(`  Searching prompt history for \"${query}\"...\\n`));\n  const matches = await searchHistory(query, maxResults);\n\n  if (matches.length === 0) {\n    console.log(warn(`No sessions found matching \"${query}\".`));\n    console.log(dim(\"  Try a shorter or different search term.\"));\n    console.log(dim(\"  Or run: \") + chalk.white(\"pai\") + dim(\" (no args) to see all sessions.\"));\n    return;\n  }\n\n  console.log(\"\\n\" + header(`Sessions matching \"${query}\"`) + \"\\n\");\n  const headers = [\"#\", \"id\", \"when\", \"project\", \"last matching prompt\"];\n  const rows = matches.map((m, idx) => {\n    const shortId = (m.sessionId ?? \"—\").slice(0, 8);\n    const when = m.lastMatchTs > 0 ? fmtTs(m.lastMatchTs) : dim(\"—\");\n    const project = shortenProject(m.project || \"—\");\n    const snippet = m.lastMatchDisplay.replace(/\\n+/g, \" \").trim().slice(0, 48);\n    const fullSnippet = m.lastMatchDisplay.replace(/\\n+/g, \" \").trim();\n    const display = snippet.length < fullSnippet.length ? `\"${snippet}…\"` : `\"${snippet}\"`;\n    return [\n      dim(String(idx + 1)),\n      chalk.cyan(shortId),\n      when,\n      dim(project),\n      chalk.dim(display),\n    ];\n  });\n\n  console.log(renderTable(headers, rows));\n  console.log();\n\n  const launchHistoryMatch = (match: SessionMatch) => {\n    const session = matchToSession(match, allSessions);\n    if (!session) {\n      console.error(err(\"Could not resolve session for launch (no project path).\"));\n      process.exitCode = 1;\n      return;\n    }\n    launchSession(session, allSessions, opts.dryRun ?? false);\n  };\n\n  if (pickN !== undefined) {\n    const idx = pickN - 1;\n    if (idx >= 0 && idx < matches.length) {\n      launchHistoryMatch(matches[idx]);\n      return;\n    }\n    console.error(err(`Invalid choice: ${pickN}`));\n    process.exitCode = 1;\n    return;\n  }\n\n  if (opts.auto) {\n    launchHistoryMatch(matches[0]);\n    return;\n  }\n\n  const choice = await askForChoice(matches.length);\n  if (choice !== null) {\n    launchHistoryMatch(matches[choice - 1]);\n  }\n}\n"],"mappings":"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"}