{"version":3,"file":"migrate.mjs","sources":["../../../src/memory/migrate.ts"],"sourcesContent":["/**\n * Idempotent schema install + vector-dimension safety check.\n *\n * Called once at agents-library startup by host's `memoryStore.js` singleton\n * factory. Safe to call multiple times — every statement is `IF NOT EXISTS`.\n */\nimport type { Pool } from 'pg';\nimport { readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport { DEFAULT_MEMORY_DIMENSIONS, DEFAULT_MEMORY_TABLE } from './constants';\nimport { getMemoryEmbedder } from './embeddings';\n\nexport interface MigrationOptions {\n  pool: Pool;\n  table?: string;\n  /** If true, skip the live embedder probe (tests). */\n  skipEmbedderProbe?: boolean;\n}\n\nfunction resolveSchemaSqlPath(): string {\n  // Works under both ESM (import.meta.url) and compiled CJS (fall back to __dirname).\n  const meta = import.meta as unknown as { url?: string };\n  const metaUrl = typeof meta.url === 'string' ? meta.url : undefined;\n  if (metaUrl) {\n    return join(dirname(fileURLToPath(metaUrl)), 'schema.sql');\n  }\n  const globalDirname = (globalThis as unknown as { __dirname?: string })\n    .__dirname;\n  return join(globalDirname ?? process.cwd(), 'schema.sql');\n}\n\nexport async function runMemoryMigration(\n  opts: MigrationOptions\n): Promise<void> {\n  const table = opts.table ?? DEFAULT_MEMORY_TABLE;\n  const schemaPath = resolveSchemaSqlPath();\n  const schemaSql = readFileSync(schemaPath, 'utf8').replace(\n    /agent_memories/g,\n    table\n  );\n\n  // Drop the legacy `(agent_id, path)` unique constraint BEFORE running\n  // the schema SQL. The schema creates the new\n  // `(agent_id, user_id, path)` NULLS-NOT-DISTINCT constraint, which\n  // must not collide with the old one. Dropping by the legacy name is a\n  // no-op on fresh installs where the constraint never existed, and on\n  // upgrades it cleanly frees the unique-index slot so the new\n  // constraint can be installed.\n  await opts.pool\n    .query(\n      `ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${table}_agent_path_uq`\n    )\n    .catch(() => undefined);\n\n  await opts.pool.query(schemaSql);\n\n  // Adopt legacy rows if the table pre-existed with an older shape.\n  // Idempotent — every statement is no-op-safe on a fresh schema.\n  await opts.pool\n    .query(`ALTER TABLE ${table} ALTER COLUMN user_id DROP NOT NULL`)\n    .catch(() => undefined);\n  await opts.pool.query(\n    `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`\n  );\n  await opts.pool.query(\n    `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS last_user_id TEXT`\n  );\n  // Belt-and-suspenders: if the schema CREATE was skipped because the\n  // table already existed AND the new constraint was absent, add it\n  // explicitly. Swallow duplicate-object errors on happy-path re-runs.\n  await opts.pool\n    .query(\n      `ALTER TABLE ${table}\n         ADD CONSTRAINT ${table}_agent_user_path_uq\n         UNIQUE NULLS NOT DISTINCT (agent_id, user_id, path)`\n    )\n    .catch(() => undefined);\n\n  if (opts.skipEmbedderProbe) return;\n\n  // Vector dimension sanity check — refuses to serve if the column width\n  // disagrees with the live embedder and the table has data. See plan §3.\n  const embedder = getMemoryEmbedder();\n  const liveDim = embedder.dimensions || DEFAULT_MEMORY_DIMENSIONS;\n\n  // pgvector stores the declared width directly in atttypmod (unlike varchar\n  // which uses atttypmod = N + VARHDRSZ). Reading it raw gives the true dim.\n  const dimResult = await opts.pool.query(\n    `\n    SELECT atttypmod AS dim\n    FROM pg_attribute\n    WHERE attrelid = to_regclass($1)\n      AND attname = 'embedding'\n    `,\n    [table]\n  );\n  const columnDim = dimResult.rows[0]?.dim;\n  if (columnDim && Number(columnDim) !== liveDim) {\n    const countRes = await opts.pool.query(\n      `SELECT COUNT(*)::int AS n FROM ${table}`\n    );\n    const rowCount = Number(countRes.rows[0]?.n ?? 0);\n    if (rowCount === 0) {\n      await opts.pool.query(\n        `ALTER TABLE ${table} ALTER COLUMN embedding TYPE VECTOR(${liveDim})`\n      );\n    } else {\n      throw new Error(\n        `Memory vector dimension mismatch: column is VECTOR(${columnDim}) but ` +\n          `embedder \"${embedder.model}\" produces ${liveDim}-d vectors, and ${rowCount} ` +\n          `rows exist. Refusing to serve memory. Admin must run a reindex job.`\n      );\n    }\n  }\n}\n"],"names":[],"mappings":"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"}