{"version":3,"file":"pg.mjs","names":[],"sources":["../../../../../../../ai/src/snapshot/pg.ts"],"sourcesContent":["import type {\n  PgClientLike,\n  SnapshotStore,\n} from \"../contracts/orchestrator/snapshot-store.contract\";\nimport type { SupervisorSnapshot } from \"../contracts/supervisor/supervisor-snapshot.type\";\n\n/**\n * Default backing table for the pg snapshot store. Matches the name used\n * in the orchestrator.md §8 reference wiring\n * (`ai.snapshot.pg({ client, table: \"warlock_supervisor_snapshots\" })`).\n */\nconst DEFAULT_TABLE = \"warlock_supervisor_snapshots\";\n\n/**\n * Allowed characters in a Postgres identifier (table name). The\n * conservative ASCII subset; anything else is rejected because the table\n * name is interpolated directly into DDL/DML, and an arbitrary string\n * there would be a SQL-injection footgun. Mirrors `@warlock.js/cache`'s\n * `PgCacheDriver`.\n */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Options for {@link pg}. The `client` is an already-built `pg.Pool` /\n * `pg.Client` (anything satisfying {@link PgClientLike}); the store never\n * opens or closes it — connection lifecycle stays with the caller.\n */\nexport type PgSnapshotStoreOptions = {\n  /** Pre-built pg client. The store only ever calls `query`. */\n  client: PgClientLike;\n  /** Table name. Defaults to `warlock_supervisor_snapshots`. */\n  table?: string;\n};\n\n/**\n * Validate and resolve the table name. Throws on an unsafe identifier so\n * the failure surfaces at construction time, not on the first query.\n */\nfunction resolveTable(table: string | undefined): string {\n  const resolved = table ?? DEFAULT_TABLE;\n\n  if (!SAFE_IDENTIFIER.test(resolved)) {\n    throw new Error(\n      `Pg snapshot store: invalid table name '${resolved}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`,\n    );\n  }\n\n  return resolved;\n}\n\n/**\n * Coerce a `payload` column value back into a {@link SupervisorSnapshot}.\n * node-postgres parses `JSONB` into a JS value already, but some pool\n * wrappers hand back the raw string — be defensive across both.\n */\nfunction parsePayload(payload: unknown): SupervisorSnapshot {\n  if (typeof payload === \"string\") {\n    return JSON.parse(payload) as SupervisorSnapshot;\n  }\n\n  return payload as SupervisorSnapshot;\n}\n\n/**\n * Postgres {@link SnapshotStore} — supervisor run snapshots persisted to a\n * single row per `runId` in a dev-provisioned table (orchestrator.md §8).\n *\n * Owns: durable round-tripping of the {@link SupervisorSnapshot} envelope\n * keyed by `runId`, so a crashed mid-turn `iterate: true` iteration can\n * resume after a restart. Does NOT own: the connection (the caller passes\n * a live `pg.Pool`/`pg.Client` and keeps owning its lifecycle) or schema\n * migration ({@link PgSnapshotStore.schema} returns DDL the dev runs\n * themselves — the framework never auto-migrates, §8.5).\n *\n * Unlike the append-only checkpoint store, a run has exactly one live\n * snapshot, so `save()` upserts on the `run_id` primary key.\n *\n * Front it with the {@link pg} factory — callers never `new` it.\n */\nclass PgSnapshotStore implements SnapshotStore {\n  /** The user-supplied pg client. The store only ever calls `query`. */\n  private readonly client: PgClientLike;\n\n  /** Validated, resolved table name. Safe to interpolate into SQL. */\n  private readonly table: string;\n\n  public constructor(options: PgSnapshotStoreOptions) {\n    if (!options || !options.client || typeof options.client.query !== \"function\") {\n      throw new Error(\n        \"Pg snapshot store requires a 'client' option implementing { query(text, params) } — pass a pg.Pool or pg.Client.\",\n      );\n    }\n\n    this.client = options.client;\n    this.table = resolveTable(options.table);\n  }\n\n  /**\n   * Load the snapshot for a `runId`, or `undefined` when no in-flight run\n   * is recorded.\n   */\n  public async load(runId: string): Promise<SupervisorSnapshot | undefined> {\n    const { rows } = await this.client.query(\n      `SELECT payload FROM ${this.table} WHERE run_id = $1`,\n      [runId],\n    );\n\n    if (rows.length === 0) {\n      return undefined;\n    }\n\n    return parsePayload((rows[0] as { payload: unknown }).payload);\n  }\n\n  /**\n   * Persist a snapshot, keyed by its own `runId`. Upserts — a run has\n   * exactly one live snapshot, so a second save for the same `runId`\n   * overwrites the payload rather than appending.\n   */\n  public async save(snapshot: SupervisorSnapshot): Promise<void> {\n    await this.client.query(\n      `INSERT INTO ${this.table} (run_id, payload, saved_at)\n       VALUES ($1, $2::jsonb, now())\n       ON CONFLICT (run_id) DO UPDATE\n         SET payload = EXCLUDED.payload,\n             saved_at = EXCLUDED.saved_at`,\n      [snapshot.runId, JSON.stringify(snapshot)],\n    );\n  }\n\n  /**\n   * Drop the snapshot for a `runId`.\n   */\n  public async delete(runId: string): Promise<void> {\n    await this.client.query(`DELETE FROM ${this.table} WHERE run_id = $1`, [\n      runId,\n    ]);\n  }\n\n  /**\n   * List the known run ids, optionally filtered by a prefix. The `_` and\n   * `%` LIKE wildcards in the prefix are escaped so an opaque runId that\n   * happens to contain them is matched literally.\n   */\n  public async list(prefix?: string): Promise<string[]> {\n    if (prefix === undefined) {\n      const { rows } = await this.client.query(\n        `SELECT run_id FROM ${this.table}`,\n      );\n\n      return rows.map((row) => (row as { run_id: string }).run_id);\n    }\n\n    const escaped = prefix\n      .replace(/\\\\/g, \"\\\\\\\\\")\n      .replace(/_/g, \"\\\\_\")\n      .replace(/%/g, \"\\\\%\");\n\n    const { rows } = await this.client.query(\n      `SELECT run_id FROM ${this.table} WHERE run_id LIKE $1 ESCAPE '\\\\'`,\n      [`${escaped}%`],\n    );\n\n    return rows.map((row) => (row as { run_id: string }).run_id);\n  }\n\n  /**\n   * Return the DDL for this store's backing table. Run once via the\n   * caller's migration tooling — the store never auto-migrates (§8.5).\n   *\n   * @example\n   * await pool.query(store.schema());\n   */\n  public schema(): string {\n    return [\n      `CREATE TABLE IF NOT EXISTS ${this.table} (`,\n      `  run_id    TEXT PRIMARY KEY,`,\n      `  payload   JSONB NOT NULL,`,\n      `  saved_at  TIMESTAMPTZ NOT NULL DEFAULT now()`,\n      `);`,\n      `CREATE INDEX IF NOT EXISTS idx_${this.table}_saved_at ON ${this.table} (saved_at);`,\n    ].join(\"\\n\");\n  }\n}\n\n/**\n * Create a Postgres-backed {@link SnapshotStore}. Pass a live\n * `pg.Pool`/`pg.Client` — the store never opens or closes it. Schema is\n * not auto-migrated: run {@link SnapshotStore.schema} through your own\n * migration tool first.\n *\n * @example\n * import { Pool } from \"pg\";\n * import { ai } from \"@warlock.js/ai\";\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n *\n * const orchestrator = ai.orchestrator({\n *   name: \"support\",\n *   intents: { ... },\n *   iterate: true,\n *   snapshotStore: ai.snapshot.pg({\n *     client: pool,\n *     table: \"warlock_supervisor_snapshots\",\n *   }),\n * });\n *\n * // Run once, via your own migration tooling:\n * // await pool.query(orchestrator's store.schema());\n */\nexport function pg(options: PgSnapshotStoreOptions): SnapshotStore {\n  return new PgSnapshotStore(options);\n}\n"],"mappings":";;;;;;AAWA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,kBAAkB;;;;;AAkBxB,SAAS,aAAa,OAAmC;CACvD,MAAM,WAAW,SAAS;CAE1B,IAAI,CAAC,gBAAgB,KAAK,QAAQ,GAChC,MAAM,IAAI,MACR,0CAA0C,SAAS,oCACrD;CAGF,OAAO;AACT;;;;;;AAOA,SAAS,aAAa,SAAsC;CAC1D,IAAI,OAAO,YAAY,UACrB,OAAO,KAAK,MAAM,OAAO;CAG3B,OAAO;AACT;;;;;;;;;;;;;;;;;AAkBA,IAAM,kBAAN,MAA+C;CAO7C,AAAO,YAAY,SAAiC;EAClD,IAAI,CAAC,WAAW,CAAC,QAAQ,UAAU,OAAO,QAAQ,OAAO,UAAU,YACjE,MAAM,IAAI,MACR,kHACF;EAGF,KAAK,SAAS,QAAQ;EACtB,KAAK,QAAQ,aAAa,QAAQ,KAAK;CACzC;;;;;CAMA,MAAa,KAAK,OAAwD;EACxE,MAAM,EAAE,SAAS,MAAM,KAAK,OAAO,MACjC,uBAAuB,KAAK,MAAM,qBAClC,CAAC,KAAK,CACR;EAEA,IAAI,KAAK,WAAW,GAClB;EAGF,OAAO,aAAc,KAAK,EAAE,CAA0B,OAAO;CAC/D;;;;;;CAOA,MAAa,KAAK,UAA6C;EAC7D,MAAM,KAAK,OAAO,MAChB,eAAe,KAAK,MAAM;;;;4CAK1B,CAAC,SAAS,OAAO,KAAK,UAAU,QAAQ,CAAC,CAC3C;CACF;;;;CAKA,MAAa,OAAO,OAA8B;EAChD,MAAM,KAAK,OAAO,MAAM,eAAe,KAAK,MAAM,qBAAqB,CACrE,KACF,CAAC;CACH;;;;;;CAOA,MAAa,KAAK,QAAoC;EACpD,IAAI,WAAW,QAAW;GACxB,MAAM,EAAE,SAAS,MAAM,KAAK,OAAO,MACjC,sBAAsB,KAAK,OAC7B;GAEA,OAAO,KAAK,KAAK,QAAS,IAA2B,MAAM;EAC7D;EAEA,MAAM,UAAU,OACb,QAAQ,OAAO,MAAM,CAAC,CACtB,QAAQ,MAAM,KAAK,CAAC,CACpB,QAAQ,MAAM,KAAK;EAEtB,MAAM,EAAE,SAAS,MAAM,KAAK,OAAO,MACjC,sBAAsB,KAAK,MAAM,oCACjC,CAAC,GAAG,QAAQ,EAAE,CAChB;EAEA,OAAO,KAAK,KAAK,QAAS,IAA2B,MAAM;CAC7D;;;;;;;;CASA,AAAO,SAAiB;EACtB,OAAO;GACL,8BAA8B,KAAK,MAAM;GACzC;GACA;GACA;GACA;GACA,kCAAkC,KAAK,MAAM,eAAe,KAAK,MAAM;EACzE,CAAC,CAAC,KAAK,IAAI;CACb;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,GAAG,SAAgD;CACjE,OAAO,IAAI,gBAAgB,OAAO;AACpC"}