@tekmidian/pai 0.66.3 → 0.66.4
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/{chain-Fz4NLm4D.mjs → chain-BhPwBDXB.mjs} +2 -2
- package/dist/{chain-Fz4NLm4D.mjs.map → chain-BhPwBDXB.mjs.map} +1 -1
- package/dist/cli/index.mjs +8 -8
- package/dist/cli/program.mjs +8 -8
- package/dist/{context-handover-cache-DcPEW6vf.mjs → context-handover-cache-BbCYQUGo.mjs} +3 -3
- package/dist/{context-handover-cache-DcPEW6vf.mjs.map → context-handover-cache-BbCYQUGo.mjs.map} +1 -1
- package/dist/daemon/index.mjs +5 -5
- package/dist/{daemon-Bl8TpHdM.mjs → daemon-DzXOfieK.mjs} +5 -5
- package/dist/{daemon-Bl8TpHdM.mjs.map → daemon-DzXOfieK.mjs.map} +1 -1
- package/dist/{daemon-ZhHU6D0i.mjs → daemon-UJtRscyO.mjs} +5 -5
- package/dist/daemon-mcp/index.mjs +5 -5
- package/dist/{factory-BUnZgnKW.mjs → factory-CXFHwvcv.mjs} +2 -2
- package/dist/{factory-BUnZgnKW.mjs.map → factory-CXFHwvcv.mjs.map} +1 -1
- package/dist/{factory-CrVlo2hY.mjs → factory-CnKsU6zX.mjs} +2 -2
- package/dist/{fallback-awOmq_9A.mjs → fallback-CoooFqyA.mjs} +2 -2
- package/dist/{fallback-awOmq_9A.mjs.map → fallback-CoooFqyA.mjs.map} +1 -1
- package/dist/hooks/load-project-context.mjs.map +2 -2
- package/dist/hooks/security-validator.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs.map +2 -2
- package/dist/{main-resolver-DDShdDBm.mjs → main-resolver-DrHtx8iW.mjs} +1 -1
- package/dist/{main-resolver-CVGPICy-.mjs → main-resolver-SfGGg1DB.mjs} +3 -3
- package/dist/{main-resolver-CVGPICy-.mjs.map → main-resolver-SfGGg1DB.mjs.map} +1 -1
- package/dist/{planner-GdiYe2FO.mjs → planner-C43lzcw5.mjs} +5 -5
- package/dist/{planner-GdiYe2FO.mjs.map → planner-C43lzcw5.mjs.map} +1 -1
- package/dist/postgres-B2BEBT-J.mjs +5 -0
- package/dist/{postgres-H85VSYHI.mjs → postgres-BxmyQHoA.mjs} +52 -4
- package/dist/postgres-BxmyQHoA.mjs.map +1 -0
- package/dist/{program-D3MSfx86.mjs → program-DB-hoyfW.mjs} +38 -121
- package/dist/program-DB-hoyfW.mjs.map +1 -0
- package/dist/{run-D9YbWsnL.mjs → run-wi5NEkJd.mjs} +116 -7
- package/dist/run-wi5NEkJd.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/postgres-DKyiCA5V.mjs +0 -5
- package/dist/postgres-H85VSYHI.mjs.map +0 -1
- package/dist/program-D3MSfx86.mjs.map +0 -1
- package/dist/run-D9YbWsnL.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory-BUnZgnKW.mjs","names":["runMigrations"],"sources":["../src/storage/outage.ts","../src/storage/sqlite/federation-schema.ts","../src/storage/sqlite/federation-db.ts","../src/storage/sqlite/indexer.ts","../src/storage/sqlite/tunnels.ts","../src/storage/sqlite/sources.ts","../src/storage/sqlite/search.ts","../src/storage/sqlite/kg-entity.ts","../src/storage/sqlite.ts","../src/registry/merge.ts","../src/storage/registry-postgres.ts","../src/storage/sqlite/registry-schema.ts","../src/storage/sqlite/registry-db.ts","../src/storage/sqlite/registry-merge.ts","../src/storage/registry-sqlite.ts","../src/storage/factory.ts"],"sourcesContent":["/**\n * outage.ts — is the storage backend actually reachable right now?\n *\n * The daemon retries a dead backend forever, which is correct: a Postgres\n * container that is down will usually come back, and giving up would lose the\n * work queue. What was wrong is that it did so in complete silence.\n *\n * Observed 2026-07-26: the container was down for roughly two days. The daemon\n * logged \"Postgres unavailable\" 144 times over 36 minutes, the work queue backed\n * up, session notes for the whole period were never written — and\n * `pai daemon status` reported \"Index: idle\" throughout. The one command anyone\n * would run to check said everything was fine.\n *\n * So the outage is recorded where the status command can see it, and escalated\n * once through the notification channels that were already configured and\n * already unused for this.\n */\n\nexport interface BackendOutage {\n backend: string;\n /** When the current run of failures began. */\n since: number;\n /** Consecutive failed attempts so far. */\n attempts: number;\n lastError: string;\n}\n\nlet current: BackendOutage | null = null;\n\n/** Record that the backend is currently unreachable. */\nexport function setBackendOutage(outage: BackendOutage): void {\n current = outage;\n}\n\n/**\n * Record that the backend answered.\n *\n * Called on every successful connection, including the first — so a daemon that\n * never had a problem reports none, and one that recovered stops reporting an\n * outage that has ended.\n */\nexport function clearBackendOutage(): void {\n current = null;\n}\n\n/** The current outage, or null when the backend is answering. */\nexport function getBackendOutage(): BackendOutage | null {\n return current;\n}\n\n/**\n * Human-readable elapsed time, for a status line rather than a log.\n *\n * Exported because the status command needs the duration on its own, to colour\n * it separately from the rest of the sentence. Without this it kept a second\n * copy of the same rounding, which is how two renderings of one outage drift.\n */\nexport function humanDuration(ms: number): string {\n const mins = Math.max(1, Math.round(ms / 60_000));\n return mins < 60 ? `${mins} min` : `${(mins / 60).toFixed(1)} h`;\n}\n\n/** Human-readable duration, for a status line rather than a log. */\nexport function describeOutage(o: BackendOutage, now = Date.now()): string {\n return `${o.backend} unreachable for ${humanDuration(now - o.since)}, ${o.attempts} attempts — ${o.lastError}`;\n}\n","/**\n * SQLite DDL for the PAI federation database (federation.db).\n *\n * The federation DB is the cross-project search index — a single SQLite file\n * at ~/.pai/federation.db that holds chunked text from every registered\n * project's memory/ and Notes/ directories.\n *\n * Tables:\n * - memory_files — file-level metadata (hash, mtime, size) for change detection\n * - memory_chunks — chunked text with line numbers, tier classification, and optional embedding\n * - memory_fts — FTS5 virtual table backed by memory_chunks text\n *\n * Vault tables (vault_files, vault_aliases, vault_links, vault_name_index, vault_health)\n * have been migrated to Postgres (docker/init.sql) and are no longer created here.\n *\n * Schema version history:\n * v1 — initial schema (BM25 search only)\n * v2 — added embedding BLOB column to memory_chunks (Phase 2.5, vector search)\n * v3 — added vault tables (now removed — vault tables live in Postgres)\n */\n\nimport type { Database } from \"better-sqlite3\";\n\n/** Current schema version. Bump when adding new columns or tables. */\nexport const SCHEMA_VERSION = 5;\n\nexport const FEDERATION_SCHEMA_SQL = `\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS memory_files (\n project_id INTEGER NOT NULL,\n path TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n hash TEXT NOT NULL,\n mtime INTEGER NOT NULL,\n size INTEGER NOT NULL,\n PRIMARY KEY (project_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS memory_chunks (\n id TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n path TEXT NOT NULL,\n start_line INTEGER NOT NULL,\n end_line INTEGER NOT NULL,\n hash TEXT NOT NULL,\n text TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n last_accessed_at INTEGER,\n relevance_score REAL DEFAULT 0.5,\n embedding BLOB\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(\n text,\n id UNINDEXED,\n project_id UNINDEXED,\n path UNINDEXED,\n source UNINDEXED,\n tier UNINDEXED,\n start_line UNINDEXED,\n end_line UNINDEXED\n);\n\nCREATE INDEX IF NOT EXISTS idx_mc_project ON memory_chunks(project_id);\nCREATE INDEX IF NOT EXISTS idx_mc_source ON memory_chunks(project_id, source);\nCREATE INDEX IF NOT EXISTS idx_mc_tier ON memory_chunks(tier);\nCREATE INDEX IF NOT EXISTS idx_mf_project ON memory_files(project_id);\n\nCREATE TABLE IF NOT EXISTS kg_entities (\n entity_id TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL DEFAULT 'default',\n name TEXT NOT NULL,\n type TEXT NOT NULL DEFAULT 'unknown',\n description TEXT,\n first_seen INTEGER,\n last_seen INTEGER,\n mention_count INTEGER NOT NULL DEFAULT 1,\n feedback_weight REAL NOT NULL DEFAULT 0.5,\n UNIQUE(tenant_id, entity_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_kge_tenant ON kg_entities(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_kge_name ON kg_entities(tenant_id, name);\nCREATE INDEX IF NOT EXISTS idx_kge_type ON kg_entities(tenant_id, type);\n`;\n\n/**\n * Apply the full federation schema to an open database.\n *\n * Idempotent — all statements use IF NOT EXISTS so calling this on an\n * already-initialised database is safe.\n *\n * Also runs any necessary migrations for existing databases (e.g. adding the\n * embedding column to an older schema that was created without it).\n */\nexport function initializeFederationSchema(db: Database): void {\n db.exec(FEDERATION_SCHEMA_SQL);\n runMigrations(db);\n}\n\n// ---------------------------------------------------------------------------\n// Migrations\n// ---------------------------------------------------------------------------\n\n/**\n * Apply incremental migrations to an existing database.\n *\n * Each migration is idempotent — safe to call on a database that has already\n * been migrated.\n */\nfunction runMigrations(db: Database): void {\n const columns = db.prepare(\"PRAGMA table_info(memory_chunks)\").all() as Array<{\n name: string;\n }>;\n\n // Migration v1→v2: add embedding BLOB column (schema v2, Phase 2.5)\n const hasEmbedding = columns.some((c) => c.name === \"embedding\");\n if (!hasEmbedding) {\n db.exec(\"ALTER TABLE memory_chunks ADD COLUMN embedding BLOB\");\n }\n\n // Create the partial index for embedded chunks (safe now that the column exists)\n db.exec(\n \"CREATE INDEX IF NOT EXISTS idx_mc_embedding ON memory_chunks(id) WHERE embedding IS NOT NULL\",\n );\n\n // Migration v4→v5: add last_accessed_at and relevance_score columns (QW2 + MR2)\n const hasLastAccessedAt = columns.some((c) => c.name === \"last_accessed_at\");\n if (!hasLastAccessedAt) {\n db.exec(\"ALTER TABLE memory_chunks ADD COLUMN last_accessed_at INTEGER\");\n }\n\n const hasRelevanceScore = columns.some((c) => c.name === \"relevance_score\");\n if (!hasRelevanceScore) {\n db.exec(\"ALTER TABLE memory_chunks ADD COLUMN relevance_score REAL DEFAULT 0.5\");\n }\n}\n","/**\n * Database connection helper for the PAI federation DB.\n *\n * Uses better-sqlite3 (synchronous API) to open or create federation.db.\n * On first open it runs the full DDL via initializeFederationSchema().\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport BetterSqlite3 from \"better-sqlite3\";\nimport type { Database } from \"better-sqlite3\";\nimport { initializeFederationSchema } from \"./federation-schema.js\";\nimport { paiHomePath, resolvePaiFile } from \"../../config/pai-home.js\";\n\nexport type { Database };\n\n/** Old federation.db path, inside the ~/.pai/ directory (pre-2026-09-19). */\nexport function oldFederationPath(): string {\n return join(homedir(), \".pai\", \"federation.db\");\n}\n\n/** Federation DB path: PAI_HOME/federation.db if present, else the old\n * ~/.pai/federation.db (one-time stderr notice), else the new path. */\nexport function federationDbPath(): string {\n return resolvePaiFile(paiHomePath(\"federation.db\"), [oldFederationPath()], \"pai config migrate --federation\");\n}\n\n/**\n * Open (or create) the PAI federation database.\n *\n * @param path Absolute path to federation.db. Defaults to PAI_HOME/federation.db\n * (falling back to the pre-2026-09-19 ~/.pai/federation.db).\n * @returns An open better-sqlite3 Database instance.\n *\n * Side effects on first call:\n * - Creates the parent directory if it does not exist.\n * - Enables WAL journal mode.\n * - Runs initializeFederationSchema() to ensure tables exist.\n */\nexport function openFederation(path: string = federationDbPath()): Database {\n // Ensure the directory exists before SQLite tries to create the file\n mkdirSync(dirname(path), { recursive: true });\n\n const db = new BetterSqlite3(path);\n\n // WAL gives better concurrent read performance and crash safety\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"foreign_keys = ON\");\n\n // Apply schema (idempotent — all statements use IF NOT EXISTS)\n initializeFederationSchema(db);\n\n return db;\n}\n","/**\n * Synchronous (SQLite) indexer — moved from memory/indexer/sync.ts so the\n * only file that opens prepared statements against the federation database\n * lives under src/storage/ (design doc docs/design/postgres-only.md, unit 7).\n *\n * Scans project memory/ and Notes/ directories, chunks markdown files, and\n * inserts the resulting chunks into the federation memory store for BM25\n * search. Uses raw better-sqlite3 Database directly for maximum SQLite\n * performance (synchronous transactions, no serialisation overhead) — the\n * reason SQLiteBackend keeps this path instead of routing through the\n * generic (StorageBackend) indexer that Postgres uses.\n */\n\nimport { readFileSync, statSync, existsSync } from \"node:fs\";\nimport { join, relative, basename } from \"node:path\";\nimport type { Database } from \"better-sqlite3\";\nimport type { RegistryBackend } from \"../registry-interface.js\";\nimport { chunkMarkdown, fileContentHash } from \"../../memory/chunker.js\";\nimport {\n sha256File,\n chunkId,\n detectTier,\n walkMdFiles,\n walkContentFiles,\n isPathTooBroadForContentScan,\n parseSessionTitleChunk,\n yieldToEventLoop,\n INDEX_YIELD_EVERY,\n} from \"../../memory/indexer/helpers.js\";\nimport type { IndexResult } from \"../../memory/indexer/types.js\";\n\n// ---------------------------------------------------------------------------\n// Single-file indexing\n// ---------------------------------------------------------------------------\n\n/**\n * Index a single file into the federation database.\n *\n * @returns true if the file was re-indexed (changed or new), false if skipped.\n */\nfunction indexFile(\n db: Database,\n projectId: number,\n rootPath: string,\n relativePath: string,\n source: string,\n tier: string,\n): boolean {\n const absPath = join(rootPath, relativePath);\n\n let content: string;\n let stat: ReturnType<typeof statSync>;\n try {\n content = readFileSync(absPath, \"utf8\");\n stat = statSync(absPath);\n } catch {\n // File unreadable or missing — skip silently\n return false;\n }\n\n const hash = fileContentHash(content);\n const mtime = Math.floor(stat.mtimeMs);\n const size = stat.size;\n\n const existing = db\n .prepare(\"SELECT hash FROM memory_files WHERE project_id = ? AND path = ?\")\n .get(projectId, relativePath) as { hash: string } | undefined;\n\n if (existing?.hash === hash) {\n return false;\n }\n\n const oldChunkIds = db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, relativePath) as Array<{ id: string }>;\n\n const deleteFts = db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunk = db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\",\n );\n\n db.transaction(() => {\n for (const row of oldChunkIds) {\n deleteFts.run(row.id);\n }\n deleteChunk.run(projectId, relativePath);\n })();\n\n const chunks = chunkMarkdown(content);\n\n const insertChunk = db.prepare(`\n INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n const insertFts = db.prepare(`\n INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n const upsertFile = db.prepare(`\n INSERT INTO memory_files (project_id, path, source, tier, hash, mtime, size)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(project_id, path) DO UPDATE SET\n source = excluded.source,\n tier = excluded.tier,\n hash = excluded.hash,\n mtime = excluded.mtime,\n size = excluded.size\n `);\n\n const updatedAt = Date.now();\n\n db.transaction(() => {\n for (let i = 0; i < chunks.length; i++) {\n const chunk = chunks[i]!;\n const id = chunkId(projectId, relativePath, i, chunk.startLine, chunk.endLine);\n insertChunk.run(\n id, projectId, source, tier, relativePath,\n chunk.startLine, chunk.endLine, chunk.hash, chunk.text, updatedAt,\n );\n insertFts.run(\n chunk.text, id, projectId, relativePath, source, tier,\n chunk.startLine, chunk.endLine,\n );\n }\n upsertFile.run(projectId, relativePath, source, tier, hash, mtime, size);\n })();\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Project-level indexing\n// ---------------------------------------------------------------------------\n\n/**\n * Index all memory, Notes, and content files for a single registered project.\n */\nasync function indexProject(\n db: Database,\n projectId: number,\n rootPath: string,\n claudeNotesDir?: string | null,\n): Promise<IndexResult> {\n const result: IndexResult = {\n filesProcessed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n };\n\n const filesToIndex: Array<{ absPath: string; rootBase: string; source: string; tier: string }> = [];\n\n const rootMemoryMd = join(rootPath, \"MEMORY.md\");\n if (existsSync(rootMemoryMd)) {\n filesToIndex.push({ absPath: rootMemoryMd, rootBase: rootPath, source: \"memory\", tier: \"evergreen\" });\n }\n\n const memoryDir = join(rootPath, \"memory\");\n for (const absPath of walkMdFiles(memoryDir)) {\n const relPath = relative(rootPath, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"memory\", tier });\n }\n\n const notesDir = join(rootPath, \"Notes\");\n for (const absPath of walkMdFiles(notesDir)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"notes\", tier: \"session\" });\n }\n\n // Synthetic session-title chunks for Notes files with the standard filename format:\n // \"NNNN - YYYY-MM-DD - Descriptive Title.md\"\n {\n const titleInsertChunk = db.prepare(`\n INSERT OR IGNORE INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n const titleInsertFts = db.prepare(`\n INSERT OR IGNORE INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n const updatedAt = Date.now();\n for (const absPath of walkMdFiles(notesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(rootPath, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n db.transaction(() => {\n titleInsertChunk.run(id, projectId, \"notes\", \"session\", syntheticPath, 0, 0, hash, text, updatedAt);\n titleInsertFts.run(text, id, projectId, syntheticPath, \"notes\", \"session\", 0, 0);\n })();\n }\n }\n\n if (!isPathTooBroadForContentScan(rootPath)) {\n for (const absPath of walkContentFiles(rootPath)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"content\", tier: \"topic\" });\n }\n }\n\n if (claudeNotesDir && claudeNotesDir !== notesDir) {\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n filesToIndex.push({ absPath, rootBase: claudeNotesDir, source: \"notes\", tier: \"session\" });\n }\n\n {\n const updatedAt = Date.now();\n const titleInsertChunk2 = db.prepare(`\n INSERT OR IGNORE INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n const titleInsertFts2 = db.prepare(`\n INSERT OR IGNORE INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(claudeNotesDir, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n db.transaction(() => {\n titleInsertChunk2.run(id, projectId, \"notes\", \"session\", syntheticPath, 0, 0, hash, text, updatedAt);\n titleInsertFts2.run(text, id, projectId, syntheticPath, \"notes\", \"session\", 0, 0);\n })();\n }\n }\n\n if (claudeNotesDir.endsWith(\"/Notes\")) {\n const claudeProjectDir = claudeNotesDir.slice(0, -\"/Notes\".length);\n const claudeMemoryDir = join(claudeProjectDir, \"memory\");\n\n const claudeMemoryMd = join(claudeProjectDir, \"MEMORY.md\");\n if (existsSync(claudeMemoryMd)) {\n filesToIndex.push({\n absPath: claudeMemoryMd,\n rootBase: claudeProjectDir,\n source: \"memory\",\n tier: \"evergreen\",\n });\n }\n\n for (const absPath of walkMdFiles(claudeMemoryDir)) {\n const relPath = relative(claudeProjectDir, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: claudeProjectDir, source: \"memory\", tier });\n }\n }\n }\n\n await yieldToEventLoop();\n\n let filesSinceYield = 0;\n\n for (const { absPath, rootBase, source, tier } of filesToIndex) {\n if (filesSinceYield >= INDEX_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const relPath = relative(rootBase, absPath);\n const changed = indexFile(db, projectId, rootBase, relPath, source, tier);\n\n if (changed) {\n const count = db\n .prepare(\"SELECT COUNT(*) as n FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .get(projectId, relPath) as { n: number };\n\n result.filesProcessed++;\n result.chunksCreated += count.n;\n } else {\n result.filesSkipped++;\n }\n }\n\n // Prune stale paths: remove DB entries for files that no longer exist on disk.\n const livePaths = new Set<string>();\n for (const { absPath, rootBase } of filesToIndex) {\n livePaths.add(relative(rootBase, absPath));\n }\n\n const dbChunkPaths = db\n .prepare(\"SELECT DISTINCT path FROM memory_chunks WHERE project_id = ?\")\n .all(projectId) as Array<{ path: string }>;\n\n const stalePaths: string[] = [];\n for (const row of dbChunkPaths) {\n const basePath = row.path.endsWith(\"::title\")\n ? row.path.slice(0, -\"::title\".length)\n : row.path;\n if (!livePaths.has(basePath)) {\n stalePaths.push(row.path);\n }\n }\n\n if (stalePaths.length > 0) {\n const deleteChunksFts = db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\",\n );\n const deleteFile = db.prepare(\n \"DELETE FROM memory_files WHERE project_id = ? AND path = ?\",\n );\n\n db.transaction(() => {\n for (const stalePath of stalePaths) {\n const chunkIds = db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, stalePath) as Array<{ id: string }>;\n for (const { id } of chunkIds) {\n deleteChunksFts.run(id);\n }\n deleteChunks.run(projectId, stalePath);\n deleteFile.run(projectId, stalePath);\n }\n })();\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Global indexing (all registered projects)\n// ---------------------------------------------------------------------------\n\n/**\n * Index all active projects registered in the registry DB.\n *\n * Async: yields to the event loop between each project so that the daemon's\n * Unix socket server can process IPC requests (e.g. status) while indexing.\n */\nexport async function indexAllSqlite(\n db: Database,\n registry: RegistryBackend,\n): Promise<{ projects: number; result: IndexResult }> {\n const projects = await registry.listProjects({ status: \"active\" });\n\n const totals: IndexResult = {\n filesProcessed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n };\n\n for (const project of projects) {\n await yieldToEventLoop();\n const r = await indexProject(db, project.id, project.root_path, project.claude_notes_dir);\n totals.filesProcessed += r.filesProcessed;\n totals.chunksCreated += r.chunksCreated;\n totals.filesSkipped += r.filesSkipped;\n }\n\n return { projects: projects.length, result: totals };\n}\n","/**\n * SQLite implementation of cross-project concept \"tunnel\" detection — moved\n * from memory/tunnels.ts so the only file that runs raw FTS5/vocab SQL\n * against the federation database lives under src/storage/.\n *\n * Algorithm: pull candidate terms from the FTS5 vocabulary shadow table (or\n * sample chunk text when the vocab view is unavailable), then for each\n * candidate count distinct projects and total occurrences.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { STOP_WORDS } from \"../../utils/stop-words.js\";\nimport type { RegistryBackend } from \"../registry-interface.js\";\nimport type { Tunnel, FindTunnelsOptions, FindTunnelsResult } from \"../../memory/tunnels.js\";\n\ninterface ProjectSlugMap {\n [id: number]: string;\n}\n\nasync function findTunnelsForSlugMap(\n db: Database,\n slugMap: ProjectSlugMap,\n opts: Required<FindTunnelsOptions>,\n): Promise<FindTunnelsResult> {\n const projectIds = Object.keys(slugMap).map(Number);\n if (projectIds.length < 2) {\n return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n }\n\n // Step 1 — collect candidate terms via FTS5 vocabulary shadow table.\n let candidateTerms: string[] = [];\n\n try {\n const vocabRows = db\n .prepare(\n `SELECT term, SUM(doc) AS doc_count, SUM(cnt) AS total_cnt\n FROM memory_fts_v\n GROUP BY term\n HAVING SUM(cnt) >= ?\n ORDER BY SUM(doc) DESC\n LIMIT 500`\n )\n .all(opts.min_occurrences) as Array<{ term: string; doc_count: number; total_cnt: number }>;\n\n candidateTerms = vocabRows\n .map((r) => r.term)\n .filter((t) => t.length >= 3 && !STOP_WORDS.has(t));\n } catch {\n // Vocab table not available — fall back to sampling common words from chunks.\n const sampleRows = db\n .prepare(\n `SELECT LOWER(text) AS text FROM memory_chunks\n WHERE LENGTH(text) > 20\n ORDER BY RANDOM()\n LIMIT 2000`\n )\n .all() as Array<{ text: string }>;\n\n const freq = new Map<string, number>();\n for (const { text } of sampleRows) {\n const tokens = text\n .split(/[\\s\\p{P}]+/u)\n .filter(Boolean)\n .filter((t: string) => t.length >= 3 && !STOP_WORDS.has(t));\n for (const t of tokens) {\n freq.set(t, (freq.get(t) ?? 0) + 1);\n }\n }\n candidateTerms = [...freq.entries()]\n .filter(([, n]) => n >= opts.min_occurrences)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 200)\n .map(([t]) => t);\n }\n\n if (candidateTerms.length === 0) {\n return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n }\n\n // Step 2 — for each candidate, count distinct projects and occurrences.\n const tunnels: Tunnel[] = [];\n\n for (const term of candidateTerms) {\n try {\n const rows = db\n .prepare(\n `SELECT c.project_id, COUNT(*) AS cnt,\n MIN(c.updated_at) AS first_seen,\n MAX(c.updated_at) AS last_seen\n FROM memory_fts f\n JOIN memory_chunks c ON c.id = f.id\n WHERE memory_fts MATCH ?\n AND c.project_id IN (${projectIds.map(() => \"?\").join(\", \")})\n GROUP BY c.project_id`\n )\n .all(`\"${term.replace(/\"/g, '\"\"')}\"`, ...projectIds) as Array<{\n project_id: number;\n cnt: number;\n first_seen: number;\n last_seen: number;\n }>;\n\n if (rows.length < opts.min_projects) continue;\n\n const totalOccurrences = rows.reduce((s, r) => s + Number(r.cnt), 0);\n if (totalOccurrences < opts.min_occurrences) continue;\n\n const projects = rows\n .map((r) => slugMap[r.project_id] ?? String(r.project_id))\n .filter(Boolean);\n const firstSeen = Math.min(...rows.map((r) => r.first_seen));\n const lastSeen = Math.max(...rows.map((r) => r.last_seen));\n\n tunnels.push({\n concept: term,\n projects,\n occurrences: totalOccurrences,\n first_seen: firstSeen,\n last_seen: lastSeen,\n });\n } catch {\n // Skip problematic terms\n continue;\n }\n }\n\n tunnels.sort((a, b) => {\n const byProjects = b.projects.length - a.projects.length;\n if (byProjects !== 0) return byProjects;\n return b.occurrences - a.occurrences;\n });\n\n return {\n tunnels: tunnels.slice(0, opts.limit),\n projects_analyzed: projectIds.length,\n total_concepts_evaluated: candidateTerms.length,\n };\n}\n\n/** Find cross-project concept tunnels against a SQLite federation database. */\nexport async function findTunnelsSqlite(\n db: Database,\n registry: RegistryBackend,\n options?: FindTunnelsOptions,\n): Promise<FindTunnelsResult> {\n const opts: Required<FindTunnelsOptions> = {\n min_projects: options?.min_projects ?? 2,\n min_occurrences: options?.min_occurrences ?? 3,\n limit: options?.limit ?? 20,\n };\n\n const projectRows = await registry.listProjects({ excludeArchived: true });\n const slugMap: ProjectSlugMap = {};\n for (const { id, slug } of projectRows) {\n slugMap[id] = slug;\n }\n\n return findTunnelsForSlugMap(db, slugMap, opts);\n}\n","/**\n * SQLite implementation of the `pai memory sources` aggregate report — moved\n * from cli/commands/memory/sources.ts so the only file running ad-hoc\n * aggregate SQL against the federation database lives under src/storage/.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { MemorySourcesReport } from \"../interface.js\";\n\nexport function getMemorySourcesReportSqlite(db: Database): MemorySourcesReport {\n const composition = db\n .prepare(\n `SELECT source, tier, COUNT(*) AS chunks,\n SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS embedded\n FROM memory_chunks GROUP BY source, tier ORDER BY COUNT(*) DESC`\n )\n .all() as Array<{ source: string; tier: string; chunks: number; embedded: number }>;\n\n const paths = db\n .prepare(`SELECT path, COUNT(*) AS chunks FROM memory_chunks GROUP BY path`)\n .all() as Array<{ path: string; chunks: number }>;\n\n const churn = db\n .prepare(\n `SELECT date(updated_at/1000,'unixepoch') AS day, COUNT(*) AS chunks,\n SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS embedded\n FROM memory_chunks GROUP BY day ORDER BY day DESC LIMIT 10`\n )\n .all() as Array<{ day: string; chunks: number; embedded: number }>;\n\n return { composition, paths, churn };\n}\n","/**\n * SQLite search implementation — moved from memory/search.ts so the only\n * file running raw FTS5/vector queries against the federation database\n * lives under src/storage/. Query building, slug population, and recency\n * boosting are pure logic with no SQL and stay in memory/search.ts.\n *\n * BM25 uses SQLite's FTS5 extension. Semantic search requires embeddings to\n * have been generated first via the indexer's embed pass.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { deserializeEmbedding, cosineSimilarity } from \"../../memory/embeddings.js\";\nimport {\n buildFtsQuery,\n isQuerySyntaxError,\n type SearchResult,\n type SearchOptions,\n} from \"../../memory/search.js\";\n\n// ---------------------------------------------------------------------------\n// Keyword (BM25) search\n// ---------------------------------------------------------------------------\n\n/**\n * Search across all indexed memory using FTS5 BM25 ranking.\n *\n * Results are ordered by BM25 score (most relevant first).\n * FTS5 bm25() returns negative values; closer to 0 = more relevant.\n * We negate the score so callers get positive values where higher = better.\n */\nexport function searchMemory(\n db: Database,\n query: string,\n opts?: SearchOptions,\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n const ftsQuery = buildFtsQuery(query);\n\n const conditions: string[] = [];\n const params: (string | number)[] = [ftsQuery];\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => \"?\").join(\", \");\n conditions.push(`c.project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => \"?\").join(\", \");\n conditions.push(`c.source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => \"?\").join(\", \");\n conditions.push(`c.tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n const whereClause = conditions.length > 0\n ? \"AND \" + conditions.join(\" AND \")\n : \"\";\n\n params.push(maxResults);\n\n const sql = `\n SELECT\n c.id,\n c.project_id,\n c.path,\n c.start_line,\n c.end_line,\n c.text AS snippet,\n c.tier,\n c.source,\n c.updated_at,\n c.last_accessed_at,\n c.relevance_score,\n bm25(memory_fts) AS bm25_score\n FROM memory_fts\n JOIN memory_chunks c ON memory_fts.id = c.id\n WHERE memory_fts MATCH ?\n ${whereClause}\n ORDER BY bm25_score\n LIMIT ?\n `;\n\n let rows: Array<{\n id: string;\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n snippet: string;\n tier: string;\n source: string;\n updated_at: number;\n last_accessed_at: number | null;\n relevance_score: number | null;\n bm25_score: number;\n }>;\n\n try {\n rows = db.prepare(sql).all(...params) as typeof rows;\n } catch (e) {\n // FTS5 MATCH throws on a malformed query, and for THAT an empty result is the\n // honest answer — nothing matches a query that cannot be parsed.\n //\n // Everything else is a failure of the store: a missing table, a corrupt\n // index, a locked database. Those used to return [] as well, which made an\n // unusable index byte-identical to a genuine miss. The Postgres path had the\n // same defect and it cost a real wrong answer on 2026-08-04 — the backend was\n // down for two hours, every search reported \"No results found\", and a sibling\n // session told the owner a DMARC note did not exist. See\n // storage/postgres/search.ts.\n if (!isQuerySyntaxError(e)) {\n throw new Error(\n `Memory keyword search failed — the index is unusable, so this is NOT an ` +\n `empty result set. Cause: ${e instanceof Error ? e.message : String(e)}`\n );\n }\n return [];\n }\n\n const minScore = opts?.minScore ?? 0.0;\n\n return rows\n .map((row) => {\n const baseScore = -row.bm25_score;\n const relevanceScore = row.relevance_score ?? 0.5;\n const score = baseScore * (0.5 + relevanceScore);\n return {\n chunkId: row.id,\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.snippet,\n score,\n tier: row.tier,\n source: row.source,\n updatedAt: row.updated_at,\n lastAccessedAt: row.last_accessed_at ?? undefined,\n };\n })\n .filter((r) => r.score >= minScore);\n}\n\n// ---------------------------------------------------------------------------\n// Semantic search\n// ---------------------------------------------------------------------------\n\n/**\n * Search chunks using brute-force cosine similarity over stored embeddings.\n *\n * Only chunks that have a non-null embedding BLOB are considered.\n *\n * @param queryEmbedding Pre-computed Float32Array for the search query.\n */\nexport function searchMemorySemantic(\n db: Database,\n queryEmbedding: Float32Array,\n opts?: SearchOptions,\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n\n const conditions: string[] = [\"embedding IS NOT NULL\"];\n const params: (string | number)[] = [];\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => \"?\").join(\", \");\n conditions.push(`project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => \"?\").join(\", \");\n conditions.push(`source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => \"?\").join(\", \");\n conditions.push(`tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n const where = \"WHERE \" + conditions.join(\" AND \");\n\n // Hard cap for SQLite semantic path — prevents OOM on large corpora.\n // Use Postgres for production semantic search.\n const sql = `\n SELECT id, project_id, path, start_line, end_line, text, tier, source, embedding, updated_at, last_accessed_at, relevance_score\n FROM memory_chunks\n ${where}\n LIMIT 5000\n `;\n\n const rows = db.prepare(sql).all(...params) as Array<{\n id: string;\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n text: string;\n tier: string;\n source: string;\n embedding: Buffer;\n updated_at: number;\n last_accessed_at: number | null;\n relevance_score: number | null;\n }>;\n\n if (rows.length === 0) return [];\n\n const scored = rows.map((row) => {\n const vec = deserializeEmbedding(row.embedding);\n const baseScore = cosineSimilarity(queryEmbedding, vec);\n const relevanceScore = row.relevance_score ?? 0.5;\n const score = baseScore * (0.5 + relevanceScore);\n return {\n chunkId: row.id,\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.text,\n score,\n tier: row.tier,\n source: row.source,\n updatedAt: row.updated_at,\n lastAccessedAt: row.last_accessed_at ?? undefined,\n };\n });\n\n const minScore = opts?.minScore ?? -Infinity;\n\n return scored\n .filter((r) => r.score >= minScore)\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults);\n}\n\n// ---------------------------------------------------------------------------\n// Hybrid search\n// ---------------------------------------------------------------------------\n\n/**\n * Combine BM25 keyword search and semantic search using normalized scores.\n *\n * Both score sets are min-max normalized to [0,1] before combining, so neither\n * dominates the other regardless of their raw scales.\n */\nexport function searchMemoryHybrid(\n db: Database,\n query: string,\n queryEmbedding: Float32Array,\n opts?: SearchOptions & { keywordWeight?: number; semanticWeight?: number },\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n const kw = opts?.keywordWeight ?? 0.5;\n const sw = opts?.semanticWeight ?? 0.5;\n\n const keywordResults = searchMemory(db, query, { ...opts, maxResults: 50 });\n const semanticResults = searchMemorySemantic(db, queryEmbedding, { ...opts, maxResults: 50 });\n\n if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n function minMaxNormalize(items: SearchResult[]): Map<string, number> {\n if (items.length === 0) return new Map();\n const min = Math.min(...items.map((r) => r.score));\n const max = Math.max(...items.map((r) => r.score));\n const range = max - min;\n const m = new Map<string, number>();\n for (const r of items) {\n m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);\n }\n return m;\n }\n\n const kwNorm = minMaxNormalize(keywordResults);\n const semNorm = minMaxNormalize(semanticResults);\n\n const allKeys = new Set<string>([\n ...keywordResults.map(keyFor),\n ...semanticResults.map(keyFor),\n ]);\n\n const metaMap = new Map<string, SearchResult>();\n for (const r of [...keywordResults, ...semanticResults]) {\n metaMap.set(keyFor(r), r);\n }\n\n const combined: Array<SearchResult & { combinedScore: number }> = [];\n for (const key of allKeys) {\n const meta = metaMap.get(key)!;\n const kwScore = kwNorm.get(key) ?? 0;\n const semScore = semNorm.get(key) ?? 0;\n const combinedScore = kw * kwScore + sw * semScore;\n combined.push({ ...meta, score: combinedScore, combinedScore });\n }\n\n return combined\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults)\n .map(({ combinedScore: _unused, ...r }) => r);\n}\n\n// ---------------------------------------------------------------------------\n// Access timestamp tracking (QW2)\n// ---------------------------------------------------------------------------\n\n/**\n * Update last_accessed_at for a set of chunk IDs to the current timestamp.\n *\n * Best-effort: errors are silently ignored so search is never blocked.\n */\nexport function touchChunksLastAccessed(db: Database, chunkIds: string[]): void {\n if (chunkIds.length === 0) return;\n try {\n const now = Date.now();\n const placeholders = chunkIds.map(() => \"?\").join(\", \");\n db.prepare(\n `UPDATE memory_chunks SET last_accessed_at = ? WHERE id IN (${placeholders})`\n ).run(now, ...chunkIds);\n } catch {\n // non-critical — do not block search results\n }\n}\n","/**\n * SQLite kg_entities CRUD — moved from memory/kg-entity.ts so the only file\n * running raw SQL against kg_entities lives under src/storage/. The pure\n * content-addressing helpers (entityContentId/edgeContentId) and the\n * KgEntity/KgEntityUpsertParams types they share with PostgresBackend stay\n * in memory/kg-entity.ts.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { entityContentId, type KgEntity, type KgEntityUpsertParams } from \"../../memory/kg-entity.js\";\n\n/**\n * Upsert a KG entity in the federation SQLite database.\n *\n * If the entity already exists for this tenant:\n * - Updates last_seen to now\n * - Increments mention_count\n * - Updates description if provided (overwrites older description)\n *\n * Returns the entity_id for use as a foreign key in kg_triples.\n */\nexport function upsertKgEntitySqlite(\n db: Database,\n params: KgEntityUpsertParams\n): string {\n const tenantId = params.tenantId ?? \"default\";\n const entityId = entityContentId(params.name, tenantId);\n const now = Date.now();\n\n db.prepare(`\n INSERT INTO kg_entities\n (entity_id, tenant_id, name, type, description, first_seen, last_seen, mention_count, feedback_weight)\n VALUES\n (?, ?, ?, ?, ?, ?, ?, 1, 0.5)\n ON CONFLICT(entity_id) DO UPDATE SET\n last_seen = excluded.last_seen,\n mention_count = mention_count + 1,\n description = COALESCE(excluded.description, description),\n type = CASE WHEN excluded.type != 'unknown' THEN excluded.type ELSE type END\n `).run(\n entityId,\n tenantId,\n params.name,\n params.type ?? \"unknown\",\n params.description ?? null,\n now,\n now\n );\n\n return entityId;\n}\n\n/**\n * Look up a KG entity by name within a tenant.\n * Returns null if the entity does not exist.\n */\nexport function findKgEntitySqlite(\n db: Database,\n name: string,\n tenantId = \"default\"\n): KgEntity | null {\n const entityId = entityContentId(name, tenantId);\n const row = db.prepare(\n \"SELECT * FROM kg_entities WHERE entity_id = ? AND tenant_id = ?\"\n ).get(entityId, tenantId) as KgEntity | undefined;\n return row ?? null;\n}\n\n/**\n * List KG entities for a tenant, optionally filtered by type.\n */\nexport function listKgEntitiesSqlite(\n db: Database,\n tenantId = \"default\",\n type?: string,\n limit = 100\n): KgEntity[] {\n if (type) {\n return db.prepare(\n \"SELECT * FROM kg_entities WHERE tenant_id = ? AND type = ? ORDER BY mention_count DESC LIMIT ?\"\n ).all(tenantId, type, limit) as KgEntity[];\n }\n return db.prepare(\n \"SELECT * FROM kg_entities WHERE tenant_id = ? ORDER BY mention_count DESC LIMIT ?\"\n ).all(tenantId, limit) as KgEntity[];\n}\n\n/**\n * Apply an EMA (Exponential Moving Average) feedback update to an entity's weight.\n *\n * EMA formula: new_weight = old_weight + alpha * (target - old_weight)\n */\nexport function updateEntityFeedbackWeightSqlite(\n db: Database,\n entityId: string,\n normalizedRating: number,\n alpha = 0.1\n): void {\n const row = db.prepare(\n \"SELECT feedback_weight FROM kg_entities WHERE entity_id = ?\"\n ).get(entityId) as { feedback_weight: number } | undefined;\n\n if (!row) return;\n\n const newWeight = row.feedback_weight + alpha * (normalizedRating - row.feedback_weight);\n db.prepare(\n \"UPDATE kg_entities SET feedback_weight = ? WHERE entity_id = ?\"\n ).run(newWeight, entityId);\n}\n","/**\n * SQLiteBackend — wraps the existing better-sqlite3 federation.db\n * behind the StorageBackend interface.\n *\n * This is a thin adapter. The heavy lifting is all in the existing\n * memory/indexer.ts and memory/search.ts code; we just provide a\n * backend-agnostic surface so the daemon and tools can call either\n * SQLite or Postgres transparently.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type {\n StorageBackend, ChunkRow, FileRow, FederationStats,\n MemorySourcesReport, FindTunnelsOptions, FindTunnelsResult,\n KgStats, ObservationRow, ObservationWithCwd, StoreObservationInput, QueryObservationsOptions,\n ObservationStats, SessionSummaryRow, StoreSessionSummaryInput,\n SkillTelemetryRow, RecordSkillInvocationInput, QuerySkillTelemetryOptions,\n} from \"./interface.js\";\nimport type { SearchResult, SearchOptions } from \"../memory/search.js\";\nimport type { KgEntity, KgEntityUpsertParams } from \"../memory/kg-entity.js\";\nimport type { KgAddParams, KgQueryParams, KgTriple, KgContradiction } from \"../memory/kg.js\";\nimport type { IndexResult } from \"../memory/indexer/types.js\";\nimport type { RegistryBackend } from \"./registry-interface.js\";\nimport { indexAllSqlite } from \"./sqlite/indexer.js\";\nimport { findTunnelsSqlite } from \"./sqlite/tunnels.js\";\nimport { getMemorySourcesReportSqlite } from \"./sqlite/sources.js\";\nimport { searchMemory, searchMemorySemantic, touchChunksLastAccessed } from \"./sqlite/search.js\";\nimport {\n upsertKgEntitySqlite,\n findKgEntitySqlite,\n listKgEntitiesSqlite,\n updateEntityFeedbackWeightSqlite,\n} from \"./sqlite/kg-entity.js\";\n\nexport class SQLiteBackend implements StorageBackend {\n readonly backendType = \"sqlite\" as const;\n readonly supportsPostgresFeatures = false;\n\n private db: Database;\n\n constructor(db: Database) {\n this.db = db;\n }\n\n /**\n * Expose the raw better-sqlite3 Database handle. Storage-internal only —\n * used by db-admin.ts for `pai db query|tables|schema` (the one place\n * outside this class allowed to reach for it). Every other operation must\n * be a StorageBackend method; nothing outside src/storage/ may call this.\n */\n getRawDb(): Database {\n return this.db;\n }\n\n // -------------------------------------------------------------------------\n // Lifecycle\n // -------------------------------------------------------------------------\n\n async close(): Promise<void> {\n try {\n this.db.close();\n } catch {\n // ignore\n }\n }\n\n async getStats(): Promise<FederationStats> {\n const files = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_files\").get() as { n: number }\n ).n;\n const chunks = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_chunks\").get() as { n: number }\n ).n;\n return { files, chunks };\n }\n\n async getProjectStats(projectId: number): Promise<FederationStats> {\n const files = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_files WHERE project_id = ?\").get(projectId) as { n: number }\n ).n;\n const chunks = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_chunks WHERE project_id = ?\").get(projectId) as { n: number }\n ).n;\n return { files, chunks };\n }\n\n // -------------------------------------------------------------------------\n // File tracking\n // -------------------------------------------------------------------------\n\n async getFileHash(projectId: number, path: string): Promise<string | undefined> {\n const row = this.db\n .prepare(\"SELECT hash FROM memory_files WHERE project_id = ? AND path = ?\")\n .get(projectId, path) as { hash: string } | undefined;\n return row?.hash;\n }\n\n async upsertFile(file: FileRow): Promise<void> {\n this.db\n .prepare(\n `INSERT INTO memory_files (project_id, path, source, tier, hash, mtime, size)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(project_id, path) DO UPDATE SET\n source = excluded.source,\n tier = excluded.tier,\n hash = excluded.hash,\n mtime = excluded.mtime,\n size = excluded.size`\n )\n .run(file.projectId, file.path, file.source, file.tier, file.hash, file.mtime, file.size);\n }\n\n // -------------------------------------------------------------------------\n // Chunk management\n // -------------------------------------------------------------------------\n\n async getChunkIds(projectId: number, path: string): Promise<string[]> {\n const rows = this.db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, path) as Array<{ id: string }>;\n return rows.map((r) => r.id);\n }\n\n async deleteChunksForFile(projectId: number, path: string): Promise<void> {\n const ids = await this.getChunkIds(projectId, path);\n const deleteFts = this.db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = this.db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\"\n );\n this.db.transaction(() => {\n for (const id of ids) {\n deleteFts.run(id);\n }\n deleteChunks.run(projectId, path);\n })();\n }\n\n async insertChunks(chunks: ChunkRow[]): Promise<void> {\n if (chunks.length === 0) return;\n\n const insertChunk = this.db.prepare(\n `INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`\n );\n const insertFts = this.db.prepare(\n `INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n );\n\n this.db.transaction(() => {\n for (const c of chunks) {\n insertChunk.run(\n c.id,\n c.projectId,\n c.source,\n c.tier,\n c.path,\n c.startLine,\n c.endLine,\n c.hash,\n c.text,\n c.updatedAt\n );\n insertFts.run(\n c.text,\n c.id,\n c.projectId,\n c.path,\n c.source,\n c.tier,\n c.startLine,\n c.endLine\n );\n }\n })();\n }\n\n async getDistinctChunkPaths(projectId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\"SELECT DISTINCT path FROM memory_chunks WHERE project_id = ?\")\n .all(projectId) as Array<{ path: string }>;\n return rows.map((r) => r.path);\n }\n\n async deletePaths(projectId: number, paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n const deleteFts = this.db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = this.db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\"\n );\n const deleteFile = this.db.prepare(\n \"DELETE FROM memory_files WHERE project_id = ? AND path = ?\"\n );\n this.db.transaction(() => {\n for (const path of paths) {\n const ids = this.db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, path) as Array<{ id: string }>;\n for (const { id } of ids) {\n deleteFts.run(id);\n }\n deleteChunks.run(projectId, path);\n deleteFile.run(projectId, path);\n }\n })();\n }\n\n async getUnembeddedChunkIds(\n projectId?: number,\n limit?: number,\n after?: { projectId: number; id: string } | null,\n ): Promise<Array<{ id: string; text: string; project_id: number; path: string }>> {\n const conditions = [\"embedding IS NULL\"];\n const params: (string | number)[] = [];\n\n if (projectId !== undefined) {\n conditions.push(\"project_id = ?\");\n params.push(projectId);\n }\n\n // Once paginating, the priority ORDER BY below no longer applies — pages\n // must be ordered by the same (project_id, id) the cursor seeks on.\n const paginating = after !== undefined;\n if (after) {\n conditions.push(\"(project_id > ? OR (project_id = ? AND id > ?))\");\n params.push(after.projectId, after.projectId, after.id);\n }\n\n const where = \"WHERE \" + conditions.join(\" AND \");\n // Prioritize real knowledge notes over PAI session/job-search noise.\n // CASE expression assigns lower priority numbers to knowledge paths.\n const orderBy = paginating\n ? \"project_id, id\"\n : `CASE\n WHEN path LIKE '🧠 Ideaverse/%' THEN 0\n WHEN path LIKE 'Z - Zettelkasten/%' THEN 0\n WHEN path LIKE '💼 Business/%' THEN 0\n WHEN path LIKE '📆 Meetings/%' THEN 1\n WHEN path LIKE '💡 Insights/%' THEN 1\n WHEN path LIKE '👨💻 People/%' THEN 1\n WHEN path LIKE 'University/%' THEN 1\n WHEN path LIKE 'Copilot/%' THEN 1\n WHEN path LIKE '🗓️ Daily Notes/%' THEN 2\n WHEN path LIKE 'PAI/%' THEN 3\n WHEN path LIKE '09-job-search/%' THEN 4\n WHEN path LIKE 'seriousletter/%' THEN 4\n WHEN path LIKE 'Attachments/%' THEN 5\n ELSE 2\n END, id`;\n const rows = this.db\n .prepare(`SELECT id, text, project_id, path FROM memory_chunks ${where}\n ORDER BY ${orderBy}${limit !== undefined ? \" LIMIT ?\" : \"\"}`)\n .all(...params, ...(limit !== undefined ? [limit] : [])) as Array<{ id: string; text: string; project_id: number; path: string }>;\n return rows;\n }\n\n async updateEmbedding(chunkId: string, embedding: Buffer): Promise<void> {\n this.db\n .prepare(\"UPDATE memory_chunks SET embedding = ? WHERE id = ?\")\n .run(embedding, chunkId);\n }\n\n // -------------------------------------------------------------------------\n // Search\n // -------------------------------------------------------------------------\n\n async searchKeyword(query: string, opts?: SearchOptions): Promise<SearchResult[]> {\n return searchMemory(this.db, query, opts);\n }\n\n async searchSemantic(queryEmbedding: Float32Array, opts?: SearchOptions): Promise<SearchResult[]> {\n return searchMemorySemantic(this.db, queryEmbedding, opts);\n }\n\n // -------------------------------------------------------------------------\n // Vault operations — not supported on SQLite backend (use Postgres)\n // -------------------------------------------------------------------------\n\n private vaultNotSupported(): never {\n throw new Error(\"Vault operations require the Postgres backend\");\n }\n\n async upsertVaultFile(): Promise<void> { this.vaultNotSupported(); }\n async deleteVaultFile(): Promise<void> { this.vaultNotSupported(); }\n async getVaultFile(): Promise<null> { this.vaultNotSupported(); }\n async getVaultFileByInode(): Promise<null> { this.vaultNotSupported(); }\n async getAllVaultFiles(): Promise<never[]> { this.vaultNotSupported(); }\n async getRecentVaultFiles(): Promise<never[]> { this.vaultNotSupported(); }\n async countVaultFiles(): Promise<number> { this.vaultNotSupported(); }\n async upsertVaultAliases(): Promise<void> { this.vaultNotSupported(); }\n async deleteVaultAliases(): Promise<void> { this.vaultNotSupported(); }\n async replaceLinksForSources(): Promise<void> { this.vaultNotSupported(); }\n async getLinksFromSource(): Promise<never[]> { this.vaultNotSupported(); }\n async getLinksToTarget(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkGraph(): Promise<never[]> { this.vaultNotSupported(); }\n async upsertVaultHealth(): Promise<void> { this.vaultNotSupported(); }\n async getVaultHealth(): Promise<null> { this.vaultNotSupported(); }\n async getOrphans(): Promise<never[]> { this.vaultNotSupported(); }\n async getDeadLinks(): Promise<never[]> { this.vaultNotSupported(); }\n async upsertNameIndex(): Promise<void> { this.vaultNotSupported(); }\n async replaceNameIndex(): Promise<void> { this.vaultNotSupported(); }\n async resolveVaultName(): Promise<never[]> { this.vaultNotSupported(); }\n async searchVaultNameIndex(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilesByPaths(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilesByPathsAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinksFromPaths(): Promise<never[]> { this.vaultNotSupported(); }\n async getChunksWithEmbeddings(): Promise<never[]> { this.vaultNotSupported(); }\n async getChunksForPath(): Promise<never[]> { this.vaultNotSupported(); }\n async searchChunksByText(): Promise<never[]> { this.vaultNotSupported(); }\n async countVaultFilesWithPrefix(): Promise<number> { this.vaultNotSupported(); }\n async countVaultFilesAfter(): Promise<number> { this.vaultNotSupported(); }\n async countVaultLinksWithPrefix(): Promise<number> { this.vaultNotSupported(); }\n async countVaultLinksAfter(): Promise<number> { this.vaultNotSupported(); }\n async getDeadLinksWithLineNumbers(): Promise<never[]> { this.vaultNotSupported(); }\n async getDeadLinksWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getDeadLinksAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getOrphansWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getOrphansAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getLowConnectivity(): Promise<never[]> { this.vaultNotSupported(); }\n async getLowConnectivityWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getLowConnectivityAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getAllVaultFilePaths(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilePathsWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilePathsAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkEdges(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkEdgesWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkEdgesAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultAlias(): Promise<null> { this.vaultNotSupported(); }\n\n // -------------------------------------------------------------------------\n // Knowledge-graph entities (kg_entities) — wraps storage/sqlite/kg-entity.ts\n // -------------------------------------------------------------------------\n\n async upsertKgEntity(params: KgEntityUpsertParams): Promise<string> {\n return upsertKgEntitySqlite(this.db, params);\n }\n\n async findKgEntity(name: string, tenantId = \"default\"): Promise<KgEntity | null> {\n return findKgEntitySqlite(this.db, name, tenantId);\n }\n\n async listKgEntities(tenantId = \"default\", type?: string, limit = 100): Promise<KgEntity[]> {\n return listKgEntitiesSqlite(this.db, tenantId, type, limit);\n }\n\n async updateEntityFeedbackWeight(entityId: string, normalizedRating: number, alpha = 0.1): Promise<void> {\n return updateEntityFeedbackWeightSqlite(this.db, entityId, normalizedRating, alpha);\n }\n\n // -------------------------------------------------------------------------\n // Chunk feedback / access tracking\n // -------------------------------------------------------------------------\n\n async getChunksForFeedback(chunkIds: string[]): Promise<Array<{ id: string; text: string; relevanceScore: number | null }>> {\n if (chunkIds.length === 0) return [];\n const placeholders = chunkIds.map(() => \"?\").join(\", \");\n const rows = this.db\n .prepare(`SELECT id, text, relevance_score FROM memory_chunks WHERE id IN (${placeholders})`)\n .all(...chunkIds) as Array<{ id: string; text: string; relevance_score: number | null }>;\n return rows.map((r) => ({ id: r.id, text: r.text, relevanceScore: r.relevance_score }));\n }\n\n async updateChunkRelevanceScore(chunkId: string, score: number): Promise<void> {\n this.db.prepare(\"UPDATE memory_chunks SET relevance_score = ? WHERE id = ?\").run(score, chunkId);\n }\n\n async touchChunksLastAccessed(chunkIds: string[]): Promise<void> {\n touchChunksLastAccessed(this.db, chunkIds);\n }\n\n // -------------------------------------------------------------------------\n // Global indexing\n // -------------------------------------------------------------------------\n\n async indexAll(registry: RegistryBackend): Promise<{ projects: number; result: IndexResult }> {\n return indexAllSqlite(this.db, registry);\n }\n\n // -------------------------------------------------------------------------\n // Cross-project concept tunnels\n // -------------------------------------------------------------------------\n\n async findTunnels(registry: RegistryBackend, options?: FindTunnelsOptions): Promise<FindTunnelsResult> {\n return findTunnelsSqlite(this.db, registry, options);\n }\n\n // -------------------------------------------------------------------------\n // Memory sources report\n // -------------------------------------------------------------------------\n\n async getMemorySourcesReport(): Promise<MemorySourcesReport> {\n return getMemorySourcesReportSqlite(this.db);\n }\n\n // -------------------------------------------------------------------------\n // Temporal knowledge graph / observations / skill telemetry — not\n // supported on SQLite (kg_triples, pai_observations, pai_session_summaries,\n // pai_skill_telemetry only exist in the Postgres schema).\n // -------------------------------------------------------------------------\n\n private postgresOnly(feature: string): never {\n throw new Error(`${feature} is not supported on the sqlite backend — set \"storageBackend\": \"postgres\" in the daemon config`);\n }\n\n async addKgTriple(): Promise<KgTriple> { this.postgresOnly(\"kg_triples\"); }\n async queryKgTriples(): Promise<KgTriple[]> { this.postgresOnly(\"kg_triples\"); }\n async invalidateKgTriple(): Promise<void> { this.postgresOnly(\"kg_triples\"); }\n async getKgContradictions(): Promise<KgContradiction[]> { this.postgresOnly(\"kg_triples\"); }\n async getKgStats(): Promise<KgStats> { this.postgresOnly(\"kg_triples\"); }\n\n async storeObservation(): Promise<number | null> { this.postgresOnly(\"observations\"); }\n async storeObservationWithProject(): Promise<number | null> { this.postgresOnly(\"observations\"); }\n async queryObservations(): Promise<ObservationRow[]> { this.postgresOnly(\"observations\"); }\n async queryRecentObservations(): Promise<ObservationRow[]> { this.postgresOnly(\"observations\"); }\n async querySessionObservations(): Promise<ObservationRow[]> { this.postgresOnly(\"observations\"); }\n async getObservationStats(): Promise<ObservationStats> { this.postgresOnly(\"observations\"); }\n async getObservationTypesForPaths(): Promise<Map<string, Record<string, number>>> { return new Map(); }\n async storeSessionSummary(): Promise<void> { this.postgresOnly(\"session summaries\"); }\n async queryRecentSummaries(): Promise<SessionSummaryRow[]> { this.postgresOnly(\"session summaries\"); }\n async recordSkillInvocation(): Promise<void> { this.postgresOnly(\"skill telemetry\"); }\n async querySkillTelemetry(): Promise<SkillTelemetryRow[]> { this.postgresOnly(\"skill telemetry\"); }\n\n // -------------------------------------------------------------------------\n // File-path rename (session cleanup: moving Notes/*.md into YYYY/MM/)\n // -------------------------------------------------------------------------\n\n async countFilesWithPaths(paths: string[]): Promise<number> {\n if (paths.length === 0) return 0;\n const placeholders = paths.map(() => \"?\").join(\", \");\n const row = this.db\n .prepare(`SELECT COUNT(*) AS n FROM memory_files WHERE path IN (${placeholders})`)\n .get(...paths) as { n: number };\n return row.n;\n }\n\n async renameFilePaths(moves: Array<{ oldPath: string; newPath: string }>): Promise<number> {\n if (moves.length === 0) return 0;\n const updateFiles = this.db.prepare(\"UPDATE memory_files SET path = ? WHERE path = ?\");\n const updateChunks = this.db.prepare(\"UPDATE memory_chunks SET path = ? WHERE path = ?\");\n let filesUpdated = 0;\n this.db.transaction(() => {\n for (const { oldPath, newPath } of moves) {\n const r = updateFiles.run(newPath, oldPath);\n filesUpdated += r.changes;\n updateChunks.run(newPath, oldPath);\n }\n })();\n return filesUpdated;\n }\n}\n","/**\n * merge.ts — pure types and error class shared by the merge implementations.\n *\n * The actual SQLite implementation (planMerge/applyMerge, taking a raw\n * better-sqlite3 Database) lives in src/storage/sqlite/registry-merge.ts —\n * moved there so nothing outside src/storage/ imports better-sqlite3. The\n * Postgres implementation lives in src/storage/registry-postgres.ts. Both are\n * reached through RegistryBackend.planProjectMerge/applyProjectMerge\n * (src/storage/registry-interface.ts), never directly.\n *\n * See src/storage/sqlite/registry-merge.ts for the \"why one function\" design\n * rationale (five tables reference a project, foreign_keys is off).\n */\n\nexport interface MergePlan {\n fromId: number;\n fromSlug: string;\n intoId: number;\n intoSlug: string;\n /** Sessions to move, with the numbers they will be given. */\n sessions: { id: number; from: number; to: number }[];\n tags: number;\n aliases: number;\n compactions: number;\n links: number;\n /** The losing slug is kept as an alias, so `pai <old-name>` still resolves. */\n aliasToAdd?: string;\n}\n\nexport class MergeError extends Error {}\n","/**\n * PostgresRegistryBackend — implements RegistryBackend over the pg pool\n * pattern already used by PostgresBackend (src/storage/postgres/backend.ts).\n *\n * Same SQL shape as SQLiteRegistryBackend, translated to $-placeholders.\n * Behavioural notes where Postgres diverges from SQLite's \"OR IGNORE\":\n * - `INSERT ... ON CONFLICT DO NOTHING` (no target) catches any unique\n * violation on the table, matching SQLite's INSERT OR IGNORE.\n * - SQLite's `UPDATE OR IGNORE` (used once, in reassignLinksTarget/\n * applyProjectMerge's link retarget) has no direct Postgres equivalent —\n * an UPDATE cannot specify ON CONFLICT. Replicated with a NOT EXISTS guard\n * that skips rows whose new (session_id, target_project_id) would collide,\n * leaving them for the caller's existing cleanup DELETE, exactly as the\n * SQLite version does.\n */\n\nimport pg from \"pg\";\nimport { MergeError } from \"../registry/merge.js\";\nimport type { Pool, PoolClient } from \"pg\";\nimport type { PostgresConfig } from \"./postgres/config.js\";\nimport type {\n RegistryBackend,\n Project,\n ProjectWithSessionStats,\n Session,\n SessionWithProject,\n NewProject,\n NewProjectWithSlugRetry,\n NewSession,\n NewCompactionLogEntry,\n ListProjectsOptions,\n ListProjectsByPathLengthOptions,\n ListProjectsWithStatsOptions,\n ListNamedProjectsOptions,\n ListSessionsOptions,\n ListSessionsForProjectOptions,\n ProjectType,\n ProjectStatus,\n SessionStatus,\n LinkType,\n MergePlan,\n} from \"./registry-interface.js\";\n\nconst { Pool: PgPool } = pg;\n\n/**\n * node-postgres returns BIGINT columns (created_at/updated_at/archived_at/\n * closed_at, and MAX() over a BIGINT column) as strings — JS numbers can't\n * safely hold the full 64-bit range. Every value here is Date.now() ms and\n * well inside Number.isSafeInteger, so a plain Number() conversion keeps the\n * shape identical to better-sqlite3's row objects (same pattern as\n * rowToKgEntity in postgres/backend.ts).\n */\nfunction toNum(v: unknown): number {\n return typeof v === \"string\" ? Number(v) : (v as number);\n}\nfunction toNumOrNull(v: unknown): number | null {\n return v === null || v === undefined ? null : toNum(v);\n}\n\nfunction mapProject(row: Record<string, unknown>): Project {\n return {\n ...row,\n created_at: toNum(row.created_at),\n updated_at: toNum(row.updated_at),\n archived_at: toNumOrNull(row.archived_at),\n } as Project;\n}\n\nfunction mapProjectWithStats(row: Record<string, unknown>): ProjectWithSessionStats {\n return {\n ...mapProject(row),\n session_count: toNum(row.session_count),\n last_active: toNumOrNull(row.last_active),\n } as ProjectWithSessionStats;\n}\n\nfunction mapSession(row: Record<string, unknown>): Session {\n return {\n ...row,\n created_at: toNum(row.created_at),\n closed_at: toNumOrNull(row.closed_at),\n } as Session;\n}\n\nexport class PostgresRegistryBackend implements RegistryBackend {\n readonly backendType = \"postgres\" as const;\n\n private pool: Pool;\n /** False when constructed from a pool owned elsewhere (the shared storage\n * backend pool, wired by src/storage/factory.ts) — close() must not end\n * a pool this instance doesn't own. */\n private readonly ownsPool: boolean;\n\n constructor(configOrPool: PostgresConfig | Pool) {\n if (configOrPool instanceof PgPool) {\n this.pool = configOrPool;\n this.ownsPool = false;\n return;\n }\n\n const config = configOrPool;\n const connStr =\n config.connectionString ??\n `postgresql://${config.user ?? \"pai\"}:${config.password ?? \"pai\"}@${config.host ?? \"localhost\"}:${config.port ?? 5432}/${config.database ?? \"pai\"}`;\n\n this.pool = new PgPool({\n connectionString: connStr,\n max: config.maxConnections ?? 5,\n connectionTimeoutMillis: config.connectionTimeoutMs ?? 5000,\n idleTimeoutMillis: 30_000,\n allowExitOnIdle: true,\n });\n this.ownsPool = true;\n\n this.pool.on(\"error\", (err) => {\n process.stderr.write(`[pai-postgres] Registry pool error: ${err.message}\\n`);\n });\n }\n\n async close(): Promise<void> {\n if (this.ownsPool) await this.pool.end();\n }\n\n async resetRegistry(): Promise<void> {\n const client = await this.pool.connect();\n try {\n await client.query(\"BEGIN\");\n await client.query(\"DELETE FROM links\");\n await client.query(\"DELETE FROM compaction_log\");\n await client.query(\"DELETE FROM session_tags\");\n await client.query(\"DELETE FROM project_tags\");\n await client.query(\"DELETE FROM aliases\");\n await client.query(\"DELETE FROM sessions\");\n await client.query(\"DELETE FROM tags\");\n await client.query(\"DELETE FROM projects\");\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n }\n\n // -------------------------------------------------------------------------\n // Projects — reads\n // -------------------------------------------------------------------------\n\n async getProjectById(id: number): Promise<Project | null> {\n const r = await this.pool.query(\"SELECT * FROM projects WHERE id = $1\", [id]);\n return r.rows[0] ? mapProject(r.rows[0]) : null;\n }\n\n async getProjectBySlug(\n slug: string,\n opts: { caseInsensitive?: boolean; excludeId?: number } = {}\n ): Promise<Project | null> {\n let sql = opts.caseInsensitive\n ? \"SELECT * FROM projects WHERE lower(slug) = lower($1)\"\n : \"SELECT * FROM projects WHERE slug = $1\";\n const params: unknown[] = [slug];\n if (opts.excludeId !== undefined) {\n params.push(opts.excludeId);\n sql += ` AND id != $${params.length}`;\n }\n const r = await this.pool.query(sql, params);\n return r.rows[0] ? mapProject(r.rows[0]) : null;\n }\n\n async getProjectByAlias(\n alias: string,\n opts: { caseInsensitive?: boolean } = {}\n ): Promise<Project | null> {\n const sql = opts.caseInsensitive\n ? \"SELECT p.* FROM projects p JOIN aliases a ON a.project_id = p.id WHERE lower(a.alias) = lower($1)\"\n : \"SELECT p.* FROM projects p JOIN aliases a ON a.project_id = p.id WHERE a.alias = $1\";\n const r = await this.pool.query(sql, [alias]);\n return r.rows[0] ? mapProject(r.rows[0]) : null;\n }\n\n async getProjectByRootPath(rootPath: string, opts: { excludeId?: number } = {}): Promise<Project | null> {\n let sql = \"SELECT * FROM projects WHERE root_path = $1\";\n const params: unknown[] = [rootPath];\n if (opts.excludeId !== undefined) {\n params.push(opts.excludeId);\n sql += ` AND id != $${params.length}`;\n }\n const r = await this.pool.query(sql, params);\n return r.rows[0] ? mapProject(r.rows[0]) : null;\n }\n\n async getProjectByEncodedDir(encodedDir: string, opts: { excludeId?: number } = {}): Promise<Project | null> {\n let sql = \"SELECT * FROM projects WHERE encoded_dir = $1\";\n const params: unknown[] = [encodedDir];\n if (opts.excludeId !== undefined) {\n params.push(opts.excludeId);\n sql += ` AND id != $${params.length}`;\n }\n const r = await this.pool.query(sql, params);\n return r.rows[0] ? mapProject(r.rows[0]) : null;\n }\n\n async findProjectByCwdPrefix(cwd: string): Promise<Project | null> {\n const r = await this.pool.query(\n `SELECT * FROM projects WHERE status = 'active' AND $1 LIKE root_path || '%'\n ORDER BY length(root_path) DESC LIMIT 1`,\n [cwd]\n );\n return r.rows[0] ? mapProject(r.rows[0]) : null;\n }\n\n async listProjects(opts: ListProjectsOptions = {}): Promise<Project[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (opts.status) {\n params.push(opts.status);\n where.push(`p.status = $${params.length}`);\n }\n if (opts.excludeArchived) {\n where.push(\"p.status != 'archived'\");\n }\n if (opts.tagName) {\n params.push(opts.tagName);\n where.push(\n `p.id IN (SELECT pt.project_id FROM project_tags pt JOIN tags t ON pt.tag_id = t.id WHERE t.name = $${params.length})`\n );\n }\n\n let sql = \"SELECT p.* FROM projects p\";\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n\n switch (opts.orderBy) {\n case \"updated_desc\":\n sql += \" ORDER BY p.updated_at DESC\";\n break;\n case \"slug\":\n sql += \" ORDER BY p.slug ASC\";\n break;\n case \"id\":\n sql += \" ORDER BY p.id\";\n break;\n case \"status_updated\":\n default:\n sql += \" ORDER BY p.status ASC, p.updated_at DESC\";\n break;\n }\n if (opts.limit !== undefined) {\n params.push(opts.limit);\n sql += ` LIMIT $${params.length}`;\n }\n\n const r = await this.pool.query(sql, params);\n return r.rows.map(mapProject);\n }\n\n async listProjectsByPathLengthDesc(opts: ListProjectsByPathLengthOptions = {}): Promise<Project[]> {\n const where = opts.excludeArchived ? \"WHERE status != 'archived'\" : \"\";\n const r = await this.pool.query(`SELECT * FROM projects ${where} ORDER BY LENGTH(root_path) DESC`);\n return r.rows.map(mapProject);\n }\n\n async listProjectsWithSessionStats(\n opts: ListProjectsWithStatsOptions = {}\n ): Promise<ProjectWithSessionStats[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (opts.status) {\n params.push(opts.status);\n where.push(`p.status = $${params.length}`);\n }\n if (opts.tagId !== undefined) {\n params.push(opts.tagId);\n where.push(`p.id IN (SELECT project_id FROM project_tags WHERE tag_id = $${params.length})`);\n }\n\n let sql = `\n SELECT p.*,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id)::int AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n `;\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n sql +=\n opts.orderBy === \"updated_desc\"\n ? \" ORDER BY p.updated_at DESC\"\n : \" ORDER BY p.status ASC, p.updated_at DESC\";\n\n const r = await this.pool.query(sql, params);\n return r.rows.map(mapProjectWithStats);\n }\n\n async listNamedProjects(\n opts: ListNamedProjectsOptions = {}\n ): Promise<Array<ProjectWithSessionStats & { name: string | null }>> {\n const join = opts.includeUnnamed ? \"LEFT JOIN\" : \"JOIN\";\n const sql = `\n SELECT p.*, a.alias AS name,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id)::int AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n ${join} aliases a ON a.project_id = p.id\n WHERE p.status = 'active'\n ORDER BY p.updated_at DESC\n `;\n const r = await this.pool.query(sql);\n return r.rows.map((row) => ({ ...mapProjectWithStats(row), name: row.name as string | null }));\n }\n\n async searchProjects(query: string, limit = 20): Promise<Project[]> {\n const q = `%${query}%`;\n const r = await this.pool.query(\n `SELECT * FROM projects WHERE slug LIKE $1 OR display_name LIKE $1 OR root_path LIKE $1\n ORDER BY updated_at DESC LIMIT $2`,\n [q, limit]\n );\n return r.rows.map(mapProject);\n }\n\n async countProjects(opts: { status?: ProjectStatus } = {}): Promise<number> {\n const r = opts.status\n ? await this.pool.query<{ n: string }>(\"SELECT COUNT(*)::text AS n FROM projects WHERE status = $1\", [\n opts.status,\n ])\n : await this.pool.query<{ n: string }>(\"SELECT COUNT(*)::text AS n FROM projects\");\n return parseInt(r.rows[0].n, 10);\n }\n\n async getMostRecentProjectUpdatedAt(): Promise<number | null> {\n const r = await this.pool.query<{ updated_at: string }>(\n \"SELECT updated_at FROM projects ORDER BY updated_at DESC LIMIT 1\"\n );\n return r.rows[0] ? Number(r.rows[0].updated_at) : null;\n }\n\n async countChildProjects(parentId: number): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM projects WHERE parent_id = $1\",\n [parentId]\n );\n return parseInt(r.rows[0].n, 10);\n }\n\n async findSiblingProjectsBySlugPattern(\n excludeProjectId: number,\n slug: string,\n limit = 3\n ): Promise<Array<{ slug: string; root_path: string; session_count: number }>> {\n const r = await this.pool.query(\n `SELECT p.slug, p.root_path, COUNT(s.id)::int AS session_count\n FROM projects p LEFT JOIN sessions s ON s.project_id = p.id\n WHERE p.id != $1 AND (p.slug = $2 OR p.slug LIKE $2 || '-%' OR $2 LIKE p.slug || '-%')\n GROUP BY p.id HAVING COUNT(s.id) > 0 ORDER BY session_count DESC LIMIT $3`,\n [excludeProjectId, slug, limit]\n );\n return r.rows as Array<{ slug: string; root_path: string; session_count: number }>;\n }\n\n // -------------------------------------------------------------------------\n // Projects — writes\n // -------------------------------------------------------------------------\n\n async createProject(input: NewProject): Promise<Project> {\n const type: ProjectType = input.type ?? \"local\";\n const status: ProjectStatus = input.status ?? \"active\";\n const r = await this.pool.query(\n `INSERT INTO projects (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,\n [input.slug, input.displayName, input.rootPath, input.encodedDir, type, status, input.createdAt, input.updatedAt]\n );\n return mapProject(r.rows[0]);\n }\n\n async createProjectWithSlugRetry(\n input: NewProjectWithSlugRetry\n ): Promise<{ id: number; slug: string; created: boolean }> {\n let slug = input.baseSlug;\n let attempt = 0;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const r = await this.pool.query<{ id: number }>(\n `INSERT INTO projects (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES ($1, $2, $3, $4, 'local', 'active', $5, $6)\n ON CONFLICT DO NOTHING RETURNING id`,\n [slug, input.displayName, input.rootPath, input.encodedDir, input.createdAt, input.updatedAt]\n );\n if (r.rows.length > 0) {\n return { id: r.rows[0].id, slug, created: true };\n }\n\n const existing = await this.pool.query<{ id: number }>(\n \"SELECT id FROM projects WHERE root_path = $1\",\n [input.rootPath]\n );\n if (existing.rows[0]) {\n return { id: existing.rows[0].id, slug, created: false };\n }\n\n attempt++;\n slug = `${input.baseSlug}-${attempt}`;\n }\n }\n\n async updateProjectPath(\n id: number,\n patch: { rootPath?: string; encodedDir?: string },\n updatedAt?: number\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [];\n if (patch.rootPath !== undefined) {\n params.push(patch.rootPath);\n sets.push(`root_path = $${params.length}`);\n }\n if (patch.encodedDir !== undefined) {\n params.push(patch.encodedDir);\n sets.push(`encoded_dir = $${params.length}`);\n }\n if (updatedAt !== undefined) {\n params.push(updatedAt);\n sets.push(`updated_at = $${params.length}`);\n }\n if (!sets.length) return;\n params.push(id);\n await this.pool.query(`UPDATE projects SET ${sets.join(\", \")} WHERE id = $${params.length}`, params);\n }\n\n async updateProjectStatus(\n id: number,\n status: ProjectStatus,\n opts: { archivedAt?: number | null; updatedAt?: number; requireCurrentStatus?: ProjectStatus } = {}\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [status];\n sets.push(\"status = $1\");\n if (opts.archivedAt !== undefined) {\n params.push(opts.archivedAt);\n sets.push(`archived_at = $${params.length}`);\n }\n if (opts.updatedAt !== undefined) {\n params.push(opts.updatedAt);\n sets.push(`updated_at = $${params.length}`);\n }\n params.push(id);\n let sql = `UPDATE projects SET ${sets.join(\", \")} WHERE id = $${params.length}`;\n if (opts.requireCurrentStatus) {\n params.push(opts.requireCurrentStatus);\n sql += ` AND status = $${params.length}`;\n }\n await this.pool.query(sql, params);\n }\n\n async updateProjectDisplayName(id: number, displayName: string, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n await this.pool.query(\"UPDATE projects SET display_name = $1, updated_at = $2 WHERE id = $3\", [\n displayName,\n updatedAt,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE projects SET display_name = $1 WHERE id = $2\", [displayName, id]);\n }\n }\n\n async updateProjectType(id: number, type: ProjectType, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n await this.pool.query(\"UPDATE projects SET type = $1, updated_at = $2 WHERE id = $3\", [\n type,\n updatedAt,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE projects SET type = $1 WHERE id = $2\", [type, id]);\n }\n }\n\n async updateProjectSessionConfig(id: number, config: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n await this.pool.query(\"UPDATE projects SET session_config = $1, updated_at = $2 WHERE id = $3\", [\n config,\n updatedAt,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE projects SET session_config = $1 WHERE id = $2\", [config, id]);\n }\n }\n\n async updateProjectClaudeNotesDir(id: number, dir: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n await this.pool.query(\"UPDATE projects SET claude_notes_dir = $1, updated_at = $2 WHERE id = $3\", [\n dir,\n updatedAt,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE projects SET claude_notes_dir = $1 WHERE id = $2\", [dir, id]);\n }\n }\n\n async updateProjectObsidianLink(id: number, link: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n await this.pool.query(\"UPDATE projects SET obsidian_link = $1, updated_at = $2 WHERE id = $3\", [\n link,\n updatedAt,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE projects SET obsidian_link = $1 WHERE id = $2\", [link, id]);\n }\n }\n\n async updateProjectSlug(id: number, slug: string, updatedAt: number): Promise<void> {\n await this.pool.query(\"UPDATE projects SET slug = $1, updated_at = $2 WHERE id = $3\", [slug, updatedAt, id]);\n }\n\n async reassignProjectParent(fromParentId: number, toParentId: number): Promise<number> {\n const r = await this.pool.query(\"UPDATE projects SET parent_id = $1 WHERE parent_id = $2\", [\n toParentId,\n fromParentId,\n ]);\n return r.rowCount ?? 0;\n }\n\n async deleteProject(id: number): Promise<void> {\n await this.pool.query(\"DELETE FROM projects WHERE id = $1\", [id]);\n }\n\n async deleteProjectCascade(id: number): Promise<void> {\n const client = await this.pool.connect();\n try {\n await client.query(\"BEGIN\");\n await client.query(\"DELETE FROM links WHERE target_project_id = $1\", [id]);\n await client.query(\n \"DELETE FROM links WHERE session_id IN (SELECT id FROM sessions WHERE project_id = $1)\",\n [id]\n );\n await client.query(\"DELETE FROM compaction_log WHERE project_id = $1\", [id]);\n await client.query(\"DELETE FROM project_tags WHERE project_id = $1\", [id]);\n await client.query(\"DELETE FROM aliases WHERE project_id = $1\", [id]);\n await client.query(\"DELETE FROM sessions WHERE project_id = $1\", [id]);\n await client.query(\"DELETE FROM projects WHERE id = $1\", [id]);\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n }\n\n // -------------------------------------------------------------------------\n // Tags\n // -------------------------------------------------------------------------\n\n async listTagsForProject(projectId: number): Promise<string[]> {\n const r = await this.pool.query<{ name: string }>(\n \"SELECT t.name FROM tags t JOIN project_tags pt ON pt.tag_id = t.id WHERE pt.project_id = $1 ORDER BY t.name\",\n [projectId]\n );\n return r.rows.map((row) => row.name);\n }\n\n async listAllTags(): Promise<Array<{ id: number; name: string }>> {\n const r = await this.pool.query<{ id: number; name: string }>(\"SELECT id, name FROM tags ORDER BY name\");\n return r.rows;\n }\n\n async upsertTag(name: string): Promise<number> {\n await this.pool.query(\"INSERT INTO tags (name) VALUES ($1) ON CONFLICT (name) DO NOTHING\", [name]);\n const r = await this.pool.query<{ id: number }>(\"SELECT id FROM tags WHERE name = $1\", [name]);\n return r.rows[0].id;\n }\n\n async addProjectTag(projectId: number, tagId: number): Promise<void> {\n await this.pool.query(\"INSERT INTO project_tags (project_id, tag_id) VALUES ($1, $2)\", [projectId, tagId]);\n }\n\n async projectHasTag(projectId: number, tagId: number): Promise<boolean> {\n const r = await this.pool.query(\"SELECT 1 FROM project_tags WHERE project_id = $1 AND tag_id = $2\", [\n projectId,\n tagId,\n ]);\n return (r.rowCount ?? 0) > 0;\n }\n\n async deleteProjectTag(projectId: number, tagId: number): Promise<void> {\n await this.pool.query(\"DELETE FROM project_tags WHERE project_id = $1 AND tag_id = $2\", [projectId, tagId]);\n }\n\n async reassignProjectTag(fromProjectId: number, toProjectId: number, tagId: number): Promise<void> {\n await this.pool.query(\"UPDATE project_tags SET project_id = $1 WHERE project_id = $2 AND tag_id = $3\", [\n toProjectId,\n fromProjectId,\n tagId,\n ]);\n }\n\n async listProjectTagIds(projectId: number): Promise<number[]> {\n const r = await this.pool.query<{ tag_id: number }>(\"SELECT tag_id FROM project_tags WHERE project_id = $1\", [\n projectId,\n ]);\n return r.rows.map((row) => row.tag_id);\n }\n\n async countProjectTagsForProject(projectId: number): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM project_tags WHERE project_id = $1\",\n [projectId]\n );\n return parseInt(r.rows[0].n, 10);\n }\n\n async copyProjectTags(fromProjectId: number, toProjectId: number): Promise<void> {\n await this.pool.query(\n `INSERT INTO project_tags (project_id, tag_id) SELECT $1, tag_id FROM project_tags WHERE project_id = $2\n ON CONFLICT (project_id, tag_id) DO NOTHING`,\n [toProjectId, fromProjectId]\n );\n }\n\n async deleteProjectTagsForProject(projectId: number): Promise<void> {\n await this.pool.query(\"DELETE FROM project_tags WHERE project_id = $1\", [projectId]);\n }\n\n // -------------------------------------------------------------------------\n // Aliases\n // -------------------------------------------------------------------------\n\n async resolveAlias(alias: string): Promise<number | null> {\n const r = await this.pool.query<{ project_id: number }>(\n \"SELECT project_id FROM aliases WHERE alias = $1\",\n [alias]\n );\n return r.rows[0]?.project_id ?? null;\n }\n\n async listAliasesForProject(projectId: number): Promise<string[]> {\n const r = await this.pool.query<{ alias: string }>(\n \"SELECT alias FROM aliases WHERE project_id = $1 ORDER BY alias\",\n [projectId]\n );\n return r.rows.map((row) => row.alias);\n }\n\n async addAlias(alias: string, projectId: number): Promise<void> {\n await this.pool.query(\"INSERT INTO aliases (alias, project_id) VALUES ($1, $2)\", [alias, projectId]);\n }\n\n async removeAlias(alias: string): Promise<void> {\n await this.pool.query(\"DELETE FROM aliases WHERE alias = $1\", [alias]);\n }\n\n async countAliasesForProject(projectId: number): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM aliases WHERE project_id = $1\",\n [projectId]\n );\n return parseInt(r.rows[0].n, 10);\n }\n\n async moveProjectAliases(fromProjectId: number, toProjectId: number): Promise<void> {\n await this.pool.query(\"UPDATE aliases SET project_id = $1 WHERE project_id = $2\", [\n toProjectId,\n fromProjectId,\n ]);\n }\n\n async reassignAlias(alias: string, toProjectId: number): Promise<void> {\n await this.pool.query(\"UPDATE aliases SET project_id = $1 WHERE alias = $2\", [toProjectId, alias]);\n }\n\n async listAliasMap(): Promise<Array<{ alias: string; slug: string; root_path: string }>> {\n const r = await this.pool.query(\n `SELECT a.alias AS alias, p.slug AS slug, p.root_path AS root_path\n FROM aliases a JOIN projects p ON p.id = a.project_id WHERE p.status != 'archived'`\n );\n return r.rows as Array<{ alias: string; slug: string; root_path: string }>;\n }\n\n // -------------------------------------------------------------------------\n // Sessions\n // -------------------------------------------------------------------------\n\n async getSessionById(id: number): Promise<Session | null> {\n const r = await this.pool.query(\"SELECT * FROM sessions WHERE id = $1\", [id]);\n return r.rows[0] ? mapSession(r.rows[0]) : null;\n }\n\n async getSessionByNumber(projectId: number, number: number): Promise<Session | null> {\n const r = await this.pool.query(\"SELECT * FROM sessions WHERE project_id = $1 AND number = $2\", [\n projectId,\n number,\n ]);\n return r.rows[0] ? mapSession(r.rows[0]) : null;\n }\n\n async getLatestSessionForProject(projectId: number): Promise<Session | null> {\n const r = await this.pool.query(\n \"SELECT * FROM sessions WHERE project_id = $1 ORDER BY number DESC LIMIT 1\",\n [projectId]\n );\n return r.rows[0] ? mapSession(r.rows[0]) : null;\n }\n\n async getMaxSessionNumber(projectId: number): Promise<number> {\n const r = await this.pool.query<{ n: number }>(\n \"SELECT COALESCE(MAX(number), 0) AS n FROM sessions WHERE project_id = $1\",\n [projectId]\n );\n return r.rows[0].n;\n }\n\n async listSessionsForProject(\n projectId: number,\n opts: ListSessionsForProjectOptions = {}\n ): Promise<Session[]> {\n const orderBy = opts.orderBy === \"created_desc\" ? \"created_at DESC\" : \"number ASC\";\n let sql = `SELECT * FROM sessions WHERE project_id = $1 ORDER BY ${orderBy}`;\n const params: unknown[] = [projectId];\n if (opts.limit !== undefined) {\n params.push(opts.limit);\n sql += ` LIMIT $${params.length}`;\n }\n const r = await this.pool.query(sql, params);\n return r.rows.map(mapSession);\n }\n\n async listSessions(opts: ListSessionsOptions = {}): Promise<SessionWithProject[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n if (opts.projectId !== undefined) {\n params.push(opts.projectId);\n where.push(`s.project_id = $${params.length}`);\n }\n if (opts.status) {\n params.push(opts.status);\n where.push(`s.status = $${params.length}`);\n }\n let sql = `\n SELECT s.*, p.slug AS project_slug, p.display_name AS project_name\n FROM sessions s JOIN projects p ON p.id = s.project_id\n `;\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n sql += \" ORDER BY s.date DESC, s.number DESC\";\n params.push(opts.limit ?? 20);\n sql += ` LIMIT $${params.length}`;\n const r = await this.pool.query(sql, params);\n return r.rows.map((row) => ({ ...mapSession(row), project_slug: row.project_slug as string, project_name: row.project_name as string }));\n }\n\n async findSessionByFilename(projectId: number, filename: string): Promise<{ id: number } | null> {\n const r = await this.pool.query<{ id: number }>(\n \"SELECT id FROM sessions WHERE project_id = $1 AND filename = $2 LIMIT 1\",\n [projectId, filename]\n );\n return r.rows[0] ?? null;\n }\n\n async sessionNumberTaken(projectId: number, number: number): Promise<boolean> {\n const r = await this.pool.query(\n \"SELECT 1 FROM sessions WHERE project_id = $1 AND number = $2 LIMIT 1\",\n [projectId, number]\n );\n return (r.rowCount ?? 0) > 0;\n }\n\n async createSession(input: NewSession): Promise<Session> {\n const status = input.status ?? \"completed\";\n const r = await this.pool.query(\n `INSERT INTO sessions (project_id, number, date, slug, title, filename, status, created_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,\n [input.projectId, input.number, input.date, input.slug, input.title, input.filename, status, input.createdAt]\n );\n return mapSession(r.rows[0]);\n }\n\n async upsertSessionIfAbsent(input: NewSession): Promise<boolean> {\n const existing = await this.pool.query(\n \"SELECT id FROM sessions WHERE project_id = $1 AND number = $2\",\n [input.projectId, input.number]\n );\n if ((existing.rowCount ?? 0) > 0) return false;\n\n const status = input.status ?? \"completed\";\n await this.pool.query(\n `INSERT INTO sessions (project_id, number, date, slug, title, filename, status, created_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [input.projectId, input.number, input.date, input.slug, input.title, input.filename, status, input.createdAt]\n );\n return true;\n }\n\n async updateSessionMeta(\n id: number,\n patch: { slug?: string; title?: string; filename?: string }\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [];\n if (patch.slug !== undefined) {\n params.push(patch.slug);\n sets.push(`slug = $${params.length}`);\n }\n if (patch.title !== undefined) {\n params.push(patch.title);\n sets.push(`title = $${params.length}`);\n }\n if (patch.filename !== undefined) {\n params.push(patch.filename);\n sets.push(`filename = $${params.length}`);\n }\n if (!sets.length) return;\n params.push(id);\n await this.pool.query(`UPDATE sessions SET ${sets.join(\", \")} WHERE id = $${params.length}`, params);\n }\n\n async updateSessionNumber(id: number, number: number, opts: { filename?: string } = {}): Promise<void> {\n if (opts.filename !== undefined) {\n await this.pool.query(\"UPDATE sessions SET number = $1, filename = $2 WHERE id = $3\", [\n number,\n opts.filename,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE sessions SET number = $1 WHERE id = $2\", [number, id]);\n }\n }\n\n async updateSessionFilename(id: number, filename: string): Promise<void> {\n await this.pool.query(\"UPDATE sessions SET filename = $1 WHERE id = $2\", [filename, id]);\n }\n\n async updateSessionStatus(id: number, status: SessionStatus, opts: { closedAt?: number } = {}): Promise<void> {\n if (opts.closedAt !== undefined) {\n await this.pool.query(\"UPDATE sessions SET status = $1, closed_at = $2 WHERE id = $3\", [\n status,\n opts.closedAt,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE sessions SET status = $1 WHERE id = $2\", [status, id]);\n }\n }\n\n async moveSessionToProject(id: number, projectId: number, opts: { number?: number } = {}): Promise<void> {\n if (opts.number !== undefined) {\n await this.pool.query(\"UPDATE sessions SET project_id = $1, number = $2 WHERE id = $3\", [\n projectId,\n opts.number,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE sessions SET project_id = $1 WHERE id = $2\", [projectId, id]);\n }\n }\n\n async deleteSession(id: number): Promise<void> {\n await this.pool.query(\"DELETE FROM sessions WHERE id = $1\", [id]);\n }\n\n async backfillFoldedSession(keepId: number, dropId: number): Promise<void> {\n await this.pool.query(\n `UPDATE sessions SET\n claude_session_id = COALESCE(claude_session_id,\n (SELECT claude_session_id FROM sessions WHERE id = $1)),\n token_count = COALESCE(token_count,\n (SELECT token_count FROM sessions WHERE id = $1)),\n closed_at = COALESCE(closed_at,\n (SELECT closed_at FROM sessions WHERE id = $1)),\n status = CASE WHEN status = 'open'\n AND (SELECT status FROM sessions WHERE id = $1) != 'open'\n THEN (SELECT status FROM sessions WHERE id = $1)\n ELSE status END\n WHERE id = $2`,\n [dropId, keepId]\n );\n }\n\n async countSessionsForProject(projectId: number): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM sessions WHERE project_id = $1\",\n [projectId]\n );\n return parseInt(r.rows[0].n, 10);\n }\n\n async countSessions(): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\"SELECT COUNT(*)::text AS n FROM sessions\");\n return parseInt(r.rows[0].n, 10);\n }\n\n async getMostRecentSessionDate(projectId: number): Promise<string | null> {\n const r = await this.pool.query<{ date: string }>(\n \"SELECT date FROM sessions WHERE project_id = $1 ORDER BY date DESC LIMIT 1\",\n [projectId]\n );\n return r.rows[0]?.date ?? null;\n }\n\n async getMostRecentSessionCreatedAt(): Promise<number | null> {\n const r = await this.pool.query<{ created_at: string }>(\n \"SELECT created_at FROM sessions ORDER BY created_at DESC LIMIT 1\"\n );\n return r.rows[0] ? Number(r.rows[0].created_at) : null;\n }\n\n // -------------------------------------------------------------------------\n // Session tags\n // -------------------------------------------------------------------------\n\n async listSessionTagIds(sessionId: number): Promise<number[]> {\n const r = await this.pool.query<{ tag_id: number }>(\n \"SELECT tag_id FROM session_tags WHERE session_id = $1\",\n [sessionId]\n );\n return r.rows.map((row) => row.tag_id);\n }\n\n async sessionHasTag(sessionId: number, tagId: number): Promise<boolean> {\n const r = await this.pool.query(\"SELECT 1 FROM session_tags WHERE session_id = $1 AND tag_id = $2\", [\n sessionId,\n tagId,\n ]);\n return (r.rowCount ?? 0) > 0;\n }\n\n async addSessionTag(sessionId: number, tagId: number): Promise<void> {\n await this.pool.query(\"INSERT INTO session_tags (session_id, tag_id) VALUES ($1, $2)\", [sessionId, tagId]);\n }\n\n async deleteSessionTag(sessionId: number, tagId: number): Promise<void> {\n await this.pool.query(\"DELETE FROM session_tags WHERE session_id = $1 AND tag_id = $2\", [sessionId, tagId]);\n }\n\n async reassignSessionTag(fromSessionId: number, toSessionId: number, tagId: number): Promise<void> {\n await this.pool.query(\n \"UPDATE session_tags SET session_id = $1 WHERE session_id = $2 AND tag_id = $3\",\n [toSessionId, fromSessionId, tagId]\n );\n }\n\n async listTagsForSession(sessionId: number): Promise<string[]> {\n const r = await this.pool.query<{ name: string }>(\n \"SELECT t.name FROM tags t JOIN session_tags st ON st.tag_id = t.id WHERE st.session_id = $1 ORDER BY t.name\",\n [sessionId]\n );\n return r.rows.map((row) => row.name);\n }\n\n // -------------------------------------------------------------------------\n // Links\n // -------------------------------------------------------------------------\n\n async addLink(input: {\n sessionId: number;\n targetProjectId: number;\n linkType: LinkType;\n createdAt: number;\n }): Promise<void> {\n await this.pool.query(\n \"INSERT INTO links (session_id, target_project_id, link_type, created_at) VALUES ($1, $2, $3, $4)\",\n [input.sessionId, input.targetProjectId, input.linkType, input.createdAt]\n );\n }\n\n async listLinksForSession(sessionId: number): Promise<Array<{ id: number; target_project_id: number }>> {\n const r = await this.pool.query(\"SELECT id, target_project_id FROM links WHERE session_id = $1\", [\n sessionId,\n ]);\n return r.rows as Array<{ id: number; target_project_id: number }>;\n }\n\n async listLinksForProject(projectId: number): Promise<Array<{ id: number; session_id: number }>> {\n const r = await this.pool.query(\"SELECT id, session_id FROM links WHERE target_project_id = $1\", [\n projectId,\n ]);\n return r.rows as Array<{ id: number; session_id: number }>;\n }\n\n async linkExists(sessionId: number, targetProjectId: number): Promise<boolean> {\n const r = await this.pool.query(\n \"SELECT 1 FROM links WHERE session_id = $1 AND target_project_id = $2 LIMIT 1\",\n [sessionId, targetProjectId]\n );\n return (r.rowCount ?? 0) > 0;\n }\n\n async deleteLink(id: number): Promise<void> {\n await this.pool.query(\"DELETE FROM links WHERE id = $1\", [id]);\n }\n\n async deleteLinksTargetingProject(projectId: number): Promise<void> {\n await this.pool.query(\"DELETE FROM links WHERE target_project_id = $1\", [projectId]);\n }\n\n async deleteLinksFromProjectSessions(projectId: number): Promise<void> {\n await this.pool.query(\n \"DELETE FROM links WHERE session_id IN (SELECT id FROM sessions WHERE project_id = $1)\",\n [projectId]\n );\n }\n\n async retargetLink(id: number, targetProjectId: number): Promise<void> {\n await this.pool.query(\"UPDATE links SET target_project_id = $1 WHERE id = $2\", [targetProjectId, id]);\n }\n\n async moveLinkToSession(id: number, sessionId: number): Promise<void> {\n await this.pool.query(\"UPDATE links SET session_id = $1 WHERE id = $2\", [sessionId, id]);\n }\n\n async reassignLinksTarget(fromProjectId: number, toProjectId: number): Promise<void> {\n // Postgres UPDATE has no OR IGNORE; skip rows that would collide with an\n // existing (session_id, target_project_id) row, matching SQLite's behaviour\n // of silently leaving those rows for the caller's follow-up cleanup.\n await this.pool.query(\n `UPDATE links SET target_project_id = $1 WHERE target_project_id = $2\n AND NOT EXISTS (\n SELECT 1 FROM links l2 WHERE l2.session_id = links.session_id AND l2.target_project_id = $1\n )`,\n [toProjectId, fromProjectId]\n );\n }\n\n async countLinksForProject(projectId: number): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM links WHERE target_project_id = $1\",\n [projectId]\n );\n return parseInt(r.rows[0].n, 10);\n }\n\n async deleteSelfLinksForProject(projectId: number): Promise<void> {\n await this.pool.query(\n `DELETE FROM links WHERE target_project_id = $1\n AND session_id IN (SELECT id FROM sessions WHERE project_id = $2)`,\n [projectId, projectId]\n );\n }\n\n // -------------------------------------------------------------------------\n // Compaction log\n // -------------------------------------------------------------------------\n\n async appendCompactionLog(entry: NewCompactionLogEntry): Promise<void> {\n await this.pool.query(\n `INSERT INTO compaction_log (project_id, session_id, trigger, files_written, token_count, created_at)\n VALUES ($1, $2, $3, $4, $5, $6)`,\n [entry.projectId, entry.sessionId, entry.trigger, entry.filesWritten, entry.tokenCount, entry.createdAt]\n );\n }\n\n async countCompactionLogsForProject(projectId: number): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM compaction_log WHERE project_id = $1\",\n [projectId]\n );\n return parseInt(r.rows[0].n, 10);\n }\n\n async moveCompactionLogsByProject(fromProjectId: number, toProjectId: number): Promise<number> {\n const r = await this.pool.query(\"UPDATE compaction_log SET project_id = $1 WHERE project_id = $2\", [\n toProjectId,\n fromProjectId,\n ]);\n return r.rowCount ?? 0;\n }\n\n async moveCompactionLogsBySession(fromSessionId: number, toSessionId: number): Promise<void> {\n await this.pool.query(\"UPDATE compaction_log SET session_id = $1 WHERE session_id = $2\", [\n toSessionId,\n fromSessionId,\n ]);\n }\n\n async deleteCompactionLogsForProject(projectId: number): Promise<void> {\n await this.pool.query(\"DELETE FROM compaction_log WHERE project_id = $1\", [projectId]);\n }\n\n // -------------------------------------------------------------------------\n // Project merge\n // -------------------------------------------------------------------------\n\n async planProjectMerge(fromSlug: string, intoSlug: string): Promise<MergePlan> {\n if (fromSlug === intoSlug) {\n throw new MergeError(`Cannot merge ${fromSlug} into itself.`);\n }\n\n const fromR = await this.pool.query<{ id: number; slug: string }>(\n \"SELECT id, slug FROM projects WHERE slug = $1\",\n [fromSlug]\n );\n const intoR = await this.pool.query<{ id: number; slug: string }>(\n \"SELECT id, slug FROM projects WHERE slug = $1\",\n [intoSlug]\n );\n const from = fromR.rows[0];\n const into = intoR.rows[0];\n if (!from) throw new MergeError(`No project with slug \"${fromSlug}\".`);\n if (!into) throw new MergeError(`No project with slug \"${intoSlug}\".`);\n\n const maxRow = await this.pool.query<{ n: number }>(\n \"SELECT COALESCE(MAX(number), 0) AS n FROM sessions WHERE project_id = $1\",\n [into.id]\n );\n let next = maxRow.rows[0].n;\n\n const incoming = await this.pool.query<{ id: number; number: number }>(\n \"SELECT id, number FROM sessions WHERE project_id = $1 ORDER BY number ASC\",\n [from.id]\n );\n const sessions = incoming.rows.map((s) => ({ id: s.id, from: s.number, to: ++next }));\n\n const count = async (sql: string, ...args: unknown[]): Promise<number> => {\n const r = await this.pool.query<{ n: string }>(sql, args);\n return parseInt(r.rows[0].n, 10);\n };\n\n const aliasTaken =\n (await count(\"SELECT COUNT(*)::text AS n FROM aliases WHERE alias = $1\", from.slug)) > 0 ||\n (await count(\"SELECT COUNT(*)::text AS n FROM projects WHERE slug = $1\", from.slug)) > 1;\n\n return {\n fromId: from.id,\n fromSlug: from.slug,\n intoId: into.id,\n intoSlug: into.slug,\n sessions,\n tags: await count(\"SELECT COUNT(*)::text AS n FROM project_tags WHERE project_id = $1\", from.id),\n aliases: await count(\"SELECT COUNT(*)::text AS n FROM aliases WHERE project_id = $1\", from.id),\n compactions: await count(\n \"SELECT COUNT(*)::text AS n FROM compaction_log WHERE project_id = $1\",\n from.id\n ),\n links: await count(\"SELECT COUNT(*)::text AS n FROM links WHERE target_project_id = $1\", from.id),\n aliasToAdd: aliasTaken ? undefined : from.slug,\n };\n }\n\n async applyProjectMerge(plan: MergePlan): Promise<void> {\n const client: PoolClient = await this.pool.connect();\n try {\n await client.query(\"BEGIN\");\n\n for (const s of plan.sessions) {\n await client.query(\"UPDATE sessions SET project_id = $1, number = $2 WHERE id = $3\", [\n plan.intoId,\n s.to,\n s.id,\n ]);\n }\n\n await client.query(\n `INSERT INTO project_tags (project_id, tag_id)\n SELECT $1, tag_id FROM project_tags WHERE project_id = $2\n ON CONFLICT (project_id, tag_id) DO NOTHING`,\n [plan.intoId, plan.fromId]\n );\n await client.query(\"DELETE FROM project_tags WHERE project_id = $1\", [plan.fromId]);\n\n await client.query(\"UPDATE aliases SET project_id = $1 WHERE project_id = $2\", [\n plan.intoId,\n plan.fromId,\n ]);\n\n await client.query(\"UPDATE compaction_log SET project_id = $1 WHERE project_id = $2\", [\n plan.intoId,\n plan.fromId,\n ]);\n\n // Links: same NOT EXISTS guard as reassignLinksTarget, plus the same\n // two follow-up cleanups SQLite's applyMerge does.\n await client.query(\n `UPDATE links SET target_project_id = $1 WHERE target_project_id = $2\n AND NOT EXISTS (\n SELECT 1 FROM links l2 WHERE l2.session_id = links.session_id AND l2.target_project_id = $1\n )`,\n [plan.intoId, plan.fromId]\n );\n await client.query(\"DELETE FROM links WHERE target_project_id = $1\", [plan.fromId]);\n await client.query(\n `DELETE FROM links WHERE target_project_id = $1\n AND session_id IN (SELECT id FROM sessions WHERE project_id = $2)`,\n [plan.intoId, plan.intoId]\n );\n\n if (plan.aliasToAdd) {\n await client.query(\n \"INSERT INTO aliases (alias, project_id) VALUES ($1, $2) ON CONFLICT (alias) DO NOTHING\",\n [plan.aliasToAdd, plan.intoId]\n );\n }\n\n await client.query(\"DELETE FROM projects WHERE id = $1\", [plan.fromId]);\n\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n }\n}\n","/**\n * SQLite DDL for the PAI registry database.\n *\n * Tables:\n * - projects — tracked project directories with type and status\n * - sessions — per-project session notes\n * - tags — normalised tag vocabulary\n * - project_tags — M:N join between projects and tags\n * - session_tags — M:N join between sessions and tags\n * - aliases — alternative slugs that resolve to a project\n * - compaction_log — audit trail for context-compaction events\n * - schema_version — single-row migration version tracking\n */\n\nimport type { Database } from \"better-sqlite3\";\n\nexport const SCHEMA_VERSION = 4;\n\nexport const CREATE_TABLES_SQL = `\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS projects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n slug TEXT NOT NULL UNIQUE,\n display_name TEXT NOT NULL,\n root_path TEXT NOT NULL UNIQUE,\n encoded_dir TEXT NOT NULL UNIQUE,\n type TEXT NOT NULL DEFAULT 'local'\n CHECK(type IN ('local','central','obsidian-linked','external')),\n status TEXT NOT NULL DEFAULT 'active'\n CHECK(status IN ('active','archived','migrating')),\n parent_id INTEGER,\n obsidian_link TEXT,\n claude_notes_dir TEXT,\n session_config TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n archived_at INTEGER,\n FOREIGN KEY (parent_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS sessions (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n number INTEGER NOT NULL,\n date TEXT NOT NULL,\n slug TEXT NOT NULL,\n title TEXT NOT NULL,\n filename TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'open'\n CHECK(status IN ('open','completed','compacted')),\n claude_session_id TEXT,\n token_count INTEGER,\n created_at INTEGER NOT NULL,\n closed_at INTEGER,\n UNIQUE (project_id, number),\n FOREIGN KEY (project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS tags (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE\n);\n\nCREATE TABLE IF NOT EXISTS project_tags (\n project_id INTEGER NOT NULL,\n tag_id INTEGER NOT NULL,\n PRIMARY KEY (project_id, tag_id),\n FOREIGN KEY (project_id) REFERENCES projects(id),\n FOREIGN KEY (tag_id) REFERENCES tags(id)\n);\n\nCREATE TABLE IF NOT EXISTS session_tags (\n session_id INTEGER NOT NULL,\n tag_id INTEGER NOT NULL,\n PRIMARY KEY (session_id, tag_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id),\n FOREIGN KEY (tag_id) REFERENCES tags(id)\n);\n\nCREATE TABLE IF NOT EXISTS aliases (\n alias TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n FOREIGN KEY (project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS compaction_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n session_id INTEGER,\n trigger TEXT NOT NULL\n CHECK(trigger IN ('precompact','manual','end-session')),\n files_written TEXT NOT NULL,\n token_count INTEGER,\n created_at INTEGER NOT NULL,\n FOREIGN KEY (project_id) REFERENCES projects(id),\n FOREIGN KEY (session_id) REFERENCES sessions(id)\n);\n\nCREATE TABLE IF NOT EXISTS links (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id INTEGER NOT NULL,\n target_project_id INTEGER NOT NULL,\n link_type TEXT NOT NULL DEFAULT 'related'\n CHECK(link_type IN ('related','follow-up','reference')),\n created_at INTEGER NOT NULL,\n UNIQUE (session_id, target_project_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id),\n FOREIGN KEY (target_project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_at INTEGER NOT NULL\n);\n\n-- Indexes\nCREATE INDEX IF NOT EXISTS idx_projects_slug ON projects(slug);\nCREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);\nCREATE INDEX IF NOT EXISTS idx_projects_type ON projects(type);\nCREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_date ON sessions(date);\nCREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\nCREATE INDEX IF NOT EXISTS idx_sessions_claude ON sessions(claude_session_id);\nCREATE INDEX IF NOT EXISTS idx_pc_project ON project_tags(project_id);\n`;\n\n/**\n * Run the full DDL against an open database connection.\n *\n * The function is idempotent — every statement uses IF NOT EXISTS so it is\n * safe to call on an already-initialised database. After creating the tables\n * it inserts the current SCHEMA_VERSION into schema_version if no row exists\n * yet.\n */\nexport function initializeSchema(db: Database): void {\n // better-sqlite3's exec() runs multiple semicolon-separated statements\n db.exec(CREATE_TABLES_SQL);\n\n const row = db\n .prepare(\"SELECT version FROM schema_version WHERE version = ?\")\n .get(SCHEMA_VERSION);\n\n if (!row) {\n db.prepare(\n \"INSERT INTO schema_version (version, applied_at) VALUES (?, ?)\"\n ).run(SCHEMA_VERSION, Date.now());\n }\n}\n\n/**\n * Apply incremental schema migrations to an already-initialised database.\n *\n * Each migration is guarded by a version check so it is safe to call on\n * databases at any schema version — already-applied migrations are skipped.\n */\nexport function runMigrations(db: Database): void {\n const currentRow = db\n .prepare(\"SELECT version FROM schema_version ORDER BY version DESC LIMIT 1\")\n .get() as { version: number } | undefined;\n\n const current = currentRow?.version ?? 0;\n\n // Migration v1 → v2: add claude_notes_dir column to projects\n if (current < 2) {\n db.transaction(() => {\n // Use a try/catch so re-running on a DB that already has the column is safe\n try {\n db.exec(\"ALTER TABLE projects ADD COLUMN claude_notes_dir TEXT\");\n } catch {\n // Column may already exist (e.g. fresh DB created with v2 DDL)\n }\n db.prepare(\n \"INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)\"\n ).run(2, Date.now());\n })();\n }\n\n // Migration v2 → v3: add links table for cross-project session references\n if (current < 3) {\n db.transaction(() => {\n try {\n db.exec(`\n CREATE TABLE IF NOT EXISTS links (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id INTEGER NOT NULL,\n target_project_id INTEGER NOT NULL,\n link_type TEXT NOT NULL DEFAULT 'related'\n CHECK(link_type IN ('related','follow-up','reference')),\n created_at INTEGER NOT NULL,\n UNIQUE (session_id, target_project_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id),\n FOREIGN KEY (target_project_id) REFERENCES projects(id)\n )\n `);\n } catch {\n // Table may already exist (fresh DB created with v3 DDL)\n }\n db.prepare(\n \"INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)\"\n ).run(3, Date.now());\n })();\n }\n\n // Migration v3 → v4: add session_config column to projects\n if (current < 4) {\n db.transaction(() => {\n try {\n db.exec(\"ALTER TABLE projects ADD COLUMN session_config TEXT\");\n } catch {\n // Column may already exist\n }\n db.prepare(\n \"INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)\"\n ).run(4, Date.now());\n })();\n }\n}\n","/**\n * Database connection helper for the PAI registry.\n *\n * Uses better-sqlite3 (synchronous API) to open or create registry.db.\n * On first open it runs the full DDL via initializeSchema().\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport BetterSqlite3 from \"better-sqlite3\";\nimport type { Database } from \"better-sqlite3\";\nimport { initializeSchema, runMigrations } from \"./registry-schema.js\";\nimport { paiHomePath, resolvePaiFile } from \"../../config/pai-home.js\";\n\nexport type { Database };\n\n/** Old registry path, inside the ~/.pai/ directory (pre-2026-09-19). */\nexport function oldRegistryPath(): string {\n return join(homedir(), \".pai\", \"registry.db\");\n}\n\n/** Registry path: PAI_HOME/registry.db if present, else the old\n * ~/.pai/registry.db (one-time stderr notice), else the new path. */\nexport function registryDbPath(): string {\n return resolvePaiFile(paiHomePath(\"registry.db\"), [oldRegistryPath()], \"pai config migrate --registry\");\n}\n\n/**\n * Open (or create) the PAI registry database.\n *\n * @param path Absolute path to registry.db. Defaults to PAI_HOME/registry.db\n * (falling back to the pre-2026-09-19 ~/.pai/registry.db).\n * @returns An open better-sqlite3 Database instance.\n *\n * Side effects on first call:\n * - Creates the parent directory if it does not exist.\n * - Enables WAL journal mode.\n * - Runs initializeSchema() if schema_version is empty.\n */\nexport function openRegistry(path: string = registryDbPath()): Database {\n // Ensure the directory exists before SQLite tries to create the file\n mkdirSync(dirname(path), { recursive: true });\n\n const db = new BetterSqlite3(path);\n\n // WAL gives better concurrent read performance and crash safety\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"foreign_keys = ON\");\n\n // Check whether the schema has been applied before\n const tableExists = db\n .prepare(\n `SELECT name FROM sqlite_master\n WHERE type = 'table' AND name = 'schema_version'`\n )\n .get();\n\n if (!tableExists) {\n // Brand-new database — apply the full schema\n initializeSchema(db);\n } else {\n const row = db\n .prepare(\"SELECT version FROM schema_version LIMIT 1\")\n .get() as { version: number } | undefined;\n\n if (!row) {\n // Table exists but is empty — apply schema (handles partial init)\n initializeSchema(db);\n }\n }\n\n // Apply any pending incremental migrations\n runMigrations(db);\n\n return db;\n}\n","/**\n * planMerge/applyMerge — fold one registered project into another without\n * losing anything, against a raw better-sqlite3 Database.\n *\n * Moved out of src/registry/merge.ts (which keeps only the pure MergePlan\n * type and MergeError class) so that the one place in the codebase touching\n * better-sqlite3 for this operation is under src/storage/. Wrapped by\n * SQLiteRegistryBackend.planProjectMerge/applyProjectMerge\n * (../registry-sqlite.ts).\n *\n * The registry accumulates rows that describe the same work twice: a project\n * registered under two spellings of one directory, a vault-notes twin of a code\n * project, a slug that collided and got a `-1`. Measured 2026-08-04 there were\n * six such rows, and between them they carried ten sessions.\n *\n * Those sessions are the only thing of value on the losing row, which is why\n * `archive` is the wrong remedy and a plain DELETE is worse. `pai project health`\n * suggested archiving for every unhealthy row, and archiving a duplicate leaves\n * the sessions attached to a row nobody will look at again.\n *\n * FIVE tables reference a project, and `PRAGMA foreign_keys` is 0 on this\n * database — so deleting a project row does not fail, it silently orphans rows in\n * sessions, project_tags, aliases, compaction_log and links. Anything that merges\n * by hand will miss at least one of them. Hence one function.\n *\n * Two constraints shape the implementation:\n *\n * sessions UNIQUE (project_id, number) — incoming sessions must be renumbered,\n * or the insert collides with the\n * target's own numbering\n * links UNIQUE (session_id, target_project_id)\n * project_tags PRIMARY KEY (project_id, tag_id)\n * — both can already hold the row we are\n * about to point at the target\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { MergeError, type MergePlan } from \"../../registry/merge.js\";\n\n/**\n * What a merge would do, without doing it.\n *\n * Built as a plan first because the session renumbering is the part a reader will\n * not predict, and printing \"0001 -> 0016\" is the difference between a command\n * that can be trusted and one that has to be taken on faith.\n */\nexport function planMerge(db: Database, fromSlug: string, intoSlug: string): MergePlan {\n if (fromSlug === intoSlug) {\n throw new MergeError(`Cannot merge ${fromSlug} into itself.`);\n }\n\n const from = db\n .prepare(\"SELECT id, slug FROM projects WHERE slug = ?\")\n .get(fromSlug) as { id: number; slug: string } | undefined;\n const into = db\n .prepare(\"SELECT id, slug FROM projects WHERE slug = ?\")\n .get(intoSlug) as { id: number; slug: string } | undefined;\n\n if (!from) throw new MergeError(`No project with slug \"${fromSlug}\".`);\n if (!into) throw new MergeError(`No project with slug \"${intoSlug}\".`);\n\n // Where the target's numbering currently ends. Incoming sessions continue from\n // there rather than keeping their own numbers, which would collide.\n const maxRow = db\n .prepare(\"SELECT COALESCE(MAX(number), 0) AS n FROM sessions WHERE project_id = ?\")\n .get(into.id) as { n: number };\n let next = maxRow.n;\n\n const incoming = db\n .prepare(\"SELECT id, number FROM sessions WHERE project_id = ? ORDER BY number ASC\")\n .all(from.id) as { id: number; number: number }[];\n\n const sessions = incoming.map((s) => ({ id: s.id, from: s.number, to: ++next }));\n\n const count = (sql: string, ...args: unknown[]): number =>\n (db.prepare(sql).get(...args) as { n: number }).n;\n\n // The losing slug becomes an alias of the winner, so anything that resolved by\n // that name keeps working — including a human who remembers the old name.\n const aliasTaken =\n count(\"SELECT COUNT(*) AS n FROM aliases WHERE alias = ?\", from.slug) > 0 ||\n count(\"SELECT COUNT(*) AS n FROM projects WHERE slug = ?\", from.slug) > 1;\n\n return {\n fromId: from.id,\n fromSlug: from.slug,\n intoId: into.id,\n intoSlug: into.slug,\n sessions,\n tags: count(\"SELECT COUNT(*) AS n FROM project_tags WHERE project_id = ?\", from.id),\n aliases: count(\"SELECT COUNT(*) AS n FROM aliases WHERE project_id = ?\", from.id),\n compactions: count(\n \"SELECT COUNT(*) AS n FROM compaction_log WHERE project_id = ?\",\n from.id\n ),\n links: count(\"SELECT COUNT(*) AS n FROM links WHERE target_project_id = ?\", from.id),\n aliasToAdd: aliasTaken ? undefined : from.slug,\n };\n}\n\n/**\n * Apply a plan. One transaction: either the whole row is folded in or nothing is.\n *\n * A half-merged project is the worst outcome available here — sessions moved but\n * the row still present, or the row gone and its tags orphaned — and with foreign\n * keys off nothing would complain.\n */\nexport function applyMerge(db: Database, plan: MergePlan): void {\n const run = db.transaction(() => {\n // Sessions, one at a time because each gets its own new number.\n const move = db.prepare(\n \"UPDATE sessions SET project_id = ?, number = ? WHERE id = ?\"\n );\n for (const s of plan.sessions) move.run(plan.intoId, s.to, s.id);\n\n // Tags: OR IGNORE because (project_id, tag_id) is the primary key and the\n // target may already carry the same tag.\n db.prepare(\n `INSERT OR IGNORE INTO project_tags (project_id, tag_id)\n SELECT ?, tag_id FROM project_tags WHERE project_id = ?`\n ).run(plan.intoId, plan.fromId);\n db.prepare(\"DELETE FROM project_tags WHERE project_id = ?\").run(plan.fromId);\n\n // Existing aliases of the loser now point at the winner.\n db.prepare(\"UPDATE aliases SET project_id = ? WHERE project_id = ?\").run(\n plan.intoId,\n plan.fromId\n );\n\n db.prepare(\"UPDATE compaction_log SET project_id = ? WHERE project_id = ?\").run(\n plan.intoId,\n plan.fromId\n );\n\n // Links: same collision shape as tags, plus one extra case — a session that\n // linked to the loser may now be linking to its own project, which is not a\n // relationship worth keeping.\n db.prepare(\n `UPDATE OR IGNORE links SET target_project_id = ? WHERE target_project_id = ?`\n ).run(plan.intoId, plan.fromId);\n db.prepare(\"DELETE FROM links WHERE target_project_id = ?\").run(plan.fromId);\n db.prepare(\n `DELETE FROM links WHERE target_project_id = ?\n AND session_id IN (SELECT id FROM sessions WHERE project_id = ?)`\n ).run(plan.intoId, plan.intoId);\n\n // And keep the old name resolvable.\n if (plan.aliasToAdd) {\n db.prepare(\"INSERT OR IGNORE INTO aliases (alias, project_id) VALUES (?, ?)\").run(\n plan.aliasToAdd,\n plan.intoId\n );\n }\n\n db.prepare(\"DELETE FROM projects WHERE id = ?\").run(plan.fromId);\n });\n\n run();\n}\n","/**\n * SQLiteRegistryBackend — wraps the existing better-sqlite3 registry.db\n * (src/registry/db.ts, src/registry/merge.ts) behind the RegistryBackend\n * interface.\n *\n * Thin adapter: every method is the same SQL the 64 direct callers run today,\n * just relocated here and wrapped in Promise.resolve(...) so the interface can\n * be async (matching StorageBackend, and Postgres in unit 4) without changing\n * SQLite behaviour.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { planMerge, applyMerge } from \"./sqlite/registry-merge.js\";\nimport type {\n RegistryBackend,\n Project,\n ProjectWithSessionStats,\n Session,\n SessionWithProject,\n NewProject,\n NewProjectWithSlugRetry,\n NewSession,\n NewCompactionLogEntry,\n ListProjectsOptions,\n ListProjectsByPathLengthOptions,\n ListProjectsWithStatsOptions,\n ListNamedProjectsOptions,\n ListSessionsOptions,\n ListSessionsForProjectOptions,\n ProjectType,\n ProjectStatus,\n SessionStatus,\n LinkType,\n MergePlan,\n} from \"./registry-interface.js\";\n\nexport class SQLiteRegistryBackend implements RegistryBackend {\n readonly backendType = \"sqlite\" as const;\n\n private db: Database;\n\n constructor(db: Database) {\n this.db = db;\n }\n\n /** Expose the raw better-sqlite3 Database handle for not-yet-converted callers. */\n getRawDb(): Database {\n return this.db;\n }\n\n async close(): Promise<void> {\n try {\n this.db.close();\n } catch {\n // ignore\n }\n }\n\n async resetRegistry(): Promise<void> {\n const run = this.db.transaction(() => {\n this.db.prepare(\"DELETE FROM links\").run();\n this.db.prepare(\"DELETE FROM compaction_log\").run();\n this.db.prepare(\"DELETE FROM session_tags\").run();\n this.db.prepare(\"DELETE FROM project_tags\").run();\n this.db.prepare(\"DELETE FROM aliases\").run();\n this.db.prepare(\"DELETE FROM sessions\").run();\n this.db.prepare(\"DELETE FROM tags\").run();\n this.db.prepare(\"DELETE FROM projects\").run();\n });\n run();\n }\n\n // -------------------------------------------------------------------------\n // Projects — reads\n // -------------------------------------------------------------------------\n\n async getProjectById(id: number): Promise<Project | null> {\n const row = this.db.prepare(\"SELECT * FROM projects WHERE id = ?\").get(id) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectBySlug(\n slug: string,\n opts: { caseInsensitive?: boolean; excludeId?: number } = {}\n ): Promise<Project | null> {\n let sql = opts.caseInsensitive\n ? \"SELECT * FROM projects WHERE lower(slug) = lower(?)\"\n : \"SELECT * FROM projects WHERE slug = ?\";\n const params: unknown[] = [slug];\n if (opts.excludeId !== undefined) {\n sql += \" AND id != ?\";\n params.push(opts.excludeId);\n }\n const row = this.db.prepare(sql).get(...params) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectByAlias(\n alias: string,\n opts: { caseInsensitive?: boolean } = {}\n ): Promise<Project | null> {\n const sql = opts.caseInsensitive\n ? \"SELECT p.* FROM projects p JOIN aliases a ON a.project_id = p.id WHERE lower(a.alias) = lower(?)\"\n : \"SELECT p.* FROM projects p JOIN aliases a ON a.project_id = p.id WHERE a.alias = ?\";\n const row = this.db.prepare(sql).get(alias) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectByRootPath(rootPath: string, opts: { excludeId?: number } = {}): Promise<Project | null> {\n let sql = \"SELECT * FROM projects WHERE root_path = ?\";\n const params: unknown[] = [rootPath];\n if (opts.excludeId !== undefined) {\n sql += \" AND id != ?\";\n params.push(opts.excludeId);\n }\n const row = this.db.prepare(sql).get(...params) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectByEncodedDir(encodedDir: string, opts: { excludeId?: number } = {}): Promise<Project | null> {\n let sql = \"SELECT * FROM projects WHERE encoded_dir = ?\";\n const params: unknown[] = [encodedDir];\n if (opts.excludeId !== undefined) {\n sql += \" AND id != ?\";\n params.push(opts.excludeId);\n }\n const row = this.db.prepare(sql).get(...params) as Project | undefined;\n return row ?? null;\n }\n\n async findProjectByCwdPrefix(cwd: string): Promise<Project | null> {\n const row = this.db\n .prepare(\n `SELECT * FROM projects WHERE status = 'active' AND ? LIKE root_path || '%'\n ORDER BY length(root_path) DESC LIMIT 1`\n )\n .get(cwd) as Project | undefined;\n return row ?? null;\n }\n\n async listProjects(opts: ListProjectsOptions = {}): Promise<Project[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (opts.status) {\n where.push(\"p.status = ?\");\n params.push(opts.status);\n }\n if (opts.excludeArchived) {\n where.push(\"p.status != 'archived'\");\n }\n if (opts.tagName) {\n where.push(\n \"p.id IN (SELECT pt.project_id FROM project_tags pt JOIN tags t ON pt.tag_id = t.id WHERE t.name = ?)\"\n );\n params.push(opts.tagName);\n }\n\n let sql = \"SELECT p.* FROM projects p\";\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n\n switch (opts.orderBy) {\n case \"updated_desc\":\n sql += \" ORDER BY p.updated_at DESC\";\n break;\n case \"slug\":\n sql += \" ORDER BY p.slug ASC\";\n break;\n case \"id\":\n sql += \" ORDER BY p.id\";\n break;\n case \"status_updated\":\n default:\n sql += \" ORDER BY p.status ASC, p.updated_at DESC\";\n break;\n }\n if (opts.limit !== undefined) {\n sql += \" LIMIT ?\";\n params.push(opts.limit);\n }\n\n return this.db.prepare(sql).all(...params) as Project[];\n }\n\n async listProjectsByPathLengthDesc(opts: ListProjectsByPathLengthOptions = {}): Promise<Project[]> {\n const where = opts.excludeArchived ? \"WHERE status != 'archived'\" : \"\";\n return this.db\n .prepare(`SELECT * FROM projects ${where} ORDER BY LENGTH(root_path) DESC`)\n .all() as Project[];\n }\n\n async listProjectsWithSessionStats(\n opts: ListProjectsWithStatsOptions = {}\n ): Promise<ProjectWithSessionStats[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (opts.status) {\n where.push(\"p.status = ?\");\n params.push(opts.status);\n }\n if (opts.tagId !== undefined) {\n where.push(\"p.id IN (SELECT project_id FROM project_tags WHERE tag_id = ?)\");\n params.push(opts.tagId);\n }\n\n let sql = `\n SELECT p.*,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n `;\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n sql +=\n opts.orderBy === \"updated_desc\"\n ? \" ORDER BY p.updated_at DESC\"\n : \" ORDER BY p.status ASC, p.updated_at DESC\";\n\n return this.db.prepare(sql).all(...params) as ProjectWithSessionStats[];\n }\n\n async listNamedProjects(\n opts: ListNamedProjectsOptions = {}\n ): Promise<Array<ProjectWithSessionStats & { name: string | null }>> {\n const join = opts.includeUnnamed ? \"LEFT JOIN\" : \"JOIN\";\n const sql = `\n SELECT p.*, a.alias AS name,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n ${join} aliases a ON a.project_id = p.id\n WHERE p.status = 'active'\n ORDER BY p.updated_at DESC\n `;\n return this.db.prepare(sql).all() as Array<ProjectWithSessionStats & { name: string | null }>;\n }\n\n async searchProjects(query: string, limit = 20): Promise<Project[]> {\n const q = `%${query}%`;\n return this.db\n .prepare(\n `SELECT * FROM projects WHERE slug LIKE ? OR display_name LIKE ? OR root_path LIKE ?\n ORDER BY updated_at DESC LIMIT ?`\n )\n .all(q, q, q, limit) as Project[];\n }\n\n async countProjects(opts: { status?: ProjectStatus } = {}): Promise<number> {\n const sql = opts.status\n ? \"SELECT COUNT(*) AS n FROM projects WHERE status = ?\"\n : \"SELECT COUNT(*) AS n FROM projects\";\n const row = opts.status\n ? (this.db.prepare(sql).get(opts.status) as { n: number })\n : (this.db.prepare(sql).get() as { n: number });\n return row.n;\n }\n\n async getMostRecentProjectUpdatedAt(): Promise<number | null> {\n const row = this.db.prepare(\"SELECT updated_at FROM projects ORDER BY updated_at DESC LIMIT 1\").get() as\n | { updated_at: number }\n | undefined;\n return row?.updated_at ?? null;\n }\n\n async countChildProjects(parentId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM projects WHERE parent_id = ?\").get(parentId) as {\n n: number;\n }).n;\n }\n\n async findSiblingProjectsBySlugPattern(\n excludeProjectId: number,\n slug: string,\n limit = 3\n ): Promise<Array<{ slug: string; root_path: string; session_count: number }>> {\n return this.db\n .prepare(\n `SELECT p.slug, p.root_path, COUNT(s.id) AS session_count\n FROM projects p LEFT JOIN sessions s ON s.project_id = p.id\n WHERE p.id != ? AND (p.slug = ? OR p.slug LIKE ? || '-%' OR ? LIKE p.slug || '-%')\n GROUP BY p.id HAVING session_count > 0 ORDER BY session_count DESC LIMIT ?`\n )\n .all(excludeProjectId, slug, slug, slug, limit) as Array<{\n slug: string;\n root_path: string;\n session_count: number;\n }>;\n }\n\n // -------------------------------------------------------------------------\n // Projects — writes\n // -------------------------------------------------------------------------\n\n async createProject(input: NewProject): Promise<Project> {\n const type: ProjectType = input.type ?? \"local\";\n const status: ProjectStatus = input.status ?? \"active\";\n const result = this.db\n .prepare(\n `INSERT INTO projects (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(\n input.slug,\n input.displayName,\n input.rootPath,\n input.encodedDir,\n type,\n status,\n input.createdAt,\n input.updatedAt\n );\n return (await this.getProjectById(result.lastInsertRowid as number))!;\n }\n\n async createProjectWithSlugRetry(\n input: NewProjectWithSlugRetry\n ): Promise<{ id: number; slug: string; created: boolean }> {\n let slug = input.baseSlug;\n let attempt = 0;\n const insert = this.db.prepare(\n `INSERT OR IGNORE INTO projects (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`\n );\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const info = insert.run(\n slug,\n input.displayName,\n input.rootPath,\n input.encodedDir,\n input.createdAt,\n input.updatedAt\n );\n if (info.changes > 0) {\n return { id: info.lastInsertRowid as number, slug, created: true };\n }\n\n const existing = this.db.prepare(\"SELECT id FROM projects WHERE root_path = ?\").get(input.rootPath) as\n | { id: number }\n | undefined;\n if (existing) {\n return { id: existing.id, slug, created: false };\n }\n\n attempt++;\n slug = `${input.baseSlug}-${attempt}`;\n }\n }\n\n async updateProjectPath(\n id: number,\n patch: { rootPath?: string; encodedDir?: string },\n updatedAt?: number\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [];\n if (patch.rootPath !== undefined) {\n sets.push(\"root_path = ?\");\n params.push(patch.rootPath);\n }\n if (patch.encodedDir !== undefined) {\n sets.push(\"encoded_dir = ?\");\n params.push(patch.encodedDir);\n }\n if (updatedAt !== undefined) {\n sets.push(\"updated_at = ?\");\n params.push(updatedAt);\n }\n if (!sets.length) return;\n params.push(id);\n this.db.prepare(`UPDATE projects SET ${sets.join(\", \")} WHERE id = ?`).run(...params);\n }\n\n async updateProjectStatus(\n id: number,\n status: ProjectStatus,\n opts: { archivedAt?: number | null; updatedAt?: number; requireCurrentStatus?: ProjectStatus } = {}\n ): Promise<void> {\n const sets: string[] = [\"status = ?\"];\n const params: unknown[] = [status];\n if (opts.archivedAt !== undefined) {\n sets.push(\"archived_at = ?\");\n params.push(opts.archivedAt);\n }\n if (opts.updatedAt !== undefined) {\n sets.push(\"updated_at = ?\");\n params.push(opts.updatedAt);\n }\n let sql = `UPDATE projects SET ${sets.join(\", \")} WHERE id = ?`;\n params.push(id);\n if (opts.requireCurrentStatus) {\n sql += \" AND status = ?\";\n params.push(opts.requireCurrentStatus);\n }\n this.db.prepare(sql).run(...params);\n }\n\n async updateProjectDisplayName(id: number, displayName: string, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET display_name = ?, updated_at = ? WHERE id = ?\")\n .run(displayName, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET display_name = ? WHERE id = ?\").run(displayName, id);\n }\n }\n\n async updateProjectType(id: number, type: ProjectType, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db.prepare(\"UPDATE projects SET type = ?, updated_at = ? WHERE id = ?\").run(type, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET type = ? WHERE id = ?\").run(type, id);\n }\n }\n\n async updateProjectSessionConfig(id: number, config: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET session_config = ?, updated_at = ? WHERE id = ?\")\n .run(config, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET session_config = ? WHERE id = ?\").run(config, id);\n }\n }\n\n async updateProjectClaudeNotesDir(id: number, dir: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET claude_notes_dir = ?, updated_at = ? WHERE id = ?\")\n .run(dir, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET claude_notes_dir = ? WHERE id = ?\").run(dir, id);\n }\n }\n\n async updateProjectObsidianLink(id: number, link: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET obsidian_link = ?, updated_at = ? WHERE id = ?\")\n .run(link, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET obsidian_link = ? WHERE id = ?\").run(link, id);\n }\n }\n\n async updateProjectSlug(id: number, slug: string, updatedAt: number): Promise<void> {\n this.db.prepare(\"UPDATE projects SET slug = ?, updated_at = ? WHERE id = ?\").run(slug, updatedAt, id);\n }\n\n async reassignProjectParent(fromParentId: number, toParentId: number): Promise<number> {\n return this.db.prepare(\"UPDATE projects SET parent_id = ? WHERE parent_id = ?\").run(toParentId, fromParentId)\n .changes;\n }\n\n async deleteProject(id: number): Promise<void> {\n this.db.prepare(\"DELETE FROM projects WHERE id = ?\").run(id);\n }\n\n async deleteProjectCascade(id: number): Promise<void> {\n const run = this.db.transaction(() => {\n this.db.prepare(\"DELETE FROM links WHERE target_project_id = ?\").run(id);\n this.db\n .prepare(\"DELETE FROM links WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)\")\n .run(id);\n this.db.prepare(\"DELETE FROM compaction_log WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM project_tags WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM aliases WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM sessions WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM projects WHERE id = ?\").run(id);\n });\n run();\n }\n\n // -------------------------------------------------------------------------\n // Tags\n // -------------------------------------------------------------------------\n\n async listTagsForProject(projectId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\n \"SELECT t.name FROM tags t JOIN project_tags pt ON pt.tag_id = t.id WHERE pt.project_id = ? ORDER BY t.name\"\n )\n .all(projectId) as Array<{ name: string }>;\n return rows.map((r) => r.name);\n }\n\n async listAllTags(): Promise<Array<{ id: number; name: string }>> {\n return this.db.prepare(\"SELECT id, name FROM tags ORDER BY name\").all() as Array<{\n id: number;\n name: string;\n }>;\n }\n\n async upsertTag(name: string): Promise<number> {\n this.db.prepare(\"INSERT OR IGNORE INTO tags (name) VALUES (?)\").run(name);\n return (this.db.prepare(\"SELECT id FROM tags WHERE name = ?\").get(name) as { id: number }).id;\n }\n\n async addProjectTag(projectId: number, tagId: number): Promise<void> {\n this.db.prepare(\"INSERT INTO project_tags (project_id, tag_id) VALUES (?, ?)\").run(projectId, tagId);\n }\n\n async projectHasTag(projectId: number, tagId: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM project_tags WHERE project_id = ? AND tag_id = ?\").get(projectId, tagId);\n }\n\n async deleteProjectTag(projectId: number, tagId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM project_tags WHERE project_id = ? AND tag_id = ?\").run(projectId, tagId);\n }\n\n async reassignProjectTag(fromProjectId: number, toProjectId: number, tagId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE project_tags SET project_id = ? WHERE project_id = ? AND tag_id = ?\")\n .run(toProjectId, fromProjectId, tagId);\n }\n\n async listProjectTagIds(projectId: number): Promise<number[]> {\n const rows = this.db.prepare(\"SELECT tag_id FROM project_tags WHERE project_id = ?\").all(projectId) as Array<{\n tag_id: number;\n }>;\n return rows.map((r) => r.tag_id);\n }\n\n async countProjectTagsForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM project_tags WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async copyProjectTags(fromProjectId: number, toProjectId: number): Promise<void> {\n this.db\n .prepare(\n \"INSERT OR IGNORE INTO project_tags (project_id, tag_id) SELECT ?, tag_id FROM project_tags WHERE project_id = ?\"\n )\n .run(toProjectId, fromProjectId);\n }\n\n async deleteProjectTagsForProject(projectId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM project_tags WHERE project_id = ?\").run(projectId);\n }\n\n // -------------------------------------------------------------------------\n // Aliases\n // -------------------------------------------------------------------------\n\n async resolveAlias(alias: string): Promise<number | null> {\n const row = this.db.prepare(\"SELECT project_id FROM aliases WHERE alias = ?\").get(alias) as\n | { project_id: number }\n | undefined;\n return row?.project_id ?? null;\n }\n\n async listAliasesForProject(projectId: number): Promise<string[]> {\n const rows = this.db.prepare(\"SELECT alias FROM aliases WHERE project_id = ? ORDER BY alias\").all(\n projectId\n ) as Array<{ alias: string }>;\n return rows.map((r) => r.alias);\n }\n\n async addAlias(alias: string, projectId: number): Promise<void> {\n this.db.prepare(\"INSERT INTO aliases (alias, project_id) VALUES (?, ?)\").run(alias, projectId);\n }\n\n async removeAlias(alias: string): Promise<void> {\n this.db.prepare(\"DELETE FROM aliases WHERE alias = ?\").run(alias);\n }\n\n async countAliasesForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM aliases WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async moveProjectAliases(fromProjectId: number, toProjectId: number): Promise<void> {\n this.db.prepare(\"UPDATE aliases SET project_id = ? WHERE project_id = ?\").run(toProjectId, fromProjectId);\n }\n\n async reassignAlias(alias: string, toProjectId: number): Promise<void> {\n this.db.prepare(\"UPDATE aliases SET project_id = ? WHERE alias = ?\").run(toProjectId, alias);\n }\n\n async listAliasMap(): Promise<Array<{ alias: string; slug: string; root_path: string }>> {\n return this.db\n .prepare(\n `SELECT a.alias AS alias, p.slug AS slug, p.root_path AS root_path\n FROM aliases a JOIN projects p ON p.id = a.project_id WHERE p.status != 'archived'`\n )\n .all() as Array<{ alias: string; slug: string; root_path: string }>;\n }\n\n // -------------------------------------------------------------------------\n // Sessions\n // -------------------------------------------------------------------------\n\n async getSessionById(id: number): Promise<Session | null> {\n const row = this.db.prepare(\"SELECT * FROM sessions WHERE id = ?\").get(id) as Session | undefined;\n return row ?? null;\n }\n\n async getSessionByNumber(projectId: number, number: number): Promise<Session | null> {\n const row = this.db.prepare(\"SELECT * FROM sessions WHERE project_id = ? AND number = ?\").get(\n projectId,\n number\n ) as Session | undefined;\n return row ?? null;\n }\n\n async getLatestSessionForProject(projectId: number): Promise<Session | null> {\n const row = this.db\n .prepare(\"SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1\")\n .get(projectId) as Session | undefined;\n return row ?? null;\n }\n\n async getMaxSessionNumber(projectId: number): Promise<number> {\n const row = this.db.prepare(\"SELECT COALESCE(MAX(number), 0) AS n FROM sessions WHERE project_id = ?\").get(\n projectId\n ) as { n: number };\n return row.n;\n }\n\n async listSessionsForProject(\n projectId: number,\n opts: ListSessionsForProjectOptions = {}\n ): Promise<Session[]> {\n const orderBy = opts.orderBy === \"created_desc\" ? \"created_at DESC\" : \"number ASC\";\n let sql = `SELECT * FROM sessions WHERE project_id = ? ORDER BY ${orderBy}`;\n const params: unknown[] = [projectId];\n if (opts.limit !== undefined) {\n sql += \" LIMIT ?\";\n params.push(opts.limit);\n }\n return this.db.prepare(sql).all(...params) as Session[];\n }\n\n async listSessions(opts: ListSessionsOptions = {}): Promise<SessionWithProject[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n if (opts.projectId !== undefined) {\n where.push(\"s.project_id = ?\");\n params.push(opts.projectId);\n }\n if (opts.status) {\n where.push(\"s.status = ?\");\n params.push(opts.status);\n }\n let sql = `\n SELECT s.*, p.slug AS project_slug, p.display_name AS project_name\n FROM sessions s JOIN projects p ON p.id = s.project_id\n `;\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n sql += \" ORDER BY s.date DESC, s.number DESC\";\n const limit = opts.limit ?? 20;\n sql += ` LIMIT ${limit}`;\n return this.db.prepare(sql).all(...params) as SessionWithProject[];\n }\n\n async findSessionByFilename(projectId: number, filename: string): Promise<{ id: number } | null> {\n const row = this.db\n .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND filename = ? LIMIT 1\")\n .get(projectId, filename) as { id: number } | undefined;\n return row ?? null;\n }\n\n async sessionNumberTaken(projectId: number, number: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM sessions WHERE project_id = ? AND number = ? LIMIT 1\").get(\n projectId,\n number\n );\n }\n\n async createSession(input: NewSession): Promise<Session> {\n const status = input.status ?? \"completed\";\n const result = this.db\n .prepare(\n `INSERT INTO sessions (project_id, number, date, slug, title, filename, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(input.projectId, input.number, input.date, input.slug, input.title, input.filename, status, input.createdAt);\n return (await this.getSessionById(result.lastInsertRowid as number))!;\n }\n\n async upsertSessionIfAbsent(input: NewSession): Promise<boolean> {\n const existing = this.db\n .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND number = ?\")\n .get(input.projectId, input.number);\n if (existing) return false;\n\n const status = input.status ?? \"completed\";\n this.db\n .prepare(\n `INSERT INTO sessions (project_id, number, date, slug, title, filename, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(input.projectId, input.number, input.date, input.slug, input.title, input.filename, status, input.createdAt);\n return true;\n }\n\n async updateSessionMeta(\n id: number,\n patch: { slug?: string; title?: string; filename?: string }\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [];\n if (patch.slug !== undefined) {\n sets.push(\"slug = ?\");\n params.push(patch.slug);\n }\n if (patch.title !== undefined) {\n sets.push(\"title = ?\");\n params.push(patch.title);\n }\n if (patch.filename !== undefined) {\n sets.push(\"filename = ?\");\n params.push(patch.filename);\n }\n if (!sets.length) return;\n params.push(id);\n this.db.prepare(`UPDATE sessions SET ${sets.join(\", \")} WHERE id = ?`).run(...params);\n }\n\n async updateSessionNumber(id: number, number: number, opts: { filename?: string } = {}): Promise<void> {\n if (opts.filename !== undefined) {\n this.db.prepare(\"UPDATE sessions SET number = ?, filename = ? WHERE id = ?\").run(number, opts.filename, id);\n } else {\n this.db.prepare(\"UPDATE sessions SET number = ? WHERE id = ?\").run(number, id);\n }\n }\n\n async updateSessionFilename(id: number, filename: string): Promise<void> {\n this.db.prepare(\"UPDATE sessions SET filename = ? WHERE id = ?\").run(filename, id);\n }\n\n async updateSessionStatus(id: number, status: SessionStatus, opts: { closedAt?: number } = {}): Promise<void> {\n if (opts.closedAt !== undefined) {\n this.db.prepare(\"UPDATE sessions SET status = ?, closed_at = ? WHERE id = ?\").run(status, opts.closedAt, id);\n } else {\n this.db.prepare(\"UPDATE sessions SET status = ? WHERE id = ?\").run(status, id);\n }\n }\n\n async moveSessionToProject(id: number, projectId: number, opts: { number?: number } = {}): Promise<void> {\n if (opts.number !== undefined) {\n this.db.prepare(\"UPDATE sessions SET project_id = ?, number = ? WHERE id = ?\").run(\n projectId,\n opts.number,\n id\n );\n } else {\n this.db.prepare(\"UPDATE sessions SET project_id = ? WHERE id = ?\").run(projectId, id);\n }\n }\n\n async deleteSession(id: number): Promise<void> {\n this.db.prepare(\"DELETE FROM sessions WHERE id = ?\").run(id);\n }\n\n async backfillFoldedSession(keepId: number, dropId: number): Promise<void> {\n this.db\n .prepare(\n `UPDATE sessions SET\n claude_session_id = COALESCE(claude_session_id,\n (SELECT claude_session_id FROM sessions WHERE id = ?)),\n token_count = COALESCE(token_count,\n (SELECT token_count FROM sessions WHERE id = ?)),\n closed_at = COALESCE(closed_at,\n (SELECT closed_at FROM sessions WHERE id = ?)),\n status = CASE WHEN status = 'open'\n AND (SELECT status FROM sessions WHERE id = ?) != 'open'\n THEN (SELECT status FROM sessions WHERE id = ?)\n ELSE status END\n WHERE id = ?`\n )\n .run(dropId, dropId, dropId, dropId, dropId, keepId);\n }\n\n async countSessionsForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async countSessions(): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM sessions\").get() as { n: number }).n;\n }\n\n async getMostRecentSessionDate(projectId: number): Promise<string | null> {\n const row = this.db\n .prepare(\"SELECT date FROM sessions WHERE project_id = ? ORDER BY date DESC LIMIT 1\")\n .get(projectId) as { date: string } | undefined;\n return row?.date ?? null;\n }\n\n async getMostRecentSessionCreatedAt(): Promise<number | null> {\n const row = this.db.prepare(\"SELECT created_at FROM sessions ORDER BY created_at DESC LIMIT 1\").get() as\n | { created_at: number }\n | undefined;\n return row?.created_at ?? null;\n }\n\n // -------------------------------------------------------------------------\n // Session tags\n // -------------------------------------------------------------------------\n\n async listSessionTagIds(sessionId: number): Promise<number[]> {\n const rows = this.db.prepare(\"SELECT tag_id FROM session_tags WHERE session_id = ?\").all(sessionId) as Array<{\n tag_id: number;\n }>;\n return rows.map((r) => r.tag_id);\n }\n\n async sessionHasTag(sessionId: number, tagId: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM session_tags WHERE session_id = ? AND tag_id = ?\").get(\n sessionId,\n tagId\n );\n }\n\n async addSessionTag(sessionId: number, tagId: number): Promise<void> {\n this.db.prepare(\"INSERT INTO session_tags (session_id, tag_id) VALUES (?, ?)\").run(sessionId, tagId);\n }\n\n async deleteSessionTag(sessionId: number, tagId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM session_tags WHERE session_id = ? AND tag_id = ?\").run(sessionId, tagId);\n }\n\n async reassignSessionTag(fromSessionId: number, toSessionId: number, tagId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE session_tags SET session_id = ? WHERE session_id = ? AND tag_id = ?\")\n .run(toSessionId, fromSessionId, tagId);\n }\n\n async listTagsForSession(sessionId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\n \"SELECT t.name FROM tags t JOIN session_tags st ON st.tag_id = t.id WHERE st.session_id = ? ORDER BY t.name\"\n )\n .all(sessionId) as Array<{ name: string }>;\n return rows.map((r) => r.name);\n }\n\n // -------------------------------------------------------------------------\n // Links\n // -------------------------------------------------------------------------\n\n async addLink(input: {\n sessionId: number;\n targetProjectId: number;\n linkType: LinkType;\n createdAt: number;\n }): Promise<void> {\n this.db\n .prepare(\"INSERT INTO links (session_id, target_project_id, link_type, created_at) VALUES (?, ?, ?, ?)\")\n .run(input.sessionId, input.targetProjectId, input.linkType, input.createdAt);\n }\n\n async listLinksForSession(sessionId: number): Promise<Array<{ id: number; target_project_id: number }>> {\n return this.db.prepare(\"SELECT id, target_project_id FROM links WHERE session_id = ?\").all(\n sessionId\n ) as Array<{ id: number; target_project_id: number }>;\n }\n\n async listLinksForProject(projectId: number): Promise<Array<{ id: number; session_id: number }>> {\n return this.db.prepare(\"SELECT id, session_id FROM links WHERE target_project_id = ?\").all(\n projectId\n ) as Array<{ id: number; session_id: number }>;\n }\n\n async linkExists(sessionId: number, targetProjectId: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM links WHERE session_id = ? AND target_project_id = ? LIMIT 1\").get(\n sessionId,\n targetProjectId\n );\n }\n\n async deleteLink(id: number): Promise<void> {\n this.db.prepare(\"DELETE FROM links WHERE id = ?\").run(id);\n }\n\n async deleteLinksTargetingProject(projectId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM links WHERE target_project_id = ?\").run(projectId);\n }\n\n async deleteLinksFromProjectSessions(projectId: number): Promise<void> {\n this.db\n .prepare(\"DELETE FROM links WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)\")\n .run(projectId);\n }\n\n async retargetLink(id: number, targetProjectId: number): Promise<void> {\n this.db.prepare(\"UPDATE links SET target_project_id = ? WHERE id = ?\").run(targetProjectId, id);\n }\n\n async moveLinkToSession(id: number, sessionId: number): Promise<void> {\n this.db.prepare(\"UPDATE links SET session_id = ? WHERE id = ?\").run(sessionId, id);\n }\n\n async reassignLinksTarget(fromProjectId: number, toProjectId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE OR IGNORE links SET target_project_id = ? WHERE target_project_id = ?\")\n .run(toProjectId, fromProjectId);\n }\n\n async countLinksForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM links WHERE target_project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async deleteSelfLinksForProject(projectId: number): Promise<void> {\n this.db\n .prepare(\n `DELETE FROM links WHERE target_project_id = ?\n AND session_id IN (SELECT id FROM sessions WHERE project_id = ?)`\n )\n .run(projectId, projectId);\n }\n\n // -------------------------------------------------------------------------\n // Compaction log\n // -------------------------------------------------------------------------\n\n async appendCompactionLog(entry: NewCompactionLogEntry): Promise<void> {\n this.db\n .prepare(\n `INSERT INTO compaction_log (project_id, session_id, trigger, files_written, token_count, created_at)\n VALUES (?, ?, ?, ?, ?, ?)`\n )\n .run(entry.projectId, entry.sessionId, entry.trigger, entry.filesWritten, entry.tokenCount, entry.createdAt);\n }\n\n async countCompactionLogsForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM compaction_log WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async moveCompactionLogsByProject(fromProjectId: number, toProjectId: number): Promise<number> {\n return this.db\n .prepare(\"UPDATE compaction_log SET project_id = ? WHERE project_id = ?\")\n .run(toProjectId, fromProjectId).changes;\n }\n\n async moveCompactionLogsBySession(fromSessionId: number, toSessionId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE compaction_log SET session_id = ? WHERE session_id = ?\")\n .run(toSessionId, fromSessionId);\n }\n\n async deleteCompactionLogsForProject(projectId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM compaction_log WHERE project_id = ?\").run(projectId);\n }\n\n // -------------------------------------------------------------------------\n // Project merge\n // -------------------------------------------------------------------------\n\n async planProjectMerge(fromSlug: string, intoSlug: string): Promise<MergePlan> {\n return planMerge(this.db, fromSlug, intoSlug);\n }\n\n async applyProjectMerge(plan: MergePlan): Promise<void> {\n applyMerge(this.db, plan);\n }\n}\n","/**\n * Storage backend factory.\n *\n * Reads the daemon config and returns the appropriate StorageBackend.\n *\n * When Postgres is the configured backend we NEVER silently fall back to\n * SQLite — doing so would split the corpus across two databases. Instead:\n * - Daemon (waitForPostgres: true) retries Postgres forever with capped\n * backoff until it comes up (handles the boot race where launchd starts\n * the daemon before Docker Desktop / Postgres is ready).\n * - CLI / one-shot callers (default) retry a few times, then throw a clear\n * error rather than returning a wrong/empty SQLite database.\n */\n\nimport type { PaiDaemonConfig } from \"../daemon/config.js\";\nimport type { StorageBackend } from \"./interface.js\";\nimport type { RegistryBackend } from \"./registry-interface.js\";\nimport { setBackendOutage, clearBackendOutage } from \"./outage.js\";\n// Static on purpose: a lazy import() of a dist chunk first hit after a rebuild\n// throws \"Cannot find module\" (prune-dist.mjs deletes superseded chunks).\nimport { PostgresBackend } from \"./postgres.js\";\nimport { routeNotification } from \"../notifications/router.js\";\nimport { loadConfig } from \"../daemon/config.js\";\nimport { openFederation } from \"./sqlite/federation-db.js\";\nimport { SQLiteBackend } from \"./sqlite.js\";\nimport { PostgresRegistryBackend } from \"./registry-postgres.js\";\nimport { openRegistry } from \"./sqlite/registry-db.js\";\nimport { SQLiteRegistryBackend } from \"./registry-sqlite.js\";\n\nexport interface StorageBackendOptions {\n /**\n * When true, retry Postgres indefinitely instead of giving up. Used by the\n * long-lived daemon so a not-yet-ready Postgres at boot is tolerated.\n * Defaults to false (one-shot CLI behaviour: bounded retries, then throw).\n */\n waitForPostgres?: boolean;\n}\n\n/** Backoff schedule (ms) for the bounded CLI retry path. */\nconst CLI_RETRY_DELAYS_MS = [500, 1_000, 2_000];\n\n/** Backoff cap (ms) for the daemon's infinite retry path. */\nconst DAEMON_RETRY_CAP_MS = 15_000;\n\n/**\n * Create and return the configured StorageBackend.\n *\n * Auto-behaviour:\n * - storageBackend = \"sqlite\" → SQLiteBackend always\n * - storageBackend = \"postgres\" → PostgresBackend (retried; never falls back)\n */\nexport async function createStorageBackend(\n config: PaiDaemonConfig,\n opts: StorageBackendOptions = {}\n): Promise<StorageBackend> {\n if (config.storageBackend === \"postgres\") {\n return await getSharedPostgresBackend(config, opts.waitForPostgres ?? false);\n }\n\n // Default: SQLite\n return createSQLiteBackend();\n}\n\n/**\n * Attempt a single Postgres connection (ensure DB + test). Returns the live\n * backend on success, or an error string describing why it failed.\n */\nasync function attemptPostgres(\n config: PaiDaemonConfig\n): Promise<{ backend: StorageBackend } | { error: string }> {\n const pgConfig = config.postgres ?? {};\n\n let backend: InstanceType<typeof PostgresBackend> | null = null;\n try {\n // Ensure the per-user database exists and has the schema applied.\n await PostgresBackend.ensureDatabase(pgConfig);\n\n backend = new PostgresBackend(pgConfig);\n const err = await backend.testConnection();\n if (err) {\n await backend.close().catch(() => {});\n return { error: err };\n }\n return { backend };\n } catch (e) {\n if (backend) await backend.close().catch(() => {});\n return { error: e instanceof Error ? e.message : String(e) };\n }\n}\n\n/**\n * Consecutive failures before the outage is escalated to the user.\n *\n * Not the first failure: a container restarting, or the daemon starting before\n * Docker is up, recovers within a few seconds and is not worth a notification.\n * By the fifth attempt the backoff has already spent tens of seconds, which is\n * long enough that something is actually wrong.\n */\nconst ESCALATE_AFTER_ATTEMPTS = 5;\n\n/** Tell the user the backend is down, through whatever channels are configured. */\nasync function notifyBackendDown(attempts: number, lastError: string): Promise<void> {\n try {\n await routeNotification(\n {\n event: \"error\",\n title: \"PAI: storage backend unreachable\",\n message:\n `Postgres has not answered in ${attempts} attempts (${lastError}). ` +\n `Indexing, session notes and the work queue are stalled until it returns. ` +\n `Check the container, then \\`pai daemon status\\`.`,\n },\n loadConfig().notifications\n );\n } catch {\n // A notification that cannot be sent must never take the daemon down with\n // it — the daemon retrying is still the useful behaviour here.\n }\n}\n\n/** And say when it comes back, so the alert is not left hanging. */\nasync function notifyBackendRecovered(\n attempts: number,\n since: number | null\n): Promise<void> {\n try {\n const mins = since ? Math.max(1, Math.round((Date.now() - since) / 60_000)) : null;\n await routeNotification(\n {\n event: \"completion\",\n title: \"PAI: storage backend back\",\n message:\n `Postgres answered after ${attempts} attempts` +\n (mins ? `, ${mins} min down` : \"\") +\n `. The queue will drain on its own.`,\n },\n loadConfig().notifications\n );\n } catch {\n /* same reasoning as above */\n }\n}\n\n/**\n * Shared retry loop: attempt a connection, back off, escalate, repeat — used\n * by both the federation and registry Postgres connectors so the retry/outage\n * behaviour (§6, \"never falls back to SQLite silently\") stays in one place.\n */\nasync function retryConnect<T>(\n attempt: () => Promise<{ backend: T } | { error: string }>,\n waitForever: boolean,\n label: string\n): Promise<T> {\n let attemptN = 0;\n let outageSince: number | null = null;\n let escalated = false;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n attemptN++;\n const result = await attempt();\n if (\"backend\" in result) {\n if (attemptN > 1) process.stderr.write(`[pai-daemon] Connected to ${label} (after ${attemptN} attempts).\\n`);\n // An outage that ended must stop being reported, or the status command\n // trades one wrong answer for another.\n clearBackendOutage();\n if (escalated) void notifyBackendRecovered(attemptN, outageSince);\n return result.backend;\n }\n\n const lastError = result.error;\n\n if (!waitForever && attemptN > CLI_RETRY_DELAYS_MS.length) {\n // Bounded CLI path exhausted — fail loudly, never silently use SQLite.\n throw new Error(\n `${label} unreachable after ${attemptN} attempts: ${lastError}. ` +\n `Is Docker Desktop / Postgres running? Refusing to fall back to SQLite ` +\n `(would split the corpus). Start Postgres and retry.`\n );\n }\n\n const delayMs = waitForever\n ? Math.min(DAEMON_RETRY_CAP_MS, 1_000 * 2 ** Math.min(attemptN - 1, 4))\n : CLI_RETRY_DELAYS_MS[attemptN - 1];\n\n process.stderr.write(\n `[pai-daemon] ${label} unavailable (${lastError}). ` +\n `Retry ${attemptN}${waitForever ? \"\" : `/${CLI_RETRY_DELAYS_MS.length + 1}`} ` +\n `in ${delayMs}ms...\\n`\n );\n\n // Publish the outage so `pai daemon status` can report it. Without this the\n // daemon retries silently forever and status still reads \"idle\" — which is\n // what happened for two days in July: 144 retries over 36 minutes, session\n // notes never written, and the one command anyone would run to check\n // reporting that everything was fine.\n setBackendOutage({\n backend: label,\n since: outageSince ?? (outageSince = Date.now()),\n attempts: attemptN,\n lastError: String(lastError),\n });\n\n // And escalate once, out loud, rather than only into a log nobody tails.\n // Once — not per retry — because a notification that repeats every few\n // seconds is filtered within a minute and stops being a signal at all.\n if (waitForever && attemptN === ESCALATE_AFTER_ATTEMPTS && !escalated) {\n escalated = true;\n void notifyBackendDown(attemptN, String(lastError));\n }\n\n await new Promise((r) => setTimeout(r, delayMs));\n }\n}\n\nasync function connectPostgres(\n config: PaiDaemonConfig,\n waitForever: boolean\n): Promise<StorageBackend> {\n return retryConnect(() => attemptPostgres(config), waitForever, \"PostgreSQL backend\");\n}\n\n/**\n * The process's one Postgres connection: storage and registry share a\n * single pg Pool (one connection budget) instead of each opening their own.\n * Whichever of createStorageBackend()/createRegistryBackend() runs first\n * creates it via connectPostgres()'s retry/outage handling; the other\n * reuses its pool through PostgresBackend.getPool(). Reset by closeStorage().\n */\nlet pgBackendPromise: Promise<StorageBackend> | null = null;\n\nfunction getSharedPostgresBackend(\n config: PaiDaemonConfig,\n waitForever: boolean\n): Promise<StorageBackend> {\n if (!pgBackendPromise) {\n pgBackendPromise = connectPostgres(config, waitForever);\n }\n return pgBackendPromise;\n}\n\nasync function createSQLiteBackend(): Promise<StorageBackend> {\n const db = openFederation();\n return new SQLiteBackend(db);\n}\n\n/**\n * Create and return the configured RegistryBackend (projects, sessions,\n * tags, aliases, links, compaction_log). Same auto-behaviour and same\n * never-fall-back-to-SQLite rule as createStorageBackend().\n */\nexport async function createRegistryBackend(\n config: PaiDaemonConfig,\n opts: StorageBackendOptions = {}\n): Promise<RegistryBackend> {\n if (config.storageBackend === \"postgres\") {\n // Reuses the storage backend's pool (getSharedPostgresBackend) rather\n // than opening a second one against the same database.\n const storage = await getSharedPostgresBackend(config, opts.waitForPostgres ?? false);\n const pool = (storage as unknown as { getPool: () => import(\"pg\").Pool }).getPool();\n return new PostgresRegistryBackend(pool);\n }\n return createSQLiteRegistryBackend();\n}\n\nasync function createSQLiteRegistryBackend(): Promise<RegistryBackend> {\n const db = openRegistry();\n return new SQLiteRegistryBackend(db);\n}\n\n/**\n * Process-wide backend accessors.\n *\n * Callers across the codebase must get their StorageBackend/RegistryBackend\n * from here rather than constructing SQLite or Postgres directly — this is\n * the one place that decides which backend a process uses, and the one place\n * that closes it on exit.\n */\n\nlet storagePromise: Promise<StorageBackend> | null = null;\nlet registryPromise: Promise<RegistryBackend> | null = null;\nlet beforeExitRegistered = false;\n\nfunction registerBeforeExit(): void {\n if (beforeExitRegistered) return;\n beforeExitRegistered = true;\n process.once(\"beforeExit\", () => {\n void closeStorage();\n });\n}\n\n/**\n * Returns the process-wide StorageBackend, creating it on first call.\n *\n * The whole body up to and including the `storagePromise = (async () =>\n * ...)()` assignment must run synchronously (no `await` before it) — two\n * calls issued in the same tick (e.g. `Promise.all([get(), get()])`) must\n * both observe the same cached promise rather than each racing to create\n * their own backend.\n */\nexport async function getStorageBackend(): Promise<StorageBackend> {\n if (!storagePromise) {\n storagePromise = (async () => {\n registerBeforeExit();\n return await createStorageBackend(loadConfig());\n })();\n }\n return storagePromise;\n}\n\n/** Returns the process-wide RegistryBackend, creating it on first call. Same synchronous-caching requirement as getStorageBackend(). */\nexport async function getRegistryBackend(): Promise<RegistryBackend> {\n if (!registryPromise) {\n registryPromise = (async () => {\n registerBeforeExit();\n return await createRegistryBackend(loadConfig());\n })();\n }\n return registryPromise;\n}\n\n/** Closes any backends created via getStorageBackend()/getRegistryBackend(). Idempotent. */\nexport async function closeStorage(): Promise<void> {\n const pending = [storagePromise, registryPromise];\n const pg = pgBackendPromise;\n storagePromise = null;\n registryPromise = null;\n pgBackendPromise = null;\n for (const p of pending) {\n if (!p) continue;\n try {\n const backend = await p;\n // Postgres backends share one pool owned by pgBackendPromise (closed\n // below) — closing them here too would end() it twice.\n if (backend.backendType === \"postgres\") continue;\n await backend.close();\n } catch {\n // A backend that never connected has nothing to close.\n }\n }\n if (pg) {\n try {\n await (await pg).close();\n } catch {\n // Never connected — nothing to close.\n }\n }\n}\n\n/** Test-only: clear the process-wide cache without closing (tests own their own lifecycle). */\nexport function __resetStorageForTests(): void {\n storagePromise = null;\n registryPromise = null;\n pgBackendPromise = null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"factory-CXFHwvcv.mjs","names":["runMigrations"],"sources":["../src/storage/outage.ts","../src/storage/sqlite/federation-schema.ts","../src/storage/sqlite/federation-db.ts","../src/storage/sqlite/indexer.ts","../src/storage/sqlite/tunnels.ts","../src/storage/sqlite/sources.ts","../src/storage/sqlite/search.ts","../src/storage/sqlite/kg-entity.ts","../src/storage/sqlite.ts","../src/registry/merge.ts","../src/storage/registry-postgres.ts","../src/storage/sqlite/registry-schema.ts","../src/storage/sqlite/registry-db.ts","../src/storage/sqlite/registry-merge.ts","../src/storage/registry-sqlite.ts","../src/storage/factory.ts"],"sourcesContent":["/**\n * outage.ts — is the storage backend actually reachable right now?\n *\n * The daemon retries a dead backend forever, which is correct: a Postgres\n * container that is down will usually come back, and giving up would lose the\n * work queue. What was wrong is that it did so in complete silence.\n *\n * Observed 2026-07-26: the container was down for roughly two days. The daemon\n * logged \"Postgres unavailable\" 144 times over 36 minutes, the work queue backed\n * up, session notes for the whole period were never written — and\n * `pai daemon status` reported \"Index: idle\" throughout. The one command anyone\n * would run to check said everything was fine.\n *\n * So the outage is recorded where the status command can see it, and escalated\n * once through the notification channels that were already configured and\n * already unused for this.\n */\n\nexport interface BackendOutage {\n backend: string;\n /** When the current run of failures began. */\n since: number;\n /** Consecutive failed attempts so far. */\n attempts: number;\n lastError: string;\n}\n\nlet current: BackendOutage | null = null;\n\n/** Record that the backend is currently unreachable. */\nexport function setBackendOutage(outage: BackendOutage): void {\n current = outage;\n}\n\n/**\n * Record that the backend answered.\n *\n * Called on every successful connection, including the first — so a daemon that\n * never had a problem reports none, and one that recovered stops reporting an\n * outage that has ended.\n */\nexport function clearBackendOutage(): void {\n current = null;\n}\n\n/** The current outage, or null when the backend is answering. */\nexport function getBackendOutage(): BackendOutage | null {\n return current;\n}\n\n/**\n * Human-readable elapsed time, for a status line rather than a log.\n *\n * Exported because the status command needs the duration on its own, to colour\n * it separately from the rest of the sentence. Without this it kept a second\n * copy of the same rounding, which is how two renderings of one outage drift.\n */\nexport function humanDuration(ms: number): string {\n const mins = Math.max(1, Math.round(ms / 60_000));\n return mins < 60 ? `${mins} min` : `${(mins / 60).toFixed(1)} h`;\n}\n\n/** Human-readable duration, for a status line rather than a log. */\nexport function describeOutage(o: BackendOutage, now = Date.now()): string {\n return `${o.backend} unreachable for ${humanDuration(now - o.since)}, ${o.attempts} attempts — ${o.lastError}`;\n}\n","/**\n * SQLite DDL for the PAI federation database (federation.db).\n *\n * The federation DB is the cross-project search index — a single SQLite file\n * at ~/.pai/federation.db that holds chunked text from every registered\n * project's memory/ and Notes/ directories.\n *\n * Tables:\n * - memory_files — file-level metadata (hash, mtime, size) for change detection\n * - memory_chunks — chunked text with line numbers, tier classification, and optional embedding\n * - memory_fts — FTS5 virtual table backed by memory_chunks text\n *\n * Vault tables (vault_files, vault_aliases, vault_links, vault_name_index, vault_health)\n * have been migrated to Postgres (docker/init.sql) and are no longer created here.\n *\n * Schema version history:\n * v1 — initial schema (BM25 search only)\n * v2 — added embedding BLOB column to memory_chunks (Phase 2.5, vector search)\n * v3 — added vault tables (now removed — vault tables live in Postgres)\n */\n\nimport type { Database } from \"better-sqlite3\";\n\n/** Current schema version. Bump when adding new columns or tables. */\nexport const SCHEMA_VERSION = 5;\n\nexport const FEDERATION_SCHEMA_SQL = `\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS memory_files (\n project_id INTEGER NOT NULL,\n path TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n hash TEXT NOT NULL,\n mtime INTEGER NOT NULL,\n size INTEGER NOT NULL,\n PRIMARY KEY (project_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS memory_chunks (\n id TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n path TEXT NOT NULL,\n start_line INTEGER NOT NULL,\n end_line INTEGER NOT NULL,\n hash TEXT NOT NULL,\n text TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n last_accessed_at INTEGER,\n relevance_score REAL DEFAULT 0.5,\n embedding BLOB\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(\n text,\n id UNINDEXED,\n project_id UNINDEXED,\n path UNINDEXED,\n source UNINDEXED,\n tier UNINDEXED,\n start_line UNINDEXED,\n end_line UNINDEXED\n);\n\nCREATE INDEX IF NOT EXISTS idx_mc_project ON memory_chunks(project_id);\nCREATE INDEX IF NOT EXISTS idx_mc_source ON memory_chunks(project_id, source);\nCREATE INDEX IF NOT EXISTS idx_mc_tier ON memory_chunks(tier);\nCREATE INDEX IF NOT EXISTS idx_mf_project ON memory_files(project_id);\n\nCREATE TABLE IF NOT EXISTS kg_entities (\n entity_id TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL DEFAULT 'default',\n name TEXT NOT NULL,\n type TEXT NOT NULL DEFAULT 'unknown',\n description TEXT,\n first_seen INTEGER,\n last_seen INTEGER,\n mention_count INTEGER NOT NULL DEFAULT 1,\n feedback_weight REAL NOT NULL DEFAULT 0.5,\n UNIQUE(tenant_id, entity_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_kge_tenant ON kg_entities(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_kge_name ON kg_entities(tenant_id, name);\nCREATE INDEX IF NOT EXISTS idx_kge_type ON kg_entities(tenant_id, type);\n`;\n\n/**\n * Apply the full federation schema to an open database.\n *\n * Idempotent — all statements use IF NOT EXISTS so calling this on an\n * already-initialised database is safe.\n *\n * Also runs any necessary migrations for existing databases (e.g. adding the\n * embedding column to an older schema that was created without it).\n */\nexport function initializeFederationSchema(db: Database): void {\n db.exec(FEDERATION_SCHEMA_SQL);\n runMigrations(db);\n}\n\n// ---------------------------------------------------------------------------\n// Migrations\n// ---------------------------------------------------------------------------\n\n/**\n * Apply incremental migrations to an existing database.\n *\n * Each migration is idempotent — safe to call on a database that has already\n * been migrated.\n */\nfunction runMigrations(db: Database): void {\n const columns = db.prepare(\"PRAGMA table_info(memory_chunks)\").all() as Array<{\n name: string;\n }>;\n\n // Migration v1→v2: add embedding BLOB column (schema v2, Phase 2.5)\n const hasEmbedding = columns.some((c) => c.name === \"embedding\");\n if (!hasEmbedding) {\n db.exec(\"ALTER TABLE memory_chunks ADD COLUMN embedding BLOB\");\n }\n\n // Create the partial index for embedded chunks (safe now that the column exists)\n db.exec(\n \"CREATE INDEX IF NOT EXISTS idx_mc_embedding ON memory_chunks(id) WHERE embedding IS NOT NULL\",\n );\n\n // Migration v4→v5: add last_accessed_at and relevance_score columns (QW2 + MR2)\n const hasLastAccessedAt = columns.some((c) => c.name === \"last_accessed_at\");\n if (!hasLastAccessedAt) {\n db.exec(\"ALTER TABLE memory_chunks ADD COLUMN last_accessed_at INTEGER\");\n }\n\n const hasRelevanceScore = columns.some((c) => c.name === \"relevance_score\");\n if (!hasRelevanceScore) {\n db.exec(\"ALTER TABLE memory_chunks ADD COLUMN relevance_score REAL DEFAULT 0.5\");\n }\n}\n","/**\n * Database connection helper for the PAI federation DB.\n *\n * Uses better-sqlite3 (synchronous API) to open or create federation.db.\n * On first open it runs the full DDL via initializeFederationSchema().\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport BetterSqlite3 from \"better-sqlite3\";\nimport type { Database } from \"better-sqlite3\";\nimport { initializeFederationSchema } from \"./federation-schema.js\";\nimport { paiHomePath, resolvePaiFile } from \"../../config/pai-home.js\";\n\nexport type { Database };\n\n/** Old federation.db path, inside the ~/.pai/ directory (pre-2026-09-19). */\nexport function oldFederationPath(): string {\n return join(homedir(), \".pai\", \"federation.db\");\n}\n\n/** Federation DB path: PAI_HOME/federation.db if present, else the old\n * ~/.pai/federation.db (one-time stderr notice), else the new path. */\nexport function federationDbPath(): string {\n return resolvePaiFile(paiHomePath(\"federation.db\"), [oldFederationPath()], \"pai config migrate --federation\");\n}\n\n/**\n * Open (or create) the PAI federation database.\n *\n * @param path Absolute path to federation.db. Defaults to PAI_HOME/federation.db\n * (falling back to the pre-2026-09-19 ~/.pai/federation.db).\n * @returns An open better-sqlite3 Database instance.\n *\n * Side effects on first call:\n * - Creates the parent directory if it does not exist.\n * - Enables WAL journal mode.\n * - Runs initializeFederationSchema() to ensure tables exist.\n */\nexport function openFederation(path: string = federationDbPath()): Database {\n // Ensure the directory exists before SQLite tries to create the file\n mkdirSync(dirname(path), { recursive: true });\n\n const db = new BetterSqlite3(path);\n\n // WAL gives better concurrent read performance and crash safety\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"foreign_keys = ON\");\n\n // Apply schema (idempotent — all statements use IF NOT EXISTS)\n initializeFederationSchema(db);\n\n return db;\n}\n","/**\n * Synchronous (SQLite) indexer — moved from memory/indexer/sync.ts so the\n * only file that opens prepared statements against the federation database\n * lives under src/storage/ (design doc docs/design/postgres-only.md, unit 7).\n *\n * Scans project memory/ and Notes/ directories, chunks markdown files, and\n * inserts the resulting chunks into the federation memory store for BM25\n * search. Uses raw better-sqlite3 Database directly for maximum SQLite\n * performance (synchronous transactions, no serialisation overhead) — the\n * reason SQLiteBackend keeps this path instead of routing through the\n * generic (StorageBackend) indexer that Postgres uses.\n */\n\nimport { readFileSync, statSync, existsSync } from \"node:fs\";\nimport { join, relative, basename } from \"node:path\";\nimport type { Database } from \"better-sqlite3\";\nimport type { RegistryBackend } from \"../registry-interface.js\";\nimport { chunkMarkdown, fileContentHash } from \"../../memory/chunker.js\";\nimport {\n sha256File,\n chunkId,\n detectTier,\n walkMdFiles,\n walkContentFiles,\n isPathTooBroadForContentScan,\n parseSessionTitleChunk,\n yieldToEventLoop,\n INDEX_YIELD_EVERY,\n} from \"../../memory/indexer/helpers.js\";\nimport type { IndexResult } from \"../../memory/indexer/types.js\";\n\n// ---------------------------------------------------------------------------\n// Single-file indexing\n// ---------------------------------------------------------------------------\n\n/**\n * Index a single file into the federation database.\n *\n * @returns true if the file was re-indexed (changed or new), false if skipped.\n */\nfunction indexFile(\n db: Database,\n projectId: number,\n rootPath: string,\n relativePath: string,\n source: string,\n tier: string,\n): boolean {\n const absPath = join(rootPath, relativePath);\n\n let content: string;\n let stat: ReturnType<typeof statSync>;\n try {\n content = readFileSync(absPath, \"utf8\");\n stat = statSync(absPath);\n } catch {\n // File unreadable or missing — skip silently\n return false;\n }\n\n const hash = fileContentHash(content);\n const mtime = Math.floor(stat.mtimeMs);\n const size = stat.size;\n\n const existing = db\n .prepare(\"SELECT hash FROM memory_files WHERE project_id = ? AND path = ?\")\n .get(projectId, relativePath) as { hash: string } | undefined;\n\n if (existing?.hash === hash) {\n return false;\n }\n\n const oldChunkIds = db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, relativePath) as Array<{ id: string }>;\n\n const deleteFts = db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunk = db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\",\n );\n\n db.transaction(() => {\n for (const row of oldChunkIds) {\n deleteFts.run(row.id);\n }\n deleteChunk.run(projectId, relativePath);\n })();\n\n const chunks = chunkMarkdown(content);\n\n const insertChunk = db.prepare(`\n INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n const insertFts = db.prepare(`\n INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n const upsertFile = db.prepare(`\n INSERT INTO memory_files (project_id, path, source, tier, hash, mtime, size)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(project_id, path) DO UPDATE SET\n source = excluded.source,\n tier = excluded.tier,\n hash = excluded.hash,\n mtime = excluded.mtime,\n size = excluded.size\n `);\n\n const updatedAt = Date.now();\n\n db.transaction(() => {\n for (let i = 0; i < chunks.length; i++) {\n const chunk = chunks[i]!;\n const id = chunkId(projectId, relativePath, i, chunk.startLine, chunk.endLine);\n insertChunk.run(\n id, projectId, source, tier, relativePath,\n chunk.startLine, chunk.endLine, chunk.hash, chunk.text, updatedAt,\n );\n insertFts.run(\n chunk.text, id, projectId, relativePath, source, tier,\n chunk.startLine, chunk.endLine,\n );\n }\n upsertFile.run(projectId, relativePath, source, tier, hash, mtime, size);\n })();\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Project-level indexing\n// ---------------------------------------------------------------------------\n\n/**\n * Index all memory, Notes, and content files for a single registered project.\n */\nasync function indexProject(\n db: Database,\n projectId: number,\n rootPath: string,\n claudeNotesDir?: string | null,\n): Promise<IndexResult> {\n const result: IndexResult = {\n filesProcessed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n };\n\n const filesToIndex: Array<{ absPath: string; rootBase: string; source: string; tier: string }> = [];\n\n const rootMemoryMd = join(rootPath, \"MEMORY.md\");\n if (existsSync(rootMemoryMd)) {\n filesToIndex.push({ absPath: rootMemoryMd, rootBase: rootPath, source: \"memory\", tier: \"evergreen\" });\n }\n\n const memoryDir = join(rootPath, \"memory\");\n for (const absPath of walkMdFiles(memoryDir)) {\n const relPath = relative(rootPath, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"memory\", tier });\n }\n\n const notesDir = join(rootPath, \"Notes\");\n for (const absPath of walkMdFiles(notesDir)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"notes\", tier: \"session\" });\n }\n\n // Synthetic session-title chunks for Notes files with the standard filename format:\n // \"NNNN - YYYY-MM-DD - Descriptive Title.md\"\n {\n const titleInsertChunk = db.prepare(`\n INSERT OR IGNORE INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n const titleInsertFts = db.prepare(`\n INSERT OR IGNORE INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n const updatedAt = Date.now();\n for (const absPath of walkMdFiles(notesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(rootPath, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n db.transaction(() => {\n titleInsertChunk.run(id, projectId, \"notes\", \"session\", syntheticPath, 0, 0, hash, text, updatedAt);\n titleInsertFts.run(text, id, projectId, syntheticPath, \"notes\", \"session\", 0, 0);\n })();\n }\n }\n\n if (!isPathTooBroadForContentScan(rootPath)) {\n for (const absPath of walkContentFiles(rootPath)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"content\", tier: \"topic\" });\n }\n }\n\n if (claudeNotesDir && claudeNotesDir !== notesDir) {\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n filesToIndex.push({ absPath, rootBase: claudeNotesDir, source: \"notes\", tier: \"session\" });\n }\n\n {\n const updatedAt = Date.now();\n const titleInsertChunk2 = db.prepare(`\n INSERT OR IGNORE INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n const titleInsertFts2 = db.prepare(`\n INSERT OR IGNORE INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(claudeNotesDir, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n db.transaction(() => {\n titleInsertChunk2.run(id, projectId, \"notes\", \"session\", syntheticPath, 0, 0, hash, text, updatedAt);\n titleInsertFts2.run(text, id, projectId, syntheticPath, \"notes\", \"session\", 0, 0);\n })();\n }\n }\n\n if (claudeNotesDir.endsWith(\"/Notes\")) {\n const claudeProjectDir = claudeNotesDir.slice(0, -\"/Notes\".length);\n const claudeMemoryDir = join(claudeProjectDir, \"memory\");\n\n const claudeMemoryMd = join(claudeProjectDir, \"MEMORY.md\");\n if (existsSync(claudeMemoryMd)) {\n filesToIndex.push({\n absPath: claudeMemoryMd,\n rootBase: claudeProjectDir,\n source: \"memory\",\n tier: \"evergreen\",\n });\n }\n\n for (const absPath of walkMdFiles(claudeMemoryDir)) {\n const relPath = relative(claudeProjectDir, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: claudeProjectDir, source: \"memory\", tier });\n }\n }\n }\n\n await yieldToEventLoop();\n\n let filesSinceYield = 0;\n\n for (const { absPath, rootBase, source, tier } of filesToIndex) {\n if (filesSinceYield >= INDEX_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const relPath = relative(rootBase, absPath);\n const changed = indexFile(db, projectId, rootBase, relPath, source, tier);\n\n if (changed) {\n const count = db\n .prepare(\"SELECT COUNT(*) as n FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .get(projectId, relPath) as { n: number };\n\n result.filesProcessed++;\n result.chunksCreated += count.n;\n } else {\n result.filesSkipped++;\n }\n }\n\n // Prune stale paths: remove DB entries for files that no longer exist on disk.\n const livePaths = new Set<string>();\n for (const { absPath, rootBase } of filesToIndex) {\n livePaths.add(relative(rootBase, absPath));\n }\n\n const dbChunkPaths = db\n .prepare(\"SELECT DISTINCT path FROM memory_chunks WHERE project_id = ?\")\n .all(projectId) as Array<{ path: string }>;\n\n const stalePaths: string[] = [];\n for (const row of dbChunkPaths) {\n const basePath = row.path.endsWith(\"::title\")\n ? row.path.slice(0, -\"::title\".length)\n : row.path;\n if (!livePaths.has(basePath)) {\n stalePaths.push(row.path);\n }\n }\n\n if (stalePaths.length > 0) {\n const deleteChunksFts = db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\",\n );\n const deleteFile = db.prepare(\n \"DELETE FROM memory_files WHERE project_id = ? AND path = ?\",\n );\n\n db.transaction(() => {\n for (const stalePath of stalePaths) {\n const chunkIds = db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, stalePath) as Array<{ id: string }>;\n for (const { id } of chunkIds) {\n deleteChunksFts.run(id);\n }\n deleteChunks.run(projectId, stalePath);\n deleteFile.run(projectId, stalePath);\n }\n })();\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Global indexing (all registered projects)\n// ---------------------------------------------------------------------------\n\n/**\n * Index all active projects registered in the registry DB.\n *\n * Async: yields to the event loop between each project so that the daemon's\n * Unix socket server can process IPC requests (e.g. status) while indexing.\n */\nexport async function indexAllSqlite(\n db: Database,\n registry: RegistryBackend,\n): Promise<{ projects: number; result: IndexResult }> {\n const projects = await registry.listProjects({ status: \"active\" });\n\n const totals: IndexResult = {\n filesProcessed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n };\n\n for (const project of projects) {\n await yieldToEventLoop();\n const r = await indexProject(db, project.id, project.root_path, project.claude_notes_dir);\n totals.filesProcessed += r.filesProcessed;\n totals.chunksCreated += r.chunksCreated;\n totals.filesSkipped += r.filesSkipped;\n }\n\n return { projects: projects.length, result: totals };\n}\n","/**\n * SQLite implementation of cross-project concept \"tunnel\" detection — moved\n * from memory/tunnels.ts so the only file that runs raw FTS5/vocab SQL\n * against the federation database lives under src/storage/.\n *\n * Algorithm: pull candidate terms from the FTS5 vocabulary shadow table (or\n * sample chunk text when the vocab view is unavailable), then for each\n * candidate count distinct projects and total occurrences.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { STOP_WORDS } from \"../../utils/stop-words.js\";\nimport type { RegistryBackend } from \"../registry-interface.js\";\nimport type { Tunnel, FindTunnelsOptions, FindTunnelsResult } from \"../../memory/tunnels.js\";\n\ninterface ProjectSlugMap {\n [id: number]: string;\n}\n\nasync function findTunnelsForSlugMap(\n db: Database,\n slugMap: ProjectSlugMap,\n opts: Required<FindTunnelsOptions>,\n): Promise<FindTunnelsResult> {\n const projectIds = Object.keys(slugMap).map(Number);\n if (projectIds.length < 2) {\n return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n }\n\n // Step 1 — collect candidate terms via FTS5 vocabulary shadow table.\n let candidateTerms: string[] = [];\n\n try {\n const vocabRows = db\n .prepare(\n `SELECT term, SUM(doc) AS doc_count, SUM(cnt) AS total_cnt\n FROM memory_fts_v\n GROUP BY term\n HAVING SUM(cnt) >= ?\n ORDER BY SUM(doc) DESC\n LIMIT 500`\n )\n .all(opts.min_occurrences) as Array<{ term: string; doc_count: number; total_cnt: number }>;\n\n candidateTerms = vocabRows\n .map((r) => r.term)\n .filter((t) => t.length >= 3 && !STOP_WORDS.has(t));\n } catch {\n // Vocab table not available — fall back to sampling common words from chunks.\n const sampleRows = db\n .prepare(\n `SELECT LOWER(text) AS text FROM memory_chunks\n WHERE LENGTH(text) > 20\n ORDER BY RANDOM()\n LIMIT 2000`\n )\n .all() as Array<{ text: string }>;\n\n const freq = new Map<string, number>();\n for (const { text } of sampleRows) {\n const tokens = text\n .split(/[\\s\\p{P}]+/u)\n .filter(Boolean)\n .filter((t: string) => t.length >= 3 && !STOP_WORDS.has(t));\n for (const t of tokens) {\n freq.set(t, (freq.get(t) ?? 0) + 1);\n }\n }\n candidateTerms = [...freq.entries()]\n .filter(([, n]) => n >= opts.min_occurrences)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 200)\n .map(([t]) => t);\n }\n\n if (candidateTerms.length === 0) {\n return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n }\n\n // Step 2 — for each candidate, count distinct projects and occurrences.\n const tunnels: Tunnel[] = [];\n\n for (const term of candidateTerms) {\n try {\n const rows = db\n .prepare(\n `SELECT c.project_id, COUNT(*) AS cnt,\n MIN(c.updated_at) AS first_seen,\n MAX(c.updated_at) AS last_seen\n FROM memory_fts f\n JOIN memory_chunks c ON c.id = f.id\n WHERE memory_fts MATCH ?\n AND c.project_id IN (${projectIds.map(() => \"?\").join(\", \")})\n GROUP BY c.project_id`\n )\n .all(`\"${term.replace(/\"/g, '\"\"')}\"`, ...projectIds) as Array<{\n project_id: number;\n cnt: number;\n first_seen: number;\n last_seen: number;\n }>;\n\n if (rows.length < opts.min_projects) continue;\n\n const totalOccurrences = rows.reduce((s, r) => s + Number(r.cnt), 0);\n if (totalOccurrences < opts.min_occurrences) continue;\n\n const projects = rows\n .map((r) => slugMap[r.project_id] ?? String(r.project_id))\n .filter(Boolean);\n const firstSeen = Math.min(...rows.map((r) => r.first_seen));\n const lastSeen = Math.max(...rows.map((r) => r.last_seen));\n\n tunnels.push({\n concept: term,\n projects,\n occurrences: totalOccurrences,\n first_seen: firstSeen,\n last_seen: lastSeen,\n });\n } catch {\n // Skip problematic terms\n continue;\n }\n }\n\n tunnels.sort((a, b) => {\n const byProjects = b.projects.length - a.projects.length;\n if (byProjects !== 0) return byProjects;\n return b.occurrences - a.occurrences;\n });\n\n return {\n tunnels: tunnels.slice(0, opts.limit),\n projects_analyzed: projectIds.length,\n total_concepts_evaluated: candidateTerms.length,\n };\n}\n\n/** Find cross-project concept tunnels against a SQLite federation database. */\nexport async function findTunnelsSqlite(\n db: Database,\n registry: RegistryBackend,\n options?: FindTunnelsOptions,\n): Promise<FindTunnelsResult> {\n const opts: Required<FindTunnelsOptions> = {\n min_projects: options?.min_projects ?? 2,\n min_occurrences: options?.min_occurrences ?? 3,\n limit: options?.limit ?? 20,\n };\n\n const projectRows = await registry.listProjects({ excludeArchived: true });\n const slugMap: ProjectSlugMap = {};\n for (const { id, slug } of projectRows) {\n slugMap[id] = slug;\n }\n\n return findTunnelsForSlugMap(db, slugMap, opts);\n}\n","/**\n * SQLite implementation of the `pai memory sources` aggregate report — moved\n * from cli/commands/memory/sources.ts so the only file running ad-hoc\n * aggregate SQL against the federation database lives under src/storage/.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { MemorySourcesReport } from \"../interface.js\";\n\nexport function getMemorySourcesReportSqlite(db: Database): MemorySourcesReport {\n const composition = db\n .prepare(\n `SELECT source, tier, COUNT(*) AS chunks,\n SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS embedded\n FROM memory_chunks GROUP BY source, tier ORDER BY COUNT(*) DESC`\n )\n .all() as Array<{ source: string; tier: string; chunks: number; embedded: number }>;\n\n const paths = db\n .prepare(`SELECT path, COUNT(*) AS chunks FROM memory_chunks GROUP BY path`)\n .all() as Array<{ path: string; chunks: number }>;\n\n const churn = db\n .prepare(\n `SELECT date(updated_at/1000,'unixepoch') AS day, COUNT(*) AS chunks,\n SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS embedded\n FROM memory_chunks GROUP BY day ORDER BY day DESC LIMIT 10`\n )\n .all() as Array<{ day: string; chunks: number; embedded: number }>;\n\n return { composition, paths, churn };\n}\n","/**\n * SQLite search implementation — moved from memory/search.ts so the only\n * file running raw FTS5/vector queries against the federation database\n * lives under src/storage/. Query building, slug population, and recency\n * boosting are pure logic with no SQL and stay in memory/search.ts.\n *\n * BM25 uses SQLite's FTS5 extension. Semantic search requires embeddings to\n * have been generated first via the indexer's embed pass.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { deserializeEmbedding, cosineSimilarity } from \"../../memory/embeddings.js\";\nimport {\n buildFtsQuery,\n isQuerySyntaxError,\n type SearchResult,\n type SearchOptions,\n} from \"../../memory/search.js\";\n\n// ---------------------------------------------------------------------------\n// Keyword (BM25) search\n// ---------------------------------------------------------------------------\n\n/**\n * Search across all indexed memory using FTS5 BM25 ranking.\n *\n * Results are ordered by BM25 score (most relevant first).\n * FTS5 bm25() returns negative values; closer to 0 = more relevant.\n * We negate the score so callers get positive values where higher = better.\n */\nexport function searchMemory(\n db: Database,\n query: string,\n opts?: SearchOptions,\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n const ftsQuery = buildFtsQuery(query);\n\n const conditions: string[] = [];\n const params: (string | number)[] = [ftsQuery];\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => \"?\").join(\", \");\n conditions.push(`c.project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => \"?\").join(\", \");\n conditions.push(`c.source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => \"?\").join(\", \");\n conditions.push(`c.tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n const whereClause = conditions.length > 0\n ? \"AND \" + conditions.join(\" AND \")\n : \"\";\n\n params.push(maxResults);\n\n const sql = `\n SELECT\n c.id,\n c.project_id,\n c.path,\n c.start_line,\n c.end_line,\n c.text AS snippet,\n c.tier,\n c.source,\n c.updated_at,\n c.last_accessed_at,\n c.relevance_score,\n bm25(memory_fts) AS bm25_score\n FROM memory_fts\n JOIN memory_chunks c ON memory_fts.id = c.id\n WHERE memory_fts MATCH ?\n ${whereClause}\n ORDER BY bm25_score\n LIMIT ?\n `;\n\n let rows: Array<{\n id: string;\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n snippet: string;\n tier: string;\n source: string;\n updated_at: number;\n last_accessed_at: number | null;\n relevance_score: number | null;\n bm25_score: number;\n }>;\n\n try {\n rows = db.prepare(sql).all(...params) as typeof rows;\n } catch (e) {\n // FTS5 MATCH throws on a malformed query, and for THAT an empty result is the\n // honest answer — nothing matches a query that cannot be parsed.\n //\n // Everything else is a failure of the store: a missing table, a corrupt\n // index, a locked database. Those used to return [] as well, which made an\n // unusable index byte-identical to a genuine miss. The Postgres path had the\n // same defect and it cost a real wrong answer on 2026-08-04 — the backend was\n // down for two hours, every search reported \"No results found\", and a sibling\n // session told the owner a DMARC note did not exist. See\n // storage/postgres/search.ts.\n if (!isQuerySyntaxError(e)) {\n throw new Error(\n `Memory keyword search failed — the index is unusable, so this is NOT an ` +\n `empty result set. Cause: ${e instanceof Error ? e.message : String(e)}`\n );\n }\n return [];\n }\n\n const minScore = opts?.minScore ?? 0.0;\n\n return rows\n .map((row) => {\n const baseScore = -row.bm25_score;\n const relevanceScore = row.relevance_score ?? 0.5;\n const score = baseScore * (0.5 + relevanceScore);\n return {\n chunkId: row.id,\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.snippet,\n score,\n tier: row.tier,\n source: row.source,\n updatedAt: row.updated_at,\n lastAccessedAt: row.last_accessed_at ?? undefined,\n };\n })\n .filter((r) => r.score >= minScore);\n}\n\n// ---------------------------------------------------------------------------\n// Semantic search\n// ---------------------------------------------------------------------------\n\n/**\n * Search chunks using brute-force cosine similarity over stored embeddings.\n *\n * Only chunks that have a non-null embedding BLOB are considered.\n *\n * @param queryEmbedding Pre-computed Float32Array for the search query.\n */\nexport function searchMemorySemantic(\n db: Database,\n queryEmbedding: Float32Array,\n opts?: SearchOptions,\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n\n const conditions: string[] = [\"embedding IS NOT NULL\"];\n const params: (string | number)[] = [];\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => \"?\").join(\", \");\n conditions.push(`project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => \"?\").join(\", \");\n conditions.push(`source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => \"?\").join(\", \");\n conditions.push(`tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n const where = \"WHERE \" + conditions.join(\" AND \");\n\n // Hard cap for SQLite semantic path — prevents OOM on large corpora.\n // Use Postgres for production semantic search.\n const sql = `\n SELECT id, project_id, path, start_line, end_line, text, tier, source, embedding, updated_at, last_accessed_at, relevance_score\n FROM memory_chunks\n ${where}\n LIMIT 5000\n `;\n\n const rows = db.prepare(sql).all(...params) as Array<{\n id: string;\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n text: string;\n tier: string;\n source: string;\n embedding: Buffer;\n updated_at: number;\n last_accessed_at: number | null;\n relevance_score: number | null;\n }>;\n\n if (rows.length === 0) return [];\n\n const scored = rows.map((row) => {\n const vec = deserializeEmbedding(row.embedding);\n const baseScore = cosineSimilarity(queryEmbedding, vec);\n const relevanceScore = row.relevance_score ?? 0.5;\n const score = baseScore * (0.5 + relevanceScore);\n return {\n chunkId: row.id,\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.text,\n score,\n tier: row.tier,\n source: row.source,\n updatedAt: row.updated_at,\n lastAccessedAt: row.last_accessed_at ?? undefined,\n };\n });\n\n const minScore = opts?.minScore ?? -Infinity;\n\n return scored\n .filter((r) => r.score >= minScore)\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults);\n}\n\n// ---------------------------------------------------------------------------\n// Hybrid search\n// ---------------------------------------------------------------------------\n\n/**\n * Combine BM25 keyword search and semantic search using normalized scores.\n *\n * Both score sets are min-max normalized to [0,1] before combining, so neither\n * dominates the other regardless of their raw scales.\n */\nexport function searchMemoryHybrid(\n db: Database,\n query: string,\n queryEmbedding: Float32Array,\n opts?: SearchOptions & { keywordWeight?: number; semanticWeight?: number },\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n const kw = opts?.keywordWeight ?? 0.5;\n const sw = opts?.semanticWeight ?? 0.5;\n\n const keywordResults = searchMemory(db, query, { ...opts, maxResults: 50 });\n const semanticResults = searchMemorySemantic(db, queryEmbedding, { ...opts, maxResults: 50 });\n\n if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n function minMaxNormalize(items: SearchResult[]): Map<string, number> {\n if (items.length === 0) return new Map();\n const min = Math.min(...items.map((r) => r.score));\n const max = Math.max(...items.map((r) => r.score));\n const range = max - min;\n const m = new Map<string, number>();\n for (const r of items) {\n m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);\n }\n return m;\n }\n\n const kwNorm = minMaxNormalize(keywordResults);\n const semNorm = minMaxNormalize(semanticResults);\n\n const allKeys = new Set<string>([\n ...keywordResults.map(keyFor),\n ...semanticResults.map(keyFor),\n ]);\n\n const metaMap = new Map<string, SearchResult>();\n for (const r of [...keywordResults, ...semanticResults]) {\n metaMap.set(keyFor(r), r);\n }\n\n const combined: Array<SearchResult & { combinedScore: number }> = [];\n for (const key of allKeys) {\n const meta = metaMap.get(key)!;\n const kwScore = kwNorm.get(key) ?? 0;\n const semScore = semNorm.get(key) ?? 0;\n const combinedScore = kw * kwScore + sw * semScore;\n combined.push({ ...meta, score: combinedScore, combinedScore });\n }\n\n return combined\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults)\n .map(({ combinedScore: _unused, ...r }) => r);\n}\n\n// ---------------------------------------------------------------------------\n// Access timestamp tracking (QW2)\n// ---------------------------------------------------------------------------\n\n/**\n * Update last_accessed_at for a set of chunk IDs to the current timestamp.\n *\n * Best-effort: errors are silently ignored so search is never blocked.\n */\nexport function touchChunksLastAccessed(db: Database, chunkIds: string[]): void {\n if (chunkIds.length === 0) return;\n try {\n const now = Date.now();\n const placeholders = chunkIds.map(() => \"?\").join(\", \");\n db.prepare(\n `UPDATE memory_chunks SET last_accessed_at = ? WHERE id IN (${placeholders})`\n ).run(now, ...chunkIds);\n } catch {\n // non-critical — do not block search results\n }\n}\n","/**\n * SQLite kg_entities CRUD — moved from memory/kg-entity.ts so the only file\n * running raw SQL against kg_entities lives under src/storage/. The pure\n * content-addressing helpers (entityContentId/edgeContentId) and the\n * KgEntity/KgEntityUpsertParams types they share with PostgresBackend stay\n * in memory/kg-entity.ts.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { entityContentId, type KgEntity, type KgEntityUpsertParams } from \"../../memory/kg-entity.js\";\n\n/**\n * Upsert a KG entity in the federation SQLite database.\n *\n * If the entity already exists for this tenant:\n * - Updates last_seen to now\n * - Increments mention_count\n * - Updates description if provided (overwrites older description)\n *\n * Returns the entity_id for use as a foreign key in kg_triples.\n */\nexport function upsertKgEntitySqlite(\n db: Database,\n params: KgEntityUpsertParams\n): string {\n const tenantId = params.tenantId ?? \"default\";\n const entityId = entityContentId(params.name, tenantId);\n const now = Date.now();\n\n db.prepare(`\n INSERT INTO kg_entities\n (entity_id, tenant_id, name, type, description, first_seen, last_seen, mention_count, feedback_weight)\n VALUES\n (?, ?, ?, ?, ?, ?, ?, 1, 0.5)\n ON CONFLICT(entity_id) DO UPDATE SET\n last_seen = excluded.last_seen,\n mention_count = mention_count + 1,\n description = COALESCE(excluded.description, description),\n type = CASE WHEN excluded.type != 'unknown' THEN excluded.type ELSE type END\n `).run(\n entityId,\n tenantId,\n params.name,\n params.type ?? \"unknown\",\n params.description ?? null,\n now,\n now\n );\n\n return entityId;\n}\n\n/**\n * Look up a KG entity by name within a tenant.\n * Returns null if the entity does not exist.\n */\nexport function findKgEntitySqlite(\n db: Database,\n name: string,\n tenantId = \"default\"\n): KgEntity | null {\n const entityId = entityContentId(name, tenantId);\n const row = db.prepare(\n \"SELECT * FROM kg_entities WHERE entity_id = ? AND tenant_id = ?\"\n ).get(entityId, tenantId) as KgEntity | undefined;\n return row ?? null;\n}\n\n/**\n * List KG entities for a tenant, optionally filtered by type.\n */\nexport function listKgEntitiesSqlite(\n db: Database,\n tenantId = \"default\",\n type?: string,\n limit = 100\n): KgEntity[] {\n if (type) {\n return db.prepare(\n \"SELECT * FROM kg_entities WHERE tenant_id = ? AND type = ? ORDER BY mention_count DESC LIMIT ?\"\n ).all(tenantId, type, limit) as KgEntity[];\n }\n return db.prepare(\n \"SELECT * FROM kg_entities WHERE tenant_id = ? ORDER BY mention_count DESC LIMIT ?\"\n ).all(tenantId, limit) as KgEntity[];\n}\n\n/**\n * Apply an EMA (Exponential Moving Average) feedback update to an entity's weight.\n *\n * EMA formula: new_weight = old_weight + alpha * (target - old_weight)\n */\nexport function updateEntityFeedbackWeightSqlite(\n db: Database,\n entityId: string,\n normalizedRating: number,\n alpha = 0.1\n): void {\n const row = db.prepare(\n \"SELECT feedback_weight FROM kg_entities WHERE entity_id = ?\"\n ).get(entityId) as { feedback_weight: number } | undefined;\n\n if (!row) return;\n\n const newWeight = row.feedback_weight + alpha * (normalizedRating - row.feedback_weight);\n db.prepare(\n \"UPDATE kg_entities SET feedback_weight = ? WHERE entity_id = ?\"\n ).run(newWeight, entityId);\n}\n","/**\n * SQLiteBackend — wraps the existing better-sqlite3 federation.db\n * behind the StorageBackend interface.\n *\n * This is a thin adapter. The heavy lifting is all in the existing\n * memory/indexer.ts and memory/search.ts code; we just provide a\n * backend-agnostic surface so the daemon and tools can call either\n * SQLite or Postgres transparently.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type {\n StorageBackend, ChunkRow, FileRow, FederationStats,\n MemorySourcesReport, FindTunnelsOptions, FindTunnelsResult,\n KgStats, ObservationRow, ObservationWithCwd, StoreObservationInput, QueryObservationsOptions,\n ObservationStats, SessionSummaryRow, StoreSessionSummaryInput,\n SkillTelemetryRow, RecordSkillInvocationInput, QuerySkillTelemetryOptions,\n} from \"./interface.js\";\nimport type { SearchResult, SearchOptions } from \"../memory/search.js\";\nimport type { KgEntity, KgEntityUpsertParams } from \"../memory/kg-entity.js\";\nimport type { KgAddParams, KgQueryParams, KgTriple, KgContradiction } from \"../memory/kg.js\";\nimport type { IndexResult } from \"../memory/indexer/types.js\";\nimport type { RegistryBackend } from \"./registry-interface.js\";\nimport { indexAllSqlite } from \"./sqlite/indexer.js\";\nimport { findTunnelsSqlite } from \"./sqlite/tunnels.js\";\nimport { getMemorySourcesReportSqlite } from \"./sqlite/sources.js\";\nimport { searchMemory, searchMemorySemantic, touchChunksLastAccessed } from \"./sqlite/search.js\";\nimport {\n upsertKgEntitySqlite,\n findKgEntitySqlite,\n listKgEntitiesSqlite,\n updateEntityFeedbackWeightSqlite,\n} from \"./sqlite/kg-entity.js\";\n\nexport class SQLiteBackend implements StorageBackend {\n readonly backendType = \"sqlite\" as const;\n readonly supportsPostgresFeatures = false;\n\n private db: Database;\n\n constructor(db: Database) {\n this.db = db;\n }\n\n /**\n * Expose the raw better-sqlite3 Database handle. Storage-internal only —\n * used by db-admin.ts for `pai db query|tables|schema` (the one place\n * outside this class allowed to reach for it). Every other operation must\n * be a StorageBackend method; nothing outside src/storage/ may call this.\n */\n getRawDb(): Database {\n return this.db;\n }\n\n // -------------------------------------------------------------------------\n // Lifecycle\n // -------------------------------------------------------------------------\n\n async close(): Promise<void> {\n try {\n this.db.close();\n } catch {\n // ignore\n }\n }\n\n async getStats(): Promise<FederationStats> {\n const files = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_files\").get() as { n: number }\n ).n;\n const chunks = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_chunks\").get() as { n: number }\n ).n;\n return { files, chunks };\n }\n\n async getProjectStats(projectId: number): Promise<FederationStats> {\n const files = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_files WHERE project_id = ?\").get(projectId) as { n: number }\n ).n;\n const chunks = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_chunks WHERE project_id = ?\").get(projectId) as { n: number }\n ).n;\n return { files, chunks };\n }\n\n // -------------------------------------------------------------------------\n // File tracking\n // -------------------------------------------------------------------------\n\n async getFileHash(projectId: number, path: string): Promise<string | undefined> {\n const row = this.db\n .prepare(\"SELECT hash FROM memory_files WHERE project_id = ? AND path = ?\")\n .get(projectId, path) as { hash: string } | undefined;\n return row?.hash;\n }\n\n async upsertFile(file: FileRow): Promise<void> {\n this.db\n .prepare(\n `INSERT INTO memory_files (project_id, path, source, tier, hash, mtime, size)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(project_id, path) DO UPDATE SET\n source = excluded.source,\n tier = excluded.tier,\n hash = excluded.hash,\n mtime = excluded.mtime,\n size = excluded.size`\n )\n .run(file.projectId, file.path, file.source, file.tier, file.hash, file.mtime, file.size);\n }\n\n // -------------------------------------------------------------------------\n // Chunk management\n // -------------------------------------------------------------------------\n\n async getChunkIds(projectId: number, path: string): Promise<string[]> {\n const rows = this.db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, path) as Array<{ id: string }>;\n return rows.map((r) => r.id);\n }\n\n async deleteChunksForFile(projectId: number, path: string): Promise<void> {\n const ids = await this.getChunkIds(projectId, path);\n const deleteFts = this.db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = this.db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\"\n );\n this.db.transaction(() => {\n for (const id of ids) {\n deleteFts.run(id);\n }\n deleteChunks.run(projectId, path);\n })();\n }\n\n async insertChunks(chunks: ChunkRow[]): Promise<void> {\n if (chunks.length === 0) return;\n\n const insertChunk = this.db.prepare(\n `INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`\n );\n const insertFts = this.db.prepare(\n `INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n );\n\n this.db.transaction(() => {\n for (const c of chunks) {\n insertChunk.run(\n c.id,\n c.projectId,\n c.source,\n c.tier,\n c.path,\n c.startLine,\n c.endLine,\n c.hash,\n c.text,\n c.updatedAt\n );\n insertFts.run(\n c.text,\n c.id,\n c.projectId,\n c.path,\n c.source,\n c.tier,\n c.startLine,\n c.endLine\n );\n }\n })();\n }\n\n async getDistinctChunkPaths(projectId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\"SELECT DISTINCT path FROM memory_chunks WHERE project_id = ?\")\n .all(projectId) as Array<{ path: string }>;\n return rows.map((r) => r.path);\n }\n\n async deletePaths(projectId: number, paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n const deleteFts = this.db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = this.db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\"\n );\n const deleteFile = this.db.prepare(\n \"DELETE FROM memory_files WHERE project_id = ? AND path = ?\"\n );\n this.db.transaction(() => {\n for (const path of paths) {\n const ids = this.db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, path) as Array<{ id: string }>;\n for (const { id } of ids) {\n deleteFts.run(id);\n }\n deleteChunks.run(projectId, path);\n deleteFile.run(projectId, path);\n }\n })();\n }\n\n async getUnembeddedChunkIds(\n projectId?: number,\n limit?: number,\n after?: { projectId: number; id: string } | null,\n ): Promise<Array<{ id: string; text: string; project_id: number; path: string }>> {\n const conditions = [\"embedding IS NULL\"];\n const params: (string | number)[] = [];\n\n if (projectId !== undefined) {\n conditions.push(\"project_id = ?\");\n params.push(projectId);\n }\n\n // Once paginating, the priority ORDER BY below no longer applies — pages\n // must be ordered by the same (project_id, id) the cursor seeks on.\n const paginating = after !== undefined;\n if (after) {\n conditions.push(\"(project_id > ? OR (project_id = ? AND id > ?))\");\n params.push(after.projectId, after.projectId, after.id);\n }\n\n const where = \"WHERE \" + conditions.join(\" AND \");\n // Prioritize real knowledge notes over PAI session/job-search noise.\n // CASE expression assigns lower priority numbers to knowledge paths.\n const orderBy = paginating\n ? \"project_id, id\"\n : `CASE\n WHEN path LIKE '🧠 Ideaverse/%' THEN 0\n WHEN path LIKE 'Z - Zettelkasten/%' THEN 0\n WHEN path LIKE '💼 Business/%' THEN 0\n WHEN path LIKE '📆 Meetings/%' THEN 1\n WHEN path LIKE '💡 Insights/%' THEN 1\n WHEN path LIKE '👨💻 People/%' THEN 1\n WHEN path LIKE 'University/%' THEN 1\n WHEN path LIKE 'Copilot/%' THEN 1\n WHEN path LIKE '🗓️ Daily Notes/%' THEN 2\n WHEN path LIKE 'PAI/%' THEN 3\n WHEN path LIKE '09-job-search/%' THEN 4\n WHEN path LIKE 'seriousletter/%' THEN 4\n WHEN path LIKE 'Attachments/%' THEN 5\n ELSE 2\n END, id`;\n const rows = this.db\n .prepare(`SELECT id, text, project_id, path FROM memory_chunks ${where}\n ORDER BY ${orderBy}${limit !== undefined ? \" LIMIT ?\" : \"\"}`)\n .all(...params, ...(limit !== undefined ? [limit] : [])) as Array<{ id: string; text: string; project_id: number; path: string }>;\n return rows;\n }\n\n async updateEmbedding(chunkId: string, embedding: Buffer): Promise<void> {\n this.db\n .prepare(\"UPDATE memory_chunks SET embedding = ? WHERE id = ?\")\n .run(embedding, chunkId);\n }\n\n // -------------------------------------------------------------------------\n // Search\n // -------------------------------------------------------------------------\n\n async searchKeyword(query: string, opts?: SearchOptions): Promise<SearchResult[]> {\n return searchMemory(this.db, query, opts);\n }\n\n async searchSemantic(queryEmbedding: Float32Array, opts?: SearchOptions): Promise<SearchResult[]> {\n return searchMemorySemantic(this.db, queryEmbedding, opts);\n }\n\n // -------------------------------------------------------------------------\n // Vault operations — not supported on SQLite backend (use Postgres)\n // -------------------------------------------------------------------------\n\n private vaultNotSupported(): never {\n throw new Error(\"Vault operations require the Postgres backend\");\n }\n\n async upsertVaultFile(): Promise<void> { this.vaultNotSupported(); }\n async deleteVaultFile(): Promise<void> { this.vaultNotSupported(); }\n async getVaultFile(): Promise<null> { this.vaultNotSupported(); }\n async getVaultFileByInode(): Promise<null> { this.vaultNotSupported(); }\n async getAllVaultFiles(): Promise<never[]> { this.vaultNotSupported(); }\n async getRecentVaultFiles(): Promise<never[]> { this.vaultNotSupported(); }\n async countVaultFiles(): Promise<number> { this.vaultNotSupported(); }\n async upsertVaultAliases(): Promise<void> { this.vaultNotSupported(); }\n async deleteVaultAliases(): Promise<void> { this.vaultNotSupported(); }\n async replaceLinksForSources(): Promise<void> { this.vaultNotSupported(); }\n async getLinksFromSource(): Promise<never[]> { this.vaultNotSupported(); }\n async getLinksToTarget(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkGraph(): Promise<never[]> { this.vaultNotSupported(); }\n async upsertVaultHealth(): Promise<void> { this.vaultNotSupported(); }\n async getVaultHealth(): Promise<null> { this.vaultNotSupported(); }\n async getOrphans(): Promise<never[]> { this.vaultNotSupported(); }\n async getDeadLinks(): Promise<never[]> { this.vaultNotSupported(); }\n async upsertNameIndex(): Promise<void> { this.vaultNotSupported(); }\n async replaceNameIndex(): Promise<void> { this.vaultNotSupported(); }\n async resolveVaultName(): Promise<never[]> { this.vaultNotSupported(); }\n async searchVaultNameIndex(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilesByPaths(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilesByPathsAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinksFromPaths(): Promise<never[]> { this.vaultNotSupported(); }\n async getChunksWithEmbeddings(): Promise<never[]> { this.vaultNotSupported(); }\n async getChunksForPath(): Promise<never[]> { this.vaultNotSupported(); }\n async searchChunksByText(): Promise<never[]> { this.vaultNotSupported(); }\n async countVaultFilesWithPrefix(): Promise<number> { this.vaultNotSupported(); }\n async countVaultFilesAfter(): Promise<number> { this.vaultNotSupported(); }\n async countVaultLinksWithPrefix(): Promise<number> { this.vaultNotSupported(); }\n async countVaultLinksAfter(): Promise<number> { this.vaultNotSupported(); }\n async getDeadLinksWithLineNumbers(): Promise<never[]> { this.vaultNotSupported(); }\n async getDeadLinksWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getDeadLinksAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getOrphansWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getOrphansAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getLowConnectivity(): Promise<never[]> { this.vaultNotSupported(); }\n async getLowConnectivityWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getLowConnectivityAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getAllVaultFilePaths(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilePathsWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilePathsAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkEdges(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkEdgesWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkEdgesAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultAlias(): Promise<null> { this.vaultNotSupported(); }\n\n // -------------------------------------------------------------------------\n // Knowledge-graph entities (kg_entities) — wraps storage/sqlite/kg-entity.ts\n // -------------------------------------------------------------------------\n\n async upsertKgEntity(params: KgEntityUpsertParams): Promise<string> {\n return upsertKgEntitySqlite(this.db, params);\n }\n\n async findKgEntity(name: string, tenantId = \"default\"): Promise<KgEntity | null> {\n return findKgEntitySqlite(this.db, name, tenantId);\n }\n\n async listKgEntities(tenantId = \"default\", type?: string, limit = 100): Promise<KgEntity[]> {\n return listKgEntitiesSqlite(this.db, tenantId, type, limit);\n }\n\n async updateEntityFeedbackWeight(entityId: string, normalizedRating: number, alpha = 0.1): Promise<void> {\n return updateEntityFeedbackWeightSqlite(this.db, entityId, normalizedRating, alpha);\n }\n\n // -------------------------------------------------------------------------\n // Chunk feedback / access tracking\n // -------------------------------------------------------------------------\n\n async getChunksForFeedback(chunkIds: string[]): Promise<Array<{ id: string; text: string; relevanceScore: number | null }>> {\n if (chunkIds.length === 0) return [];\n const placeholders = chunkIds.map(() => \"?\").join(\", \");\n const rows = this.db\n .prepare(`SELECT id, text, relevance_score FROM memory_chunks WHERE id IN (${placeholders})`)\n .all(...chunkIds) as Array<{ id: string; text: string; relevance_score: number | null }>;\n return rows.map((r) => ({ id: r.id, text: r.text, relevanceScore: r.relevance_score }));\n }\n\n async updateChunkRelevanceScore(chunkId: string, score: number): Promise<void> {\n this.db.prepare(\"UPDATE memory_chunks SET relevance_score = ? WHERE id = ?\").run(score, chunkId);\n }\n\n async touchChunksLastAccessed(chunkIds: string[]): Promise<void> {\n touchChunksLastAccessed(this.db, chunkIds);\n }\n\n // -------------------------------------------------------------------------\n // Global indexing\n // -------------------------------------------------------------------------\n\n async indexAll(registry: RegistryBackend): Promise<{ projects: number; result: IndexResult }> {\n return indexAllSqlite(this.db, registry);\n }\n\n // -------------------------------------------------------------------------\n // Cross-project concept tunnels\n // -------------------------------------------------------------------------\n\n async findTunnels(registry: RegistryBackend, options?: FindTunnelsOptions): Promise<FindTunnelsResult> {\n return findTunnelsSqlite(this.db, registry, options);\n }\n\n // -------------------------------------------------------------------------\n // Memory sources report\n // -------------------------------------------------------------------------\n\n async getMemorySourcesReport(): Promise<MemorySourcesReport> {\n return getMemorySourcesReportSqlite(this.db);\n }\n\n // -------------------------------------------------------------------------\n // Temporal knowledge graph / observations / skill telemetry — not\n // supported on SQLite (kg_triples, pai_observations, pai_session_summaries,\n // pai_skill_telemetry only exist in the Postgres schema).\n // -------------------------------------------------------------------------\n\n private postgresOnly(feature: string): never {\n throw new Error(`${feature} is not supported on the sqlite backend — set \"storageBackend\": \"postgres\" in the daemon config`);\n }\n\n async addKgTriple(): Promise<KgTriple> { this.postgresOnly(\"kg_triples\"); }\n async queryKgTriples(): Promise<KgTriple[]> { this.postgresOnly(\"kg_triples\"); }\n async invalidateKgTriple(): Promise<void> { this.postgresOnly(\"kg_triples\"); }\n async getKgContradictions(): Promise<KgContradiction[]> { this.postgresOnly(\"kg_triples\"); }\n async getKgStats(): Promise<KgStats> { this.postgresOnly(\"kg_triples\"); }\n\n async storeObservation(): Promise<number | null> { this.postgresOnly(\"observations\"); }\n async storeObservationWithProject(): Promise<number | null> { this.postgresOnly(\"observations\"); }\n async queryObservations(): Promise<ObservationRow[]> { this.postgresOnly(\"observations\"); }\n async queryRecentObservations(): Promise<ObservationRow[]> { this.postgresOnly(\"observations\"); }\n async querySessionObservations(): Promise<ObservationRow[]> { this.postgresOnly(\"observations\"); }\n async getObservationStats(): Promise<ObservationStats> { this.postgresOnly(\"observations\"); }\n async getObservationTypesForPaths(): Promise<Map<string, Record<string, number>>> { return new Map(); }\n async storeSessionSummary(): Promise<void> { this.postgresOnly(\"session summaries\"); }\n async queryRecentSummaries(): Promise<SessionSummaryRow[]> { this.postgresOnly(\"session summaries\"); }\n async recordSkillInvocation(): Promise<void> { this.postgresOnly(\"skill telemetry\"); }\n async querySkillTelemetry(): Promise<SkillTelemetryRow[]> { this.postgresOnly(\"skill telemetry\"); }\n\n // -------------------------------------------------------------------------\n // File-path rename (session cleanup: moving Notes/*.md into YYYY/MM/)\n // -------------------------------------------------------------------------\n\n async countFilesWithPaths(paths: string[]): Promise<number> {\n if (paths.length === 0) return 0;\n const placeholders = paths.map(() => \"?\").join(\", \");\n const row = this.db\n .prepare(`SELECT COUNT(*) AS n FROM memory_files WHERE path IN (${placeholders})`)\n .get(...paths) as { n: number };\n return row.n;\n }\n\n async renameFilePaths(moves: Array<{ oldPath: string; newPath: string }>): Promise<number> {\n if (moves.length === 0) return 0;\n const updateFiles = this.db.prepare(\"UPDATE memory_files SET path = ? WHERE path = ?\");\n const updateChunks = this.db.prepare(\"UPDATE memory_chunks SET path = ? WHERE path = ?\");\n let filesUpdated = 0;\n this.db.transaction(() => {\n for (const { oldPath, newPath } of moves) {\n const r = updateFiles.run(newPath, oldPath);\n filesUpdated += r.changes;\n updateChunks.run(newPath, oldPath);\n }\n })();\n return filesUpdated;\n }\n}\n","/**\n * merge.ts — pure types and error class shared by the merge implementations.\n *\n * The actual SQLite implementation (planMerge/applyMerge, taking a raw\n * better-sqlite3 Database) lives in src/storage/sqlite/registry-merge.ts —\n * moved there so nothing outside src/storage/ imports better-sqlite3. The\n * Postgres implementation lives in src/storage/registry-postgres.ts. Both are\n * reached through RegistryBackend.planProjectMerge/applyProjectMerge\n * (src/storage/registry-interface.ts), never directly.\n *\n * See src/storage/sqlite/registry-merge.ts for the \"why one function\" design\n * rationale (five tables reference a project, foreign_keys is off).\n */\n\nexport interface MergePlan {\n fromId: number;\n fromSlug: string;\n intoId: number;\n intoSlug: string;\n /** Sessions to move, with the numbers they will be given. */\n sessions: { id: number; from: number; to: number }[];\n tags: number;\n aliases: number;\n compactions: number;\n links: number;\n /** The losing slug is kept as an alias, so `pai <old-name>` still resolves. */\n aliasToAdd?: string;\n}\n\nexport class MergeError extends Error {}\n","/**\n * PostgresRegistryBackend — implements RegistryBackend over the pg pool\n * pattern already used by PostgresBackend (src/storage/postgres/backend.ts).\n *\n * Same SQL shape as SQLiteRegistryBackend, translated to $-placeholders.\n * Behavioural notes where Postgres diverges from SQLite's \"OR IGNORE\":\n * - `INSERT ... ON CONFLICT DO NOTHING` (no target) catches any unique\n * violation on the table, matching SQLite's INSERT OR IGNORE.\n * - SQLite's `UPDATE OR IGNORE` (used once, in reassignLinksTarget/\n * applyProjectMerge's link retarget) has no direct Postgres equivalent —\n * an UPDATE cannot specify ON CONFLICT. Replicated with a NOT EXISTS guard\n * that skips rows whose new (session_id, target_project_id) would collide,\n * leaving them for the caller's existing cleanup DELETE, exactly as the\n * SQLite version does.\n */\n\nimport pg from \"pg\";\nimport { MergeError } from \"../registry/merge.js\";\nimport type { Pool, PoolClient } from \"pg\";\nimport type { PostgresConfig } from \"./postgres/config.js\";\nimport type {\n RegistryBackend,\n Project,\n ProjectWithSessionStats,\n Session,\n SessionWithProject,\n NewProject,\n NewProjectWithSlugRetry,\n NewSession,\n NewCompactionLogEntry,\n ListProjectsOptions,\n ListProjectsByPathLengthOptions,\n ListProjectsWithStatsOptions,\n ListNamedProjectsOptions,\n ListSessionsOptions,\n ListSessionsForProjectOptions,\n ProjectType,\n ProjectStatus,\n SessionStatus,\n LinkType,\n MergePlan,\n} from \"./registry-interface.js\";\n\nconst { Pool: PgPool } = pg;\n\n/**\n * node-postgres returns BIGINT columns (created_at/updated_at/archived_at/\n * closed_at, and MAX() over a BIGINT column) as strings — JS numbers can't\n * safely hold the full 64-bit range. Every value here is Date.now() ms and\n * well inside Number.isSafeInteger, so a plain Number() conversion keeps the\n * shape identical to better-sqlite3's row objects (same pattern as\n * rowToKgEntity in postgres/backend.ts).\n */\nfunction toNum(v: unknown): number {\n return typeof v === \"string\" ? Number(v) : (v as number);\n}\nfunction toNumOrNull(v: unknown): number | null {\n return v === null || v === undefined ? null : toNum(v);\n}\n\nfunction mapProject(row: Record<string, unknown>): Project {\n return {\n ...row,\n created_at: toNum(row.created_at),\n updated_at: toNum(row.updated_at),\n archived_at: toNumOrNull(row.archived_at),\n } as Project;\n}\n\nfunction mapProjectWithStats(row: Record<string, unknown>): ProjectWithSessionStats {\n return {\n ...mapProject(row),\n session_count: toNum(row.session_count),\n last_active: toNumOrNull(row.last_active),\n } as ProjectWithSessionStats;\n}\n\nfunction mapSession(row: Record<string, unknown>): Session {\n return {\n ...row,\n created_at: toNum(row.created_at),\n closed_at: toNumOrNull(row.closed_at),\n } as Session;\n}\n\nexport class PostgresRegistryBackend implements RegistryBackend {\n readonly backendType = \"postgres\" as const;\n\n private pool: Pool;\n /** False when constructed from a pool owned elsewhere (the shared storage\n * backend pool, wired by src/storage/factory.ts) — close() must not end\n * a pool this instance doesn't own. */\n private readonly ownsPool: boolean;\n\n constructor(configOrPool: PostgresConfig | Pool) {\n if (configOrPool instanceof PgPool) {\n this.pool = configOrPool;\n this.ownsPool = false;\n return;\n }\n\n const config = configOrPool;\n const connStr =\n config.connectionString ??\n `postgresql://${config.user ?? \"pai\"}:${config.password ?? \"pai\"}@${config.host ?? \"localhost\"}:${config.port ?? 5432}/${config.database ?? \"pai\"}`;\n\n this.pool = new PgPool({\n connectionString: connStr,\n max: config.maxConnections ?? 5,\n connectionTimeoutMillis: config.connectionTimeoutMs ?? 5000,\n idleTimeoutMillis: 30_000,\n allowExitOnIdle: true,\n });\n this.ownsPool = true;\n\n this.pool.on(\"error\", (err) => {\n process.stderr.write(`[pai-postgres] Registry pool error: ${err.message}\\n`);\n });\n }\n\n async close(): Promise<void> {\n if (this.ownsPool) await this.pool.end();\n }\n\n async resetRegistry(): Promise<void> {\n const client = await this.pool.connect();\n try {\n await client.query(\"BEGIN\");\n await client.query(\"DELETE FROM links\");\n await client.query(\"DELETE FROM compaction_log\");\n await client.query(\"DELETE FROM session_tags\");\n await client.query(\"DELETE FROM project_tags\");\n await client.query(\"DELETE FROM aliases\");\n await client.query(\"DELETE FROM sessions\");\n await client.query(\"DELETE FROM tags\");\n await client.query(\"DELETE FROM projects\");\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n }\n\n // -------------------------------------------------------------------------\n // Projects — reads\n // -------------------------------------------------------------------------\n\n async getProjectById(id: number): Promise<Project | null> {\n const r = await this.pool.query(\"SELECT * FROM projects WHERE id = $1\", [id]);\n return r.rows[0] ? mapProject(r.rows[0]) : null;\n }\n\n async getProjectBySlug(\n slug: string,\n opts: { caseInsensitive?: boolean; excludeId?: number } = {}\n ): Promise<Project | null> {\n let sql = opts.caseInsensitive\n ? \"SELECT * FROM projects WHERE lower(slug) = lower($1)\"\n : \"SELECT * FROM projects WHERE slug = $1\";\n const params: unknown[] = [slug];\n if (opts.excludeId !== undefined) {\n params.push(opts.excludeId);\n sql += ` AND id != $${params.length}`;\n }\n const r = await this.pool.query(sql, params);\n return r.rows[0] ? mapProject(r.rows[0]) : null;\n }\n\n async getProjectByAlias(\n alias: string,\n opts: { caseInsensitive?: boolean } = {}\n ): Promise<Project | null> {\n const sql = opts.caseInsensitive\n ? \"SELECT p.* FROM projects p JOIN aliases a ON a.project_id = p.id WHERE lower(a.alias) = lower($1)\"\n : \"SELECT p.* FROM projects p JOIN aliases a ON a.project_id = p.id WHERE a.alias = $1\";\n const r = await this.pool.query(sql, [alias]);\n return r.rows[0] ? mapProject(r.rows[0]) : null;\n }\n\n async getProjectByRootPath(rootPath: string, opts: { excludeId?: number } = {}): Promise<Project | null> {\n let sql = \"SELECT * FROM projects WHERE root_path = $1\";\n const params: unknown[] = [rootPath];\n if (opts.excludeId !== undefined) {\n params.push(opts.excludeId);\n sql += ` AND id != $${params.length}`;\n }\n const r = await this.pool.query(sql, params);\n return r.rows[0] ? mapProject(r.rows[0]) : null;\n }\n\n async getProjectByEncodedDir(encodedDir: string, opts: { excludeId?: number } = {}): Promise<Project | null> {\n let sql = \"SELECT * FROM projects WHERE encoded_dir = $1\";\n const params: unknown[] = [encodedDir];\n if (opts.excludeId !== undefined) {\n params.push(opts.excludeId);\n sql += ` AND id != $${params.length}`;\n }\n const r = await this.pool.query(sql, params);\n return r.rows[0] ? mapProject(r.rows[0]) : null;\n }\n\n async findProjectByCwdPrefix(cwd: string): Promise<Project | null> {\n const r = await this.pool.query(\n `SELECT * FROM projects WHERE status = 'active' AND $1 LIKE root_path || '%'\n ORDER BY length(root_path) DESC LIMIT 1`,\n [cwd]\n );\n return r.rows[0] ? mapProject(r.rows[0]) : null;\n }\n\n async listProjects(opts: ListProjectsOptions = {}): Promise<Project[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (opts.status) {\n params.push(opts.status);\n where.push(`p.status = $${params.length}`);\n }\n if (opts.excludeArchived) {\n where.push(\"p.status != 'archived'\");\n }\n if (opts.tagName) {\n params.push(opts.tagName);\n where.push(\n `p.id IN (SELECT pt.project_id FROM project_tags pt JOIN tags t ON pt.tag_id = t.id WHERE t.name = $${params.length})`\n );\n }\n\n let sql = \"SELECT p.* FROM projects p\";\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n\n switch (opts.orderBy) {\n case \"updated_desc\":\n sql += \" ORDER BY p.updated_at DESC\";\n break;\n case \"slug\":\n sql += \" ORDER BY p.slug ASC\";\n break;\n case \"id\":\n sql += \" ORDER BY p.id\";\n break;\n case \"status_updated\":\n default:\n sql += \" ORDER BY p.status ASC, p.updated_at DESC\";\n break;\n }\n if (opts.limit !== undefined) {\n params.push(opts.limit);\n sql += ` LIMIT $${params.length}`;\n }\n\n const r = await this.pool.query(sql, params);\n return r.rows.map(mapProject);\n }\n\n async listProjectsByPathLengthDesc(opts: ListProjectsByPathLengthOptions = {}): Promise<Project[]> {\n const where = opts.excludeArchived ? \"WHERE status != 'archived'\" : \"\";\n const r = await this.pool.query(`SELECT * FROM projects ${where} ORDER BY LENGTH(root_path) DESC`);\n return r.rows.map(mapProject);\n }\n\n async listProjectsWithSessionStats(\n opts: ListProjectsWithStatsOptions = {}\n ): Promise<ProjectWithSessionStats[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (opts.status) {\n params.push(opts.status);\n where.push(`p.status = $${params.length}`);\n }\n if (opts.tagId !== undefined) {\n params.push(opts.tagId);\n where.push(`p.id IN (SELECT project_id FROM project_tags WHERE tag_id = $${params.length})`);\n }\n\n let sql = `\n SELECT p.*,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id)::int AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n `;\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n sql +=\n opts.orderBy === \"updated_desc\"\n ? \" ORDER BY p.updated_at DESC\"\n : \" ORDER BY p.status ASC, p.updated_at DESC\";\n\n const r = await this.pool.query(sql, params);\n return r.rows.map(mapProjectWithStats);\n }\n\n async listNamedProjects(\n opts: ListNamedProjectsOptions = {}\n ): Promise<Array<ProjectWithSessionStats & { name: string | null }>> {\n const join = opts.includeUnnamed ? \"LEFT JOIN\" : \"JOIN\";\n const sql = `\n SELECT p.*, a.alias AS name,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id)::int AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n ${join} aliases a ON a.project_id = p.id\n WHERE p.status = 'active'\n ORDER BY p.updated_at DESC\n `;\n const r = await this.pool.query(sql);\n return r.rows.map((row) => ({ ...mapProjectWithStats(row), name: row.name as string | null }));\n }\n\n async searchProjects(query: string, limit = 20): Promise<Project[]> {\n const q = `%${query}%`;\n const r = await this.pool.query(\n `SELECT * FROM projects WHERE slug LIKE $1 OR display_name LIKE $1 OR root_path LIKE $1\n ORDER BY updated_at DESC LIMIT $2`,\n [q, limit]\n );\n return r.rows.map(mapProject);\n }\n\n async countProjects(opts: { status?: ProjectStatus } = {}): Promise<number> {\n const r = opts.status\n ? await this.pool.query<{ n: string }>(\"SELECT COUNT(*)::text AS n FROM projects WHERE status = $1\", [\n opts.status,\n ])\n : await this.pool.query<{ n: string }>(\"SELECT COUNT(*)::text AS n FROM projects\");\n return parseInt(r.rows[0].n, 10);\n }\n\n async getMostRecentProjectUpdatedAt(): Promise<number | null> {\n const r = await this.pool.query<{ updated_at: string }>(\n \"SELECT updated_at FROM projects ORDER BY updated_at DESC LIMIT 1\"\n );\n return r.rows[0] ? Number(r.rows[0].updated_at) : null;\n }\n\n async countChildProjects(parentId: number): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM projects WHERE parent_id = $1\",\n [parentId]\n );\n return parseInt(r.rows[0].n, 10);\n }\n\n async findSiblingProjectsBySlugPattern(\n excludeProjectId: number,\n slug: string,\n limit = 3\n ): Promise<Array<{ slug: string; root_path: string; session_count: number }>> {\n const r = await this.pool.query(\n `SELECT p.slug, p.root_path, COUNT(s.id)::int AS session_count\n FROM projects p LEFT JOIN sessions s ON s.project_id = p.id\n WHERE p.id != $1 AND (p.slug = $2 OR p.slug LIKE $2 || '-%' OR $2 LIKE p.slug || '-%')\n GROUP BY p.id HAVING COUNT(s.id) > 0 ORDER BY session_count DESC LIMIT $3`,\n [excludeProjectId, slug, limit]\n );\n return r.rows as Array<{ slug: string; root_path: string; session_count: number }>;\n }\n\n // -------------------------------------------------------------------------\n // Projects — writes\n // -------------------------------------------------------------------------\n\n async createProject(input: NewProject): Promise<Project> {\n const type: ProjectType = input.type ?? \"local\";\n const status: ProjectStatus = input.status ?? \"active\";\n const r = await this.pool.query(\n `INSERT INTO projects (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,\n [input.slug, input.displayName, input.rootPath, input.encodedDir, type, status, input.createdAt, input.updatedAt]\n );\n return mapProject(r.rows[0]);\n }\n\n async createProjectWithSlugRetry(\n input: NewProjectWithSlugRetry\n ): Promise<{ id: number; slug: string; created: boolean }> {\n let slug = input.baseSlug;\n let attempt = 0;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const r = await this.pool.query<{ id: number }>(\n `INSERT INTO projects (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES ($1, $2, $3, $4, 'local', 'active', $5, $6)\n ON CONFLICT DO NOTHING RETURNING id`,\n [slug, input.displayName, input.rootPath, input.encodedDir, input.createdAt, input.updatedAt]\n );\n if (r.rows.length > 0) {\n return { id: r.rows[0].id, slug, created: true };\n }\n\n const existing = await this.pool.query<{ id: number }>(\n \"SELECT id FROM projects WHERE root_path = $1\",\n [input.rootPath]\n );\n if (existing.rows[0]) {\n return { id: existing.rows[0].id, slug, created: false };\n }\n\n attempt++;\n slug = `${input.baseSlug}-${attempt}`;\n }\n }\n\n async updateProjectPath(\n id: number,\n patch: { rootPath?: string; encodedDir?: string },\n updatedAt?: number\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [];\n if (patch.rootPath !== undefined) {\n params.push(patch.rootPath);\n sets.push(`root_path = $${params.length}`);\n }\n if (patch.encodedDir !== undefined) {\n params.push(patch.encodedDir);\n sets.push(`encoded_dir = $${params.length}`);\n }\n if (updatedAt !== undefined) {\n params.push(updatedAt);\n sets.push(`updated_at = $${params.length}`);\n }\n if (!sets.length) return;\n params.push(id);\n await this.pool.query(`UPDATE projects SET ${sets.join(\", \")} WHERE id = $${params.length}`, params);\n }\n\n async updateProjectStatus(\n id: number,\n status: ProjectStatus,\n opts: { archivedAt?: number | null; updatedAt?: number; requireCurrentStatus?: ProjectStatus } = {}\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [status];\n sets.push(\"status = $1\");\n if (opts.archivedAt !== undefined) {\n params.push(opts.archivedAt);\n sets.push(`archived_at = $${params.length}`);\n }\n if (opts.updatedAt !== undefined) {\n params.push(opts.updatedAt);\n sets.push(`updated_at = $${params.length}`);\n }\n params.push(id);\n let sql = `UPDATE projects SET ${sets.join(\", \")} WHERE id = $${params.length}`;\n if (opts.requireCurrentStatus) {\n params.push(opts.requireCurrentStatus);\n sql += ` AND status = $${params.length}`;\n }\n await this.pool.query(sql, params);\n }\n\n async updateProjectDisplayName(id: number, displayName: string, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n await this.pool.query(\"UPDATE projects SET display_name = $1, updated_at = $2 WHERE id = $3\", [\n displayName,\n updatedAt,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE projects SET display_name = $1 WHERE id = $2\", [displayName, id]);\n }\n }\n\n async updateProjectType(id: number, type: ProjectType, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n await this.pool.query(\"UPDATE projects SET type = $1, updated_at = $2 WHERE id = $3\", [\n type,\n updatedAt,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE projects SET type = $1 WHERE id = $2\", [type, id]);\n }\n }\n\n async updateProjectSessionConfig(id: number, config: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n await this.pool.query(\"UPDATE projects SET session_config = $1, updated_at = $2 WHERE id = $3\", [\n config,\n updatedAt,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE projects SET session_config = $1 WHERE id = $2\", [config, id]);\n }\n }\n\n async updateProjectClaudeNotesDir(id: number, dir: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n await this.pool.query(\"UPDATE projects SET claude_notes_dir = $1, updated_at = $2 WHERE id = $3\", [\n dir,\n updatedAt,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE projects SET claude_notes_dir = $1 WHERE id = $2\", [dir, id]);\n }\n }\n\n async updateProjectObsidianLink(id: number, link: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n await this.pool.query(\"UPDATE projects SET obsidian_link = $1, updated_at = $2 WHERE id = $3\", [\n link,\n updatedAt,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE projects SET obsidian_link = $1 WHERE id = $2\", [link, id]);\n }\n }\n\n async updateProjectSlug(id: number, slug: string, updatedAt: number): Promise<void> {\n await this.pool.query(\"UPDATE projects SET slug = $1, updated_at = $2 WHERE id = $3\", [slug, updatedAt, id]);\n }\n\n async reassignProjectParent(fromParentId: number, toParentId: number): Promise<number> {\n const r = await this.pool.query(\"UPDATE projects SET parent_id = $1 WHERE parent_id = $2\", [\n toParentId,\n fromParentId,\n ]);\n return r.rowCount ?? 0;\n }\n\n async deleteProject(id: number): Promise<void> {\n await this.pool.query(\"DELETE FROM projects WHERE id = $1\", [id]);\n }\n\n async deleteProjectCascade(id: number): Promise<void> {\n const client = await this.pool.connect();\n try {\n await client.query(\"BEGIN\");\n await client.query(\"DELETE FROM links WHERE target_project_id = $1\", [id]);\n await client.query(\n \"DELETE FROM links WHERE session_id IN (SELECT id FROM sessions WHERE project_id = $1)\",\n [id]\n );\n await client.query(\"DELETE FROM compaction_log WHERE project_id = $1\", [id]);\n await client.query(\"DELETE FROM project_tags WHERE project_id = $1\", [id]);\n await client.query(\"DELETE FROM aliases WHERE project_id = $1\", [id]);\n await client.query(\"DELETE FROM sessions WHERE project_id = $1\", [id]);\n await client.query(\"DELETE FROM projects WHERE id = $1\", [id]);\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n }\n\n // -------------------------------------------------------------------------\n // Tags\n // -------------------------------------------------------------------------\n\n async listTagsForProject(projectId: number): Promise<string[]> {\n const r = await this.pool.query<{ name: string }>(\n \"SELECT t.name FROM tags t JOIN project_tags pt ON pt.tag_id = t.id WHERE pt.project_id = $1 ORDER BY t.name\",\n [projectId]\n );\n return r.rows.map((row) => row.name);\n }\n\n async listAllTags(): Promise<Array<{ id: number; name: string }>> {\n const r = await this.pool.query<{ id: number; name: string }>(\"SELECT id, name FROM tags ORDER BY name\");\n return r.rows;\n }\n\n async upsertTag(name: string): Promise<number> {\n await this.pool.query(\"INSERT INTO tags (name) VALUES ($1) ON CONFLICT (name) DO NOTHING\", [name]);\n const r = await this.pool.query<{ id: number }>(\"SELECT id FROM tags WHERE name = $1\", [name]);\n return r.rows[0].id;\n }\n\n async addProjectTag(projectId: number, tagId: number): Promise<void> {\n await this.pool.query(\"INSERT INTO project_tags (project_id, tag_id) VALUES ($1, $2)\", [projectId, tagId]);\n }\n\n async projectHasTag(projectId: number, tagId: number): Promise<boolean> {\n const r = await this.pool.query(\"SELECT 1 FROM project_tags WHERE project_id = $1 AND tag_id = $2\", [\n projectId,\n tagId,\n ]);\n return (r.rowCount ?? 0) > 0;\n }\n\n async deleteProjectTag(projectId: number, tagId: number): Promise<void> {\n await this.pool.query(\"DELETE FROM project_tags WHERE project_id = $1 AND tag_id = $2\", [projectId, tagId]);\n }\n\n async reassignProjectTag(fromProjectId: number, toProjectId: number, tagId: number): Promise<void> {\n await this.pool.query(\"UPDATE project_tags SET project_id = $1 WHERE project_id = $2 AND tag_id = $3\", [\n toProjectId,\n fromProjectId,\n tagId,\n ]);\n }\n\n async listProjectTagIds(projectId: number): Promise<number[]> {\n const r = await this.pool.query<{ tag_id: number }>(\"SELECT tag_id FROM project_tags WHERE project_id = $1\", [\n projectId,\n ]);\n return r.rows.map((row) => row.tag_id);\n }\n\n async countProjectTagsForProject(projectId: number): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM project_tags WHERE project_id = $1\",\n [projectId]\n );\n return parseInt(r.rows[0].n, 10);\n }\n\n async copyProjectTags(fromProjectId: number, toProjectId: number): Promise<void> {\n await this.pool.query(\n `INSERT INTO project_tags (project_id, tag_id) SELECT $1, tag_id FROM project_tags WHERE project_id = $2\n ON CONFLICT (project_id, tag_id) DO NOTHING`,\n [toProjectId, fromProjectId]\n );\n }\n\n async deleteProjectTagsForProject(projectId: number): Promise<void> {\n await this.pool.query(\"DELETE FROM project_tags WHERE project_id = $1\", [projectId]);\n }\n\n // -------------------------------------------------------------------------\n // Aliases\n // -------------------------------------------------------------------------\n\n async resolveAlias(alias: string): Promise<number | null> {\n const r = await this.pool.query<{ project_id: number }>(\n \"SELECT project_id FROM aliases WHERE alias = $1\",\n [alias]\n );\n return r.rows[0]?.project_id ?? null;\n }\n\n async listAliasesForProject(projectId: number): Promise<string[]> {\n const r = await this.pool.query<{ alias: string }>(\n \"SELECT alias FROM aliases WHERE project_id = $1 ORDER BY alias\",\n [projectId]\n );\n return r.rows.map((row) => row.alias);\n }\n\n async addAlias(alias: string, projectId: number): Promise<void> {\n await this.pool.query(\"INSERT INTO aliases (alias, project_id) VALUES ($1, $2)\", [alias, projectId]);\n }\n\n async removeAlias(alias: string): Promise<void> {\n await this.pool.query(\"DELETE FROM aliases WHERE alias = $1\", [alias]);\n }\n\n async countAliasesForProject(projectId: number): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM aliases WHERE project_id = $1\",\n [projectId]\n );\n return parseInt(r.rows[0].n, 10);\n }\n\n async moveProjectAliases(fromProjectId: number, toProjectId: number): Promise<void> {\n await this.pool.query(\"UPDATE aliases SET project_id = $1 WHERE project_id = $2\", [\n toProjectId,\n fromProjectId,\n ]);\n }\n\n async reassignAlias(alias: string, toProjectId: number): Promise<void> {\n await this.pool.query(\"UPDATE aliases SET project_id = $1 WHERE alias = $2\", [toProjectId, alias]);\n }\n\n async listAliasMap(): Promise<Array<{ alias: string; slug: string; root_path: string }>> {\n const r = await this.pool.query(\n `SELECT a.alias AS alias, p.slug AS slug, p.root_path AS root_path\n FROM aliases a JOIN projects p ON p.id = a.project_id WHERE p.status != 'archived'`\n );\n return r.rows as Array<{ alias: string; slug: string; root_path: string }>;\n }\n\n // -------------------------------------------------------------------------\n // Sessions\n // -------------------------------------------------------------------------\n\n async getSessionById(id: number): Promise<Session | null> {\n const r = await this.pool.query(\"SELECT * FROM sessions WHERE id = $1\", [id]);\n return r.rows[0] ? mapSession(r.rows[0]) : null;\n }\n\n async getSessionByNumber(projectId: number, number: number): Promise<Session | null> {\n const r = await this.pool.query(\"SELECT * FROM sessions WHERE project_id = $1 AND number = $2\", [\n projectId,\n number,\n ]);\n return r.rows[0] ? mapSession(r.rows[0]) : null;\n }\n\n async getLatestSessionForProject(projectId: number): Promise<Session | null> {\n const r = await this.pool.query(\n \"SELECT * FROM sessions WHERE project_id = $1 ORDER BY number DESC LIMIT 1\",\n [projectId]\n );\n return r.rows[0] ? mapSession(r.rows[0]) : null;\n }\n\n async getMaxSessionNumber(projectId: number): Promise<number> {\n const r = await this.pool.query<{ n: number }>(\n \"SELECT COALESCE(MAX(number), 0) AS n FROM sessions WHERE project_id = $1\",\n [projectId]\n );\n return r.rows[0].n;\n }\n\n async listSessionsForProject(\n projectId: number,\n opts: ListSessionsForProjectOptions = {}\n ): Promise<Session[]> {\n const orderBy = opts.orderBy === \"created_desc\" ? \"created_at DESC\" : \"number ASC\";\n let sql = `SELECT * FROM sessions WHERE project_id = $1 ORDER BY ${orderBy}`;\n const params: unknown[] = [projectId];\n if (opts.limit !== undefined) {\n params.push(opts.limit);\n sql += ` LIMIT $${params.length}`;\n }\n const r = await this.pool.query(sql, params);\n return r.rows.map(mapSession);\n }\n\n async listSessions(opts: ListSessionsOptions = {}): Promise<SessionWithProject[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n if (opts.projectId !== undefined) {\n params.push(opts.projectId);\n where.push(`s.project_id = $${params.length}`);\n }\n if (opts.status) {\n params.push(opts.status);\n where.push(`s.status = $${params.length}`);\n }\n let sql = `\n SELECT s.*, p.slug AS project_slug, p.display_name AS project_name\n FROM sessions s JOIN projects p ON p.id = s.project_id\n `;\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n sql += \" ORDER BY s.date DESC, s.number DESC\";\n params.push(opts.limit ?? 20);\n sql += ` LIMIT $${params.length}`;\n const r = await this.pool.query(sql, params);\n return r.rows.map((row) => ({ ...mapSession(row), project_slug: row.project_slug as string, project_name: row.project_name as string }));\n }\n\n async findSessionByFilename(projectId: number, filename: string): Promise<{ id: number } | null> {\n const r = await this.pool.query<{ id: number }>(\n \"SELECT id FROM sessions WHERE project_id = $1 AND filename = $2 LIMIT 1\",\n [projectId, filename]\n );\n return r.rows[0] ?? null;\n }\n\n async sessionNumberTaken(projectId: number, number: number): Promise<boolean> {\n const r = await this.pool.query(\n \"SELECT 1 FROM sessions WHERE project_id = $1 AND number = $2 LIMIT 1\",\n [projectId, number]\n );\n return (r.rowCount ?? 0) > 0;\n }\n\n async createSession(input: NewSession): Promise<Session> {\n const status = input.status ?? \"completed\";\n const r = await this.pool.query(\n `INSERT INTO sessions (project_id, number, date, slug, title, filename, status, created_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,\n [input.projectId, input.number, input.date, input.slug, input.title, input.filename, status, input.createdAt]\n );\n return mapSession(r.rows[0]);\n }\n\n async upsertSessionIfAbsent(input: NewSession): Promise<boolean> {\n const existing = await this.pool.query(\n \"SELECT id FROM sessions WHERE project_id = $1 AND number = $2\",\n [input.projectId, input.number]\n );\n if ((existing.rowCount ?? 0) > 0) return false;\n\n const status = input.status ?? \"completed\";\n await this.pool.query(\n `INSERT INTO sessions (project_id, number, date, slug, title, filename, status, created_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [input.projectId, input.number, input.date, input.slug, input.title, input.filename, status, input.createdAt]\n );\n return true;\n }\n\n async updateSessionMeta(\n id: number,\n patch: { slug?: string; title?: string; filename?: string }\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [];\n if (patch.slug !== undefined) {\n params.push(patch.slug);\n sets.push(`slug = $${params.length}`);\n }\n if (patch.title !== undefined) {\n params.push(patch.title);\n sets.push(`title = $${params.length}`);\n }\n if (patch.filename !== undefined) {\n params.push(patch.filename);\n sets.push(`filename = $${params.length}`);\n }\n if (!sets.length) return;\n params.push(id);\n await this.pool.query(`UPDATE sessions SET ${sets.join(\", \")} WHERE id = $${params.length}`, params);\n }\n\n async updateSessionNumber(id: number, number: number, opts: { filename?: string } = {}): Promise<void> {\n if (opts.filename !== undefined) {\n await this.pool.query(\"UPDATE sessions SET number = $1, filename = $2 WHERE id = $3\", [\n number,\n opts.filename,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE sessions SET number = $1 WHERE id = $2\", [number, id]);\n }\n }\n\n async updateSessionFilename(id: number, filename: string): Promise<void> {\n await this.pool.query(\"UPDATE sessions SET filename = $1 WHERE id = $2\", [filename, id]);\n }\n\n async updateSessionStatus(id: number, status: SessionStatus, opts: { closedAt?: number } = {}): Promise<void> {\n if (opts.closedAt !== undefined) {\n await this.pool.query(\"UPDATE sessions SET status = $1, closed_at = $2 WHERE id = $3\", [\n status,\n opts.closedAt,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE sessions SET status = $1 WHERE id = $2\", [status, id]);\n }\n }\n\n async moveSessionToProject(id: number, projectId: number, opts: { number?: number } = {}): Promise<void> {\n if (opts.number !== undefined) {\n await this.pool.query(\"UPDATE sessions SET project_id = $1, number = $2 WHERE id = $3\", [\n projectId,\n opts.number,\n id,\n ]);\n } else {\n await this.pool.query(\"UPDATE sessions SET project_id = $1 WHERE id = $2\", [projectId, id]);\n }\n }\n\n async deleteSession(id: number): Promise<void> {\n await this.pool.query(\"DELETE FROM sessions WHERE id = $1\", [id]);\n }\n\n async backfillFoldedSession(keepId: number, dropId: number): Promise<void> {\n await this.pool.query(\n `UPDATE sessions SET\n claude_session_id = COALESCE(claude_session_id,\n (SELECT claude_session_id FROM sessions WHERE id = $1)),\n token_count = COALESCE(token_count,\n (SELECT token_count FROM sessions WHERE id = $1)),\n closed_at = COALESCE(closed_at,\n (SELECT closed_at FROM sessions WHERE id = $1)),\n status = CASE WHEN status = 'open'\n AND (SELECT status FROM sessions WHERE id = $1) != 'open'\n THEN (SELECT status FROM sessions WHERE id = $1)\n ELSE status END\n WHERE id = $2`,\n [dropId, keepId]\n );\n }\n\n async countSessionsForProject(projectId: number): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM sessions WHERE project_id = $1\",\n [projectId]\n );\n return parseInt(r.rows[0].n, 10);\n }\n\n async countSessions(): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\"SELECT COUNT(*)::text AS n FROM sessions\");\n return parseInt(r.rows[0].n, 10);\n }\n\n async getMostRecentSessionDate(projectId: number): Promise<string | null> {\n const r = await this.pool.query<{ date: string }>(\n \"SELECT date FROM sessions WHERE project_id = $1 ORDER BY date DESC LIMIT 1\",\n [projectId]\n );\n return r.rows[0]?.date ?? null;\n }\n\n async getMostRecentSessionCreatedAt(): Promise<number | null> {\n const r = await this.pool.query<{ created_at: string }>(\n \"SELECT created_at FROM sessions ORDER BY created_at DESC LIMIT 1\"\n );\n return r.rows[0] ? Number(r.rows[0].created_at) : null;\n }\n\n // -------------------------------------------------------------------------\n // Session tags\n // -------------------------------------------------------------------------\n\n async listSessionTagIds(sessionId: number): Promise<number[]> {\n const r = await this.pool.query<{ tag_id: number }>(\n \"SELECT tag_id FROM session_tags WHERE session_id = $1\",\n [sessionId]\n );\n return r.rows.map((row) => row.tag_id);\n }\n\n async sessionHasTag(sessionId: number, tagId: number): Promise<boolean> {\n const r = await this.pool.query(\"SELECT 1 FROM session_tags WHERE session_id = $1 AND tag_id = $2\", [\n sessionId,\n tagId,\n ]);\n return (r.rowCount ?? 0) > 0;\n }\n\n async addSessionTag(sessionId: number, tagId: number): Promise<void> {\n await this.pool.query(\"INSERT INTO session_tags (session_id, tag_id) VALUES ($1, $2)\", [sessionId, tagId]);\n }\n\n async deleteSessionTag(sessionId: number, tagId: number): Promise<void> {\n await this.pool.query(\"DELETE FROM session_tags WHERE session_id = $1 AND tag_id = $2\", [sessionId, tagId]);\n }\n\n async reassignSessionTag(fromSessionId: number, toSessionId: number, tagId: number): Promise<void> {\n await this.pool.query(\n \"UPDATE session_tags SET session_id = $1 WHERE session_id = $2 AND tag_id = $3\",\n [toSessionId, fromSessionId, tagId]\n );\n }\n\n async listTagsForSession(sessionId: number): Promise<string[]> {\n const r = await this.pool.query<{ name: string }>(\n \"SELECT t.name FROM tags t JOIN session_tags st ON st.tag_id = t.id WHERE st.session_id = $1 ORDER BY t.name\",\n [sessionId]\n );\n return r.rows.map((row) => row.name);\n }\n\n // -------------------------------------------------------------------------\n // Links\n // -------------------------------------------------------------------------\n\n async addLink(input: {\n sessionId: number;\n targetProjectId: number;\n linkType: LinkType;\n createdAt: number;\n }): Promise<void> {\n await this.pool.query(\n \"INSERT INTO links (session_id, target_project_id, link_type, created_at) VALUES ($1, $2, $3, $4)\",\n [input.sessionId, input.targetProjectId, input.linkType, input.createdAt]\n );\n }\n\n async listLinksForSession(sessionId: number): Promise<Array<{ id: number; target_project_id: number }>> {\n const r = await this.pool.query(\"SELECT id, target_project_id FROM links WHERE session_id = $1\", [\n sessionId,\n ]);\n return r.rows as Array<{ id: number; target_project_id: number }>;\n }\n\n async listLinksForProject(projectId: number): Promise<Array<{ id: number; session_id: number }>> {\n const r = await this.pool.query(\"SELECT id, session_id FROM links WHERE target_project_id = $1\", [\n projectId,\n ]);\n return r.rows as Array<{ id: number; session_id: number }>;\n }\n\n async linkExists(sessionId: number, targetProjectId: number): Promise<boolean> {\n const r = await this.pool.query(\n \"SELECT 1 FROM links WHERE session_id = $1 AND target_project_id = $2 LIMIT 1\",\n [sessionId, targetProjectId]\n );\n return (r.rowCount ?? 0) > 0;\n }\n\n async deleteLink(id: number): Promise<void> {\n await this.pool.query(\"DELETE FROM links WHERE id = $1\", [id]);\n }\n\n async deleteLinksTargetingProject(projectId: number): Promise<void> {\n await this.pool.query(\"DELETE FROM links WHERE target_project_id = $1\", [projectId]);\n }\n\n async deleteLinksFromProjectSessions(projectId: number): Promise<void> {\n await this.pool.query(\n \"DELETE FROM links WHERE session_id IN (SELECT id FROM sessions WHERE project_id = $1)\",\n [projectId]\n );\n }\n\n async retargetLink(id: number, targetProjectId: number): Promise<void> {\n await this.pool.query(\"UPDATE links SET target_project_id = $1 WHERE id = $2\", [targetProjectId, id]);\n }\n\n async moveLinkToSession(id: number, sessionId: number): Promise<void> {\n await this.pool.query(\"UPDATE links SET session_id = $1 WHERE id = $2\", [sessionId, id]);\n }\n\n async reassignLinksTarget(fromProjectId: number, toProjectId: number): Promise<void> {\n // Postgres UPDATE has no OR IGNORE; skip rows that would collide with an\n // existing (session_id, target_project_id) row, matching SQLite's behaviour\n // of silently leaving those rows for the caller's follow-up cleanup.\n await this.pool.query(\n `UPDATE links SET target_project_id = $1 WHERE target_project_id = $2\n AND NOT EXISTS (\n SELECT 1 FROM links l2 WHERE l2.session_id = links.session_id AND l2.target_project_id = $1\n )`,\n [toProjectId, fromProjectId]\n );\n }\n\n async countLinksForProject(projectId: number): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM links WHERE target_project_id = $1\",\n [projectId]\n );\n return parseInt(r.rows[0].n, 10);\n }\n\n async deleteSelfLinksForProject(projectId: number): Promise<void> {\n await this.pool.query(\n `DELETE FROM links WHERE target_project_id = $1\n AND session_id IN (SELECT id FROM sessions WHERE project_id = $2)`,\n [projectId, projectId]\n );\n }\n\n // -------------------------------------------------------------------------\n // Compaction log\n // -------------------------------------------------------------------------\n\n async appendCompactionLog(entry: NewCompactionLogEntry): Promise<void> {\n await this.pool.query(\n `INSERT INTO compaction_log (project_id, session_id, trigger, files_written, token_count, created_at)\n VALUES ($1, $2, $3, $4, $5, $6)`,\n [entry.projectId, entry.sessionId, entry.trigger, entry.filesWritten, entry.tokenCount, entry.createdAt]\n );\n }\n\n async countCompactionLogsForProject(projectId: number): Promise<number> {\n const r = await this.pool.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM compaction_log WHERE project_id = $1\",\n [projectId]\n );\n return parseInt(r.rows[0].n, 10);\n }\n\n async moveCompactionLogsByProject(fromProjectId: number, toProjectId: number): Promise<number> {\n const r = await this.pool.query(\"UPDATE compaction_log SET project_id = $1 WHERE project_id = $2\", [\n toProjectId,\n fromProjectId,\n ]);\n return r.rowCount ?? 0;\n }\n\n async moveCompactionLogsBySession(fromSessionId: number, toSessionId: number): Promise<void> {\n await this.pool.query(\"UPDATE compaction_log SET session_id = $1 WHERE session_id = $2\", [\n toSessionId,\n fromSessionId,\n ]);\n }\n\n async deleteCompactionLogsForProject(projectId: number): Promise<void> {\n await this.pool.query(\"DELETE FROM compaction_log WHERE project_id = $1\", [projectId]);\n }\n\n // -------------------------------------------------------------------------\n // Project merge\n // -------------------------------------------------------------------------\n\n async planProjectMerge(fromSlug: string, intoSlug: string): Promise<MergePlan> {\n if (fromSlug === intoSlug) {\n throw new MergeError(`Cannot merge ${fromSlug} into itself.`);\n }\n\n const fromR = await this.pool.query<{ id: number; slug: string }>(\n \"SELECT id, slug FROM projects WHERE slug = $1\",\n [fromSlug]\n );\n const intoR = await this.pool.query<{ id: number; slug: string }>(\n \"SELECT id, slug FROM projects WHERE slug = $1\",\n [intoSlug]\n );\n const from = fromR.rows[0];\n const into = intoR.rows[0];\n if (!from) throw new MergeError(`No project with slug \"${fromSlug}\".`);\n if (!into) throw new MergeError(`No project with slug \"${intoSlug}\".`);\n\n const maxRow = await this.pool.query<{ n: number }>(\n \"SELECT COALESCE(MAX(number), 0) AS n FROM sessions WHERE project_id = $1\",\n [into.id]\n );\n let next = maxRow.rows[0].n;\n\n const incoming = await this.pool.query<{ id: number; number: number }>(\n \"SELECT id, number FROM sessions WHERE project_id = $1 ORDER BY number ASC\",\n [from.id]\n );\n const sessions = incoming.rows.map((s) => ({ id: s.id, from: s.number, to: ++next }));\n\n const count = async (sql: string, ...args: unknown[]): Promise<number> => {\n const r = await this.pool.query<{ n: string }>(sql, args);\n return parseInt(r.rows[0].n, 10);\n };\n\n const aliasTaken =\n (await count(\"SELECT COUNT(*)::text AS n FROM aliases WHERE alias = $1\", from.slug)) > 0 ||\n (await count(\"SELECT COUNT(*)::text AS n FROM projects WHERE slug = $1\", from.slug)) > 1;\n\n return {\n fromId: from.id,\n fromSlug: from.slug,\n intoId: into.id,\n intoSlug: into.slug,\n sessions,\n tags: await count(\"SELECT COUNT(*)::text AS n FROM project_tags WHERE project_id = $1\", from.id),\n aliases: await count(\"SELECT COUNT(*)::text AS n FROM aliases WHERE project_id = $1\", from.id),\n compactions: await count(\n \"SELECT COUNT(*)::text AS n FROM compaction_log WHERE project_id = $1\",\n from.id\n ),\n links: await count(\"SELECT COUNT(*)::text AS n FROM links WHERE target_project_id = $1\", from.id),\n aliasToAdd: aliasTaken ? undefined : from.slug,\n };\n }\n\n async applyProjectMerge(plan: MergePlan): Promise<void> {\n const client: PoolClient = await this.pool.connect();\n try {\n await client.query(\"BEGIN\");\n\n for (const s of plan.sessions) {\n await client.query(\"UPDATE sessions SET project_id = $1, number = $2 WHERE id = $3\", [\n plan.intoId,\n s.to,\n s.id,\n ]);\n }\n\n await client.query(\n `INSERT INTO project_tags (project_id, tag_id)\n SELECT $1, tag_id FROM project_tags WHERE project_id = $2\n ON CONFLICT (project_id, tag_id) DO NOTHING`,\n [plan.intoId, plan.fromId]\n );\n await client.query(\"DELETE FROM project_tags WHERE project_id = $1\", [plan.fromId]);\n\n await client.query(\"UPDATE aliases SET project_id = $1 WHERE project_id = $2\", [\n plan.intoId,\n plan.fromId,\n ]);\n\n await client.query(\"UPDATE compaction_log SET project_id = $1 WHERE project_id = $2\", [\n plan.intoId,\n plan.fromId,\n ]);\n\n // Links: same NOT EXISTS guard as reassignLinksTarget, plus the same\n // two follow-up cleanups SQLite's applyMerge does.\n await client.query(\n `UPDATE links SET target_project_id = $1 WHERE target_project_id = $2\n AND NOT EXISTS (\n SELECT 1 FROM links l2 WHERE l2.session_id = links.session_id AND l2.target_project_id = $1\n )`,\n [plan.intoId, plan.fromId]\n );\n await client.query(\"DELETE FROM links WHERE target_project_id = $1\", [plan.fromId]);\n await client.query(\n `DELETE FROM links WHERE target_project_id = $1\n AND session_id IN (SELECT id FROM sessions WHERE project_id = $2)`,\n [plan.intoId, plan.intoId]\n );\n\n if (plan.aliasToAdd) {\n await client.query(\n \"INSERT INTO aliases (alias, project_id) VALUES ($1, $2) ON CONFLICT (alias) DO NOTHING\",\n [plan.aliasToAdd, plan.intoId]\n );\n }\n\n await client.query(\"DELETE FROM projects WHERE id = $1\", [plan.fromId]);\n\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n }\n}\n","/**\n * SQLite DDL for the PAI registry database.\n *\n * Tables:\n * - projects — tracked project directories with type and status\n * - sessions — per-project session notes\n * - tags — normalised tag vocabulary\n * - project_tags — M:N join between projects and tags\n * - session_tags — M:N join between sessions and tags\n * - aliases — alternative slugs that resolve to a project\n * - compaction_log — audit trail for context-compaction events\n * - schema_version — single-row migration version tracking\n */\n\nimport type { Database } from \"better-sqlite3\";\n\nexport const SCHEMA_VERSION = 4;\n\nexport const CREATE_TABLES_SQL = `\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS projects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n slug TEXT NOT NULL UNIQUE,\n display_name TEXT NOT NULL,\n root_path TEXT NOT NULL UNIQUE,\n encoded_dir TEXT NOT NULL UNIQUE,\n type TEXT NOT NULL DEFAULT 'local'\n CHECK(type IN ('local','central','obsidian-linked','external')),\n status TEXT NOT NULL DEFAULT 'active'\n CHECK(status IN ('active','archived','migrating')),\n parent_id INTEGER,\n obsidian_link TEXT,\n claude_notes_dir TEXT,\n session_config TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n archived_at INTEGER,\n FOREIGN KEY (parent_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS sessions (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n number INTEGER NOT NULL,\n date TEXT NOT NULL,\n slug TEXT NOT NULL,\n title TEXT NOT NULL,\n filename TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'open'\n CHECK(status IN ('open','completed','compacted')),\n claude_session_id TEXT,\n token_count INTEGER,\n created_at INTEGER NOT NULL,\n closed_at INTEGER,\n UNIQUE (project_id, number),\n FOREIGN KEY (project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS tags (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE\n);\n\nCREATE TABLE IF NOT EXISTS project_tags (\n project_id INTEGER NOT NULL,\n tag_id INTEGER NOT NULL,\n PRIMARY KEY (project_id, tag_id),\n FOREIGN KEY (project_id) REFERENCES projects(id),\n FOREIGN KEY (tag_id) REFERENCES tags(id)\n);\n\nCREATE TABLE IF NOT EXISTS session_tags (\n session_id INTEGER NOT NULL,\n tag_id INTEGER NOT NULL,\n PRIMARY KEY (session_id, tag_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id),\n FOREIGN KEY (tag_id) REFERENCES tags(id)\n);\n\nCREATE TABLE IF NOT EXISTS aliases (\n alias TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n FOREIGN KEY (project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS compaction_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n session_id INTEGER,\n trigger TEXT NOT NULL\n CHECK(trigger IN ('precompact','manual','end-session')),\n files_written TEXT NOT NULL,\n token_count INTEGER,\n created_at INTEGER NOT NULL,\n FOREIGN KEY (project_id) REFERENCES projects(id),\n FOREIGN KEY (session_id) REFERENCES sessions(id)\n);\n\nCREATE TABLE IF NOT EXISTS links (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id INTEGER NOT NULL,\n target_project_id INTEGER NOT NULL,\n link_type TEXT NOT NULL DEFAULT 'related'\n CHECK(link_type IN ('related','follow-up','reference')),\n created_at INTEGER NOT NULL,\n UNIQUE (session_id, target_project_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id),\n FOREIGN KEY (target_project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_at INTEGER NOT NULL\n);\n\n-- Indexes\nCREATE INDEX IF NOT EXISTS idx_projects_slug ON projects(slug);\nCREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);\nCREATE INDEX IF NOT EXISTS idx_projects_type ON projects(type);\nCREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_date ON sessions(date);\nCREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\nCREATE INDEX IF NOT EXISTS idx_sessions_claude ON sessions(claude_session_id);\nCREATE INDEX IF NOT EXISTS idx_pc_project ON project_tags(project_id);\n`;\n\n/**\n * Run the full DDL against an open database connection.\n *\n * The function is idempotent — every statement uses IF NOT EXISTS so it is\n * safe to call on an already-initialised database. After creating the tables\n * it inserts the current SCHEMA_VERSION into schema_version if no row exists\n * yet.\n */\nexport function initializeSchema(db: Database): void {\n // better-sqlite3's exec() runs multiple semicolon-separated statements\n db.exec(CREATE_TABLES_SQL);\n\n const row = db\n .prepare(\"SELECT version FROM schema_version WHERE version = ?\")\n .get(SCHEMA_VERSION);\n\n if (!row) {\n db.prepare(\n \"INSERT INTO schema_version (version, applied_at) VALUES (?, ?)\"\n ).run(SCHEMA_VERSION, Date.now());\n }\n}\n\n/**\n * Apply incremental schema migrations to an already-initialised database.\n *\n * Each migration is guarded by a version check so it is safe to call on\n * databases at any schema version — already-applied migrations are skipped.\n */\nexport function runMigrations(db: Database): void {\n const currentRow = db\n .prepare(\"SELECT version FROM schema_version ORDER BY version DESC LIMIT 1\")\n .get() as { version: number } | undefined;\n\n const current = currentRow?.version ?? 0;\n\n // Migration v1 → v2: add claude_notes_dir column to projects\n if (current < 2) {\n db.transaction(() => {\n // Use a try/catch so re-running on a DB that already has the column is safe\n try {\n db.exec(\"ALTER TABLE projects ADD COLUMN claude_notes_dir TEXT\");\n } catch {\n // Column may already exist (e.g. fresh DB created with v2 DDL)\n }\n db.prepare(\n \"INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)\"\n ).run(2, Date.now());\n })();\n }\n\n // Migration v2 → v3: add links table for cross-project session references\n if (current < 3) {\n db.transaction(() => {\n try {\n db.exec(`\n CREATE TABLE IF NOT EXISTS links (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id INTEGER NOT NULL,\n target_project_id INTEGER NOT NULL,\n link_type TEXT NOT NULL DEFAULT 'related'\n CHECK(link_type IN ('related','follow-up','reference')),\n created_at INTEGER NOT NULL,\n UNIQUE (session_id, target_project_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id),\n FOREIGN KEY (target_project_id) REFERENCES projects(id)\n )\n `);\n } catch {\n // Table may already exist (fresh DB created with v3 DDL)\n }\n db.prepare(\n \"INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)\"\n ).run(3, Date.now());\n })();\n }\n\n // Migration v3 → v4: add session_config column to projects\n if (current < 4) {\n db.transaction(() => {\n try {\n db.exec(\"ALTER TABLE projects ADD COLUMN session_config TEXT\");\n } catch {\n // Column may already exist\n }\n db.prepare(\n \"INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)\"\n ).run(4, Date.now());\n })();\n }\n}\n","/**\n * Database connection helper for the PAI registry.\n *\n * Uses better-sqlite3 (synchronous API) to open or create registry.db.\n * On first open it runs the full DDL via initializeSchema().\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport BetterSqlite3 from \"better-sqlite3\";\nimport type { Database } from \"better-sqlite3\";\nimport { initializeSchema, runMigrations } from \"./registry-schema.js\";\nimport { paiHomePath, resolvePaiFile } from \"../../config/pai-home.js\";\n\nexport type { Database };\n\n/** Old registry path, inside the ~/.pai/ directory (pre-2026-09-19). */\nexport function oldRegistryPath(): string {\n return join(homedir(), \".pai\", \"registry.db\");\n}\n\n/** Registry path: PAI_HOME/registry.db if present, else the old\n * ~/.pai/registry.db (one-time stderr notice), else the new path. */\nexport function registryDbPath(): string {\n return resolvePaiFile(paiHomePath(\"registry.db\"), [oldRegistryPath()], \"pai config migrate --registry\");\n}\n\n/**\n * Open (or create) the PAI registry database.\n *\n * @param path Absolute path to registry.db. Defaults to PAI_HOME/registry.db\n * (falling back to the pre-2026-09-19 ~/.pai/registry.db).\n * @returns An open better-sqlite3 Database instance.\n *\n * Side effects on first call:\n * - Creates the parent directory if it does not exist.\n * - Enables WAL journal mode.\n * - Runs initializeSchema() if schema_version is empty.\n */\nexport function openRegistry(path: string = registryDbPath()): Database {\n // Ensure the directory exists before SQLite tries to create the file\n mkdirSync(dirname(path), { recursive: true });\n\n const db = new BetterSqlite3(path);\n\n // WAL gives better concurrent read performance and crash safety\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"foreign_keys = ON\");\n\n // Check whether the schema has been applied before\n const tableExists = db\n .prepare(\n `SELECT name FROM sqlite_master\n WHERE type = 'table' AND name = 'schema_version'`\n )\n .get();\n\n if (!tableExists) {\n // Brand-new database — apply the full schema\n initializeSchema(db);\n } else {\n const row = db\n .prepare(\"SELECT version FROM schema_version LIMIT 1\")\n .get() as { version: number } | undefined;\n\n if (!row) {\n // Table exists but is empty — apply schema (handles partial init)\n initializeSchema(db);\n }\n }\n\n // Apply any pending incremental migrations\n runMigrations(db);\n\n return db;\n}\n","/**\n * planMerge/applyMerge — fold one registered project into another without\n * losing anything, against a raw better-sqlite3 Database.\n *\n * Moved out of src/registry/merge.ts (which keeps only the pure MergePlan\n * type and MergeError class) so that the one place in the codebase touching\n * better-sqlite3 for this operation is under src/storage/. Wrapped by\n * SQLiteRegistryBackend.planProjectMerge/applyProjectMerge\n * (../registry-sqlite.ts).\n *\n * The registry accumulates rows that describe the same work twice: a project\n * registered under two spellings of one directory, a vault-notes twin of a code\n * project, a slug that collided and got a `-1`. Measured 2026-08-04 there were\n * six such rows, and between them they carried ten sessions.\n *\n * Those sessions are the only thing of value on the losing row, which is why\n * `archive` is the wrong remedy and a plain DELETE is worse. `pai project health`\n * suggested archiving for every unhealthy row, and archiving a duplicate leaves\n * the sessions attached to a row nobody will look at again.\n *\n * FIVE tables reference a project, and `PRAGMA foreign_keys` is 0 on this\n * database — so deleting a project row does not fail, it silently orphans rows in\n * sessions, project_tags, aliases, compaction_log and links. Anything that merges\n * by hand will miss at least one of them. Hence one function.\n *\n * Two constraints shape the implementation:\n *\n * sessions UNIQUE (project_id, number) — incoming sessions must be renumbered,\n * or the insert collides with the\n * target's own numbering\n * links UNIQUE (session_id, target_project_id)\n * project_tags PRIMARY KEY (project_id, tag_id)\n * — both can already hold the row we are\n * about to point at the target\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { MergeError, type MergePlan } from \"../../registry/merge.js\";\n\n/**\n * What a merge would do, without doing it.\n *\n * Built as a plan first because the session renumbering is the part a reader will\n * not predict, and printing \"0001 -> 0016\" is the difference between a command\n * that can be trusted and one that has to be taken on faith.\n */\nexport function planMerge(db: Database, fromSlug: string, intoSlug: string): MergePlan {\n if (fromSlug === intoSlug) {\n throw new MergeError(`Cannot merge ${fromSlug} into itself.`);\n }\n\n const from = db\n .prepare(\"SELECT id, slug FROM projects WHERE slug = ?\")\n .get(fromSlug) as { id: number; slug: string } | undefined;\n const into = db\n .prepare(\"SELECT id, slug FROM projects WHERE slug = ?\")\n .get(intoSlug) as { id: number; slug: string } | undefined;\n\n if (!from) throw new MergeError(`No project with slug \"${fromSlug}\".`);\n if (!into) throw new MergeError(`No project with slug \"${intoSlug}\".`);\n\n // Where the target's numbering currently ends. Incoming sessions continue from\n // there rather than keeping their own numbers, which would collide.\n const maxRow = db\n .prepare(\"SELECT COALESCE(MAX(number), 0) AS n FROM sessions WHERE project_id = ?\")\n .get(into.id) as { n: number };\n let next = maxRow.n;\n\n const incoming = db\n .prepare(\"SELECT id, number FROM sessions WHERE project_id = ? ORDER BY number ASC\")\n .all(from.id) as { id: number; number: number }[];\n\n const sessions = incoming.map((s) => ({ id: s.id, from: s.number, to: ++next }));\n\n const count = (sql: string, ...args: unknown[]): number =>\n (db.prepare(sql).get(...args) as { n: number }).n;\n\n // The losing slug becomes an alias of the winner, so anything that resolved by\n // that name keeps working — including a human who remembers the old name.\n const aliasTaken =\n count(\"SELECT COUNT(*) AS n FROM aliases WHERE alias = ?\", from.slug) > 0 ||\n count(\"SELECT COUNT(*) AS n FROM projects WHERE slug = ?\", from.slug) > 1;\n\n return {\n fromId: from.id,\n fromSlug: from.slug,\n intoId: into.id,\n intoSlug: into.slug,\n sessions,\n tags: count(\"SELECT COUNT(*) AS n FROM project_tags WHERE project_id = ?\", from.id),\n aliases: count(\"SELECT COUNT(*) AS n FROM aliases WHERE project_id = ?\", from.id),\n compactions: count(\n \"SELECT COUNT(*) AS n FROM compaction_log WHERE project_id = ?\",\n from.id\n ),\n links: count(\"SELECT COUNT(*) AS n FROM links WHERE target_project_id = ?\", from.id),\n aliasToAdd: aliasTaken ? undefined : from.slug,\n };\n}\n\n/**\n * Apply a plan. One transaction: either the whole row is folded in or nothing is.\n *\n * A half-merged project is the worst outcome available here — sessions moved but\n * the row still present, or the row gone and its tags orphaned — and with foreign\n * keys off nothing would complain.\n */\nexport function applyMerge(db: Database, plan: MergePlan): void {\n const run = db.transaction(() => {\n // Sessions, one at a time because each gets its own new number.\n const move = db.prepare(\n \"UPDATE sessions SET project_id = ?, number = ? WHERE id = ?\"\n );\n for (const s of plan.sessions) move.run(plan.intoId, s.to, s.id);\n\n // Tags: OR IGNORE because (project_id, tag_id) is the primary key and the\n // target may already carry the same tag.\n db.prepare(\n `INSERT OR IGNORE INTO project_tags (project_id, tag_id)\n SELECT ?, tag_id FROM project_tags WHERE project_id = ?`\n ).run(plan.intoId, plan.fromId);\n db.prepare(\"DELETE FROM project_tags WHERE project_id = ?\").run(plan.fromId);\n\n // Existing aliases of the loser now point at the winner.\n db.prepare(\"UPDATE aliases SET project_id = ? WHERE project_id = ?\").run(\n plan.intoId,\n plan.fromId\n );\n\n db.prepare(\"UPDATE compaction_log SET project_id = ? WHERE project_id = ?\").run(\n plan.intoId,\n plan.fromId\n );\n\n // Links: same collision shape as tags, plus one extra case — a session that\n // linked to the loser may now be linking to its own project, which is not a\n // relationship worth keeping.\n db.prepare(\n `UPDATE OR IGNORE links SET target_project_id = ? WHERE target_project_id = ?`\n ).run(plan.intoId, plan.fromId);\n db.prepare(\"DELETE FROM links WHERE target_project_id = ?\").run(plan.fromId);\n db.prepare(\n `DELETE FROM links WHERE target_project_id = ?\n AND session_id IN (SELECT id FROM sessions WHERE project_id = ?)`\n ).run(plan.intoId, plan.intoId);\n\n // And keep the old name resolvable.\n if (plan.aliasToAdd) {\n db.prepare(\"INSERT OR IGNORE INTO aliases (alias, project_id) VALUES (?, ?)\").run(\n plan.aliasToAdd,\n plan.intoId\n );\n }\n\n db.prepare(\"DELETE FROM projects WHERE id = ?\").run(plan.fromId);\n });\n\n run();\n}\n","/**\n * SQLiteRegistryBackend — wraps the existing better-sqlite3 registry.db\n * (src/registry/db.ts, src/registry/merge.ts) behind the RegistryBackend\n * interface.\n *\n * Thin adapter: every method is the same SQL the 64 direct callers run today,\n * just relocated here and wrapped in Promise.resolve(...) so the interface can\n * be async (matching StorageBackend, and Postgres in unit 4) without changing\n * SQLite behaviour.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { planMerge, applyMerge } from \"./sqlite/registry-merge.js\";\nimport type {\n RegistryBackend,\n Project,\n ProjectWithSessionStats,\n Session,\n SessionWithProject,\n NewProject,\n NewProjectWithSlugRetry,\n NewSession,\n NewCompactionLogEntry,\n ListProjectsOptions,\n ListProjectsByPathLengthOptions,\n ListProjectsWithStatsOptions,\n ListNamedProjectsOptions,\n ListSessionsOptions,\n ListSessionsForProjectOptions,\n ProjectType,\n ProjectStatus,\n SessionStatus,\n LinkType,\n MergePlan,\n} from \"./registry-interface.js\";\n\nexport class SQLiteRegistryBackend implements RegistryBackend {\n readonly backendType = \"sqlite\" as const;\n\n private db: Database;\n\n constructor(db: Database) {\n this.db = db;\n }\n\n /** Expose the raw better-sqlite3 Database handle for not-yet-converted callers. */\n getRawDb(): Database {\n return this.db;\n }\n\n async close(): Promise<void> {\n try {\n this.db.close();\n } catch {\n // ignore\n }\n }\n\n async resetRegistry(): Promise<void> {\n const run = this.db.transaction(() => {\n this.db.prepare(\"DELETE FROM links\").run();\n this.db.prepare(\"DELETE FROM compaction_log\").run();\n this.db.prepare(\"DELETE FROM session_tags\").run();\n this.db.prepare(\"DELETE FROM project_tags\").run();\n this.db.prepare(\"DELETE FROM aliases\").run();\n this.db.prepare(\"DELETE FROM sessions\").run();\n this.db.prepare(\"DELETE FROM tags\").run();\n this.db.prepare(\"DELETE FROM projects\").run();\n });\n run();\n }\n\n // -------------------------------------------------------------------------\n // Projects — reads\n // -------------------------------------------------------------------------\n\n async getProjectById(id: number): Promise<Project | null> {\n const row = this.db.prepare(\"SELECT * FROM projects WHERE id = ?\").get(id) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectBySlug(\n slug: string,\n opts: { caseInsensitive?: boolean; excludeId?: number } = {}\n ): Promise<Project | null> {\n let sql = opts.caseInsensitive\n ? \"SELECT * FROM projects WHERE lower(slug) = lower(?)\"\n : \"SELECT * FROM projects WHERE slug = ?\";\n const params: unknown[] = [slug];\n if (opts.excludeId !== undefined) {\n sql += \" AND id != ?\";\n params.push(opts.excludeId);\n }\n const row = this.db.prepare(sql).get(...params) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectByAlias(\n alias: string,\n opts: { caseInsensitive?: boolean } = {}\n ): Promise<Project | null> {\n const sql = opts.caseInsensitive\n ? \"SELECT p.* FROM projects p JOIN aliases a ON a.project_id = p.id WHERE lower(a.alias) = lower(?)\"\n : \"SELECT p.* FROM projects p JOIN aliases a ON a.project_id = p.id WHERE a.alias = ?\";\n const row = this.db.prepare(sql).get(alias) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectByRootPath(rootPath: string, opts: { excludeId?: number } = {}): Promise<Project | null> {\n let sql = \"SELECT * FROM projects WHERE root_path = ?\";\n const params: unknown[] = [rootPath];\n if (opts.excludeId !== undefined) {\n sql += \" AND id != ?\";\n params.push(opts.excludeId);\n }\n const row = this.db.prepare(sql).get(...params) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectByEncodedDir(encodedDir: string, opts: { excludeId?: number } = {}): Promise<Project | null> {\n let sql = \"SELECT * FROM projects WHERE encoded_dir = ?\";\n const params: unknown[] = [encodedDir];\n if (opts.excludeId !== undefined) {\n sql += \" AND id != ?\";\n params.push(opts.excludeId);\n }\n const row = this.db.prepare(sql).get(...params) as Project | undefined;\n return row ?? null;\n }\n\n async findProjectByCwdPrefix(cwd: string): Promise<Project | null> {\n const row = this.db\n .prepare(\n `SELECT * FROM projects WHERE status = 'active' AND ? LIKE root_path || '%'\n ORDER BY length(root_path) DESC LIMIT 1`\n )\n .get(cwd) as Project | undefined;\n return row ?? null;\n }\n\n async listProjects(opts: ListProjectsOptions = {}): Promise<Project[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (opts.status) {\n where.push(\"p.status = ?\");\n params.push(opts.status);\n }\n if (opts.excludeArchived) {\n where.push(\"p.status != 'archived'\");\n }\n if (opts.tagName) {\n where.push(\n \"p.id IN (SELECT pt.project_id FROM project_tags pt JOIN tags t ON pt.tag_id = t.id WHERE t.name = ?)\"\n );\n params.push(opts.tagName);\n }\n\n let sql = \"SELECT p.* FROM projects p\";\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n\n switch (opts.orderBy) {\n case \"updated_desc\":\n sql += \" ORDER BY p.updated_at DESC\";\n break;\n case \"slug\":\n sql += \" ORDER BY p.slug ASC\";\n break;\n case \"id\":\n sql += \" ORDER BY p.id\";\n break;\n case \"status_updated\":\n default:\n sql += \" ORDER BY p.status ASC, p.updated_at DESC\";\n break;\n }\n if (opts.limit !== undefined) {\n sql += \" LIMIT ?\";\n params.push(opts.limit);\n }\n\n return this.db.prepare(sql).all(...params) as Project[];\n }\n\n async listProjectsByPathLengthDesc(opts: ListProjectsByPathLengthOptions = {}): Promise<Project[]> {\n const where = opts.excludeArchived ? \"WHERE status != 'archived'\" : \"\";\n return this.db\n .prepare(`SELECT * FROM projects ${where} ORDER BY LENGTH(root_path) DESC`)\n .all() as Project[];\n }\n\n async listProjectsWithSessionStats(\n opts: ListProjectsWithStatsOptions = {}\n ): Promise<ProjectWithSessionStats[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (opts.status) {\n where.push(\"p.status = ?\");\n params.push(opts.status);\n }\n if (opts.tagId !== undefined) {\n where.push(\"p.id IN (SELECT project_id FROM project_tags WHERE tag_id = ?)\");\n params.push(opts.tagId);\n }\n\n let sql = `\n SELECT p.*,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n `;\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n sql +=\n opts.orderBy === \"updated_desc\"\n ? \" ORDER BY p.updated_at DESC\"\n : \" ORDER BY p.status ASC, p.updated_at DESC\";\n\n return this.db.prepare(sql).all(...params) as ProjectWithSessionStats[];\n }\n\n async listNamedProjects(\n opts: ListNamedProjectsOptions = {}\n ): Promise<Array<ProjectWithSessionStats & { name: string | null }>> {\n const join = opts.includeUnnamed ? \"LEFT JOIN\" : \"JOIN\";\n const sql = `\n SELECT p.*, a.alias AS name,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n ${join} aliases a ON a.project_id = p.id\n WHERE p.status = 'active'\n ORDER BY p.updated_at DESC\n `;\n return this.db.prepare(sql).all() as Array<ProjectWithSessionStats & { name: string | null }>;\n }\n\n async searchProjects(query: string, limit = 20): Promise<Project[]> {\n const q = `%${query}%`;\n return this.db\n .prepare(\n `SELECT * FROM projects WHERE slug LIKE ? OR display_name LIKE ? OR root_path LIKE ?\n ORDER BY updated_at DESC LIMIT ?`\n )\n .all(q, q, q, limit) as Project[];\n }\n\n async countProjects(opts: { status?: ProjectStatus } = {}): Promise<number> {\n const sql = opts.status\n ? \"SELECT COUNT(*) AS n FROM projects WHERE status = ?\"\n : \"SELECT COUNT(*) AS n FROM projects\";\n const row = opts.status\n ? (this.db.prepare(sql).get(opts.status) as { n: number })\n : (this.db.prepare(sql).get() as { n: number });\n return row.n;\n }\n\n async getMostRecentProjectUpdatedAt(): Promise<number | null> {\n const row = this.db.prepare(\"SELECT updated_at FROM projects ORDER BY updated_at DESC LIMIT 1\").get() as\n | { updated_at: number }\n | undefined;\n return row?.updated_at ?? null;\n }\n\n async countChildProjects(parentId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM projects WHERE parent_id = ?\").get(parentId) as {\n n: number;\n }).n;\n }\n\n async findSiblingProjectsBySlugPattern(\n excludeProjectId: number,\n slug: string,\n limit = 3\n ): Promise<Array<{ slug: string; root_path: string; session_count: number }>> {\n return this.db\n .prepare(\n `SELECT p.slug, p.root_path, COUNT(s.id) AS session_count\n FROM projects p LEFT JOIN sessions s ON s.project_id = p.id\n WHERE p.id != ? AND (p.slug = ? OR p.slug LIKE ? || '-%' OR ? LIKE p.slug || '-%')\n GROUP BY p.id HAVING session_count > 0 ORDER BY session_count DESC LIMIT ?`\n )\n .all(excludeProjectId, slug, slug, slug, limit) as Array<{\n slug: string;\n root_path: string;\n session_count: number;\n }>;\n }\n\n // -------------------------------------------------------------------------\n // Projects — writes\n // -------------------------------------------------------------------------\n\n async createProject(input: NewProject): Promise<Project> {\n const type: ProjectType = input.type ?? \"local\";\n const status: ProjectStatus = input.status ?? \"active\";\n const result = this.db\n .prepare(\n `INSERT INTO projects (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(\n input.slug,\n input.displayName,\n input.rootPath,\n input.encodedDir,\n type,\n status,\n input.createdAt,\n input.updatedAt\n );\n return (await this.getProjectById(result.lastInsertRowid as number))!;\n }\n\n async createProjectWithSlugRetry(\n input: NewProjectWithSlugRetry\n ): Promise<{ id: number; slug: string; created: boolean }> {\n let slug = input.baseSlug;\n let attempt = 0;\n const insert = this.db.prepare(\n `INSERT OR IGNORE INTO projects (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`\n );\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const info = insert.run(\n slug,\n input.displayName,\n input.rootPath,\n input.encodedDir,\n input.createdAt,\n input.updatedAt\n );\n if (info.changes > 0) {\n return { id: info.lastInsertRowid as number, slug, created: true };\n }\n\n const existing = this.db.prepare(\"SELECT id FROM projects WHERE root_path = ?\").get(input.rootPath) as\n | { id: number }\n | undefined;\n if (existing) {\n return { id: existing.id, slug, created: false };\n }\n\n attempt++;\n slug = `${input.baseSlug}-${attempt}`;\n }\n }\n\n async updateProjectPath(\n id: number,\n patch: { rootPath?: string; encodedDir?: string },\n updatedAt?: number\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [];\n if (patch.rootPath !== undefined) {\n sets.push(\"root_path = ?\");\n params.push(patch.rootPath);\n }\n if (patch.encodedDir !== undefined) {\n sets.push(\"encoded_dir = ?\");\n params.push(patch.encodedDir);\n }\n if (updatedAt !== undefined) {\n sets.push(\"updated_at = ?\");\n params.push(updatedAt);\n }\n if (!sets.length) return;\n params.push(id);\n this.db.prepare(`UPDATE projects SET ${sets.join(\", \")} WHERE id = ?`).run(...params);\n }\n\n async updateProjectStatus(\n id: number,\n status: ProjectStatus,\n opts: { archivedAt?: number | null; updatedAt?: number; requireCurrentStatus?: ProjectStatus } = {}\n ): Promise<void> {\n const sets: string[] = [\"status = ?\"];\n const params: unknown[] = [status];\n if (opts.archivedAt !== undefined) {\n sets.push(\"archived_at = ?\");\n params.push(opts.archivedAt);\n }\n if (opts.updatedAt !== undefined) {\n sets.push(\"updated_at = ?\");\n params.push(opts.updatedAt);\n }\n let sql = `UPDATE projects SET ${sets.join(\", \")} WHERE id = ?`;\n params.push(id);\n if (opts.requireCurrentStatus) {\n sql += \" AND status = ?\";\n params.push(opts.requireCurrentStatus);\n }\n this.db.prepare(sql).run(...params);\n }\n\n async updateProjectDisplayName(id: number, displayName: string, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET display_name = ?, updated_at = ? WHERE id = ?\")\n .run(displayName, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET display_name = ? WHERE id = ?\").run(displayName, id);\n }\n }\n\n async updateProjectType(id: number, type: ProjectType, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db.prepare(\"UPDATE projects SET type = ?, updated_at = ? WHERE id = ?\").run(type, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET type = ? WHERE id = ?\").run(type, id);\n }\n }\n\n async updateProjectSessionConfig(id: number, config: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET session_config = ?, updated_at = ? WHERE id = ?\")\n .run(config, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET session_config = ? WHERE id = ?\").run(config, id);\n }\n }\n\n async updateProjectClaudeNotesDir(id: number, dir: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET claude_notes_dir = ?, updated_at = ? WHERE id = ?\")\n .run(dir, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET claude_notes_dir = ? WHERE id = ?\").run(dir, id);\n }\n }\n\n async updateProjectObsidianLink(id: number, link: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET obsidian_link = ?, updated_at = ? WHERE id = ?\")\n .run(link, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET obsidian_link = ? WHERE id = ?\").run(link, id);\n }\n }\n\n async updateProjectSlug(id: number, slug: string, updatedAt: number): Promise<void> {\n this.db.prepare(\"UPDATE projects SET slug = ?, updated_at = ? WHERE id = ?\").run(slug, updatedAt, id);\n }\n\n async reassignProjectParent(fromParentId: number, toParentId: number): Promise<number> {\n return this.db.prepare(\"UPDATE projects SET parent_id = ? WHERE parent_id = ?\").run(toParentId, fromParentId)\n .changes;\n }\n\n async deleteProject(id: number): Promise<void> {\n this.db.prepare(\"DELETE FROM projects WHERE id = ?\").run(id);\n }\n\n async deleteProjectCascade(id: number): Promise<void> {\n const run = this.db.transaction(() => {\n this.db.prepare(\"DELETE FROM links WHERE target_project_id = ?\").run(id);\n this.db\n .prepare(\"DELETE FROM links WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)\")\n .run(id);\n this.db.prepare(\"DELETE FROM compaction_log WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM project_tags WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM aliases WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM sessions WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM projects WHERE id = ?\").run(id);\n });\n run();\n }\n\n // -------------------------------------------------------------------------\n // Tags\n // -------------------------------------------------------------------------\n\n async listTagsForProject(projectId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\n \"SELECT t.name FROM tags t JOIN project_tags pt ON pt.tag_id = t.id WHERE pt.project_id = ? ORDER BY t.name\"\n )\n .all(projectId) as Array<{ name: string }>;\n return rows.map((r) => r.name);\n }\n\n async listAllTags(): Promise<Array<{ id: number; name: string }>> {\n return this.db.prepare(\"SELECT id, name FROM tags ORDER BY name\").all() as Array<{\n id: number;\n name: string;\n }>;\n }\n\n async upsertTag(name: string): Promise<number> {\n this.db.prepare(\"INSERT OR IGNORE INTO tags (name) VALUES (?)\").run(name);\n return (this.db.prepare(\"SELECT id FROM tags WHERE name = ?\").get(name) as { id: number }).id;\n }\n\n async addProjectTag(projectId: number, tagId: number): Promise<void> {\n this.db.prepare(\"INSERT INTO project_tags (project_id, tag_id) VALUES (?, ?)\").run(projectId, tagId);\n }\n\n async projectHasTag(projectId: number, tagId: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM project_tags WHERE project_id = ? AND tag_id = ?\").get(projectId, tagId);\n }\n\n async deleteProjectTag(projectId: number, tagId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM project_tags WHERE project_id = ? AND tag_id = ?\").run(projectId, tagId);\n }\n\n async reassignProjectTag(fromProjectId: number, toProjectId: number, tagId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE project_tags SET project_id = ? WHERE project_id = ? AND tag_id = ?\")\n .run(toProjectId, fromProjectId, tagId);\n }\n\n async listProjectTagIds(projectId: number): Promise<number[]> {\n const rows = this.db.prepare(\"SELECT tag_id FROM project_tags WHERE project_id = ?\").all(projectId) as Array<{\n tag_id: number;\n }>;\n return rows.map((r) => r.tag_id);\n }\n\n async countProjectTagsForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM project_tags WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async copyProjectTags(fromProjectId: number, toProjectId: number): Promise<void> {\n this.db\n .prepare(\n \"INSERT OR IGNORE INTO project_tags (project_id, tag_id) SELECT ?, tag_id FROM project_tags WHERE project_id = ?\"\n )\n .run(toProjectId, fromProjectId);\n }\n\n async deleteProjectTagsForProject(projectId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM project_tags WHERE project_id = ?\").run(projectId);\n }\n\n // -------------------------------------------------------------------------\n // Aliases\n // -------------------------------------------------------------------------\n\n async resolveAlias(alias: string): Promise<number | null> {\n const row = this.db.prepare(\"SELECT project_id FROM aliases WHERE alias = ?\").get(alias) as\n | { project_id: number }\n | undefined;\n return row?.project_id ?? null;\n }\n\n async listAliasesForProject(projectId: number): Promise<string[]> {\n const rows = this.db.prepare(\"SELECT alias FROM aliases WHERE project_id = ? ORDER BY alias\").all(\n projectId\n ) as Array<{ alias: string }>;\n return rows.map((r) => r.alias);\n }\n\n async addAlias(alias: string, projectId: number): Promise<void> {\n this.db.prepare(\"INSERT INTO aliases (alias, project_id) VALUES (?, ?)\").run(alias, projectId);\n }\n\n async removeAlias(alias: string): Promise<void> {\n this.db.prepare(\"DELETE FROM aliases WHERE alias = ?\").run(alias);\n }\n\n async countAliasesForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM aliases WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async moveProjectAliases(fromProjectId: number, toProjectId: number): Promise<void> {\n this.db.prepare(\"UPDATE aliases SET project_id = ? WHERE project_id = ?\").run(toProjectId, fromProjectId);\n }\n\n async reassignAlias(alias: string, toProjectId: number): Promise<void> {\n this.db.prepare(\"UPDATE aliases SET project_id = ? WHERE alias = ?\").run(toProjectId, alias);\n }\n\n async listAliasMap(): Promise<Array<{ alias: string; slug: string; root_path: string }>> {\n return this.db\n .prepare(\n `SELECT a.alias AS alias, p.slug AS slug, p.root_path AS root_path\n FROM aliases a JOIN projects p ON p.id = a.project_id WHERE p.status != 'archived'`\n )\n .all() as Array<{ alias: string; slug: string; root_path: string }>;\n }\n\n // -------------------------------------------------------------------------\n // Sessions\n // -------------------------------------------------------------------------\n\n async getSessionById(id: number): Promise<Session | null> {\n const row = this.db.prepare(\"SELECT * FROM sessions WHERE id = ?\").get(id) as Session | undefined;\n return row ?? null;\n }\n\n async getSessionByNumber(projectId: number, number: number): Promise<Session | null> {\n const row = this.db.prepare(\"SELECT * FROM sessions WHERE project_id = ? AND number = ?\").get(\n projectId,\n number\n ) as Session | undefined;\n return row ?? null;\n }\n\n async getLatestSessionForProject(projectId: number): Promise<Session | null> {\n const row = this.db\n .prepare(\"SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1\")\n .get(projectId) as Session | undefined;\n return row ?? null;\n }\n\n async getMaxSessionNumber(projectId: number): Promise<number> {\n const row = this.db.prepare(\"SELECT COALESCE(MAX(number), 0) AS n FROM sessions WHERE project_id = ?\").get(\n projectId\n ) as { n: number };\n return row.n;\n }\n\n async listSessionsForProject(\n projectId: number,\n opts: ListSessionsForProjectOptions = {}\n ): Promise<Session[]> {\n const orderBy = opts.orderBy === \"created_desc\" ? \"created_at DESC\" : \"number ASC\";\n let sql = `SELECT * FROM sessions WHERE project_id = ? ORDER BY ${orderBy}`;\n const params: unknown[] = [projectId];\n if (opts.limit !== undefined) {\n sql += \" LIMIT ?\";\n params.push(opts.limit);\n }\n return this.db.prepare(sql).all(...params) as Session[];\n }\n\n async listSessions(opts: ListSessionsOptions = {}): Promise<SessionWithProject[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n if (opts.projectId !== undefined) {\n where.push(\"s.project_id = ?\");\n params.push(opts.projectId);\n }\n if (opts.status) {\n where.push(\"s.status = ?\");\n params.push(opts.status);\n }\n let sql = `\n SELECT s.*, p.slug AS project_slug, p.display_name AS project_name\n FROM sessions s JOIN projects p ON p.id = s.project_id\n `;\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n sql += \" ORDER BY s.date DESC, s.number DESC\";\n const limit = opts.limit ?? 20;\n sql += ` LIMIT ${limit}`;\n return this.db.prepare(sql).all(...params) as SessionWithProject[];\n }\n\n async findSessionByFilename(projectId: number, filename: string): Promise<{ id: number } | null> {\n const row = this.db\n .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND filename = ? LIMIT 1\")\n .get(projectId, filename) as { id: number } | undefined;\n return row ?? null;\n }\n\n async sessionNumberTaken(projectId: number, number: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM sessions WHERE project_id = ? AND number = ? LIMIT 1\").get(\n projectId,\n number\n );\n }\n\n async createSession(input: NewSession): Promise<Session> {\n const status = input.status ?? \"completed\";\n const result = this.db\n .prepare(\n `INSERT INTO sessions (project_id, number, date, slug, title, filename, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(input.projectId, input.number, input.date, input.slug, input.title, input.filename, status, input.createdAt);\n return (await this.getSessionById(result.lastInsertRowid as number))!;\n }\n\n async upsertSessionIfAbsent(input: NewSession): Promise<boolean> {\n const existing = this.db\n .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND number = ?\")\n .get(input.projectId, input.number);\n if (existing) return false;\n\n const status = input.status ?? \"completed\";\n this.db\n .prepare(\n `INSERT INTO sessions (project_id, number, date, slug, title, filename, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(input.projectId, input.number, input.date, input.slug, input.title, input.filename, status, input.createdAt);\n return true;\n }\n\n async updateSessionMeta(\n id: number,\n patch: { slug?: string; title?: string; filename?: string }\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [];\n if (patch.slug !== undefined) {\n sets.push(\"slug = ?\");\n params.push(patch.slug);\n }\n if (patch.title !== undefined) {\n sets.push(\"title = ?\");\n params.push(patch.title);\n }\n if (patch.filename !== undefined) {\n sets.push(\"filename = ?\");\n params.push(patch.filename);\n }\n if (!sets.length) return;\n params.push(id);\n this.db.prepare(`UPDATE sessions SET ${sets.join(\", \")} WHERE id = ?`).run(...params);\n }\n\n async updateSessionNumber(id: number, number: number, opts: { filename?: string } = {}): Promise<void> {\n if (opts.filename !== undefined) {\n this.db.prepare(\"UPDATE sessions SET number = ?, filename = ? WHERE id = ?\").run(number, opts.filename, id);\n } else {\n this.db.prepare(\"UPDATE sessions SET number = ? WHERE id = ?\").run(number, id);\n }\n }\n\n async updateSessionFilename(id: number, filename: string): Promise<void> {\n this.db.prepare(\"UPDATE sessions SET filename = ? WHERE id = ?\").run(filename, id);\n }\n\n async updateSessionStatus(id: number, status: SessionStatus, opts: { closedAt?: number } = {}): Promise<void> {\n if (opts.closedAt !== undefined) {\n this.db.prepare(\"UPDATE sessions SET status = ?, closed_at = ? WHERE id = ?\").run(status, opts.closedAt, id);\n } else {\n this.db.prepare(\"UPDATE sessions SET status = ? WHERE id = ?\").run(status, id);\n }\n }\n\n async moveSessionToProject(id: number, projectId: number, opts: { number?: number } = {}): Promise<void> {\n if (opts.number !== undefined) {\n this.db.prepare(\"UPDATE sessions SET project_id = ?, number = ? WHERE id = ?\").run(\n projectId,\n opts.number,\n id\n );\n } else {\n this.db.prepare(\"UPDATE sessions SET project_id = ? WHERE id = ?\").run(projectId, id);\n }\n }\n\n async deleteSession(id: number): Promise<void> {\n this.db.prepare(\"DELETE FROM sessions WHERE id = ?\").run(id);\n }\n\n async backfillFoldedSession(keepId: number, dropId: number): Promise<void> {\n this.db\n .prepare(\n `UPDATE sessions SET\n claude_session_id = COALESCE(claude_session_id,\n (SELECT claude_session_id FROM sessions WHERE id = ?)),\n token_count = COALESCE(token_count,\n (SELECT token_count FROM sessions WHERE id = ?)),\n closed_at = COALESCE(closed_at,\n (SELECT closed_at FROM sessions WHERE id = ?)),\n status = CASE WHEN status = 'open'\n AND (SELECT status FROM sessions WHERE id = ?) != 'open'\n THEN (SELECT status FROM sessions WHERE id = ?)\n ELSE status END\n WHERE id = ?`\n )\n .run(dropId, dropId, dropId, dropId, dropId, keepId);\n }\n\n async countSessionsForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async countSessions(): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM sessions\").get() as { n: number }).n;\n }\n\n async getMostRecentSessionDate(projectId: number): Promise<string | null> {\n const row = this.db\n .prepare(\"SELECT date FROM sessions WHERE project_id = ? ORDER BY date DESC LIMIT 1\")\n .get(projectId) as { date: string } | undefined;\n return row?.date ?? null;\n }\n\n async getMostRecentSessionCreatedAt(): Promise<number | null> {\n const row = this.db.prepare(\"SELECT created_at FROM sessions ORDER BY created_at DESC LIMIT 1\").get() as\n | { created_at: number }\n | undefined;\n return row?.created_at ?? null;\n }\n\n // -------------------------------------------------------------------------\n // Session tags\n // -------------------------------------------------------------------------\n\n async listSessionTagIds(sessionId: number): Promise<number[]> {\n const rows = this.db.prepare(\"SELECT tag_id FROM session_tags WHERE session_id = ?\").all(sessionId) as Array<{\n tag_id: number;\n }>;\n return rows.map((r) => r.tag_id);\n }\n\n async sessionHasTag(sessionId: number, tagId: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM session_tags WHERE session_id = ? AND tag_id = ?\").get(\n sessionId,\n tagId\n );\n }\n\n async addSessionTag(sessionId: number, tagId: number): Promise<void> {\n this.db.prepare(\"INSERT INTO session_tags (session_id, tag_id) VALUES (?, ?)\").run(sessionId, tagId);\n }\n\n async deleteSessionTag(sessionId: number, tagId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM session_tags WHERE session_id = ? AND tag_id = ?\").run(sessionId, tagId);\n }\n\n async reassignSessionTag(fromSessionId: number, toSessionId: number, tagId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE session_tags SET session_id = ? WHERE session_id = ? AND tag_id = ?\")\n .run(toSessionId, fromSessionId, tagId);\n }\n\n async listTagsForSession(sessionId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\n \"SELECT t.name FROM tags t JOIN session_tags st ON st.tag_id = t.id WHERE st.session_id = ? ORDER BY t.name\"\n )\n .all(sessionId) as Array<{ name: string }>;\n return rows.map((r) => r.name);\n }\n\n // -------------------------------------------------------------------------\n // Links\n // -------------------------------------------------------------------------\n\n async addLink(input: {\n sessionId: number;\n targetProjectId: number;\n linkType: LinkType;\n createdAt: number;\n }): Promise<void> {\n this.db\n .prepare(\"INSERT INTO links (session_id, target_project_id, link_type, created_at) VALUES (?, ?, ?, ?)\")\n .run(input.sessionId, input.targetProjectId, input.linkType, input.createdAt);\n }\n\n async listLinksForSession(sessionId: number): Promise<Array<{ id: number; target_project_id: number }>> {\n return this.db.prepare(\"SELECT id, target_project_id FROM links WHERE session_id = ?\").all(\n sessionId\n ) as Array<{ id: number; target_project_id: number }>;\n }\n\n async listLinksForProject(projectId: number): Promise<Array<{ id: number; session_id: number }>> {\n return this.db.prepare(\"SELECT id, session_id FROM links WHERE target_project_id = ?\").all(\n projectId\n ) as Array<{ id: number; session_id: number }>;\n }\n\n async linkExists(sessionId: number, targetProjectId: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM links WHERE session_id = ? AND target_project_id = ? LIMIT 1\").get(\n sessionId,\n targetProjectId\n );\n }\n\n async deleteLink(id: number): Promise<void> {\n this.db.prepare(\"DELETE FROM links WHERE id = ?\").run(id);\n }\n\n async deleteLinksTargetingProject(projectId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM links WHERE target_project_id = ?\").run(projectId);\n }\n\n async deleteLinksFromProjectSessions(projectId: number): Promise<void> {\n this.db\n .prepare(\"DELETE FROM links WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)\")\n .run(projectId);\n }\n\n async retargetLink(id: number, targetProjectId: number): Promise<void> {\n this.db.prepare(\"UPDATE links SET target_project_id = ? WHERE id = ?\").run(targetProjectId, id);\n }\n\n async moveLinkToSession(id: number, sessionId: number): Promise<void> {\n this.db.prepare(\"UPDATE links SET session_id = ? WHERE id = ?\").run(sessionId, id);\n }\n\n async reassignLinksTarget(fromProjectId: number, toProjectId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE OR IGNORE links SET target_project_id = ? WHERE target_project_id = ?\")\n .run(toProjectId, fromProjectId);\n }\n\n async countLinksForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM links WHERE target_project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async deleteSelfLinksForProject(projectId: number): Promise<void> {\n this.db\n .prepare(\n `DELETE FROM links WHERE target_project_id = ?\n AND session_id IN (SELECT id FROM sessions WHERE project_id = ?)`\n )\n .run(projectId, projectId);\n }\n\n // -------------------------------------------------------------------------\n // Compaction log\n // -------------------------------------------------------------------------\n\n async appendCompactionLog(entry: NewCompactionLogEntry): Promise<void> {\n this.db\n .prepare(\n `INSERT INTO compaction_log (project_id, session_id, trigger, files_written, token_count, created_at)\n VALUES (?, ?, ?, ?, ?, ?)`\n )\n .run(entry.projectId, entry.sessionId, entry.trigger, entry.filesWritten, entry.tokenCount, entry.createdAt);\n }\n\n async countCompactionLogsForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM compaction_log WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async moveCompactionLogsByProject(fromProjectId: number, toProjectId: number): Promise<number> {\n return this.db\n .prepare(\"UPDATE compaction_log SET project_id = ? WHERE project_id = ?\")\n .run(toProjectId, fromProjectId).changes;\n }\n\n async moveCompactionLogsBySession(fromSessionId: number, toSessionId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE compaction_log SET session_id = ? WHERE session_id = ?\")\n .run(toSessionId, fromSessionId);\n }\n\n async deleteCompactionLogsForProject(projectId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM compaction_log WHERE project_id = ?\").run(projectId);\n }\n\n // -------------------------------------------------------------------------\n // Project merge\n // -------------------------------------------------------------------------\n\n async planProjectMerge(fromSlug: string, intoSlug: string): Promise<MergePlan> {\n return planMerge(this.db, fromSlug, intoSlug);\n }\n\n async applyProjectMerge(plan: MergePlan): Promise<void> {\n applyMerge(this.db, plan);\n }\n}\n","/**\n * Storage backend factory.\n *\n * Reads the daemon config and returns the appropriate StorageBackend.\n *\n * When Postgres is the configured backend we NEVER silently fall back to\n * SQLite — doing so would split the corpus across two databases. Instead:\n * - Daemon (waitForPostgres: true) retries Postgres forever with capped\n * backoff until it comes up (handles the boot race where launchd starts\n * the daemon before Docker Desktop / Postgres is ready).\n * - CLI / one-shot callers (default) retry a few times, then throw a clear\n * error rather than returning a wrong/empty SQLite database.\n */\n\nimport type { PaiDaemonConfig } from \"../daemon/config.js\";\nimport type { StorageBackend } from \"./interface.js\";\nimport type { RegistryBackend } from \"./registry-interface.js\";\nimport { setBackendOutage, clearBackendOutage } from \"./outage.js\";\n// Static on purpose: a lazy import() of a dist chunk first hit after a rebuild\n// throws \"Cannot find module\" (prune-dist.mjs deletes superseded chunks).\nimport { PostgresBackend } from \"./postgres.js\";\nimport { routeNotification } from \"../notifications/router.js\";\nimport { loadConfig } from \"../daemon/config.js\";\nimport { openFederation } from \"./sqlite/federation-db.js\";\nimport { SQLiteBackend } from \"./sqlite.js\";\nimport { PostgresRegistryBackend } from \"./registry-postgres.js\";\nimport { openRegistry } from \"./sqlite/registry-db.js\";\nimport { SQLiteRegistryBackend } from \"./registry-sqlite.js\";\n\nexport interface StorageBackendOptions {\n /**\n * When true, retry Postgres indefinitely instead of giving up. Used by the\n * long-lived daemon so a not-yet-ready Postgres at boot is tolerated.\n * Defaults to false (one-shot CLI behaviour: bounded retries, then throw).\n */\n waitForPostgres?: boolean;\n}\n\n/** Backoff schedule (ms) for the bounded CLI retry path. */\nconst CLI_RETRY_DELAYS_MS = [500, 1_000, 2_000];\n\n/** Backoff cap (ms) for the daemon's infinite retry path. */\nconst DAEMON_RETRY_CAP_MS = 15_000;\n\n/**\n * Create and return the configured StorageBackend.\n *\n * Auto-behaviour:\n * - storageBackend = \"sqlite\" → SQLiteBackend always\n * - storageBackend = \"postgres\" → PostgresBackend (retried; never falls back)\n */\nexport async function createStorageBackend(\n config: PaiDaemonConfig,\n opts: StorageBackendOptions = {}\n): Promise<StorageBackend> {\n if (config.storageBackend === \"postgres\") {\n return await getSharedPostgresBackend(config, opts.waitForPostgres ?? false);\n }\n\n // Default: SQLite\n return createSQLiteBackend();\n}\n\n/**\n * Attempt a single Postgres connection (ensure DB + test). Returns the live\n * backend on success, or an error string describing why it failed.\n */\nasync function attemptPostgres(\n config: PaiDaemonConfig\n): Promise<{ backend: StorageBackend } | { error: string }> {\n const pgConfig = config.postgres ?? {};\n\n let backend: InstanceType<typeof PostgresBackend> | null = null;\n try {\n // Ensure the per-user database exists and has the schema applied.\n await PostgresBackend.ensureDatabase(pgConfig);\n\n backend = new PostgresBackend(pgConfig);\n const err = await backend.testConnection();\n if (err) {\n await backend.close().catch(() => {});\n return { error: err };\n }\n return { backend };\n } catch (e) {\n if (backend) await backend.close().catch(() => {});\n return { error: e instanceof Error ? e.message : String(e) };\n }\n}\n\n/**\n * Consecutive failures before the outage is escalated to the user.\n *\n * Not the first failure: a container restarting, or the daemon starting before\n * Docker is up, recovers within a few seconds and is not worth a notification.\n * By the fifth attempt the backoff has already spent tens of seconds, which is\n * long enough that something is actually wrong.\n */\nconst ESCALATE_AFTER_ATTEMPTS = 5;\n\n/** Tell the user the backend is down, through whatever channels are configured. */\nasync function notifyBackendDown(attempts: number, lastError: string): Promise<void> {\n try {\n await routeNotification(\n {\n event: \"error\",\n title: \"PAI: storage backend unreachable\",\n message:\n `Postgres has not answered in ${attempts} attempts (${lastError}). ` +\n `Indexing, session notes and the work queue are stalled until it returns. ` +\n `Check the container, then \\`pai daemon status\\`.`,\n },\n loadConfig().notifications\n );\n } catch {\n // A notification that cannot be sent must never take the daemon down with\n // it — the daemon retrying is still the useful behaviour here.\n }\n}\n\n/** And say when it comes back, so the alert is not left hanging. */\nasync function notifyBackendRecovered(\n attempts: number,\n since: number | null\n): Promise<void> {\n try {\n const mins = since ? Math.max(1, Math.round((Date.now() - since) / 60_000)) : null;\n await routeNotification(\n {\n event: \"completion\",\n title: \"PAI: storage backend back\",\n message:\n `Postgres answered after ${attempts} attempts` +\n (mins ? `, ${mins} min down` : \"\") +\n `. The queue will drain on its own.`,\n },\n loadConfig().notifications\n );\n } catch {\n /* same reasoning as above */\n }\n}\n\n/**\n * Shared retry loop: attempt a connection, back off, escalate, repeat — used\n * by both the federation and registry Postgres connectors so the retry/outage\n * behaviour (§6, \"never falls back to SQLite silently\") stays in one place.\n */\nasync function retryConnect<T>(\n attempt: () => Promise<{ backend: T } | { error: string }>,\n waitForever: boolean,\n label: string\n): Promise<T> {\n let attemptN = 0;\n let outageSince: number | null = null;\n let escalated = false;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n attemptN++;\n const result = await attempt();\n if (\"backend\" in result) {\n if (attemptN > 1) process.stderr.write(`[pai-daemon] Connected to ${label} (after ${attemptN} attempts).\\n`);\n // An outage that ended must stop being reported, or the status command\n // trades one wrong answer for another.\n clearBackendOutage();\n if (escalated) void notifyBackendRecovered(attemptN, outageSince);\n return result.backend;\n }\n\n const lastError = result.error;\n\n if (!waitForever && attemptN > CLI_RETRY_DELAYS_MS.length) {\n // Bounded CLI path exhausted — fail loudly, never silently use SQLite.\n throw new Error(\n `${label} unreachable after ${attemptN} attempts: ${lastError}. ` +\n `Is Docker Desktop / Postgres running? Refusing to fall back to SQLite ` +\n `(would split the corpus). Start Postgres and retry.`\n );\n }\n\n const delayMs = waitForever\n ? Math.min(DAEMON_RETRY_CAP_MS, 1_000 * 2 ** Math.min(attemptN - 1, 4))\n : CLI_RETRY_DELAYS_MS[attemptN - 1];\n\n process.stderr.write(\n `[pai-daemon] ${label} unavailable (${lastError}). ` +\n `Retry ${attemptN}${waitForever ? \"\" : `/${CLI_RETRY_DELAYS_MS.length + 1}`} ` +\n `in ${delayMs}ms...\\n`\n );\n\n // Publish the outage so `pai daemon status` can report it. Without this the\n // daemon retries silently forever and status still reads \"idle\" — which is\n // what happened for two days in July: 144 retries over 36 minutes, session\n // notes never written, and the one command anyone would run to check\n // reporting that everything was fine.\n setBackendOutage({\n backend: label,\n since: outageSince ?? (outageSince = Date.now()),\n attempts: attemptN,\n lastError: String(lastError),\n });\n\n // And escalate once, out loud, rather than only into a log nobody tails.\n // Once — not per retry — because a notification that repeats every few\n // seconds is filtered within a minute and stops being a signal at all.\n if (waitForever && attemptN === ESCALATE_AFTER_ATTEMPTS && !escalated) {\n escalated = true;\n void notifyBackendDown(attemptN, String(lastError));\n }\n\n await new Promise((r) => setTimeout(r, delayMs));\n }\n}\n\nasync function connectPostgres(\n config: PaiDaemonConfig,\n waitForever: boolean\n): Promise<StorageBackend> {\n return retryConnect(() => attemptPostgres(config), waitForever, \"PostgreSQL backend\");\n}\n\n/**\n * The process's one Postgres connection: storage and registry share a\n * single pg Pool (one connection budget) instead of each opening their own.\n * Whichever of createStorageBackend()/createRegistryBackend() runs first\n * creates it via connectPostgres()'s retry/outage handling; the other\n * reuses its pool through PostgresBackend.getPool(). Reset by closeStorage().\n */\nlet pgBackendPromise: Promise<StorageBackend> | null = null;\n\nfunction getSharedPostgresBackend(\n config: PaiDaemonConfig,\n waitForever: boolean\n): Promise<StorageBackend> {\n if (!pgBackendPromise) {\n pgBackendPromise = connectPostgres(config, waitForever);\n }\n return pgBackendPromise;\n}\n\nasync function createSQLiteBackend(): Promise<StorageBackend> {\n const db = openFederation();\n return new SQLiteBackend(db);\n}\n\n/**\n * Create and return the configured RegistryBackend (projects, sessions,\n * tags, aliases, links, compaction_log). Same auto-behaviour and same\n * never-fall-back-to-SQLite rule as createStorageBackend().\n */\nexport async function createRegistryBackend(\n config: PaiDaemonConfig,\n opts: StorageBackendOptions = {}\n): Promise<RegistryBackend> {\n if (config.storageBackend === \"postgres\") {\n // Reuses the storage backend's pool (getSharedPostgresBackend) rather\n // than opening a second one against the same database.\n const storage = await getSharedPostgresBackend(config, opts.waitForPostgres ?? false);\n const pool = (storage as unknown as { getPool: () => import(\"pg\").Pool }).getPool();\n return new PostgresRegistryBackend(pool);\n }\n return createSQLiteRegistryBackend();\n}\n\nasync function createSQLiteRegistryBackend(): Promise<RegistryBackend> {\n const db = openRegistry();\n return new SQLiteRegistryBackend(db);\n}\n\n/**\n * Process-wide backend accessors.\n *\n * Callers across the codebase must get their StorageBackend/RegistryBackend\n * from here rather than constructing SQLite or Postgres directly — this is\n * the one place that decides which backend a process uses, and the one place\n * that closes it on exit.\n */\n\nlet storagePromise: Promise<StorageBackend> | null = null;\nlet registryPromise: Promise<RegistryBackend> | null = null;\nlet beforeExitRegistered = false;\n\nfunction registerBeforeExit(): void {\n if (beforeExitRegistered) return;\n beforeExitRegistered = true;\n process.once(\"beforeExit\", () => {\n void closeStorage();\n });\n}\n\n/**\n * Returns the process-wide StorageBackend, creating it on first call.\n *\n * The whole body up to and including the `storagePromise = (async () =>\n * ...)()` assignment must run synchronously (no `await` before it) — two\n * calls issued in the same tick (e.g. `Promise.all([get(), get()])`) must\n * both observe the same cached promise rather than each racing to create\n * their own backend.\n */\nexport async function getStorageBackend(): Promise<StorageBackend> {\n if (!storagePromise) {\n storagePromise = (async () => {\n registerBeforeExit();\n return await createStorageBackend(loadConfig());\n })();\n }\n return storagePromise;\n}\n\n/** Returns the process-wide RegistryBackend, creating it on first call. Same synchronous-caching requirement as getStorageBackend(). */\nexport async function getRegistryBackend(): Promise<RegistryBackend> {\n if (!registryPromise) {\n registryPromise = (async () => {\n registerBeforeExit();\n return await createRegistryBackend(loadConfig());\n })();\n }\n return registryPromise;\n}\n\n/** Closes any backends created via getStorageBackend()/getRegistryBackend(). Idempotent. */\nexport async function closeStorage(): Promise<void> {\n const pending = [storagePromise, registryPromise];\n const pg = pgBackendPromise;\n storagePromise = null;\n registryPromise = null;\n pgBackendPromise = null;\n for (const p of pending) {\n if (!p) continue;\n try {\n const backend = await p;\n // Postgres backends share one pool owned by pgBackendPromise (closed\n // below) — closing them here too would end() it twice.\n if (backend.backendType === \"postgres\") continue;\n await backend.close();\n } catch {\n // A backend that never connected has nothing to close.\n }\n }\n if (pg) {\n try {\n await (await pg).close();\n } catch {\n // Never connected — nothing to close.\n }\n }\n}\n\n/** Test-only: clear the process-wide cache without closing (tests own their own lifecycle). */\nexport function __resetStorageForTests(): void {\n storagePromise = null;\n registryPromise = null;\n pgBackendPromise = null;\n}\n"],"mappings":"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"}
|