@tekmidian/pai 0.36.0 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.mjs +2 -2
- package/dist/cli/program.mjs +2 -2
- package/dist/{checkpoint-block-DKYxCkBL.mjs → context-handover-cache-PtNvj_8D.mjs} +34 -3
- package/dist/context-handover-cache-PtNvj_8D.mjs.map +1 -0
- package/dist/daemon/index.mjs +3 -3
- package/dist/{daemon--N2JFnUs.mjs → daemon-BZ93KRBo.mjs} +4 -4
- package/dist/{daemon--N2JFnUs.mjs.map → daemon-BZ93KRBo.mjs.map} +1 -1
- package/dist/{daemon-DJEFqV84.mjs → daemon-DRdoA489.mjs} +3 -3
- package/dist/hooks/context-compression-hook.mjs +18 -10
- package/dist/hooks/context-compression-hook.mjs.map +2 -2
- package/dist/{program-BnMNFb4O.mjs → program-C-fUghPv.mjs} +200 -73
- package/dist/{program-BnMNFb4O.mjs.map → program-C-fUghPv.mjs.map} +1 -1
- package/dist/{work-queue-worker-228XjABm.mjs → work-queue-worker-B8W8_3Rn.mjs} +23 -55
- package/dist/work-queue-worker-B8W8_3Rn.mjs.map +1 -0
- package/dist/{work-queue-worker-LRA9Fj9z.mjs → work-queue-worker-HN2Ufg-L.mjs} +2 -2
- package/package.json +1 -1
- package/src/hooks/ts/lib/context-fill.test.ts +117 -4
- package/src/hooks/ts/lib/context-fill.ts +136 -53
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +37 -18
- package/dist/checkpoint-block-DKYxCkBL.mjs.map +0 -1
- package/dist/work-queue-worker-228XjABm.mjs.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { t as openFederation } from "./db-a1ixZQjr.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { c as extractAndStoreTriples$1, d as performScan, i as applyContinue, n as writeContextHandoverCache, t as readContextHandoverCache } from "./context-handover-cache-PtNvj_8D.mjs";
|
|
3
3
|
import { D as storageBackend, t as daemonConfig, u as registryDb } from "./state-WaXhLr6R.mjs";
|
|
4
4
|
import { t as detectTopicShift } from "./detector--Gg5JRN5.mjs";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { homedir
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
7
|
import { basename, dirname, join } from "node:path";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
import { copyFileSync as copyFileSync$1, existsSync as existsSync$1, linkSync as linkSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, renameSync as renameSync$1, writeFileSync as writeFileSync$1 } from "fs";
|
|
@@ -1648,37 +1648,6 @@ GIT COMMITS:
|
|
|
1648
1648
|
${gitSection}`;
|
|
1649
1649
|
}
|
|
1650
1650
|
|
|
1651
|
-
//#endregion
|
|
1652
|
-
//#region src/hooks/ts/lib/context-handover-cache.ts
|
|
1653
|
-
/**
|
|
1654
|
-
* context-handover-cache.ts — the well-known file where the threshold-
|
|
1655
|
-
* triggered pre-compaction handover (see ../../../daemon/context-handover-
|
|
1656
|
-
* worker.ts) is written, and where context-compression-hook.ts looks for it
|
|
1657
|
-
* at compaction time.
|
|
1658
|
-
*
|
|
1659
|
-
* Deliberately dependency-light (fs/os/path only). The daemon worker that
|
|
1660
|
-
* WRITES this file needs the heavier machinery in session-summary-worker.ts
|
|
1661
|
-
* (spawning Claude, git log, etc.), but the PreCompact hook that READS it
|
|
1662
|
-
* runs as a short-lived process on every compaction and must not drag in
|
|
1663
|
-
* daemon state, database pools, or anything else that module graph carries —
|
|
1664
|
-
* this file is the shared seam so neither side has to.
|
|
1665
|
-
*/
|
|
1666
|
-
function contextHandoverCachePath(sessionId) {
|
|
1667
|
-
return join(tmpdir(), `pai-context-handover-${sessionId}.json`);
|
|
1668
|
-
}
|
|
1669
|
-
function readContextHandoverCache(sessionId) {
|
|
1670
|
-
const path = contextHandoverCachePath(sessionId);
|
|
1671
|
-
if (!existsSync(path)) return null;
|
|
1672
|
-
try {
|
|
1673
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
1674
|
-
} catch {
|
|
1675
|
-
return null;
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
function writeContextHandoverCache(cache) {
|
|
1679
|
-
writeFileSync(contextHandoverCachePath(cache.sessionId), JSON.stringify(cache, null, 2), "utf-8");
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
1651
|
//#endregion
|
|
1683
1652
|
//#region src/daemon/context-handover-worker.ts
|
|
1684
1653
|
/**
|
|
@@ -1749,30 +1718,32 @@ function extractTurns(jsonlPath) {
|
|
|
1749
1718
|
return turns.slice(-MAX_TURNS);
|
|
1750
1719
|
}
|
|
1751
1720
|
/**
|
|
1752
|
-
* Process a `context-handover` work item.
|
|
1753
|
-
*
|
|
1754
|
-
*
|
|
1755
|
-
*
|
|
1756
|
-
*
|
|
1721
|
+
* Process a `context-handover` work item.
|
|
1722
|
+
*
|
|
1723
|
+
* THROWS on every failure path (missing input, no transcript, no turns, an
|
|
1724
|
+
* empty/failed summarizer run) rather than logging and returning silently.
|
|
1725
|
+
* That used to be a deliberate choice — avoid the work queue's own retry
|
|
1726
|
+
* piling up redundant LLM calls — but it made a real failure invisible: a
|
|
1727
|
+
* daemon restart mid-spawn (session 77084e72-...) left no cache, no error
|
|
1728
|
+
* anywhere the work queue's own stats could show, and a trigger-side marker
|
|
1729
|
+
* that (at the time) had already been set to "done". Throwing here makes
|
|
1730
|
+
* work-queue-worker.ts call markFailed(), which logs prominently AND is
|
|
1731
|
+
* visible via `work_queue_stats` / `pai daemon status` — not just a stderr
|
|
1732
|
+
* line that can scroll away. It is a second, faster (seconds-to-minutes
|
|
1733
|
+
* backoff) retry path alongside the primary one in context-handover-
|
|
1734
|
+
* trigger.ts (which retries on its own ~5-minute cadence based on whether a
|
|
1735
|
+
* cache actually appeared); the two are complementary, not redundant — a
|
|
1736
|
+
* failed cache write costs nothing to retry twice.
|
|
1757
1737
|
*/
|
|
1758
1738
|
async function handleContextHandover(payload) {
|
|
1759
1739
|
const { cwd, sessionId, transcriptPath, threshold, urgent } = payload;
|
|
1760
|
-
if (!cwd || !sessionId)
|
|
1761
|
-
process.stderr.write("[context-handover] payload missing cwd or sessionId — skipping.\n");
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1740
|
+
if (!cwd || !sessionId) throw new Error("[context-handover] payload missing cwd or sessionId");
|
|
1764
1741
|
process.stderr.write(`[context-handover] Starting for ${cwd} (session=${sessionId}, threshold=${threshold}${urgent ? ", urgent" : ""}).\n`);
|
|
1765
1742
|
let jsonlPath = transcriptPath && existsSync(transcriptPath) ? transcriptPath : null;
|
|
1766
1743
|
if (!jsonlPath) jsonlPath = findLatestJsonl(cwd);
|
|
1767
|
-
if (!jsonlPath) {
|
|
1768
|
-
process.stderr.write("[context-handover] No transcript found — skipping.\n");
|
|
1769
|
-
return;
|
|
1770
|
-
}
|
|
1744
|
+
if (!jsonlPath) throw new Error(`[context-handover] No transcript found for ${cwd} (session=${sessionId})`);
|
|
1771
1745
|
const turns = extractTurns(jsonlPath);
|
|
1772
|
-
if (turns.length === 0) {
|
|
1773
|
-
process.stderr.write("[context-handover] No turns extracted — skipping.\n");
|
|
1774
|
-
return;
|
|
1775
|
-
}
|
|
1746
|
+
if (turns.length === 0) throw new Error(`[context-handover] No turns extracted from ${jsonlPath} (session=${sessionId})`);
|
|
1776
1747
|
const prompt = buildContextHandoverPrompt({
|
|
1777
1748
|
turns,
|
|
1778
1749
|
gitLog: await getGitContext(cwd),
|
|
@@ -1781,10 +1752,7 @@ async function handleContextHandover(payload) {
|
|
|
1781
1752
|
});
|
|
1782
1753
|
process.stderr.write(`[context-handover] Sending ${prompt.length} char prompt to sonnet...\n`);
|
|
1783
1754
|
const summary = await spawnSummarizer(prompt, "sonnet");
|
|
1784
|
-
if (!summary || !summary.trim()) {
|
|
1785
|
-
process.stderr.write("[context-handover] sonnet produced no output — leaving any existing cache untouched.\n");
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1755
|
+
if (!summary || !summary.trim()) throw new Error(`[context-handover] sonnet produced no output (session=${sessionId})`);
|
|
1788
1756
|
writeContextHandoverCache({
|
|
1789
1757
|
sessionId,
|
|
1790
1758
|
cwd,
|
|
@@ -2168,4 +2136,4 @@ function extractWorkFromTranscript(lines) {
|
|
|
2168
2136
|
|
|
2169
2137
|
//#endregion
|
|
2170
2138
|
export { enqueue as a, stopWorker as i, notifyNewWork as n, getStats as o, startWorker as r, loadQueue as s, enqueueRegistryScan as t };
|
|
2171
|
-
//# sourceMappingURL=work-queue-worker-
|
|
2139
|
+
//# sourceMappingURL=work-queue-worker-B8W8_3Rn.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-queue-worker-B8W8_3Rn.mjs","names":["resolve","homedir","existsSync","readFileSync","join","join","basename","existsSync","readdirSync","join","existsSync","readdirSync","basename","readFileSync","existsSync","join","basename","readFileSync","contentToText","kgExtractAndStoreTriples","contentToText","contentToText"],"sources":["../src/daemon/work-queue.ts","../src/hooks/ts/lib/pai-paths.ts","../src/hooks/ts/lib/project-utils/paths.ts","../src/hooks/ts/lib/project-utils/session-notes.ts","../src/hooks/ts/lib/project-utils/todo.ts","../src/daemon/templates/session-summary-prompt.ts","../src/daemon/session-summary-worker.ts","../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 * work-queue.ts — Persistent work queue for the PAI Daemon\n *\n * Provides a durable, file-backed queue that survives daemon restarts.\n * Items are processed sequentially to avoid concurrent writes to the same\n * session note. Failed items are retried with exponential backoff.\n *\n * Queue file: ~/.config/pai/work-queue.json\n * Written atomically (write temp → rename) to prevent corruption.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n renameSync,\n mkdirSync,\n statSync,\n} from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { randomUUID } from \"node:crypto\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type WorkItemType =\n | \"session-end\"\n | \"session-summary\"\n | \"context-handover\"\n | \"note-update\"\n | \"todo-update\"\n | \"topic-detect\"\n | \"registry-scan\";\n\nexport type WorkItemStatus =\n | \"pending\"\n | \"processing\"\n | \"completed\"\n | \"failed\";\n\nexport interface WorkItem {\n id: string;\n type: WorkItemType;\n priority: number; // 1=high, 5=low\n payload: Record<string, unknown>;\n status: WorkItemStatus;\n createdAt: string; // ISO timestamp\n attempts: number;\n maxAttempts: number; // default 3\n nextRetryAt?: string; // ISO timestamp — undefined means ready now\n error?: string; // last error message\n completedAt?: string; // ISO timestamp\n}\n\nexport interface WorkQueueStats {\n pending: number;\n processing: number;\n completed: number;\n failed: number;\n total: number;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst QUEUE_FILE = join(homedir(), \".config\", \"pai\", \"work-queue.json\");\nconst MAX_QUEUE_SIZE = 1000;\nconst MAX_QUEUE_FILE_BYTES = 1024 * 1024; // 1 MB\nconst COMPLETED_TTL_MS = 60 * 60 * 1000; // 1 hour\nconst FAILED_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\n\n/** Backoff delays in ms by attempt number (0-indexed). */\nconst BACKOFF_MS = [\n 5_000, // attempt 1 → wait 5 s\n 30_000, // attempt 2 → wait 30 s\n 300_000, // attempt 3 → wait 5 min\n];\n\n// ---------------------------------------------------------------------------\n// In-memory state\n// ---------------------------------------------------------------------------\n\nlet _queue: WorkItem[] = [];\nlet _dirty = false;\n\n// ---------------------------------------------------------------------------\n// Persistence helpers\n// ---------------------------------------------------------------------------\n\n/** Load queue from disk. Call once at daemon startup. */\nexport function loadQueue(): void {\n if (!existsSync(QUEUE_FILE)) {\n _queue = [];\n return;\n }\n\n try {\n const raw = readFileSync(QUEUE_FILE, \"utf-8\");\n const parsed = JSON.parse(raw) as WorkItem[];\n if (!Array.isArray(parsed)) {\n process.stderr.write(\"[work-queue] Invalid queue file format — starting empty.\\n\");\n _queue = [];\n return;\n }\n\n // On restart, reset any 'processing' items back to 'pending' — they\n // were interrupted mid-flight and need to be retried.\n _queue = parsed.map((item) => {\n if (item.status === \"processing\") {\n return { ...item, status: \"pending\" as WorkItemStatus };\n }\n return item;\n });\n\n const stats = getStats();\n process.stderr.write(\n `[work-queue] Loaded ${_queue.length} items from disk ` +\n `(pending=${stats.pending}, failed=${stats.failed}).\\n`\n );\n } catch (e) {\n process.stderr.write(`[work-queue] Could not load queue file: ${e}\\n`);\n _queue = [];\n }\n}\n\n/** Persist queue to disk atomically. */\nexport function saveQueue(): void {\n const dir = dirname(QUEUE_FILE);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const tmpFile = QUEUE_FILE + \".tmp\";\n try {\n writeFileSync(tmpFile, JSON.stringify(_queue, null, 2), \"utf-8\");\n renameSync(tmpFile, QUEUE_FILE);\n _dirty = false;\n } catch (e) {\n process.stderr.write(`[work-queue] Could not persist queue: ${e}\\n`);\n }\n}\n\n/** Persist only if there are unsaved changes. */\nfunction saveIfDirty(): void {\n if (_dirty) saveQueue();\n}\n\n// ---------------------------------------------------------------------------\n// Queue management\n// ---------------------------------------------------------------------------\n\n/**\n * Enforce the maximum queue size cap.\n * Strategy: first drop oldest completed, then oldest low-priority pending.\n */\nfunction enforceMaxSize(): void {\n if (_queue.length <= MAX_QUEUE_SIZE) return;\n\n const excess = _queue.length - MAX_QUEUE_SIZE;\n\n // Step 1: drop oldest completed items\n const completed = _queue\n .filter((i) => i.status === \"completed\")\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n\n const toDropCompleted = completed.slice(0, excess);\n const dropIds = new Set(toDropCompleted.map((i) => i.id));\n _queue = _queue.filter((i) => !dropIds.has(i.id));\n\n if (_queue.length <= MAX_QUEUE_SIZE) return;\n\n // Step 2: drop oldest low-priority pending items (priority 4-5)\n const remainingExcess = _queue.length - MAX_QUEUE_SIZE;\n const lowPriorityPending = _queue\n .filter((i) => i.status === \"pending\" && i.priority >= 4)\n .sort((a, b) => a.priority - b.priority || a.createdAt.localeCompare(b.createdAt));\n\n const toDropLow = lowPriorityPending.slice(0, remainingExcess);\n const dropLowIds = new Set(toDropLow.map((i) => i.id));\n _queue = _queue.filter((i) => !dropLowIds.has(i.id));\n\n process.stderr.write(\n `[work-queue] Pruned queue to ${_queue.length} items (cap=${MAX_QUEUE_SIZE}).\\n`\n );\n}\n\n/**\n * Add a new work item to the queue.\n * Returns the created WorkItem.\n */\nexport function enqueue(params: {\n type: WorkItemType;\n priority?: number;\n payload: Record<string, unknown>;\n maxAttempts?: number;\n}): WorkItem {\n const item: WorkItem = {\n id: randomUUID(),\n type: params.type,\n priority: params.priority ?? 3,\n payload: params.payload,\n status: \"pending\",\n createdAt: new Date().toISOString(),\n attempts: 0,\n maxAttempts: params.maxAttempts ?? 3,\n };\n\n _queue.push(item);\n enforceMaxSize();\n _dirty = true;\n saveIfDirty();\n\n process.stderr.write(\n `[work-queue] Enqueued ${item.type} (id=${item.id}, priority=${item.priority}).\\n`\n );\n\n return item;\n}\n\n/**\n * Pick the next pending item that is ready to process (respects nextRetryAt).\n * Returns null if no eligible item exists.\n * Highest priority (lowest number) is processed first; ties broken by createdAt.\n */\nexport function dequeue(): WorkItem | null {\n const now = new Date().toISOString();\n\n const eligible = _queue\n .filter((i) => {\n if (i.status !== \"pending\") return false;\n if (i.nextRetryAt && i.nextRetryAt > now) return false;\n return true;\n })\n .sort((a, b) => {\n if (a.priority !== b.priority) return a.priority - b.priority;\n return a.createdAt.localeCompare(b.createdAt);\n });\n\n if (eligible.length === 0) return null;\n\n const item = eligible[0];\n item.status = \"processing\";\n item.attempts += 1;\n _dirty = true;\n saveIfDirty();\n\n return item;\n}\n\n/** Peek at the next eligible pending item without changing its status. */\nexport function peek(): WorkItem | null {\n const now = new Date().toISOString();\n\n return (\n _queue\n .filter((i) => {\n if (i.status !== \"pending\") return false;\n if (i.nextRetryAt && i.nextRetryAt > now) return false;\n return true;\n })\n .sort((a, b) => {\n if (a.priority !== b.priority) return a.priority - b.priority;\n return a.createdAt.localeCompare(b.createdAt);\n })[0] ?? null\n );\n}\n\n/**\n * Mark an item as completed.\n */\nexport function markCompleted(id: string): void {\n const item = _queue.find((i) => i.id === id);\n if (!item) return;\n item.status = \"completed\";\n item.completedAt = new Date().toISOString();\n item.error = undefined;\n _dirty = true;\n saveIfDirty();\n}\n\n/**\n * Mark an item as failed.\n * If attempts < maxAttempts, schedules a retry with exponential backoff.\n * Otherwise, leaves status as 'failed'.\n */\nexport function markFailed(id: string, errorMsg: string): void {\n const item = _queue.find((i) => i.id === id);\n if (!item) return;\n\n item.error = errorMsg;\n\n if (item.attempts < item.maxAttempts) {\n const backoffMs = BACKOFF_MS[item.attempts - 1] ?? BACKOFF_MS[BACKOFF_MS.length - 1];\n item.status = \"pending\";\n item.nextRetryAt = new Date(Date.now() + backoffMs).toISOString();\n process.stderr.write(\n `[work-queue] Item ${id} failed (attempt ${item.attempts}/${item.maxAttempts}), ` +\n `retry in ${backoffMs / 1000}s: ${errorMsg}\\n`\n );\n } else {\n item.status = \"failed\";\n process.stderr.write(\n `[work-queue] Item ${id} exhausted retries (${item.maxAttempts} attempts): ${errorMsg}\\n`\n );\n }\n\n _dirty = true;\n saveIfDirty();\n}\n\n// ---------------------------------------------------------------------------\n// Stats\n// ---------------------------------------------------------------------------\n\nexport function getStats(): WorkQueueStats {\n const stats: WorkQueueStats = {\n pending: 0,\n processing: 0,\n completed: 0,\n failed: 0,\n total: _queue.length,\n };\n for (const item of _queue) {\n stats[item.status as keyof Omit<WorkQueueStats, \"total\">]++;\n }\n return stats;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if any pending or processing item of the given type exists.\n * Used for debouncing work items that are expensive to run concurrently.\n */\nexport function hasPendingOrProcessingOfType(type: WorkItemType): boolean {\n return _queue.some(\n (i) =>\n i.type === type &&\n (i.status === \"pending\" || i.status === \"processing\")\n );\n}\n\n// ---------------------------------------------------------------------------\n// Housekeeping\n// ---------------------------------------------------------------------------\n\n/**\n * Remove completed and permanently-failed items older than their TTL.\n * Also force-cleans all completed items if the queue file exceeds 1 MB.\n */\nexport function cleanup(): void {\n const now = Date.now();\n const before = _queue.length;\n\n // Check file size for force-clean\n let forceCleanCompleted = false;\n try {\n if (existsSync(QUEUE_FILE)) {\n const { size } = statSync(QUEUE_FILE);\n if (size > MAX_QUEUE_FILE_BYTES) {\n forceCleanCompleted = true;\n process.stderr.write(\n `[work-queue] Queue file exceeds 1 MB (${size} bytes) — force-cleaning completed items.\\n`\n );\n }\n }\n } catch {\n // non-fatal\n }\n\n _queue = _queue.filter((item) => {\n if (item.status === \"completed\") {\n if (forceCleanCompleted) return false;\n const completedMs = item.completedAt ? new Date(item.completedAt).getTime() : 0;\n return now - completedMs < COMPLETED_TTL_MS;\n }\n if (item.status === \"failed\") {\n const createdMs = new Date(item.createdAt).getTime();\n return now - createdMs < FAILED_TTL_MS;\n }\n return true;\n });\n\n const removed = before - _queue.length;\n const stats = getStats();\n\n if (removed > 0 || before === 0) {\n process.stderr.write(\n `[work-queue] Cleanup: removed ${removed} items. ` +\n `Queue stats: pending=${stats.pending}, processing=${stats.processing}, ` +\n `completed=${stats.completed}, failed=${stats.failed}.\\n`\n );\n }\n\n _dirty = removed > 0;\n saveIfDirty();\n}\n","/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n","/**\n * Path utilities — encoding, Notes/Sessions directory discovery and creation.\n */\n\nimport { existsSync, mkdirSync, readdirSync, linkSync, copyFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { PAI_DIR } from '../pai-paths.js';\n\n// Re-export PAI_DIR for consumers\nexport { PAI_DIR };\nexport const PROJECTS_DIR = join(PAI_DIR, 'projects');\n\n/**\n * Directories known to be automated health-check / probe sessions.\n * Hooks should exit early for these to avoid registry clutter and wasted work.\n */\nconst PROBE_CWD_PATTERNS = [\n '/CodexBar/ClaudeProbe',\n '/ClaudeProbe',\n];\n\n/**\n * Check if the current working directory belongs to a probe/health-check session.\n * Returns true if hooks should skip this session entirely.\n */\nexport function isProbeSession(cwd?: string): boolean {\n const dir = cwd || process.cwd();\n return PROBE_CWD_PATTERNS.some(pattern => dir.includes(pattern));\n}\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with -\n * - Replace space with -\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-')\n .replace(/\\./g, '-')\n .replace(/ /g, '-');\n}\n\n/** Get the project directory for a given working directory. */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/** Get the Notes directory for a project (central location). */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory — checks local first, falls back to central.\n * Does NOT create the directory.\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n const localPaths = [\n join(cwd, 'Notes'),\n join(cwd, 'notes'),\n join(cwd, '.claude', 'Notes'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return { path, isLocal: true };\n }\n }\n\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/** Get the sessions/ directory for a project (stores .jsonl transcripts). */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/** Get the sessions/ directory from a project directory path. */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n// ---------------------------------------------------------------------------\n// Directory creation helpers\n// ---------------------------------------------------------------------------\n\n/** Ensure the Notes directory exists for a project. @deprecated Use ensureNotesDirSmart() */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\n return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists → use it (don't create anything new)\n * - If no local Notes/ → ensure central exists and use that\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n if (found.isLocal) return found;\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n return found;\n}\n\n/** Ensure the sessions/ directory exists for a project. */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/** Ensure the sessions/ directory exists (from project dir path). */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/**\n * Publish every project-root .jsonl into sessions/ as well, WITHOUT removing it.\n *\n * This used to renameSync, and that is how PAI destroyed its users' sessions.\n *\n * `claude --resume <uuid>` finds a transcript only at the project root. Move it\n * into sessions/ and the session becomes permanently unresumable — measured\n * 2026-08-04: `claude --resume b3462801` (867 KB of real work, sessions/ only)\n * answers \"No conversation found with session ID\", while a top-level id is found\n * fine. Nothing warned; the id still looked valid everywhere PAI displayed it.\n *\n * The damage was not occasional. This ran from a UserPromptSubmit hook excluding\n * only the CURRENT session, so every prompt anyone typed unresumed every other\n * session in the project. One PAI project measured 1 transcript at top level\n * against 52 underneath. Among the casualties was 046bb712 — the exact id PAI's\n * own handover tells the user to resume.\n *\n * A hardlink satisfies both sides, which is why this is a two-line fix rather\n * than a redesign: the archive genuinely has consumers that read sessions/\n * (session-summary-worker, registry/moved, session/autosave), and `--resume`\n * needs the root path. One inode, two names, no copy, no window where the file\n * is missing from either place.\n *\n * Never unlink the source. Tidying up another tool's store was the whole\n * mistake; a stale duplicate is free, a lost session is not. Every caller of\n * `transcriptFiles()` was checked before choosing this — they all test emptiness\n * (`.length > 0`), never count, so the duplicate cannot skew a project's stats.\n *\n * `excludeFile` keeps a hook from archiving the transcript it is itself watching\n * being written. It is not a \"finished sessions only\" guard and must not be read\n * as one: it excludes exactly one file, the caller's own, so with two sessions\n * live in one project each still archives the other mid-turn. A consumer that\n * needs \"finished only\" has to enforce that itself — this function cannot know\n * what is live.\n *\n * Returns the number of files newly archived.\n */\nexport function archiveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) return 0;\n\n const files = readdirSync(projectDir);\n let archivedCount = 0;\n\n for (const file of files) {\n if (!file.endsWith('.jsonl') || file === excludeFile) continue;\n\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n\n // Already archived — including by an earlier rename, before this was a\n // hardlink. Those are the sessions that need restoring to the root, which is\n // a separate repair and not this function's job.\n if (existsSync(destPath)) continue;\n\n try {\n linkSync(sourcePath, destPath);\n if (!silent) console.error(`Archived ${file} → sessions/ (still resumable)`);\n archivedCount++;\n } catch (error) {\n // Cross-device (EXDEV) is the realistic failure: sessions/ on another\n // volume. Copy instead, and still leave the original alone.\n try {\n copyFileSync(sourcePath, destPath);\n if (!silent) console.error(`Copied ${file} → sessions/ (hardlink unavailable)`);\n archivedCount++;\n } catch {\n if (!silent) console.error(`Could not archive ${file}: ${error}`);\n }\n }\n }\n\n return archivedCount;\n}\n\n/**\n * @deprecated Renamed to `archiveSessionFilesToSessionsDir`, which is what it\n * now does. Kept so an out-of-tree caller fails loudly at the type level rather\n * than silently keeping the old destructive name for a non-destructive action.\n */\nexport const moveSessionFilesToSessionsDir = archiveSessionFilesToSessionsDir;\n\n// ---------------------------------------------------------------------------\n// CLAUDE.md / TODO.md discovery\n// ---------------------------------------------------------------------------\n\n/** Find TODO.md — check local first, fallback to central. */\nexport function findTodoPath(cwd: string): string {\n const localPaths = [\n join(cwd, 'TODO.md'),\n join(cwd, 'notes', 'TODO.md'),\n join(cwd, 'Notes', 'TODO.md'),\n join(cwd, '.claude', 'TODO.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) return path;\n }\n\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/** Find CLAUDE.md — returns the FIRST found path. */\nexport function findClaudeMdPath(cwd: string): string | null {\n const paths = findAllClaudeMdPaths(cwd);\n return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations in priority order.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n const localPaths = [\n join(cwd, '.claude', 'CLAUDE.md'),\n join(cwd, 'CLAUDE.md'),\n join(cwd, 'Notes', 'CLAUDE.md'),\n join(cwd, 'notes', 'CLAUDE.md'),\n join(cwd, 'Prompts', 'CLAUDE.md'),\n join(cwd, 'prompts', 'CLAUDE.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) foundPaths.push(path);\n }\n\n return foundPaths;\n}\n","/**\n * Session note creation, editing, checkpointing, renaming, and finalization.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Get or create the YYYY/MM subdirectory for the current month inside notesDir. */\nfunction getMonthDir(notesDir: string): string {\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const monthDir = join(notesDir, year, month);\n if (!existsSync(monthDir)) {\n mkdirSync(monthDir, { recursive: true });\n }\n return monthDir;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Get the next note number (4-digit format: 0001, 0002, etc.).\n * Numbers are scoped per YYYY/MM directory.\n */\nexport function getNextNoteNumber(notesDir: string): string {\n const monthDir = getMonthDir(notesDir);\n\n const files = readdirSync(monthDir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-]/))\n .sort();\n\n if (files.length === 0) return '0001';\n\n let maxNumber = 0;\n for (const file of files) {\n const digitMatch = file.match(/^(\\d+)/);\n if (digitMatch) {\n const num = parseInt(digitMatch[1], 10);\n if (num > maxNumber) maxNumber = num;\n }\n }\n\n return String(maxNumber + 1).padStart(4, '0');\n}\n\n/**\n * Get the current (latest) note file path, or null if none exists.\n * Searches current month → previous month → flat notesDir (legacy).\n */\nexport function getCurrentNotePath(notesDir: string): string | null {\n if (!existsSync(notesDir)) return null;\n\n const findLatestIn = (dir: string): string | null => {\n if (!existsSync(dir)) return null;\n const files = readdirSync(dir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-].*\\.md$/))\n .sort((a, b) => {\n const numA = parseInt(a.match(/^(\\d+)/)?.[1] || '0', 10);\n const numB = parseInt(b.match(/^(\\d+)/)?.[1] || '0', 10);\n return numA - numB;\n });\n if (files.length === 0) return null;\n return join(dir, files[files.length - 1]);\n };\n\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const currentMonthDir = join(notesDir, year, month);\n const found = findLatestIn(currentMonthDir);\n if (found) return found;\n\n const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const prevYear = String(prevDate.getFullYear());\n const prevMonth = String(prevDate.getMonth() + 1).padStart(2, '0');\n const prevMonthDir = join(notesDir, prevYear, prevMonth);\n const prevFound = findLatestIn(prevMonthDir);\n if (prevFound) return prevFound;\n\n return findLatestIn(notesDir);\n}\n\n/**\n * Create a new session note.\n * Format: \"NNNN - YYYY-MM-DD - New Session.md\" filed into YYYY/MM subdirectory.\n * Claude MUST rename at session end with a meaningful description.\n */\nexport function createSessionNote(notesDir: string, description: string): string {\n const noteNumber = getNextNoteNumber(notesDir);\n const date = new Date().toISOString().split('T')[0];\n const monthDir = getMonthDir(notesDir);\n const filename = `${noteNumber} - ${date} - New Session.md`;\n const filepath = join(monthDir, filename);\n\n const content = `# Session ${noteNumber}: ${description}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n<!-- PAI will add completed work here during session -->\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n writeFileSync(filepath, content);\n console.error(`Created session note: ${filename}`);\n\n return filepath;\n}\n\n/** Append a checkpoint to the current session note. */\nexport function appendCheckpoint(notePath: string, checkpoint: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found, recreating: ${notePath}`);\n try {\n const parentDir = join(notePath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n const noteFilename = basename(notePath);\n const numberMatch = noteFilename.match(/^(\\d+)/);\n const noteNumber = numberMatch ? numberMatch[1] : '0000';\n const date = new Date().toISOString().split('T')[0];\n const content = `# Session ${noteNumber}: Recovered\\n\\n**Date:** ${date}\\n**Status:** In Progress\\n\\n---\\n\\n## Work Done\\n\\n<!-- PAI will add completed work here during session -->\\n\\n---\\n\\n## Next Steps\\n\\n<!-- To be filled at session end -->\\n\\n---\\n\\n**Tags:** #Session\\n`;\n writeFileSync(notePath, content);\n console.error(`Recreated session note: ${noteFilename}`);\n } catch (err) {\n console.error(`Failed to recreate note: ${err}`);\n return;\n }\n }\n\n const content = readFileSync(notePath, 'utf-8');\n const timestamp = new Date().toISOString();\n const checkpointText = `\\n### Checkpoint ${timestamp}\\n\\n${checkpoint}\\n`;\n\n const nextStepsIndex = content.indexOf('## Next Steps');\n const newContent = nextStepsIndex !== -1\n ? content.substring(0, nextStepsIndex) + checkpointText + content.substring(nextStepsIndex)\n : content + checkpointText;\n\n writeFileSync(notePath, newContent);\n console.error(`Checkpoint added to: ${basename(notePath)}`);\n}\n\n/** Work item for session notes. */\nexport interface WorkItem {\n title: string;\n details?: string[];\n completed?: boolean;\n}\n\n/** Add work items to the \"Work Done\" section of a session note. */\nexport function addWorkToSessionNote(notePath: string, workItems: WorkItem[], sectionTitle?: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n let workText = '';\n if (sectionTitle) workText += `\\n### ${sectionTitle}\\n\\n`;\n\n for (const item of workItems) {\n const checkbox = item.completed !== false ? '[x]' : '[ ]';\n workText += `- ${checkbox} **${item.title}**\\n`;\n if (item.details && item.details.length > 0) {\n for (const detail of item.details) {\n workText += ` - ${detail}\\n`;\n }\n }\n }\n\n const workDoneMatch = content.match(/## Work Done\\n\\n(<!-- .*? -->)?/);\n if (workDoneMatch) {\n const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;\n content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);\n } else {\n const nextStepsIndex = content.indexOf('## Next Steps');\n if (nextStepsIndex !== -1) {\n content = content.substring(0, nextStepsIndex) + workText + '\\n' + content.substring(nextStepsIndex);\n }\n }\n\n writeFileSync(notePath, content);\n console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);\n}\n\n/**\n * Check if a candidate title is meaningless / garbage.\n * Public wrapper around the internal filter for use by other hooks.\n */\nexport function isMeaningfulTitle(text: string): boolean {\n return !isMeaninglessCandidate(text);\n}\n\n/** Sanitize a string for use in a filename. */\nexport function sanitizeForFilename(str: string): string {\n return str\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .substring(0, 50);\n}\n\n/**\n * Return true if the candidate string should be rejected as a meaningful name.\n * Rejects file paths, shebangs, timestamps, system noise, XML tags, hashes, etc.\n */\nfunction isMeaninglessCandidate(text: string): boolean {\n const t = text.trim();\n if (!t) return true;\n if (t.length < 5) return true; // too short to be meaningful\n if (t.startsWith('/') || t.startsWith('~')) return true; // file path\n if (t.startsWith('#!')) return true; // shebang\n if (t.includes('[object Object]')) return true; // serialization artifact\n if (/^\\d{4}-\\d{2}-\\d{2}(T[\\d:.Z+-]+)?$/.test(t)) return true; // ISO timestamp\n if (/^\\d{1,2}:\\d{2}(:\\d{2})?(\\s*(AM|PM))?$/i.test(t)) return true; // time-only\n if (/^<[a-z-]+[\\s/>]/i.test(t)) return true; // XML/HTML tags (<task-notification>, etc.)\n if (/^[0-9a-f]{10,}$/i.test(t)) return true; // hex hash strings\n if (/^Exit code \\d+/i.test(t)) return true; // exit code messages\n if (/^Error:/i.test(t)) return true; // error messages\n if (/^This session is being continued/i.test(t)) return true; // continuation boilerplate\n if (/^\\(Bash completed/i.test(t)) return true; // bash output noise\n if (/^Task Notification$/i.test(t)) return true; // literal \"Task Notification\"\n if (/^New Session$/i.test(t)) return true; // placeholder title\n if (/^Recovered Session$/i.test(t)) return true; // placeholder title\n if (/^Continued Session$/i.test(t)) return true; // placeholder title\n if (/^Untitled Session$/i.test(t)) return true; // placeholder title\n if (/^Context Compression$/i.test(t)) return true; // compression artifact\n if (/^[A-Fa-f0-9]{8,}\\s+Output$/i.test(t)) return true; // hash + \"Output\" pattern\n return false;\n}\n\n/**\n * Extract a meaningful name from session note content and summary.\n * Looks at Work Done section headers, bold text, and summary.\n */\nexport function extractMeaningfulName(noteContent: string, summary: string): string {\n const workDoneMatch = noteContent.match(/## Work Done\\n\\n([\\s\\S]*?)(?=\\n---|\\n## Next)/);\n\n if (workDoneMatch) {\n const workDoneSection = workDoneMatch[1];\n\n const subheadings = workDoneSection.match(/### ([^\\n]+)/g);\n if (subheadings && subheadings.length > 0) {\n const firstHeading = subheadings[0].replace('### ', '').trim();\n if (!isMeaninglessCandidate(firstHeading) && firstHeading.length > 5 && firstHeading.length < 60) {\n return sanitizeForFilename(firstHeading);\n }\n }\n\n const boldMatches = workDoneSection.match(/\\*\\*([^*]+)\\*\\*/g);\n if (boldMatches && boldMatches.length > 0) {\n const firstBold = boldMatches[0].replace(/\\*\\*/g, '').trim();\n if (!isMeaninglessCandidate(firstBold) && firstBold.length > 3 && firstBold.length < 50) {\n return sanitizeForFilename(firstBold);\n }\n }\n\n const numberedItems = workDoneSection.match(/^\\d+\\.\\s+\\*\\*([^*]+)\\*\\*/m);\n if (numberedItems && !isMeaninglessCandidate(numberedItems[1])) {\n return sanitizeForFilename(numberedItems[1]);\n }\n }\n\n if (summary && summary.length > 5 && summary !== 'Session completed.' && !isMeaninglessCandidate(summary)) {\n const cleanSummary = summary\n .replace(/[^\\w\\s-]/g, ' ')\n .trim()\n .split(/\\s+/)\n .slice(0, 5)\n .join(' ');\n if (cleanSummary.length > 3 && !isMeaninglessCandidate(cleanSummary)) {\n return sanitizeForFilename(cleanSummary);\n }\n }\n\n return '';\n}\n\n/**\n * Rename a session note with a meaningful name.\n * Always uses \"NNNN - YYYY-MM-DD - Description.md\" format.\n * Returns the new path, or original path if rename fails.\n */\nexport function renameSessionNote(notePath: string, meaningfulName: string): string {\n if (!meaningfulName || !existsSync(notePath)) return notePath;\n\n const dir = join(notePath, '..');\n const oldFilename = basename(notePath);\n\n const correctMatch = oldFilename.match(/^(\\d{3,4}) - (\\d{4}-\\d{2}-\\d{2}) - .*\\.md$/);\n const legacyMatch = oldFilename.match(/^(\\d{3,4})_(\\d{4}-\\d{2}-\\d{2})_.*\\.md$/);\n const match = correctMatch || legacyMatch;\n if (!match) return notePath;\n\n const [, noteNumber, date] = match;\n\n const titleCaseName = meaningfulName\n .split(/[\\s_-]+/)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n .trim();\n\n const paddedNumber = noteNumber.padStart(4, '0');\n const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;\n const newPath = join(dir, newFilename);\n\n if (newFilename === oldFilename) return notePath;\n\n try {\n renameSync(notePath, newPath);\n console.error(`Renamed note: ${oldFilename} → ${newFilename}`);\n return newPath;\n } catch (error) {\n console.error(`Could not rename note: ${error}`);\n return notePath;\n }\n}\n\n/** Update the session note's H1 title and rename the file. */\nexport function updateSessionNoteTitle(notePath: string, newTitle: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n content = content.replace(/^# Session \\d+:.*$/m, (match) => {\n const sessionNum = match.match(/Session (\\d+)/)?.[1] || '';\n return `# Session ${sessionNum}: ${newTitle}`;\n });\n writeFileSync(notePath, content);\n renameSessionNote(notePath, sanitizeForFilename(newTitle));\n}\n\n/**\n * Finalize session note — mark as complete, add summary, rename with meaningful name.\n * IDEMPOTENT: subsequent calls are no-ops if already finalized.\n * Returns the final path (may be renamed).\n */\nexport function finalizeSessionNote(notePath: string, summary: string): string {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return notePath;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n if (content.includes('**Status:** Completed')) {\n console.error(`Note already finalized: ${basename(notePath)}`);\n return notePath;\n }\n\n content = content.replace('**Status:** In Progress', '**Status:** Completed');\n\n if (!content.includes('**Completed:**')) {\n const completionTime = new Date().toISOString();\n content = content.replace(\n '---\\n\\n## Work Done',\n `**Completed:** ${completionTime}\\n\\n---\\n\\n## Work Done`\n );\n }\n\n const nextStepsMatch = content.match(/## Next Steps\\n\\n(<!-- .*? -->)/);\n if (nextStepsMatch) {\n content = content.replace(\n nextStepsMatch[0],\n `## Next Steps\\n\\n${summary || 'Session completed.'}`\n );\n }\n\n writeFileSync(notePath, content);\n console.error(`Session note finalized: ${basename(notePath)}`);\n\n const meaningfulName = extractMeaningfulName(content, summary);\n if (meaningfulName) {\n return renameSessionNote(notePath, meaningfulName);\n }\n\n return notePath;\n}\n","/**\n * TODO.md management — creation, task updates, checkpoints, and Continue section.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { findTodoPath } from './paths.js';\nimport { applyContinue } from '../../../../session/checkpoint-block.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Task item for TODO.md. */\nexport interface TodoItem {\n content: string;\n completed: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Ensure TODO.md exists. Creates it with default structure if missing.\n * Returns the path to the TODO.md file.\n */\nexport function ensureTodoMd(cwd: string): string {\n const todoPath = findTodoPath(cwd);\n\n if (!existsSync(todoPath)) {\n const parentDir = join(todoPath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n\n const content = `# TODO\n\n## Current Session\n\n- [ ] (Tasks will be tracked here)\n\n## Backlog\n\n- [ ] (Future tasks)\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Created TODO.md: ${todoPath}`);\n }\n\n return todoPath;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Update TODO.md with current session tasks.\n * Preserves the Backlog section and ensures exactly ONE timestamp at the end.\n */\nexport function updateTodoMd(cwd: string, tasks: TodoItem[], sessionSummary?: string): void {\n const todoPath = ensureTodoMd(cwd);\n const content = readFileSync(todoPath, 'utf-8');\n\n const backlogMatch = content.match(/## Backlog[\\s\\S]*?(?=\\n---|\\n\\*Last updated|$)/);\n const backlogSection = backlogMatch\n ? backlogMatch[0].trim()\n : '## Backlog\\n\\n- [ ] (Future tasks)';\n\n const taskLines = tasks.length > 0\n ? tasks.map(t => `- [${t.completed ? 'x' : ' '}] ${t.content}`).join('\\n')\n : '- [ ] (No active tasks)';\n\n const newContent = `# TODO\n\n## Current Session\n\n${taskLines}\n\n${sessionSummary ? `**Session Summary:** ${sessionSummary}\\n\\n` : ''}${backlogSection}\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, newContent);\n console.error(`Updated TODO.md: ${todoPath}`);\n}\n\n/**\n * Add a checkpoint entry to TODO.md (without replacing tasks).\n * Ensures exactly ONE timestamp line at the end.\n */\nexport function addTodoCheckpoint(cwd: string, checkpoint: string): void {\n const todoPath = ensureTodoMd(cwd);\n let content = readFileSync(todoPath, 'utf-8');\n\n // Remove ALL existing timestamp lines and trailing separators\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n\n const checkpointText = `\\n**Checkpoint (${new Date().toISOString()}):** ${checkpoint}\\n\\n`;\n\n const backlogIndex = content.indexOf('## Backlog');\n if (backlogIndex !== -1) {\n content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);\n } else {\n const continueIndex = content.indexOf('## Continue');\n if (continueIndex !== -1) {\n const afterContinue = content.indexOf('\\n---', continueIndex);\n if (afterContinue !== -1) {\n const insertAt = afterContinue + 4;\n content = content.substring(0, insertAt) + '\\n' + checkpointText + content.substring(insertAt);\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n }\n\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${new Date().toISOString()}*\\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Checkpoint added to TODO.md`);\n}\n\n/**\n * The Claude Code session UUID, read off the transcript path.\n *\n * Claude Code names a transcript `<session-uuid>.jsonl`, so the hooks that\n * receive a transcript path are holding the session's identity without knowing\n * it. Both session-end writers had one available and neither used it.\n *\n * Shape-checked rather than trusted: only a UUID is returned, so a renamed,\n * archived or otherwise unexpected filename yields undefined and the caller\n * falls back to the session line rather than writing a `session-id` that\n * identifies nothing. A wrong id is worse than none — it would make two\n * different sessions compare equal.\n */\nexport function sessionIdFromTranscript(transcriptPath: string | undefined): string | undefined {\n if (!transcriptPath) return undefined;\n const base = basename(transcriptPath).replace(/\\.jsonl$/, '');\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(base)\n ? base\n : undefined;\n}\n\n/**\n * Update the ## Continue section at the top of TODO.md.\n *\n * This is the unattended writer: the pre-compact hook and the daemon's\n * work-queue worker both come through here. It used to build its own block and\n * strip the existing section with `/## Continue\\n[\\s\\S]*?\\n---\\n+/` — a\n * non-greedy match to the first `---`, which cuts a rich checkpoint in half at\n * the first horizontal rule inside its body and leaves the remainder orphaned\n * in the document.\n *\n * More importantly it was the writer that actually destroyed model-authored\n * checkpoints: `pai pause` wrote one, then this ran seconds later on session\n * end and replaced it with `Working directory: … Check the latest session note\n * for details.`\n *\n * It now delegates to the shared checkpoint module in \"auto\" mode, which means\n * it inherits the preservation rules rather than reimplementing them. Guarding\n * here rather than at each of the three call sites is deliberate: any future\n * caller inherits the behaviour without knowing it exists.\n */\nexport function updateTodoContinue(\n cwd: string,\n noteFilename: string,\n state: string | null,\n tokenDisplay: string,\n /**\n * The Claude Code session UUID, when the caller knows it.\n *\n * Optional only because a caller may genuinely not have it; supply it\n * whenever you can. Without it `isSameSession` falls back to comparing the\n * note FILENAME, and pausing renames the note — so a session stops\n * recognising its own checkpoint seconds after writing it, and an automated\n * write is then free to replace it as though it belonged to a predecessor.\n * That is how a live session lost a model-authored handover on 2026-08-04.\n */\n sessionId?: string\n): void {\n // Ensure a TODO.md exists so applyContinue writes to the same file the rest\n // of the hooks lib uses.\n ensureTodoMd(cwd);\n\n const result = applyContinue({\n rootPath: cwd,\n authored: 'auto',\n // The hooks identify a session by its note filename; `pai pause` resolves\n // the same string from the registry (and falls back to this filename when\n // the registry has no session row). They must agree, because this string\n // is the key that decides whether an authored checkpoint belongs to the\n // current session and must be preserved.\n sessionLine: noteFilename.replace(/\\.md$/, ''),\n // The UUID is authoritative when present; the line above is the fallback\n // for callers that cannot supply one, and it is mutable by design.\n sessionId,\n cwd,\n body: state?.trim() || undefined,\n });\n\n if (result.action === 'preserved') {\n console.error(\n 'TODO.md ## Continue left intact — authored checkpoint for this session'\n );\n return;\n }\n\n if (result.action === 'failed') {\n console.error(`TODO.md ## Continue update failed: ${result.error}`);\n return;\n }\n\n // Refresh the trailing \"Last updated\" stamp without disturbing the block.\n try {\n const todoPath = result.path!;\n const now = new Date().toISOString();\n let content = readFileSync(todoPath, 'utf-8');\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${now}*\\n`;\n writeFileSync(todoPath, content);\n } catch {\n // Non-fatal — the checkpoint itself is already written.\n }\n\n console.error(\n result.carriedForward\n ? 'TODO.md ## Continue section updated (previous content carried forward)'\n : 'TODO.md ## Continue section updated'\n );\n}\n","/**\n * session-summary-prompt.ts — Prompt template for AI-powered session summaries\n *\n * Produces a prompt that instructs the summarizer model to generate a structured\n * session note from extracted user messages and git commits. The output format\n * matches PAI's existing session note structure (Reconstruct skill format).\n *\n * The prompt also requests a TOPIC line on the first line of output, which the\n * session-summary-worker uses to detect topic shifts and decide whether to\n * create a new note or update the existing one.\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SummaryPromptParams {\n /** Extracted user messages from the JSONL transcript. */\n userMessages: string[];\n /** Git log output (--oneline --stat) for the session period. */\n gitLog: string;\n /** Working directory of the session. */\n cwd: string;\n /** ISO date string for the session. */\n date: string;\n /** Files modified during the session (from tool_use blocks). */\n filesModified?: string[];\n /** Existing session note content (if updating). */\n existingNote?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Prompt builder\n// ---------------------------------------------------------------------------\n\n/**\n * Build the prompt string to send to the summarizer model.\n *\n * Returns a single string suitable for piping to `claude --model <model> --print`.\n */\nexport function buildSessionSummaryPrompt(params: SummaryPromptParams): string {\n const {\n userMessages,\n gitLog,\n cwd,\n date,\n filesModified,\n existingNote,\n } = params;\n\n const userSection = userMessages.length > 0\n ? userMessages.map((m, i) => `[${i + 1}] ${m}`).join(\"\\n\\n\")\n : \"(No user messages extracted)\";\n\n const gitSection = gitLog.trim() || \"(No git commits during this session)\";\n\n const filesSection = filesModified && filesModified.length > 0\n ? filesModified.map(f => `- ${f}`).join(\"\\n\")\n : \"\";\n\n const updateInstruction = existingNote\n ? `\\nAn existing session note is provided below. Merge the new information into it,\npreserving what was already written. Add new work items and update the summary.\nDo NOT duplicate existing content.\n\nEXISTING NOTE:\n${existingNote}\n`\n : \"\";\n\n return `You are summarizing a coding session. Given the user messages and git commits below, write a session note.\n\nProject directory: ${cwd}\nDate: ${date}\n\nFocus on:\n- What problems were encountered and how they were solved\n- Key architectural decisions and their rationale\n- What was built (reference actual files and code patterns)\n- What was left unfinished or needs follow-up\n\nDo NOT include:\n- Mechanical metadata (token counts, checkpoint timestamps)\n- System messages or tool results verbatim\n- Generic descriptions — be specific about what happened\n- Markdown frontmatter or YAML headers\n${updateInstruction}\nFormat your response EXACTLY as follows (no extra text before or after):\n\nTOPIC: [A short topic label, max 60 characters, describing the WORK DONE — not quoting user messages. Format as \"Topic1, Topic2, and Topic3\" if multiple themes. Example: \"Session Summary Worker, Topic Detection\"]\n\n# Session: [Descriptive title summarizing what was ACCOMPLISHED, max 60 characters. Describe the work done, not the user's request. Bad: \"Dark Mode Button Does Nothing\". Good: \"Dark Mode Toggle, Keyboard IPC, and Audio Fix\"]\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n[Organize by theme, not chronologically. Group related work under descriptive bullet points.\nUse checkbox format: - [x] for completed items, - [ ] for incomplete items.\nInclude specific file names, function names, and technical details.]\n\n## Key Decisions\n\n[List important choices made during the session with brief rationale.\nSkip this section entirely if no significant decisions were made.]\n\n## Known Issues\n\n[What was left unfinished, bugs discovered, or follow-up items needed.\nSkip this section entirely if nothing is pending.]\n\n---\n\nUSER MESSAGES:\n${userSection}\n\nGIT COMMITS:\n${gitSection}\n${filesSection ? `\\nFILES MODIFIED:\\n${filesSection}` : \"\"}`;\n}\n","/**\n * session-summary-worker.ts — AI-powered session note generation\n *\n * Processes `session-summary` work items by:\n * 1. Finding the current session's JSONL transcript\n * 2. Extracting user messages and assistant context\n * 3. Gathering git commits from the session period\n * 4. Spawning Claude (sonnet for compaction, opus for session end) to generate a structured summary\n * 5. Comparing the new topic against the existing note's topic\n * 6. Creating a NEW note if the topic shifted, or updating the existing one\n *\n * Topic detection: the summarizer outputs a TOPIC: line as the first line of\n * its response. This is compared against the existing note's title using word\n * overlap. If overlap is below ~30%, a new note is created.\n *\n * Designed to run inside the daemon's work queue worker. All errors are\n * thrown (not swallowed) so the work queue retry logic handles them.\n */\n\nimport {\n existsSync,\n readFileSync,\n readdirSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nimport {\n findNotesDir,\n getCurrentNotePath,\n createSessionNote,\n addWorkToSessionNote,\n renameSessionNote,\n} from \"../hooks/ts/lib/project-utils/index.js\";\n\nimport { buildSessionSummaryPrompt } from \"./templates/session-summary-prompt.js\";\nimport {\n extractAndStoreTriples as kgExtractAndStoreTriples,\n} from \"../memory/kg-extraction.js\";\nimport { openFederation } from \"../memory/db.js\";\nimport { registryDb, storageBackend, daemonConfig } from \"./daemon/state.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Minimum interval between summaries for the same project (ms). */\nconst SUMMARY_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes\n\n/** Maximum JSONL content to feed to the summarizer (characters). */\n/** Max JSONL chars per model. Opus/Sonnet can handle much more than Haiku. */\nconst MAX_JSONL_CHARS: Record<string, number> = {\n haiku: 50_000,\n sonnet: 200_000,\n opus: 500_000,\n};\n\n/** Maximum user messages to include in the prompt. */\nconst MAX_USER_MESSAGES = 30;\n\n/** Timeout for the claude CLI process (ms). */\nconst CLAUDE_TIMEOUT_MS: Record<string, number> = {\n haiku: 60_000, // 60 seconds\n sonnet: 120_000, // 2 minutes\n opus: 300_000, // 5 minutes — opus is thorough\n};\n\n/** File tracking last summary timestamps per project. */\nconst COOLDOWN_FILE = join(homedir(), \".config\", \"pai\", \"summary-cooldowns.json\");\n\n/** Claude Code projects directory. */\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SessionSummaryPayload {\n cwd: string;\n sessionId?: string;\n projectSlug?: string;\n transcriptPath?: string;\n /** If true, bypass the cooldown check (e.g. triggered by stop-hook at session end). */\n force?: boolean;\n /** Model to use for summarization. Defaults based on trigger:\n * - forced (mid-session auto-save, every N messages): \"haiku\" — runs often\n * - everything else (manual, reconstruct, batch): \"sonnet\"\n * Session end does not summarise via an LLM at all; it is mechanical. */\n model?: \"haiku\" | \"sonnet\" | \"opus\";\n}\n\n// ---------------------------------------------------------------------------\n// Cooldown tracking\n// ---------------------------------------------------------------------------\n\nfunction loadCooldowns(): Record<string, number> {\n try {\n if (existsSync(COOLDOWN_FILE)) {\n return JSON.parse(readFileSync(COOLDOWN_FILE, \"utf-8\"));\n }\n } catch { /* ignore */ }\n return {};\n}\n\nfunction saveCooldowns(cooldowns: Record<string, number>): void {\n try {\n writeFileSync(COOLDOWN_FILE, JSON.stringify(cooldowns, null, 2), \"utf-8\");\n } catch { /* ignore */ }\n}\n\nfunction isOnCooldown(cwd: string): boolean {\n const cooldowns = loadCooldowns();\n const lastRun = cooldowns[cwd];\n if (!lastRun) return false;\n return Date.now() - lastRun < SUMMARY_COOLDOWN_MS;\n}\n\nfunction markCooldown(cwd: string): void {\n const cooldowns = loadCooldowns();\n cooldowns[cwd] = Date.now();\n // Prune entries older than 24 hours\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n for (const key of Object.keys(cooldowns)) {\n if (cooldowns[key] < cutoff) delete cooldowns[key];\n }\n saveCooldowns(cooldowns);\n}\n\n// ---------------------------------------------------------------------------\n// JSONL discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Encode a cwd path the same way Claude Code does for its project directories.\n * Replaces /, space, dot, and hyphen with -.\n */\nfunction encodeProjectPath(cwd: string): string {\n return cwd.replace(/[\\/\\s.\\-]/g, \"-\");\n}\n\n/**\n * Find the most recently modified JSONL file for the given project.\n *\n * Claude Code stores transcripts in:\n * ~/.claude/projects/<encoded-path>/sessions/*.jsonl\n * ~/.claude/projects/<encoded-path>/<uuid>.jsonl (legacy)\n */\nexport function findLatestJsonl(cwd: string): string | null {\n const encoded = encodeProjectPath(cwd);\n const projectDir = join(CLAUDE_PROJECTS_DIR, encoded);\n\n if (!existsSync(projectDir)) {\n process.stderr.write(\n `[session-summary] No Claude project dir found: ${projectDir}\\n`\n );\n return null;\n }\n\n // Collect all JSONL candidates\n const candidates: Array<{ path: string; mtime: number }> = [];\n\n // Check sessions/ subdirectory first (current layout)\n const sessionsDir = join(projectDir, \"sessions\");\n if (existsSync(sessionsDir)) {\n try {\n for (const f of readdirSync(sessionsDir)) {\n if (!f.endsWith(\".jsonl\")) continue;\n const fullPath = join(sessionsDir, f);\n try {\n const st = statSync(fullPath);\n candidates.push({ path: fullPath, mtime: st.mtimeMs });\n } catch { /* skip */ }\n }\n } catch { /* skip */ }\n }\n\n // Also check top-level for legacy .jsonl files\n try {\n for (const f of readdirSync(projectDir)) {\n if (!f.endsWith(\".jsonl\")) continue;\n const fullPath = join(projectDir, f);\n try {\n const st = statSync(fullPath);\n candidates.push({ path: fullPath, mtime: st.mtimeMs });\n } catch { /* skip */ }\n }\n } catch { /* skip */ }\n\n if (candidates.length === 0) {\n process.stderr.write(\n `[session-summary] No JSONL files found in ${projectDir}\\n`\n );\n return null;\n }\n\n // Sort by modification time descending — pick the most recent\n candidates.sort((a, b) => b.mtime - a.mtime);\n return candidates[0].path;\n}\n\n// ---------------------------------------------------------------------------\n// JSONL content extraction\n// ---------------------------------------------------------------------------\n\ninterface ExtractedContent {\n userMessages: string[];\n filesModified: string[];\n sessionStartTime: string;\n}\n\n/**\n * Parse a JSONL transcript and extract relevant content.\n * Filters noise, truncates to model-appropriate size from the end of the file.\n */\nfunction extractFromJsonl(jsonlPath: string, model: string = \"sonnet\"): ExtractedContent {\n const result: ExtractedContent = {\n userMessages: [],\n filesModified: [],\n sessionStartTime: \"\",\n };\n\n let raw: string;\n try {\n raw = readFileSync(jsonlPath, \"utf-8\");\n } catch (e) {\n throw new Error(`Could not read JSONL at ${jsonlPath}: ${e}`);\n }\n\n // Truncate from the start if too large (keep the most recent content)\n const maxChars = MAX_JSONL_CHARS[model] ?? 200_000;\n if (raw.length > maxChars) {\n const truncPoint = raw.indexOf(\"\\n\", raw.length - maxChars);\n raw = truncPoint >= 0 ? raw.slice(truncPoint + 1) : raw.slice(-MAX_JSONL_CHARS);\n }\n\n const lines = raw.trim().split(\"\\n\");\n const seenMessages = new Set<string>();\n\n for (const line of lines) {\n if (!line.trim()) continue;\n\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n\n // Track earliest timestamp\n if (entry.timestamp && !result.sessionStartTime) {\n result.sessionStartTime = String(entry.timestamp);\n }\n\n // Extract user messages\n if (entry.type === \"user\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n if (msg?.content) {\n const text = contentToText(msg.content);\n if (text && !isNoise(text) && !seenMessages.has(text)) {\n seenMessages.add(text);\n result.userMessages.push(text.slice(0, 500));\n }\n }\n }\n\n // Extract file modifications from assistant tool_use blocks\n if (entry.type === \"assistant\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n if (msg?.content && Array.isArray(msg.content)) {\n for (const block of msg.content as Array<Record<string, unknown>>) {\n if (block.type === \"tool_use\") {\n const name = block.name as string;\n const input = block.input as Record<string, unknown> | undefined;\n if ((name === \"Edit\" || name === \"Write\") && input?.file_path) {\n const fp = String(input.file_path);\n if (!result.filesModified.includes(fp)) {\n result.filesModified.push(fp);\n }\n }\n }\n }\n }\n }\n }\n\n // Limit user messages\n if (result.userMessages.length > MAX_USER_MESSAGES) {\n result.userMessages = result.userMessages.slice(-MAX_USER_MESSAGES);\n }\n\n return result;\n}\n\n/** Convert Claude content (string or content block array) to plain text. */\nexport function contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === \"string\") return c;\n const block = c as Record<string, unknown>;\n if (block?.text) return String(block.text);\n if (block?.content) return String(block.content);\n return \"\";\n })\n .join(\" \")\n .trim();\n }\n return \"\";\n}\n\n/** Filter out noise entries that shouldn't be included in the summary. */\nfunction isNoise(text: string): boolean {\n if (!text || text.length < 3) return true;\n if (text.includes(\"<task-notification>\")) return true;\n if (text.includes(\"[object Object]\")) return true;\n if (text.startsWith(\"<system-reminder>\")) return true;\n if (/^(yes|ok|sure|go|continue|weiter|thanks|thank you)\\.?$/i.test(text.trim())) return true;\n // Skip pure tool result blocks\n if (text.startsWith(\"Tool Result:\") || text.startsWith(\"tool_result\")) return true;\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Git context\n// ---------------------------------------------------------------------------\n\n/**\n * Get git log for the session period.\n * Falls back gracefully if git is not available or the dir is not a repo.\n */\nexport async function getGitContext(cwd: string, sinceTime?: string): Promise<string> {\n let since = \"6 hours ago\";\n if (sinceTime) {\n // sinceTime may be a Unix epoch (seconds as string) or already ISO 8601\n const asNum = Number(sinceTime);\n if (!isNaN(asNum) && asNum > 1_000_000_000) {\n // Unix epoch seconds → ISO 8601 (git accepts this unambiguously)\n since = new Date(asNum * 1000).toISOString();\n } else {\n since = sinceTime;\n }\n }\n\n try {\n const { execFile: execFileCb } = await import(\"node:child_process\");\n const { promisify } = await import(\"node:util\");\n const execFileAsync = promisify(execFileCb);\n\n const { stdout } = await execFileAsync(\n \"git\",\n [\"log\", \"--format=%h %ai %s\", `--since=${since}`, \"--stat\", \"--no-color\"],\n {\n cwd,\n timeout: 10_000,\n env: { ...process.env, GIT_TERMINAL_PROMPT: \"0\" },\n }\n );\n return stdout.trim();\n } catch {\n return \"\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Claude CLI spawning\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `claude` CLI binary.\n * Checks PATH first, then common installation locations.\n */\nfunction findClaudeBinary(): string | null {\n // Check known locations first (launchd PATH is minimal, bare \"claude\" won't resolve)\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"claude\"),\n join(homedir(), \".claude\", \"local\", \"claude\"),\n \"/usr/local/bin/claude\",\n \"/opt/homebrew/bin/claude\",\n ];\n\n for (const candidate of candidates) {\n try {\n if (existsSync(candidate)) return candidate;\n } catch { /* skip */ }\n }\n\n // Last resort: try bare \"claude\" in case PATH has it\n return \"claude\";\n}\n\n/**\n * Spawn a Claude model via the CLI to generate a session summary.\n * Pipes the prompt via stdin. Model selection:\n * - opus: session end (best quality for final summary, runs once)\n * - sonnet: auto-compaction (good quality for incremental checkpoints, runs often)\n * - haiku: fallback / budget mode\n * Returns the generated text, or null if spawning fails.\n */\nexport async function spawnSummarizer(prompt: string, model: string = \"sonnet\"): Promise<string | null> {\n const claudeBin = findClaudeBinary();\n if (!claudeBin) {\n process.stderr.write(\n \"[session-summary] Claude CLI not found in PATH or common locations.\\n\"\n );\n return null;\n }\n\n const { spawn } = await import(\"node:child_process\");\n\n return new Promise((resolve) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n // Strip ANTHROPIC_API_KEY so claude CLI uses the Max plan (free)\n // instead of billing against the API key\n const { ANTHROPIC_API_KEY: _, ...envWithoutApiKey } = process.env;\n const child = spawn(claudeBin, [\"--model\", model, \"-p\", \"--no-session-persistence\"], {\n env: envWithoutApiKey,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString();\n });\n\n child.stderr.on(\"data\", (chunk: Buffer) => {\n stderr += chunk.toString();\n });\n\n child.on(\"error\", (err: Error) => {\n if (timer) { clearTimeout(timer); timer = null; }\n process.stderr.write(`[session-summary] ${model} spawn error: ${err.message}\\n`);\n resolve(null);\n });\n\n child.on(\"close\", (code: number | null) => {\n if (timer) { clearTimeout(timer); timer = null; }\n if (code !== 0) {\n process.stderr.write(\n `[session-summary] ${model} exited with code ${code}: ${stderr.slice(0, 300)}\\n`\n );\n resolve(null);\n } else {\n resolve(stdout.trim() || null);\n }\n });\n\n // Timeout protection\n timer = setTimeout(() => {\n process.stderr.write(`[session-summary] ${model} timed out — killing process.\\n`);\n child.kill(\"SIGTERM\");\n resolve(null);\n }, CLAUDE_TIMEOUT_MS[model] ?? 120_000);\n\n // Write prompt to stdin and close\n child.stdin.write(prompt);\n child.stdin.end();\n });\n}\n\n// ---------------------------------------------------------------------------\n// Topic extraction and comparison\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the TOPIC: line from the summarizer output.\n * Returns the topic string, or null if not found.\n */\nfunction extractTopic(summaryText: string): string | null {\n const match = summaryText.match(/^TOPIC:\\s*(.+)$/m);\n if (!match) return null;\n return match[1].trim();\n}\n\n/**\n * Extract the topic from an existing session note.\n * First checks for a <!-- TOPIC: ... --> comment (stored by previous summaries).\n * Falls back to the H1 \"# Session NNNN: Title\" line.\n *\n * The HTML comment is the reliable source because the H1 gets renamed by\n * renameSessionNote, which can add/change words and cause false topic shifts.\n */\nfunction extractExistingNoteTitle(notePath: string): string | null {\n try {\n const content = readFileSync(notePath, \"utf-8\");\n // Prefer stored TOPIC comment (exact match to what the summarizer produced)\n const topicComment = content.match(/<!-- TOPIC:\\s*(.+?)\\s*-->/);\n if (topicComment) return topicComment[1].trim();\n // Fallback to H1\n const match = content.match(/^# Session \\d+:\\s*(.+)$/m);\n if (match) return match[1].trim();\n } catch { /* ignore */ }\n return null;\n}\n\n/**\n * Compute word overlap ratio between two topic strings.\n * Returns a value in [0, 1] — 1.0 means identical word sets.\n *\n * Uses lowercased, normalized words. Stop words and very short words\n * are excluded to avoid false positives on common terms.\n */\nfunction computeTopicOverlap(topicA: string, topicB: string): number {\n const stopWords = new Set([\n \"a\", \"an\", \"the\", \"and\", \"or\", \"but\", \"in\", \"on\", \"at\", \"to\", \"for\",\n \"of\", \"with\", \"by\", \"from\", \"is\", \"was\", \"are\", \"were\", \"be\", \"been\",\n \"being\", \"have\", \"has\", \"had\", \"do\", \"does\", \"did\", \"will\", \"would\",\n \"could\", \"should\", \"may\", \"might\", \"can\", \"shall\", \"this\", \"that\",\n \"these\", \"those\", \"it\", \"its\", \"new\", \"session\", \"work\", \"done\",\n ]);\n\n const normalize = (text: string): Set<string> => {\n const words = text\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 2 && !stopWords.has(w));\n return new Set(words);\n };\n\n const wordsA = normalize(topicA);\n const wordsB = normalize(topicB);\n\n if (wordsA.size === 0 || wordsB.size === 0) return 0;\n\n let intersection = 0;\n for (const w of wordsA) {\n if (wordsB.has(w)) intersection++;\n }\n\n // Jaccard similarity\n const union = new Set([...wordsA, ...wordsB]).size;\n return union > 0 ? intersection / union : 0;\n}\n\n/** Threshold: below this overlap ratio, we consider topics different. */\n// Raised from 0.3 to 0.15 — lower threshold means topics must be MORE different\n// to trigger a split. 0.3 was too aggressive: incremental work on the same project\n// (e.g., \"Flutter Rewrite\" vs \"Fix Transcription in Flutter\") was splitting into\n// separate notes on every compaction.\nconst TOPIC_OVERLAP_THRESHOLD = 0.15;\n\n// ---------------------------------------------------------------------------\n// Session note writing\n// ---------------------------------------------------------------------------\n\n/**\n * Write (or update) the session note with the AI-generated summary.\n *\n * Strategy:\n * - Find the current month's latest note\n * - If it's from today, compare topics:\n * - Same topic (overlap >= 30%) → update existing note\n * - Different topic (overlap < 30%) → create a NEW note\n * - If it's from a different day, create a new note\n */\n/**\n * Return true if the summarizer output has a meaningful body — i.e. content\n * beyond the TOPIC:/title/metadata/horizontal-rule scaffolding.\n *\n * A summary that is only headers (no Work Done items) must NOT create or update\n * a note: doing so births an empty-bodied scaffold that later rename/finalize\n * can strip to a footer-only stub. This is the born-stub failure mode.\n */\nfunction summaryHasContent(summaryText: string): boolean {\n const body = summaryText\n .replace(/^TOPIC:.*$/m, \"\")\n .replace(/^# Session:.*$/m, \"\")\n .replace(/^\\*\\*(Date|Status|Completed):\\*\\*.*$/gm, \"\")\n .replace(/^#{1,6}\\s.*$/gm, \"\") // bare section headers (## Work Done, etc.)\n .replace(/^---$/gm, \"\")\n .replace(/<!--[\\s\\S]*?-->/g, \"\") // template placeholder comments\n .trim();\n return body.length > 0;\n}\n\nfunction writeSessionNote(\n cwd: string,\n summaryText: string,\n filesModified: string[],\n sessionId?: string,\n): string | null {\n // Never create/update a note from a body-less summary — that is the born-stub\n // path (empty scaffold → stripped to a footer-only stub on finalize/rename).\n if (!summaryHasContent(summaryText)) {\n process.stderr.write(\n `[session-summary] Summary has no meaningful body — skipping note write.\\n`\n );\n return null;\n }\n\n const notesInfo = findNotesDir(cwd);\n // Identity first: the note this session already owns. Falling back to the\n // positional lookup only when there is no session id keeps older callers\n // working, but the fallback is what used to create duplicates.\n let notePath = (sessionId && findNoteBySessionId(notesInfo.path, sessionId))\n || getCurrentNotePath(notesInfo.path);\n\n const today = new Date().toISOString().split(\"T\")[0];\n\n // Extract the new topic from the summarizer output\n const newTopic = extractTopic(summaryText);\n\n if (notePath) {\n const noteFilename = basename(notePath);\n // Check if this note is from today\n const dateMatch = noteFilename.match(/(\\d{4}-\\d{2}-\\d{2})/);\n const noteDate = dateMatch ? dateMatch[1] : \"\";\n\n if (noteDate === today) {\n // Check for topic shift — two signals:\n // 1. TOPIC: line from summarizer vs existing note title (word overlap)\n // 2. topic-boundary.json written by topic-detect-worker (project-level shift)\n const existingTitle = extractExistingNoteTitle(notePath);\n let topicShifted = false;\n\n // Signal 1: summarizer topic vs existing note title\n if (newTopic && existingTitle) {\n const overlap = computeTopicOverlap(newTopic, existingTitle);\n process.stderr.write(\n `[session-summary] Topic overlap: ${(overlap * 100).toFixed(1)}%` +\n ` (new=\"${newTopic}\", existing=\"${existingTitle}\")\\n`\n );\n\n if (overlap < TOPIC_OVERLAP_THRESHOLD) {\n topicShifted = true;\n process.stderr.write(\n `[session-summary] Topic shift detected (word overlap) — creating new note.\\n`\n );\n }\n }\n\n // Signal 2: topic boundary marker from topic-detect-worker\n if (!topicShifted) {\n const boundaryPath = join(notesInfo.path, \"topic-boundary.json\");\n if (existsSync(boundaryPath)) {\n try {\n const boundary = JSON.parse(readFileSync(boundaryPath, \"utf-8\"));\n if (boundary.timestamp) {\n const boundaryAge = Date.now() - new Date(boundary.timestamp).getTime();\n // Only honor boundaries from the last 30 minutes\n if (boundaryAge < 30 * 60 * 1000) {\n topicShifted = true;\n process.stderr.write(\n `[session-summary] Topic shift detected (boundary marker) — ` +\n `${boundary.previousProject} → ${boundary.suggestedProject}\\n`\n );\n }\n }\n // Consume the boundary file (one-shot)\n unlinkSync(boundaryPath);\n } catch { /* ignore invalid boundary file */ }\n }\n }\n\n if (topicShifted) {\n // Different topic — create a NEW note (topic-based split)\n notePath = createNoteFromSummary(notesInfo.path, summaryText, sessionId);\n } else {\n // Same topic — update existing note\n updateNoteWithSummary(notePath, summaryText);\n process.stderr.write(\n `[session-summary] Updated existing note: ${noteFilename}\\n`\n );\n }\n } else {\n // Different day — create a new note\n notePath = createNoteFromSummary(notesInfo.path, summaryText, sessionId);\n }\n } else {\n // No note exists — create one\n notePath = createNoteFromSummary(notesInfo.path, summaryText, sessionId);\n }\n\n // Try to rename with a meaningful title from the summary\n if (notePath) {\n const titleMatch = summaryText.match(/^# Session:\\s*(.+)$/m);\n if (titleMatch) {\n const title = titleMatch[1].trim();\n if (title.length > 5 && title.length < 80) {\n const newPath = renameSessionNote(notePath, title);\n if (newPath !== notePath) {\n notePath = newPath;\n }\n }\n }\n }\n\n return notePath;\n}\n\n/**\n * Update an existing session note's Work Done section with AI-generated content.\n */\nfunction updateNoteWithSummary(notePath: string, summaryText: string): void {\n if (!existsSync(notePath)) return;\n\n let content = readFileSync(notePath, \"utf-8\");\n\n // Update the TOPIC comment if present (keeps topic comparison stable across renames)\n const newTopic = extractTopic(summaryText);\n if (newTopic) {\n if (content.includes(\"<!-- TOPIC:\")) {\n content = content.replace(/<!-- TOPIC:.*?-->/, `<!-- TOPIC: ${newTopic} -->`);\n } else {\n // Insert after H1 line\n content = content.replace(/^(# Session .+)$/m, `$1\\n<!-- TOPIC: ${newTopic} -->`);\n }\n }\n\n // Extract the work items from the AI summary\n const workDoneMatch = summaryText.match(\n /## Work Done\\n\\n([\\s\\S]*?)(?=\\n## Key Decisions|\\n## Known Issues|\\n\\*\\*Tags|\\n$)/\n );\n\n if (workDoneMatch) {\n const aiWorkContent = workDoneMatch[1].trim();\n const timestamp = new Date().toISOString().split(\"T\")[1].split(\".\")[0];\n\n // Add as a new subsection under Work Done\n const sectionHeader = `\\n### AI Summary (${timestamp})\\n\\n${aiWorkContent}\\n`;\n\n const nextStepsIdx = content.indexOf(\"## Next Steps\");\n const knownIssuesIdx = content.indexOf(\"## Known Issues\");\n const insertBefore = knownIssuesIdx !== -1 ? knownIssuesIdx :\n nextStepsIdx !== -1 ? nextStepsIdx :\n content.length;\n\n content = content.slice(0, insertBefore) + sectionHeader + \"\\n\" + content.slice(insertBefore);\n }\n\n // Extract and add Key Decisions if present\n const decisionsMatch = summaryText.match(\n /## Key Decisions\\n\\n([\\s\\S]*?)(?=\\n## Known Issues|\\n\\*\\*Tags|\\n$)/\n );\n if (decisionsMatch) {\n const decisions = decisionsMatch[1].trim();\n if (decisions && !content.includes(\"## Key Decisions\")) {\n const nextStepsIdx = content.indexOf(\"## Next Steps\");\n const insertAt = nextStepsIdx !== -1 ? nextStepsIdx : content.length;\n content = content.slice(0, insertAt) + `## Key Decisions\\n\\n${decisions}\\n\\n` + content.slice(insertAt);\n }\n }\n\n // Extract and add Known Issues if present\n const issuesMatch = summaryText.match(\n /## Known Issues\\n\\n([\\s\\S]*?)(?=\\n\\*\\*Tags|\\n$)/\n );\n if (issuesMatch) {\n const issues = issuesMatch[1].trim();\n if (issues && !content.includes(\"## Known Issues\")) {\n const nextStepsIdx = content.indexOf(\"## Next Steps\");\n const insertAt = nextStepsIdx !== -1 ? nextStepsIdx : content.length;\n content = content.slice(0, insertAt) + `## Known Issues\\n\\n${issues}\\n\\n` + content.slice(insertAt);\n }\n }\n\n writeFileSync(notePath, content, \"utf-8\");\n}\n\n/**\n * Create a brand new session note from the AI summary.\n */\n/**\n * Find the note this session already owns, by the marker its first write left.\n *\n * The previous resolver, getCurrentNotePath, returned the numerically highest\n * note in the month directory. That is a position, not an identity, and it is\n * why one session produced many notes: whenever a session's own note was not\n * the current maximum — two sessions in one project, or a number collision, of\n * which one corpus had 110 — the lookup missed and the checkpoint created a\n * new file instead of updating. Measured consequence in that corpus: 303 of\n * 407 notes sat in 31 same-title groups, 48 of them sharing a single title.\n *\n * Matching on the session id makes the answer exact: a session either has a\n * note or it does not, regardless of what else the directory contains.\n */\nfunction findNoteBySessionId(notesDir: string, sessionId: string): string | null {\n if (!sessionId || !existsSync(notesDir)) return null;\n const marker = `<!-- SESSION: ${sessionId} -->`;\n\n const now = new Date();\n const dirs = [0, -1].map((delta) => {\n const d = new Date(now.getFullYear(), now.getMonth() + delta, 1);\n return join(notesDir, String(d.getFullYear()),\n String(d.getMonth() + 1).padStart(2, \"0\"));\n });\n\n for (const dir of dirs) {\n if (!existsSync(dir)) continue;\n for (const f of readdirSync(dir)) {\n if (!f.endsWith(\".md\")) continue;\n const p = join(dir, f);\n try {\n if (readFileSync(p, \"utf-8\").includes(marker)) return p;\n } catch { /* unreadable file is not a match */ }\n }\n }\n return null;\n}\n\nfunction createNoteFromSummary(\n notesDir: string,\n summaryText: string,\n sessionId?: string,\n): string | null {\n try {\n // Create the note with a placeholder title\n const notePath = createSessionNote(notesDir, \"New Session\");\n\n // We will overwrite the entire content with the AI-generated summary, preserving\n // the note number (derived from the filename) and adding the standard footer.\n const noteFilename = basename(notePath);\n const numberMatch = noteFilename.match(/^(\\d+)/);\n const noteNumber = numberMatch ? numberMatch[1] : \"0000\";\n\n // Replace the H1 title from the AI summary with the numbered format\n const titleMatch = summaryText.match(/^# Session:\\s*(.+)$/m);\n const title = titleMatch ? titleMatch[1].trim() : \"New Session\";\n\n const date = new Date().toISOString().split(\"T\")[0];\n\n // Extract topic before stripping it (stored as HTML comment for future comparison)\n const topic = extractTopic(summaryText);\n\n // Build the final note content, merging AI output with the PAI note structure.\n // Strip the TOPIC: line (used for topic detection, not for the note body).\n const aiBody = summaryText\n .replace(/^TOPIC:.*$/m, \"\")\n .replace(/^# Session:.*$/m, \"\")\n .replace(/^\\*\\*Date:\\*\\*.*$/m, \"\")\n .replace(/^\\*\\*Status:\\*\\*.*$/m, \"\")\n .replace(/^---$/m, \"\")\n .trim();\n\n // The session marker is what later checkpoints of THIS session match on to\n // find and update this note rather than creating another one beside it.\n const finalContent = `# Session ${noteNumber}: ${title}\n${topic ? `<!-- TOPIC: ${topic} -->` : \"\"}${sessionId ? `\\n<!-- SESSION: ${sessionId} -->` : \"\"}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n${aiBody}\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n writeFileSync(notePath, finalContent, \"utf-8\");\n process.stderr.write(`[session-summary] Created AI-powered note: ${noteFilename}\\n`);\n return notePath;\n } catch (e) {\n process.stderr.write(`[session-summary] Failed to create note: ${e}\\n`);\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// KG triple extraction\n// ---------------------------------------------------------------------------\n\n/** Narrow cast for backends that expose a Postgres pool. */\ninterface BackendWithPool {\n getPool?(): import(\"pg\").Pool;\n}\n\n/**\n * Look up the integer project_id from the registry DB for a given slug.\n * Returns null if not found or registryDb is not yet initialized.\n */\nfunction lookupProjectId(slug: string): number | null {\n try {\n if (!registryDb) return null;\n const row = (registryDb as import(\"better-sqlite3\").Database)\n .prepare(\"SELECT id FROM projects WHERE slug = ? LIMIT 1\")\n .get(slug) as { id: number } | undefined;\n return row?.id ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Extract structured KG triples from a session summary and store them.\n *\n * This is best-effort: any error is logged but never propagated.\n * Requires Postgres backend — silently no-ops on SQLite.\n */\nasync function extractAndStoreTriples(params: {\n summaryText: string;\n projectSlug: string;\n projectId: number | null;\n sessionId: string;\n gitLog: string;\n model: string;\n}): Promise<void> {\n try {\n // Only works with Postgres backend — KG tables live in Postgres\n if (!storageBackend || storageBackend.backendType !== \"postgres\") {\n return;\n }\n\n const pool = (storageBackend as BackendWithPool).getPool?.();\n if (!pool) {\n process.stderr.write(\"[session-summary] Triple extraction: no pool available.\\n\");\n return;\n }\n\n // Check config flag (default: true)\n const cfg = daemonConfig as (typeof daemonConfig & { kg_extraction_enabled?: boolean }) | undefined;\n if (cfg && cfg.kg_extraction_enabled === false) {\n process.stderr.write(\"[session-summary] Triple extraction disabled via kg_extraction_enabled=false.\\n\");\n return;\n }\n\n const federationDb = openFederation();\n let result;\n try {\n result = await kgExtractAndStoreTriples(pool, {\n summaryText: params.summaryText,\n projectSlug: params.projectSlug,\n projectId: params.projectId,\n sessionId: params.sessionId,\n gitLog: params.gitLog,\n model: \"sonnet\",\n federationDb,\n });\n } finally {\n federationDb.close();\n }\n\n process.stderr.write(\n `[session-summary] Triple extraction complete: ` +\n `${result.extracted} extracted, ${result.added} added, ${result.superseded} superseded.\\n`\n );\n } catch (err) {\n // Entire extraction is best-effort — never fail the session summary\n process.stderr.write(`[session-summary] Triple extraction failed: ${err}\\n`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Process a `session-summary` work item.\n *\n * This is the main function called by work-queue-worker.ts.\n * Throws on fatal errors (work queue will retry with backoff).\n */\nexport async function handleSessionSummary(payload: SessionSummaryPayload): Promise<void> {\n const { cwd, sessionId, projectSlug, transcriptPath, force } = payload;\n\n if (!cwd) {\n throw new Error(\"session-summary payload missing cwd\");\n }\n\n process.stderr.write(\n `[session-summary] Starting for ${cwd}` +\n `${sessionId ? ` (session=${sessionId})` : \"\"}` +\n `${force ? \" (force=true)\" : \"\"}\\n`\n );\n\n // -------------------------------------------------------------------------\n // Cooldown check — don't summarize too frequently\n // force=true bypasses the cooldown (used by stop-hook at session end so a\n // final summary is always produced regardless of recent PreCompact runs).\n // -------------------------------------------------------------------------\n if (!force && isOnCooldown(cwd)) {\n process.stderr.write(\n \"[session-summary] Skipping — last summary was less than 30 minutes ago.\\n\"\n );\n return;\n }\n\n // -------------------------------------------------------------------------\n // Step 1: Find the JSONL transcript\n // -------------------------------------------------------------------------\n let jsonlPath: string | null = transcriptPath || null;\n\n if (jsonlPath && !existsSync(jsonlPath)) {\n process.stderr.write(\n `[session-summary] Provided transcript path not found: ${jsonlPath}\\n`\n );\n jsonlPath = null;\n }\n\n if (!jsonlPath) {\n jsonlPath = findLatestJsonl(cwd);\n }\n\n if (!jsonlPath) {\n process.stderr.write(\n \"[session-summary] No JSONL transcript found — skipping.\\n\"\n );\n return;\n }\n\n process.stderr.write(`[session-summary] Using transcript: ${jsonlPath}\\n`);\n\n // -------------------------------------------------------------------------\n // Step 2: Extract content from the JSONL\n // -------------------------------------------------------------------------\n // Model selection: opus for session end (force=true), sonnet for auto-compact\n // `force` means \"skip the cooldown\" and nothing else. It used to also select\n // opus, on the assumption that a forced run was the one final session-end\n // summary. It is not: session end is handled mechanically by handleSessionEnd\n // and never reaches here. The only thing that sets force is the stop-hook's\n // mid-session auto-save, which fires every AUTO_SAVE_INTERVAL human messages\n // — so the most expensive model was running over a full transcript every 15\n // messages, in every open session. Measured: 102 opus vs 5 sonnet in 16h.\n //\n // Incremental checkpoints are a summarisation task with a clear input; haiku\n // is sufficient. Callers wanting better can still pass `model` explicitly.\n const selectedModel = payload.model ?? (force ? \"haiku\" : \"sonnet\");\n const extracted = extractFromJsonl(jsonlPath, selectedModel);\n\n if (extracted.userMessages.length === 0) {\n process.stderr.write(\n \"[session-summary] No user messages found in transcript — skipping.\\n\"\n );\n return;\n }\n\n process.stderr.write(\n `[session-summary] Extracted ${extracted.userMessages.length} user messages, ` +\n `${extracted.filesModified.length} modified files.\\n`\n );\n\n // -------------------------------------------------------------------------\n // Step 3: Get git context\n // -------------------------------------------------------------------------\n const gitLog = await getGitContext(cwd, extracted.sessionStartTime);\n\n if (gitLog) {\n process.stderr.write(\n `[session-summary] Got git context (${gitLog.split(\"\\n\").length} lines).\\n`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 4: Build and send prompt to summarizer\n // -------------------------------------------------------------------------\n const today = new Date().toISOString().split(\"T\")[0];\n\n // Check for existing note to merge with\n const notesInfo = findNotesDir(cwd);\n const existingNotePath = getCurrentNotePath(notesInfo.path);\n let existingNote: string | undefined;\n\n if (existingNotePath) {\n const noteFilename = basename(existingNotePath);\n const dateMatch = noteFilename.match(/(\\d{4}-\\d{2}-\\d{2})/);\n if (dateMatch && dateMatch[1] === today) {\n try {\n existingNote = readFileSync(existingNotePath, \"utf-8\");\n } catch { /* ignore */ }\n }\n }\n\n const prompt = buildSessionSummaryPrompt({\n userMessages: extracted.userMessages,\n gitLog,\n cwd,\n date: today,\n filesModified: extracted.filesModified,\n existingNote,\n });\n\n process.stderr.write(\n `[session-summary] Sending ${prompt.length} char prompt to ${selectedModel}...\\n`\n );\n\n const summaryText = await spawnSummarizer(prompt, selectedModel);\n\n if (!summaryText) {\n process.stderr.write(\n `[session-summary] ${selectedModel} did not produce output — falling back to mechanical checkpoint.\\n`\n );\n // Don't throw — this is a soft failure. The existing PreCompact checkpoint\n // is sufficient. Just mark the cooldown so we don't retry too soon.\n markCooldown(cwd);\n return;\n }\n\n process.stderr.write(\n `[session-summary] ${selectedModel} produced ${summaryText.length} char summary.\\n`\n );\n\n // -------------------------------------------------------------------------\n // Step 5: Write the session note\n // -------------------------------------------------------------------------\n const notePath = writeSessionNote(cwd, summaryText, extracted.filesModified, sessionId);\n\n if (notePath) {\n process.stderr.write(\n `[session-summary] Session note written: ${basename(notePath)}\\n`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 6: Best-effort KG triple extraction (Postgres only)\n // -------------------------------------------------------------------------\n const effectiveSlug = projectSlug ?? basename(cwd);\n const projectId = projectSlug ? lookupProjectId(projectSlug) : null;\n await extractAndStoreTriples({\n summaryText,\n projectSlug: effectiveSlug,\n projectId,\n sessionId: sessionId ?? cwd,\n gitLog,\n model: selectedModel,\n });\n\n // Mark cooldown\n markCooldown(cwd);\n\n process.stderr.write(\"[session-summary] Done.\\n\");\n}\n","/**\n * 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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"}
|
|
@@ -3,9 +3,9 @@ import "./migrate-Cjzeefn9.mjs";
|
|
|
3
3
|
import "./db-a1ixZQjr.mjs";
|
|
4
4
|
import "./embeddings-DOLZnT1X.mjs";
|
|
5
5
|
import "./search-Rpk1cSBC.mjs";
|
|
6
|
-
import "./
|
|
6
|
+
import "./context-handover-cache-PtNvj_8D.mjs";
|
|
7
7
|
import "./state-WaXhLr6R.mjs";
|
|
8
8
|
import "./detector--Gg5JRN5.mjs";
|
|
9
|
-
import { i as stopWorker, n as notifyNewWork, r as startWorker, t as enqueueRegistryScan } from "./work-queue-worker-
|
|
9
|
+
import { i as stopWorker, n as notifyNewWork, r as startWorker, t as enqueueRegistryScan } from "./work-queue-worker-B8W8_3Rn.mjs";
|
|
10
10
|
|
|
11
11
|
export { enqueueRegistryScan };
|