{"version":3,"file":"work-queue-worker-B9zVWImP.mjs","names":["contentToText","contentToText"],"sources":["../src/daemon/templates/context-handover-prompt.ts","../src/daemon/context-handover-worker.ts","../src/daemon/topic-detect-worker.ts","../src/daemon/work-queue-worker.ts"],"sourcesContent":["/**\n * context-handover-prompt.ts — prompt template for the threshold-triggered\n * pre-compaction handover.\n *\n * This is NOT the session-note summary (session-summary-prompt.ts already\n * does that well, and Claude Code's own native compaction summary is also\n * genuinely detailed on file paths and issue numbers). This exists for the\n * one thing neither of those reliably carries: the reasoning behind what\n * happened, which compresses to bullets or vanishes outright once a native\n * compaction fires.\n *\n * FRAMING (lifted from a working example of this exact mechanism, read\n * before writing this prompt): the handover is a DIFF against everything\n * already durable, not a summary of the session. The successor can read\n * git, the tracker, and the notes — so this must not repeat what is\n * recoverable there. What cannot be recovered is what only ever existed in\n * the conversation: reasoning, rejected alternatives, negative results and\n * what they cost, traps discovered but invisible in the repo, and specific\n * values/ids/paths that would otherwise have to be dug up again.\n *\n * Length is not a target. A short session earns a short handover; every\n * line in a long one must be something the successor could not have\n * obtained anywhere else.\n */\n\nexport interface HandoverPromptParams {\n  /** Interleaved user + assistant turns, oldest first, each prefixed with\n   *  who said it — this prompt needs the assistant's reasoning, not just\n   *  what the user asked for. */\n  turns: string[];\n  /** Git log output for the session period, if any. */\n  gitLog: string;\n  cwd: string;\n  /** The previous cached handover's summary text, if this session already\n   *  produced one (the warmup handover, when this call is the refresh).\n   *  Passed through so the new handover can carry it forward rather than\n   *  silently dropping everything it already captured — see the \"pointer\n   *  to the previous handover, kept rather than superseded\" structure. */\n  previousHandover?: string;\n}\n\nexport function buildContextHandoverPrompt(params: HandoverPromptParams): string {\n  const { turns, gitLog, cwd, previousHandover } = params;\n\n  const turnsSection = turns.length > 0\n    ? turns.join(\"\\n\\n\")\n    : \"(No turns extracted)\";\n\n  const gitSection = gitLog.trim() || \"(No git commits during this session)\";\n\n  const previousSection = previousHandover\n    ? `\\nA PREVIOUS HANDOVER already exists for this session (from an earlier, \\\nlower threshold). Carry it forward — start your response with a one-line \\\npointer to it (\"Carries forward from the previous handover, which stays for \\\nits detail.\") and then write only what has happened SINCE it that a compact \\\nwould also lose. Do not re-derive or repeat what it already covered.\\n\\n\\\nPREVIOUS HANDOVER:\\n${previousHandover}\\n`\n    : \"\";\n\n  return `This session is approaching a context-window compaction. Write the \\\nhandover that would let a successor continue WITHOUT re-reading this \\\nconversation and WITHOUT asking the user to repeat themselves.\n\nProject directory: ${cwd}\n${previousSection}\nSCOPING RULE — the single most important instruction here: this is a DIFF \\\nagainst everything already durable, not a summary of the session. Assume \\\nthe successor can and will read git history, the issue tracker, and any \\\nnotes files themselves. Do NOT restate anything recoverable there — no file \\\ninventories, no \"implemented X\" restatements, no chronology of commands run. \\\nWrite down ONLY what existed exclusively in this conversation and would \\\notherwise be gone: the reasoning, not the result.\n\nWrite ONLY the sections below that this session actually has content for — \\\nomit a section entirely rather than writing \"none\" or padding it:\n\nWHERE THIS SESSION ENDED — the concrete state right now: what is verified \\\nworking (tests green, tree clean, etc.) versus merely attempted, any branch \\\nor working-tree state that isn't obvious from git status alone (e.g. a \\\ndetached HEAD, an intentionally uncommitted change, a stash).\n\nWHAT CLOSED — each item paired with the evidence that closed it, not just \\\nthe claim that it did.\n\nWHAT IS FILED AND STILL OPEN — each with its next concrete step or who/what \\\nit is waiting on.\n\nDECISIONS & REASONING — choices actually made, each with why this way and \\\nnot another way.\n\nREJECTED ALTERNATIVES — approaches considered and set aside, and why. An \\\nalternative without a reason is not worth recording.\n\nNEGATIVE RESULTS — something investigated that turned out NOT to be the \\\nanswer, kept WITH the reasoning that ruled it out (e.g. \"X looked like the \\\ncause — it wasn't; the actual mechanism was Y\"). Without the reasoning \\\nattached, a successor re-investigates the same dead end.\n\nTRAPS — anything discovered that is invisible from the repo alone and would \\\notherwise bite a successor immediately (a required manual step, an ordering \\\nconstraint, a state that looks fine but isn't).\n\nCRITICAL VALUES — any specific id, path, URL, credential name, version \\\nnumber, or configuration value that appeared in conversation and whose loss \\\nwould force someone to go find it again.\n\nTHE FEW THINGS THAT MATTER MOST, IF NOTHING ELSE IS READ — at most 3 bullets. \\\nForces a priority judgement; do not pad this to fill space.\n\nFormat your response as markdown with those section headers (only the ones \\\nyou have content for), no other structure.\n\n---\n\nCONVERSATION:\n${turnsSection}\n\nGIT COMMITS:\n${gitSection}`;\n}\n","/**\n * context-handover-worker.ts — the threshold-triggered pre-compaction\n * handover.\n *\n * `session-summary-worker.ts` already spawns a model to write session notes,\n * but only from the PreCompact hook itself — which fires at the compaction\n * boundary, far too late to be injected INTO that same compaction's\n * post-compact context. This worker runs earlier, off a token-count\n * threshold (see ../hooks/ts/lib/context-fill.ts), so a model-written\n * handover already exists in a cache file by the time compaction happens.\n *\n * Deliberately narrow: this does not write a session note, does not touch\n * the session-summary cooldown file, and does not run KG extraction. It\n * writes one thing — a cache file the PreCompact hook can read — because its\n * only job is to exist before compaction does, not to duplicate what\n * session-summary-worker already does well.\n *\n * The prompt itself (see templates/context-handover-prompt.ts) treats the\n * handover as a DIFF against everything already durable — git, the tracker,\n * the notes — not a session summary, and a refresh run carries the previous\n * cached handover forward rather than overwriting it.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\n\nimport {\n  findLatestJsonl,\n  getGitContext,\n  spawnSummarizer,\n  contentToText,\n} from \"./session-summary-worker.js\";\nimport { buildContextHandoverPrompt } from \"./templates/context-handover-prompt.js\";\nimport {\n  readContextHandoverCache,\n  writeContextHandoverCache,\n  type HandoverThreshold,\n} from \"../hooks/ts/lib/context-handover-cache.js\";\n\nexport type { HandoverThreshold } from \"../hooks/ts/lib/context-handover-cache.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ContextHandoverPayload {\n  cwd: string;\n  sessionId: string;\n  transcriptPath?: string;\n  threshold: HandoverThreshold;\n  /** True when this fired via the \"no time for a clean crossing\" path\n   *  (see isImmediate in context-fill.ts) — logged, not otherwise acted on. */\n  urgent?: boolean;\n}\n\n/** How much of the tail of the transcript to feed the summarizer. Generous —\n *  this runs at most twice per session, not on every tool call. */\nconst MAX_TURN_CHARS = 150_000;\n\n/** Max turns to include (a hard floor even if MAX_TURN_CHARS is not hit). */\nconst MAX_TURNS = 120;\n\n// ---------------------------------------------------------------------------\n// Transcript → turns\n// ---------------------------------------------------------------------------\n\n/**\n * Interleaved \"User:\"/\"Assistant:\" turns, oldest-kept-from-the-end so a long\n * transcript is truncated to its most recent content rather than its\n * earliest. Unlike session-summary-worker's extractFromJsonl (which keeps\n * only user messages, because a session note doesn't need the assistant's\n * own reasoning) this keeps BOTH sides — the whole point of this prompt is\n * the assistant's reasoning, which lives in assistant turns.\n */\nfunction extractTurns(jsonlPath: string): string[] {\n  let raw: string;\n  try {\n    raw = readFileSync(jsonlPath, \"utf-8\");\n  } catch {\n    return [];\n  }\n\n  if (raw.length > MAX_TURN_CHARS) {\n    const truncPoint = raw.indexOf(\"\\n\", raw.length - MAX_TURN_CHARS);\n    raw = truncPoint >= 0 ? raw.slice(truncPoint + 1) : raw.slice(-MAX_TURN_CHARS);\n  }\n\n  const turns: string[] = [];\n  for (const line of raw.trim().split(\"\\n\")) {\n    if (!line.trim()) continue;\n    let entry: Record<string, unknown>;\n    try {\n      entry = JSON.parse(line);\n    } catch {\n      continue;\n    }\n\n    if (entry.type === \"user\") {\n      const msg = entry.message as Record<string, unknown> | undefined;\n      const text = contentToText(msg?.content);\n      if (text && text.length >= 3 && !text.startsWith(\"<system-reminder>\")) {\n        turns.push(`User: ${text.slice(0, 2000)}`);\n      }\n    } else if (entry.type === \"assistant\") {\n      const msg = entry.message as Record<string, unknown> | undefined;\n      const text = contentToText(msg?.content);\n      if (text && text.length >= 3) {\n        turns.push(`Assistant: ${text.slice(0, 4000)}`);\n      }\n    }\n  }\n\n  return turns.slice(-MAX_TURNS);\n}\n\n// ---------------------------------------------------------------------------\n// Main entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Process a `context-handover` work item.\n *\n * THROWS on every failure path (missing input, no transcript, no turns, an\n * empty/failed summarizer run) rather than logging and returning silently.\n * That used to be a deliberate choice — avoid the work queue's own retry\n * piling up redundant LLM calls — but it made a real failure invisible: a\n * daemon restart mid-spawn (session 77084e72-...) left no cache, no error\n * anywhere the work queue's own stats could show, and a trigger-side marker\n * that (at the time) had already been set to \"done\". Throwing here makes\n * work-queue-worker.ts call markFailed(), which logs prominently AND is\n * visible via `work_queue_stats` / `pai daemon status` — not just a stderr\n * line that can scroll away. It is a second, faster (seconds-to-minutes\n * backoff) retry path alongside the primary one in context-handover-\n * trigger.ts (which retries on its own ~5-minute cadence based on whether a\n * cache actually appeared); the two are complementary, not redundant — a\n * failed cache write costs nothing to retry twice.\n */\nexport async function handleContextHandover(payload: ContextHandoverPayload): Promise<void> {\n  const { cwd, sessionId, transcriptPath, threshold, urgent } = payload;\n\n  if (!cwd || !sessionId) {\n    throw new Error(\"[context-handover] payload missing cwd or sessionId\");\n  }\n\n  process.stderr.write(\n    `[context-handover] Starting for ${cwd} (session=${sessionId}, threshold=${threshold}` +\n    `${urgent ? \", urgent\" : \"\"}).\\n`\n  );\n\n  let jsonlPath: string | null = transcriptPath && existsSync(transcriptPath) ? transcriptPath : null;\n  if (!jsonlPath) jsonlPath = findLatestJsonl(cwd);\n  if (!jsonlPath) {\n    throw new Error(`[context-handover] No transcript found for ${cwd} (session=${sessionId})`);\n  }\n\n  const turns = extractTurns(jsonlPath);\n  if (turns.length === 0) {\n    throw new Error(`[context-handover] No turns extracted from ${jsonlPath} (session=${sessionId})`);\n  }\n\n  const gitLog = await getGitContext(cwd);\n\n  // Carry the previous handover forward (see buildContextHandoverPrompt's\n  // scoping rule) rather than silently overwriting it — a refresh at the\n  // second threshold should not lose what the warmup handover already\n  // captured.\n  const previous = readContextHandoverCache(sessionId);\n  const prompt = buildContextHandoverPrompt({\n    turns,\n    gitLog,\n    cwd,\n    previousHandover: previous?.summary,\n  });\n  process.stderr.write(`[context-handover] Sending ${prompt.length} char prompt to sonnet...\\n`);\n\n  const summary = await spawnSummarizer(prompt, \"sonnet\");\n  if (!summary || !summary.trim()) {\n    // No cache write on this path — an existing cache from an earlier\n    // successful run is left untouched rather than clobbered with nothing.\n    throw new Error(`[context-handover] sonnet produced no output (session=${sessionId})`);\n  }\n\n  writeContextHandoverCache({\n    sessionId,\n    cwd,\n    threshold,\n    generatedAt: new Date().toISOString(),\n    model: \"sonnet\",\n    summary: summary.trim(),\n  });\n\n  process.stderr.write(\n    `[context-handover] Wrote cache for session ${sessionId} (${summary.length} chars).\\n`\n  );\n}\n","/**\n * topic-detect-worker.ts — Topic shift detection for session note splitting\n *\n * Processes `topic-detect` work items by:\n *   1. Extracting recent user messages from the JSONL transcript\n *   2. Running the BM25-based topic shift detector against the PAI memory DB\n *   3. If a shift is detected, recording a topic boundary marker\n *\n * The actual note splitting is handled by session-summary-worker.ts when it\n * processes the next `session-summary` work item — it uses the TOPIC: line\n * from the summarizer to decide whether to create a new note.\n *\n * This worker provides an additional signal: project-level topic shift\n * (e.g., conversation moved from project A to project B). The session\n * summary worker handles intra-project topic shifts (e.g., from \"dark mode\"\n * to \"keyboard IPC\" within the same project).\n */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nimport { detectTopicShift } from \"../topics/detector.js\";\nimport { registryDb, storageBackend } from \"./daemon/state.js\";\nimport {\n  findNotesDir,\n  getCurrentNotePath,\n  appendCheckpoint,\n} from \"../hooks/ts/lib/project-utils/index.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TopicDetectPayload {\n  /** Working directory of the session. */\n  cwd: string;\n  /** Recent conversation context (extracted user messages). */\n  context?: string;\n  /** The project slug the session is currently routed to. */\n  currentProject?: string;\n  /** Path to the JSONL transcript (optional — used to extract context if not provided). */\n  transcriptPath?: string;\n  /** Session ID (for logging). */\n  sessionId?: string;\n}\n\n// ---------------------------------------------------------------------------\n// JSONL context extraction (lightweight — just last few user messages)\n// ---------------------------------------------------------------------------\n\nconst MAX_CONTEXT_MESSAGES = 5;\nconst MAX_CONTEXT_CHARS = 2000;\n\n/**\n * Extract recent user messages from a JSONL transcript for topic detection.\n * Takes only the last few messages to represent the current topic.\n */\nfunction extractRecentContext(jsonlPath: string): string {\n  try {\n    const raw = readFileSync(jsonlPath, \"utf-8\");\n    // Read from the end — last 50KB should be more than enough\n    const tail = raw.length > 50_000 ? raw.slice(-50_000) : raw;\n    const lines = tail.trim().split(\"\\n\");\n\n    const messages: string[] = [];\n\n    for (let i = lines.length - 1; i >= 0 && messages.length < MAX_CONTEXT_MESSAGES; i--) {\n      const line = lines[i].trim();\n      if (!line) continue;\n\n      try {\n        const entry = JSON.parse(line) as Record<string, unknown>;\n        if (entry.type === \"user\") {\n          const msg = entry.message as Record<string, unknown> | undefined;\n          if (msg?.content) {\n            const text = contentToText(msg.content);\n            if (text && text.length > 3) {\n              messages.unshift(text.slice(0, 500));\n            }\n          }\n        }\n      } catch { /* skip invalid JSON */ }\n    }\n\n    return messages.join(\"\\n\\n\").slice(0, MAX_CONTEXT_CHARS);\n  } catch {\n    return \"\";\n  }\n}\n\n/** Convert Claude content (string or content block array) to plain text. */\nfunction contentToText(content: unknown): string {\n  if (typeof content === \"string\") return content;\n  if (Array.isArray(content)) {\n    return content\n      .map((c) => {\n        if (typeof c === \"string\") return c;\n        const block = c as Record<string, unknown>;\n        if (block?.text) return String(block.text);\n        if (block?.content) return String(block.content);\n        return \"\";\n      })\n      .join(\" \")\n      .trim();\n  }\n  return \"\";\n}\n\n// ---------------------------------------------------------------------------\n// Topic boundary file — signals to session-summary-worker\n// ---------------------------------------------------------------------------\n\nconst TOPIC_BOUNDARY_FILE = \"topic-boundary.json\";\n\ninterface TopicBoundary {\n  timestamp: string;\n  previousProject: string | null;\n  suggestedProject: string | null;\n  confidence: number;\n  context: string;\n}\n\n/**\n * Write a topic boundary marker into the Notes directory.\n * The session-summary-worker checks for this file and uses it as an\n * additional signal that a new note should be created.\n */\nfunction writeTopicBoundary(\n  cwd: string,\n  boundary: TopicBoundary\n): void {\n  try {\n    const notesInfo = findNotesDir(cwd);\n    const boundaryPath = join(notesInfo.path, TOPIC_BOUNDARY_FILE);\n    writeFileSync(boundaryPath, JSON.stringify(boundary, null, 2), \"utf-8\");\n    process.stderr.write(\n      `[topic-detect] Wrote topic boundary marker: ${boundaryPath}\\n`\n    );\n  } catch (e) {\n    process.stderr.write(`[topic-detect] Could not write boundary marker: ${e}\\n`);\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Main entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Process a `topic-detect` work item.\n *\n * Called by work-queue-worker.ts. Throws on fatal errors so the work queue\n * retry logic handles them.\n */\nexport async function handleTopicDetect(payload: TopicDetectPayload): Promise<void> {\n  const { cwd, currentProject, transcriptPath, sessionId } = payload;\n\n  if (!cwd) {\n    throw new Error(\"topic-detect payload missing cwd\");\n  }\n\n  process.stderr.write(\n    `[topic-detect] Starting for ${cwd}` +\n    `${currentProject ? ` (project=${currentProject})` : \"\"}` +\n    `${sessionId ? ` (session=${sessionId})` : \"\"}\\n`\n  );\n\n  // Check that daemon state is available\n  if (!registryDb || !storageBackend) {\n    process.stderr.write(\n      \"[topic-detect] Registry DB or storage backend not available — skipping.\\n\"\n    );\n    return;\n  }\n\n  // Extract context from payload or transcript\n  let context = payload.context || \"\";\n\n  if (!context && transcriptPath && existsSync(transcriptPath)) {\n    context = extractRecentContext(transcriptPath);\n  }\n\n  if (!context || context.trim().length < 10) {\n    process.stderr.write(\n      \"[topic-detect] Insufficient context for topic detection — skipping.\\n\"\n    );\n    return;\n  }\n\n  process.stderr.write(\n    `[topic-detect] Context: ${context.length} chars, checking against memory...\\n`\n  );\n\n  // Run the BM25-based topic shift detector\n  const result = await detectTopicShift(registryDb, storageBackend, {\n    context,\n    currentProject,\n    threshold: 0.6,\n    candidates: 20,\n  });\n\n  process.stderr.write(\n    `[topic-detect] Result: shifted=${result.shifted}, ` +\n    `suggested=${result.suggestedProject}, confidence=${result.confidence.toFixed(2)}, ` +\n    `chunks=${result.chunkCount}\\n`\n  );\n\n  if (result.topProjects.length > 0) {\n    process.stderr.write(\n      `[topic-detect] Top projects: ${result.topProjects.map(\n        (p) => `${p.slug}(${(p.score * 100).toFixed(0)}%)`\n      ).join(\", \")}\\n`\n    );\n  }\n\n  if (result.shifted) {\n    // Record the topic boundary\n    writeTopicBoundary(cwd, {\n      timestamp: new Date().toISOString(),\n      previousProject: result.currentProject,\n      suggestedProject: result.suggestedProject,\n      confidence: result.confidence,\n      context: context.slice(0, 200),\n    });\n\n    // Also append a checkpoint to the current session note\n    try {\n      const notesInfo = findNotesDir(cwd);\n      const notePath = getCurrentNotePath(notesInfo.path);\n      if (notePath) {\n        appendCheckpoint(\n          notePath,\n          `Topic shift detected: conversation moved from **${result.currentProject}** ` +\n          `to **${result.suggestedProject}** (confidence: ${(result.confidence * 100).toFixed(0)}%). ` +\n          `A new session note will be created for the new topic.`\n        );\n      }\n    } catch (e) {\n      process.stderr.write(`[topic-detect] Could not append checkpoint: ${e}\\n`);\n    }\n  }\n\n  process.stderr.write(\"[topic-detect] Done.\\n\");\n}\n","/**\n * work-queue-worker.ts — Daemon worker loop for the persistent work queue\n *\n * Runs every 5 seconds to drain the queue.\n * Handles 'session-end' work items by reading the transcript, extracting\n * work summaries, updating the session note, and updating TODO.md.\n * Handles 'session-summary' items by spawning Haiku for AI-powered note generation.\n * Handles 'context-handover' items by spawning Sonnet to write the\n * threshold-triggered pre-compaction reasoning handover (see\n * context-handover-worker.ts and hooks/ts/lib/context-fill.ts).\n * Handles 'registry-scan' items by running performScan() against the registry DB.\n *\n * Handles 'topic-detect' items by running BM25-based topic shift detection.\n * Other item types (note-update, todo-update) are stubs — they log and\n * complete immediately, ready for future expansion.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { basename, dirname } from \"node:path\";\n\nimport {\n  dequeue,\n  enqueue,\n  markCompleted,\n  markFailed,\n  cleanup,\n  getStats,\n  hasPendingOrProcessingOfType,\n  type WorkItem,\n} from \"./work-queue.js\";\n\nimport {\n  handleSessionSummary,\n  type SessionSummaryPayload,\n} from \"./session-summary-worker.js\";\n\nimport {\n  handleContextHandover,\n  type ContextHandoverPayload,\n} from \"./context-handover-worker.js\";\n\nimport {\n  handleTopicDetect,\n  type TopicDetectPayload,\n} from \"./topic-detect-worker.js\";\n\n// Registry scan — called from handleRegistryScan()\nimport { performScan } from \"../cli/commands/registry/scan.js\";\nimport { registryDb } from \"./daemon/state.js\";\n\n// Hooks lib imports — resolving through the compiled JS path.\n// These are the same utilities used by stop-hook.ts.\nimport {\n  findNotesDir,\n  getCurrentNotePath,\n  addWorkToSessionNote,\n  finalizeSessionNote,\n  updateTodoContinue, sessionIdFromTranscript,\n  archiveSessionFilesToSessionsDir,\n  type WorkItem as NoteWorkItem,\n} from \"../hooks/ts/lib/project-utils/index.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst WORKER_INTERVAL_MS = 5_000;\nconst HOUSEKEEPING_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes\n\n// ---------------------------------------------------------------------------\n// Timers (stored so shutdown can clear them)\n// ---------------------------------------------------------------------------\n\nlet workerTimer: ReturnType<typeof setInterval> | null = null;\nlet housekeepingTimer: ReturnType<typeof setInterval> | null = null;\nlet _immediateSignal = false;\n\n// ---------------------------------------------------------------------------\n// Worker loop\n// ---------------------------------------------------------------------------\n\n/** Start the background worker and housekeeping timers. */\nexport function startWorker(): void {\n  process.stderr.write(\"[work-queue-worker] Starting worker loop.\\n\");\n\n  workerTimer = setInterval(async () => {\n    try {\n      await processNextItem();\n    } catch (e) {\n      process.stderr.write(`[work-queue-worker] Uncaught error in worker loop: ${e}\\n`);\n    }\n  }, WORKER_INTERVAL_MS);\n\n  housekeepingTimer = setInterval(() => {\n    try {\n      cleanup();\n    } catch (e) {\n      process.stderr.write(`[work-queue-worker] Housekeeping error: ${e}\\n`);\n    }\n  }, HOUSEKEEPING_INTERVAL_MS);\n\n  process.stderr.write(\"[work-queue-worker] Worker started (interval=5s, housekeeping=10min).\\n\");\n}\n\n/** Stop the worker timers gracefully. */\nexport function stopWorker(): void {\n  if (workerTimer !== null) {\n    clearInterval(workerTimer);\n    workerTimer = null;\n  }\n  if (housekeepingTimer !== null) {\n    clearInterval(housekeepingTimer);\n    housekeepingTimer = null;\n  }\n  process.stderr.write(\"[work-queue-worker] Worker stopped.\\n\");\n}\n\n/**\n * Signal that new work has been enqueued.\n * The worker will run on its next tick — we don't need to reset the timer\n * since 5 s is fast enough. The flag allows future optimisations.\n */\nexport function notifyNewWork(): void {\n  _immediateSignal = true;\n}\n\n// ---------------------------------------------------------------------------\n// Item processor (sequential — one item per tick)\n// ---------------------------------------------------------------------------\n\nasync function processNextItem(): Promise<void> {\n  const item = dequeue();\n  if (!item) return;\n\n  process.stderr.write(\n    `[work-queue-worker] Processing ${item.type} (id=${item.id}, attempt=${item.attempts}).\\n`\n  );\n\n  try {\n    switch (item.type) {\n      case \"session-end\":\n        await handleSessionEnd(item);\n        break;\n\n      case \"session-summary\":\n        await handleSessionSummary(item.payload as SessionSummaryPayload);\n        break;\n\n      case \"context-handover\":\n        await handleContextHandover(item.payload as unknown as ContextHandoverPayload);\n        break;\n\n      case \"topic-detect\":\n        await handleTopicDetect(item.payload as TopicDetectPayload);\n        break;\n\n      case \"registry-scan\":\n        await handleRegistryScan();\n        break;\n\n      case \"note-update\":\n      case \"todo-update\":\n        // Stubs — log and complete\n        process.stderr.write(\n          `[work-queue-worker] Item type '${item.type}' is not yet implemented — completing as no-op.\\n`\n        );\n        break;\n\n      default:\n        throw new Error(`Unknown work item type: ${(item as WorkItem).type}`);\n    }\n\n    markCompleted(item.id);\n    process.stderr.write(`[work-queue-worker] Completed ${item.type} (id=${item.id}).\\n`);\n  } catch (e) {\n    const msg = e instanceof Error ? e.message : String(e);\n    markFailed(item.id, msg);\n  }\n}\n\n// ---------------------------------------------------------------------------\n// session-end handler\n// ---------------------------------------------------------------------------\n\n/**\n * Process a 'session-end' work item.\n *\n * Expected payload:\n *   transcriptPath: string   — absolute path to the .jsonl transcript\n *   cwd: string              — working directory of the session\n *   message?: string         — COMPLETED: line extracted by the hook (optional)\n */\nasync function handleSessionEnd(item: WorkItem): Promise<void> {\n  const { transcriptPath, cwd, message: hookMessage } = item.payload as {\n    transcriptPath: string;\n    cwd: string;\n    message?: string;\n  };\n\n  if (!transcriptPath) throw new Error(\"session-end payload missing transcriptPath\");\n  if (!cwd) throw new Error(\"session-end payload missing cwd\");\n\n  // Read transcript\n  let transcript: string;\n  try {\n    transcript = readFileSync(transcriptPath, \"utf-8\");\n  } catch (e) {\n    throw new Error(`Could not read transcript at ${transcriptPath}: ${e}`);\n  }\n\n  const lines = transcript.trim().split(\"\\n\");\n\n  // Extract work items from transcript\n  const workItems = extractWorkFromTranscript(lines);\n\n  // Determine completion message\n  let message = hookMessage ?? \"\";\n  if (!message) {\n    const lastEntry = tryParseJson(lines[lines.length - 1]);\n    if (lastEntry?.type === \"assistant\" && lastEntry.message?.content) {\n      const content = contentToText(lastEntry.message.content);\n      const m = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n      if (m) {\n        message = m[1].trim().replace(/\\*+/g, \"\").replace(/\\[.*?\\]/g, \"\").trim();\n      }\n    }\n  }\n\n  // Find notes directory and current note\n  const notesInfo = findNotesDir(cwd);\n  const currentNotePath = getCurrentNotePath(notesInfo.path);\n\n  if (currentNotePath) {\n    // Add work items to session note\n    if (workItems.length > 0) {\n      addWorkToSessionNote(currentNotePath, workItems);\n      process.stderr.write(\n        `[work-queue-worker] Added ${workItems.length} work item(s) to note.\\n`\n      );\n    } else if (message) {\n      addWorkToSessionNote(currentNotePath, [{ title: message, completed: true }]);\n      process.stderr.write(\"[work-queue-worker] Added completion message to note.\\n\");\n    }\n\n    // Finalize the note\n    const summary = message || \"Session completed.\";\n    finalizeSessionNote(currentNotePath, summary);\n    process.stderr.write(\n      `[work-queue-worker] Finalized session note: ${basename(currentNotePath)}.\\n`\n    );\n\n    // Update TODO.md ## Continue section.\n    //\n    // A session that produced neither work items nor a completion message has\n    // no handover to write. Claiming the slot anyway hands the next session a\n    // block naming a session that did nothing — see the 2026-08-04 incident\n    // recorded in AIBroker's Notes. applyContinue now refuses such a write on\n    // its own; this skips building it in the first place, so the \"last session\"\n    // line keeps pointing at the last session that actually did something.\n    if (workItems.length === 0 && !message) {\n      process.stderr.write(\n        \"[work-queue-worker] Nothing to hand over — leaving TODO.md ## Continue intact.\\n\"\n      );\n    } else {\n      try {\n        const stateLines: string[] = [];\n        stateLines.push(`Working directory: ${cwd}`);\n        if (workItems.length > 0) {\n          stateLines.push(\"\", \"Work completed:\");\n          for (const wi of workItems.slice(0, 5)) {\n            stateLines.push(`- ${wi.title}`);\n          }\n        }\n        if (message) {\n          stateLines.push(\"\", `Last completed: ${message}`);\n        }\n        updateTodoContinue(\n          cwd,\n          basename(currentNotePath),\n          stateLines.join(\"\\n\"),\n          \"session-end\",\n          // Same reasoning as the stop hook: the transcript filename is the\n          // session's real identity, and the note title is not.\n          sessionIdFromTranscript(transcriptPath)\n        );\n      } catch (todoError) {\n        // Non-fatal — log and continue\n        process.stderr.write(\n          `[work-queue-worker] Could not update TODO.md: ${todoError}\\n`\n        );\n      }\n    }\n  } else {\n    process.stderr.write(\n      \"[work-queue-worker] No current session note found — skipping note update.\\n\"\n    );\n  }\n\n  // Move session .jsonl files to sessions/ subdirectory\n  try {\n    const transcriptDir = dirname(transcriptPath);\n    const archivedCount = archiveSessionFilesToSessionsDir(transcriptDir);\n    if (archivedCount > 0) {\n      process.stderr.write(\n        `[work-queue-worker] Archived ${archivedCount} session file(s) to sessions/.\\n`\n      );\n    }\n  } catch (moveError) {\n    // Non-fatal\n    process.stderr.write(`[work-queue-worker] Could not move session files: ${moveError}\\n`);\n  }\n}\n\n// ---------------------------------------------------------------------------\n// registry-scan handler\n// ---------------------------------------------------------------------------\n\n/**\n * Run performScan() against the registry DB.\n *\n * Debounce: if another registry-scan job is already pending in the queue,\n * we still run this one (dequeue already picked it), but enqueueRegistryScan()\n * checks before enqueuing so duplicates rarely reach here.\n */\nasync function handleRegistryScan(): Promise<void> {\n  if (!registryDb) {\n    throw new Error(\"registry-scan: registryDb not initialized yet\");\n  }\n\n  const t0 = Date.now();\n  process.stderr.write(\"[work-queue-worker] Running registry scan...\\n\");\n\n  const result = performScan(registryDb);\n  const elapsed = Date.now() - t0;\n\n  process.stderr.write(\n    `[work-queue-worker] Registry scan complete: ` +\n    `${result.projectsScanned} projects (${result.projectsNew} new, ${result.projectsUpdated} updated), ` +\n    `${result.sessionsScanned} sessions (${result.sessionsNew} new) in ${elapsed}ms.\\n`\n  );\n\n  if (result.skipped.length > 0) {\n    process.stderr.write(\n      `[work-queue-worker] Registry scan: ${result.skipped.length} project(s) skipped (path not found).\\n`\n    );\n  }\n}\n\n/**\n * Enqueue a registry-scan work item — debounced.\n * If a registry-scan item is already pending or processing, skip enqueue.\n */\nexport function enqueueRegistryScan(): void {\n  // Debounce: skip if already queued or processing\n  if (hasPendingOrProcessingOfType(\"registry-scan\")) {\n    process.stderr.write(\"[work-queue-worker] Registry scan already pending/processing — skipping duplicate enqueue.\\n\");\n    return;\n  }\n\n  enqueue({\n    type: \"registry-scan\",\n    priority: 5, // lowest priority — runs after session-end, session-summary\n    payload: {},\n  });\n}\n\n// ---------------------------------------------------------------------------\n// Transcript parsing helpers (mirrors stop-hook.ts logic)\n// ---------------------------------------------------------------------------\n\nfunction tryParseJson(line: string): Record<string, unknown> | null {\n  try {\n    return JSON.parse(line) as Record<string, unknown>;\n  } catch {\n    return null;\n  }\n}\n\nfunction contentToText(content: unknown): string {\n  if (typeof content === \"string\") return content;\n  if (Array.isArray(content)) {\n    return content\n      .map((c) => {\n        if (typeof c === \"string\") return c;\n        const block = c as Record<string, unknown>;\n        if (block?.text) return String(block.text);\n        if (block?.content) return String(block.content);\n        return \"\";\n      })\n      .join(\" \")\n      .trim();\n  }\n  return \"\";\n}\n\nfunction extractWorkFromTranscript(lines: string[]): NoteWorkItem[] {\n  const workItems: NoteWorkItem[] = [];\n  const seenSummaries = new Set<string>();\n\n  for (const line of lines) {\n    const entry = tryParseJson(line);\n    if (!entry || entry.type !== \"assistant\") continue;\n\n    const msg = entry.message as Record<string, unknown> | undefined;\n    if (!msg?.content) continue;\n\n    const content = contentToText(msg.content);\n\n    // SUMMARY: line (preferred)\n    const summaryMatch = content.match(/SUMMARY:\\s*(.+?)(?:\\n|$)/i);\n    if (summaryMatch) {\n      const summary = summaryMatch[1].trim();\n      if (summary && !seenSummaries.has(summary) && summary.length > 5) {\n        seenSummaries.add(summary);\n\n        const details: string[] = [];\n        const actionsMatch = content.match(/ACTIONS:\\s*(.+?)(?=\\n[A-Z]+:|$)/is);\n        if (actionsMatch) {\n          const actionLines = actionsMatch[1]\n            .split(\"\\n\")\n            .map((l) =>\n              l.replace(/^[-*•]\\s*/, \"\").replace(/^\\d+\\.\\s*/, \"\").trim()\n            )\n            .filter((l) => l.length > 3 && l.length < 100);\n          details.push(...actionLines.slice(0, 3));\n        }\n\n        workItems.push({\n          title: summary,\n          details: details.length > 0 ? details : undefined,\n          completed: true,\n        });\n      }\n    }\n\n    // COMPLETED: line (fallback)\n    const completedMatch = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n    if (completedMatch && workItems.length === 0) {\n      const completed = completedMatch[1]\n        .trim()\n        .replace(/\\*+/g, \"\")\n        .replace(/\\[.*?\\]/g, \"\")\n        .trim();\n      if (completed && !seenSummaries.has(completed) && completed.length > 5) {\n        seenSummaries.add(completed);\n        workItems.push({ title: completed, completed: true });\n      }\n    }\n  }\n\n  return workItems;\n}\n"],"mappings":"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"}