{"version":3,"file":"wakeup-Yk5lQ0jb.mjs","names":[],"sources":["../src/memory/wakeup.ts"],"sourcesContent":["/**\n * Wake-up context system — progressive context loading inspired by mempalace.\n *\n * Layers:\n *   L0 Identity     (~100 tokens)   — user identity from ~/.pai/identity.txt. Always loaded.\n *   L1 Essential Story (~500-800t)  — top session notes for the project, key lines extracted.\n *   L2 On-Demand                    — triggered by topic queries (handled by memory_search).\n *   L3 Deep Search                  — unlimited federated memory search (memory_search tool).\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { paiHomePath, migratePaiFile, type MigrateFileResult } from \"../config/pai-home.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Maximum tokens for the L1 essential story block. Approx 4 chars/token.\n * Measured 2026-09-20: 800 tokens was mostly stale bullets. Reduced to 300.\n */\nconst L1_TOKEN_BUDGET = 300;\nconst L1_CHAR_BUDGET = L1_TOKEN_BUDGET * 4; // ~1600 chars\n\n/** Maximum session notes to scan when building L1. Reduced from 10 to 3 (2026-09-20). */\nconst L1_MAX_NOTES = 3;\n\n/** Sections to extract from session notes (in priority order). */\nconst EXTRACT_SECTIONS = [\n  \"Work Done\",\n  \"Key Decisions\",\n  \"Next Steps\",\n  \"Checkpoint\",\n];\n\n/** Identity file location. */\nconst IDENTITY_FILE = join(homedir(), \".pai\", \"identity.txt\");\n\n// ---------------------------------------------------------------------------\n// L0: Identity\n// ---------------------------------------------------------------------------\n\n/**\n * Load L0 identity. Prefers the PAI_HOME location (identity.txt migrated by\n * `pai config migrate`), falling back to the legacy ~/.pai/identity.txt so a\n * not-yet-migrated file keeps working. Returns \"\" if neither exists. Never\n * throws.\n */\nexport function loadL0Identity(\n  opts: { newPath?: string; legacyPath?: string } = {}\n): string {\n  const newPath = opts.newPath ?? paiHomePath(\"identity.txt\");\n  const legacyPath = opts.legacyPath ?? IDENTITY_FILE;\n  const path = existsSync(newPath) ? newPath : legacyPath;\n  if (!existsSync(path)) return \"\";\n  try {\n    return readFileSync(path, \"utf-8\").trim();\n  } catch {\n    return \"\";\n  }\n}\n\n/**\n * Move ~/.pai/identity.txt into PAI_HOME, following the same copy /\n * verify-byte-identical / rename-aside semantics as every other per-user\n * file (see migratePaiFile). `from`/`to` are overridable for tests.\n */\nexport function migrateIdentityFile(\n  opts: { dryRun?: boolean; from?: string; to?: string } = {}\n): MigrateFileResult {\n  const toPath = opts.to ?? paiHomePath(\"identity.txt\");\n  const fromPath = opts.from ?? IDENTITY_FILE;\n  return migratePaiFile(toPath, [fromPath], { dryRun: opts.dryRun });\n}\n\n// ---------------------------------------------------------------------------\n// L1: Essential Story\n// ---------------------------------------------------------------------------\n\n/**\n * Find the Notes directory for a project given its root_path from the registry.\n * Checks local Notes/ first, then central ~/.claude/projects/... path.\n */\nfunction findNotesDirForProject(rootPath: string): string | null {\n  // Check local Notes directories first\n  const localCandidates = [\n    join(rootPath, \"Notes\"),\n    join(rootPath, \"notes\"),\n    join(rootPath, \".claude\", \"Notes\"),\n  ];\n  for (const p of localCandidates) {\n    if (existsSync(p)) return p;\n  }\n\n  // Fall back to central ~/.claude/projects/{encoded}/Notes\n  const encoded = rootPath\n    .replace(/\\//g, \"-\")\n    .replace(/\\./g, \"-\")\n    .replace(/ /g, \"-\");\n  const centralNotes = join(\n    homedir(),\n    \".claude\",\n    \"projects\",\n    encoded,\n    \"Notes\"\n  );\n  if (existsSync(centralNotes)) return centralNotes;\n\n  return null;\n}\n\n/**\n * Recursively find all .md session note files in a Notes directory.\n * Handles both flat layout (Notes/*.md) and month-subdirectory layout\n * (Notes/YYYY/MM/*.md). Returns files sorted newest-first by filename\n * (note numbers are monotonically increasing, so lexicographic = newest-last,\n * so we reverse).\n */\nfunction findSessionNotes(notesDir: string): string[] {\n  const result: string[] = [];\n\n  const scanDir = (dir: string) => {\n    if (!existsSync(dir)) return;\n    let entries: string[];\n    try {\n      entries = readdirSync(dir, { withFileTypes: true } as Parameters<typeof readdirSync>[1] as any)\n        .map((e: any) => ({ name: e.name, isDir: e.isDirectory() }));\n    } catch {\n      return;\n    }\n\n    for (const entry of entries as Array<{ name: string; isDir: boolean }>) {\n      const fullPath = join(dir, entry.name);\n      if (entry.isDir) {\n        // Recurse into YYYY/MM subdirectories\n        scanDir(fullPath);\n      } else if (entry.name.match(/^\\d{3,4}[\\s_-].*\\.md$/)) {\n        result.push(fullPath);\n      }\n    }\n  };\n\n  scanDir(notesDir);\n\n  // Sort newest-first by the DATE in the filename, not the note number.\n  //\n  // The note number is only monotonic within one uninterrupted numbering run.\n  // It restarts — per month directory, and after a registry merge renumbers a\n  // project — so \"highest number\" and \"most recent\" diverge. Observed live:\n  // `0184 - 2026-02-22` outranked `0008 - 2026-08-01`, and the wake-up context\n  // handed a resumed session five-month-old material as its recent history.\n  //\n  // `NNNN - YYYY-MM-DD - Title.md` is the enforced layout, so the date is the\n  // reliable key. Number breaks ties within a day; mtime is the last resort\n  // for notes that predate the naming convention.\n  const dateOf = (p: string): string =>\n    basename(p).match(/(\\d{4}-\\d{2}-\\d{2})/)?.[1] ?? \"\";\n  const numberOf = (p: string): number =>\n    parseInt(basename(p).match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n  const mtimeOf = (p: string): number => {\n    try {\n      return statSync(p).mtimeMs;\n    } catch {\n      return 0;\n    }\n  };\n\n  result.sort((a, b) => {\n    const dateA = dateOf(a);\n    const dateB = dateOf(b);\n    // Undated notes sort last rather than winning on an empty string.\n    if (dateA !== dateB) {\n      if (!dateA) return 1;\n      if (!dateB) return -1;\n      return dateB.localeCompare(dateA);\n    }\n    const byNumber = numberOf(b) - numberOf(a);\n    if (byNumber !== 0) return byNumber;\n    return mtimeOf(b) - mtimeOf(a);\n  });\n\n  return result;\n}\n\n/**\n * Extract the most important lines from a session note.\n * Prioritises: Work Done items, Key Decisions, Next Steps, Checkpoint headings.\n * Returns a condensed string under maxChars.\n */\nfunction normalizeLine(line: string): string {\n  return line\n    .replace(/^(- \\[[ x]\\] |- |\\* |\\d+\\.\\s)/, \"\")\n    .trim()\n    .replace(/\\s+/g, \" \");\n}\n\nfunction extractKeyLines(\n  content: string,\n  maxChars: number,\n  seen: Set<string> = new Set(),\n  seenLabels: Set<string> = new Set()\n): string {\n  const lines = content.split(\"\\n\");\n  const selected: string[] = [];\n  let inTargetSection = false;\n  let currentSection = \"\";\n  let charCount = 0;\n  // Heading text seen since the last line was emitted. Buffered rather than\n  // pushed immediately: a heading with no surviving lines under it (every\n  // line deduped against an earlier note) must not appear at all.\n  let pendingLabel: string | null = null;\n\n  // First pass: collect lines from priority sections\n  for (const line of lines) {\n    // Detect section headers\n    const h2Match = line.match(/^## (.+)$/);\n    const h3Match = line.match(/^### (.+)$/);\n    if (h2Match) {\n      currentSection = h2Match[1];\n      inTargetSection = EXTRACT_SECTIONS.some((s) =>\n        currentSection.toLowerCase().includes(s.toLowerCase())\n      );\n      pendingLabel = null;\n      continue;\n    }\n    if (h3Match) {\n      // Checkpoints / sub-sections — buffer as a candidate label.\n      if (inTargetSection) {\n        pendingLabel = h3Match[1];\n      }\n      continue;\n    }\n\n    if (!inTargetSection) continue;\n\n    // Skip blank lines and HTML comments\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith(\"<!--\") || trimmed === \"---\") continue;\n\n    // Include checkbox items, bold text, and plain text lines\n    if (\n      trimmed.startsWith(\"- \") ||\n      trimmed.startsWith(\"* \") ||\n      trimmed.match(/^\\d+\\./) ||\n      trimmed.startsWith(\"**\")\n    ) {\n      const normalized = normalizeLine(trimmed);\n      if (seen.has(normalized)) continue;\n\n      let labelText = \"\";\n      let labelKey = \"\";\n      if (pendingLabel !== null) {\n        labelKey = pendingLabel.toLowerCase().trim();\n        if (!seenLabels.has(labelKey)) {\n          labelText = `[${pendingLabel}]`;\n        }\n      }\n\n      if (charCount + labelText.length + trimmed.length + 2 > maxChars) break;\n\n      if (labelText) {\n        selected.push(labelText);\n        seenLabels.add(labelKey);\n        charCount += labelText.length + 1;\n      }\n      pendingLabel = null;\n\n      seen.add(normalized);\n      selected.push(trimmed);\n      charCount += trimmed.length + 1;\n    }\n  }\n\n  return selected.join(\"\\n\");\n}\n\n/**\n * Build the L1 essential story block.\n *\n * Reads the most recent session notes for the project and extracts the key\n * lines (Work Done, Key Decisions, Next Steps) within the token budget.\n *\n * @param rootPath   The project root path (from the registry).\n * @param tokenBudget  Max tokens to consume. Default 800 (~3200 chars).\n * @returns Formatted L1 block, or empty string if no notes found.\n */\nexport function buildL1EssentialStory(\n  rootPath: string,\n  tokenBudget = L1_TOKEN_BUDGET\n): string {\n  const charBudget = tokenBudget * 4;\n  const notesDir = findNotesDirForProject(rootPath);\n  if (!notesDir) return \"\";\n\n  const noteFiles = findSessionNotes(notesDir).slice(0, L1_MAX_NOTES);\n  if (noteFiles.length === 0) return \"\";\n\n  const sections: string[] = [];\n  const seen = new Set<string>();\n  const seenLabels = new Set<string>();\n  let remaining = charBudget;\n\n  for (const noteFile of noteFiles) {\n    if (remaining <= 50) break;\n\n    let content: string;\n    try {\n      content = readFileSync(noteFile, \"utf-8\");\n    } catch {\n      continue;\n    }\n\n    // Extract the note date and title from the filename\n    const name = basename(noteFile);\n    const titleMatch = name.match(/^\\d+ - (\\d{4}-\\d{2}-\\d{2}) - (.+)\\.md$/);\n    const dateLabel = titleMatch ? titleMatch[1] : \"\";\n    const titleLabel = titleMatch\n      ? titleMatch[2]\n      : name.replace(/^\\d+ - /, \"\").replace(/\\.md$/, \"\");\n\n    // Skip if nothing useful extracted from this note\n    const perNoteChars = Math.min(remaining, Math.floor(charBudget / noteFiles.length) + 200);\n    const extracted = extractKeyLines(content, perNoteChars, seen, seenLabels);\n    // A note whose every extracted line survived only as a label (all its\n    // real lines deduped against an earlier, newer note) contributes nothing\n    // — including its title.\n    const hasContentLine = extracted\n      .split(\"\\n\")\n      .some((l) => !/^\\[.*\\]$/.test(l));\n    if (!extracted || !hasContentLine) continue;\n\n    const noteBlock = `[${dateLabel} - ${titleLabel}]\\n${extracted}`;\n    sections.push(noteBlock);\n    remaining -= noteBlock.length + 1;\n  }\n\n  if (sections.length === 0) return \"\";\n\n  return sections.join(\"\\n\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Combined: buildWakeupContext\n// ---------------------------------------------------------------------------\n\n/**\n * Build the combined wake-up context block (L0 + L1).\n *\n * Returns a formatted string suitable for injection as a system-reminder,\n * or an empty string if both layers are empty.\n *\n * @param rootPath   Project root path for L1 note lookup. Optional.\n * @param tokenBudget  L1 token budget. Default 800.\n * @param opts.skipStory  Skip the L1 essential story. Set when a handover\n *   checkpoint was already injected — the story re-summarises the same note\n *   the handover came from, at lower quality.\n */\nexport function buildWakeupContext(\n  rootPath?: string,\n  tokenBudget = L1_TOKEN_BUDGET,\n  opts: { skipStory?: boolean } = {}\n): string {\n  const identity = loadL0Identity();\n  const essentialStory = rootPath && !opts.skipStory\n    ? buildL1EssentialStory(rootPath, tokenBudget)\n    : \"\";\n\n  if (!identity && !essentialStory) return \"\";\n\n  const parts: string[] = [];\n\n  if (identity) {\n    parts.push(`## L0 Identity\\n\\n${identity}`);\n  }\n\n  if (essentialStory) {\n    parts.push(`## L1 Essential Story\\n\\n${essentialStory}`);\n  }\n\n  return parts.join(\"\\n\\n\");\n}\n"],"mappings":"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"}