@alma-harness/postgres 0.1.0

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/index.js ADDED
@@ -0,0 +1,1671 @@
1
+ // src/session-store.ts
2
+ import { assertWellFormed, scopePath as scopePath2 } from "@alma-harness/core";
3
+
4
+ // src/scoped.ts
5
+ import { scopePath } from "@alma-harness/core";
6
+
7
+ // src/schema.ts
8
+ var DEFAULT_RLS_ROLE = "alma_app";
9
+ var IDENTIFIER = /^[a-z_][a-z0-9_]*$/;
10
+ function assertRoleIdentifier(role) {
11
+ if (!IDENTIFIER.test(role)) {
12
+ throw new Error(`Invalid Postgres role identifier: ${JSON.stringify(role)}`);
13
+ }
14
+ }
15
+ function rlsPolicySql(table, keys = ["org", "uid"]) {
16
+ assertRoleIdentifier(table);
17
+ const name = keys.includes("uid") ? "alma_scope_isolation" : "alma_org_isolation";
18
+ const predicate = keys.map((key) => `${key} = current_setting('alma.${key}', true)`).join("\n and ");
19
+ return `
20
+ alter table ${table} enable row level security;
21
+ alter table ${table} force row level security;
22
+
23
+ drop policy if exists ${name} on ${table};
24
+ create policy ${name} on ${table}
25
+ using (
26
+ ${predicate}
27
+ )
28
+ with check (
29
+ ${predicate}
30
+ );
31
+ `;
32
+ }
33
+ function roleBootstrapSql(role) {
34
+ assertRoleIdentifier(role);
35
+ return `
36
+ do $$
37
+ begin
38
+ if not exists (select from pg_roles where rolname = '${role}') then
39
+ begin
40
+ create role ${role} nologin;
41
+ exception when duplicate_object then
42
+ -- Another instance won the race between the existence check and the
43
+ -- create (pg_roles is cluster-wide and CREATE ROLE has no IF NOT
44
+ -- EXISTS); losing it must not roll back the rest of the migration.
45
+ null;
46
+ end;
47
+ end if;
48
+ end $$;
49
+ `;
50
+ }
51
+ function sessionStoreMigrationSql(role = DEFAULT_RLS_ROLE) {
52
+ assertRoleIdentifier(role);
53
+ return `
54
+ create table if not exists alma_sessions (
55
+ org text not null,
56
+ uid text not null,
57
+ session_id text not null,
58
+ last_seq bigint not null default 0,
59
+ created_at timestamptz not null default now(),
60
+ updated_at timestamptz not null default now(),
61
+ primary key (org, uid, session_id)
62
+ );
63
+
64
+ create table if not exists alma_session_entries (
65
+ org text not null,
66
+ uid text not null,
67
+ session_id text not null,
68
+ seq bigint not null,
69
+ msg jsonb not null,
70
+ created_at timestamptz not null default now(),
71
+ primary key (org, uid, session_id, seq),
72
+ foreign key (org, uid, session_id)
73
+ references alma_sessions (org, uid, session_id) on delete cascade
74
+ );
75
+ ${rlsPolicySql("alma_sessions")}${rlsPolicySql("alma_session_entries")}
76
+ ${roleBootstrapSql(role)}
77
+ grant select, insert, update, delete
78
+ on alma_sessions, alma_session_entries
79
+ to ${role};
80
+ `;
81
+ }
82
+ async function migrateSessionStore(pool, opts = {}) {
83
+ await pool.query(sessionStoreMigrationSql(opts.role));
84
+ }
85
+
86
+ // src/scoped.ts
87
+ var DEFAULT_STATEMENT_TIMEOUT_MS = 3e4;
88
+ function resolveRlsRole(opts) {
89
+ const role = opts.role === void 0 ? DEFAULT_RLS_ROLE : opts.role;
90
+ if (role !== null) assertRoleIdentifier(role);
91
+ return role;
92
+ }
93
+ function resolveStatementTimeout(opts) {
94
+ const ms = opts.statementTimeoutMs === void 0 ? DEFAULT_STATEMENT_TIMEOUT_MS : opts.statementTimeoutMs;
95
+ if (ms !== null && (!Number.isInteger(ms) || ms <= 0)) {
96
+ throw new Error(`statementTimeoutMs must be a positive integer or null, got ${ms}`);
97
+ }
98
+ return ms;
99
+ }
100
+ async function inScope(pool, role, scope, fn, statementTimeoutMs = DEFAULT_STATEMENT_TIMEOUT_MS) {
101
+ scopePath(scope);
102
+ const client = await pool.connect();
103
+ let rollbackFailed;
104
+ try {
105
+ await client.query("begin");
106
+ if (role !== null) await client.query(`set local role ${role}`);
107
+ await client.query(
108
+ `select set_config('alma.org', $1, true),
109
+ set_config('alma.uid', $2, true),
110
+ set_config('statement_timeout', $3, true)`,
111
+ [scope.org, scope.uid, statementTimeoutMs === null ? "0" : String(statementTimeoutMs)]
112
+ );
113
+ const result = await fn(client);
114
+ await client.query("commit");
115
+ return result;
116
+ } catch (err) {
117
+ await client.query("rollback").catch((rollbackErr) => {
118
+ rollbackFailed = rollbackErr;
119
+ });
120
+ throw err;
121
+ } finally {
122
+ client.release(rollbackFailed === void 0 ? void 0 : rollbackFailed);
123
+ }
124
+ }
125
+
126
+ // src/session-store.ts
127
+ var PostgresSessionStore = class {
128
+ #pool;
129
+ #role;
130
+ #timeoutMs;
131
+ constructor(pool, opts = {}) {
132
+ this.#pool = pool;
133
+ this.#role = resolveRlsRole(opts);
134
+ this.#timeoutMs = resolveStatementTimeout(opts);
135
+ }
136
+ async append(scope, sessionId, entries) {
137
+ scopePath2(scope);
138
+ if (entries.length === 0) return;
139
+ for (const [i, entry] of entries.entries()) assertWellFormed(entry, `entries[${i}]`);
140
+ await this.#inScope(scope, async (client) => {
141
+ const { rows } = await client.query(
142
+ `insert into alma_sessions (org, uid, session_id, last_seq)
143
+ values ($1, $2, $3, $4)
144
+ on conflict (org, uid, session_id)
145
+ do update set last_seq = alma_sessions.last_seq + $4, updated_at = now()
146
+ returning last_seq`,
147
+ [scope.org, scope.uid, sessionId, entries.length]
148
+ );
149
+ const lastSeq = Number(rows[0]?.last_seq);
150
+ const firstSeq = lastSeq - entries.length + 1;
151
+ const params = [scope.org, scope.uid, sessionId];
152
+ const tuples = entries.map((msg, i) => {
153
+ params.push(firstSeq + i, JSON.stringify(msg));
154
+ return `($1, $2, $3, $${params.length - 1}, $${params.length}::jsonb)`;
155
+ });
156
+ await client.query(
157
+ `insert into alma_session_entries (org, uid, session_id, seq, msg)
158
+ values ${tuples.join(", ")}`,
159
+ params
160
+ );
161
+ });
162
+ }
163
+ async load(scope, sessionId, opts) {
164
+ scopePath2(scope);
165
+ const limit = opts?.limit;
166
+ if (limit !== void 0 && limit <= 0) return [];
167
+ return this.#inScope(scope, async (client) => {
168
+ const { rows } = await client.query(
169
+ limit === void 0 ? `select msg from alma_session_entries
170
+ where org = $1 and uid = $2 and session_id = $3
171
+ order by seq asc` : `select msg from alma_session_entries
172
+ where org = $1 and uid = $2 and session_id = $3
173
+ order by seq desc limit $4`,
174
+ limit === void 0 ? [scope.org, scope.uid, sessionId] : [scope.org, scope.uid, sessionId, limit]
175
+ );
176
+ const msgs = rows.map((r) => r.msg);
177
+ return limit === void 0 ? msgs : msgs.reverse();
178
+ });
179
+ }
180
+ async expireToolTraffic(scope, sessionId, opts) {
181
+ scopePath2(scope);
182
+ const cutoff = instant(opts.inactiveSince);
183
+ return this.#inScope(scope, async (client) => {
184
+ await client.query(
185
+ `select last_seq from alma_sessions
186
+ where org = $1 and uid = $2 and session_id = $3
187
+ for update`,
188
+ [scope.org, scope.uid, sessionId]
189
+ );
190
+ const { rows } = await client.query(
191
+ `select seq, msg from alma_session_entries
192
+ where org = $1 and uid = $2 and session_id = $3
193
+ order by seq asc`,
194
+ [scope.org, scope.uid, sessionId]
195
+ );
196
+ const active = rows.some((r) => {
197
+ const at = r.msg.meta?.at;
198
+ if (at === void 0) return true;
199
+ const ms = Date.parse(at);
200
+ return Number.isNaN(ms) || ms > cutoff;
201
+ });
202
+ if (active) return { blocks: 0, messages: 0, expired: false };
203
+ let blocks = 0;
204
+ const rewrites = [];
205
+ const removals = [];
206
+ for (const row of rows) {
207
+ const survivors = row.msg.blocks.filter(
208
+ (b) => b.type !== "tool_call" && b.type !== "tool_result"
209
+ );
210
+ if (survivors.length === row.msg.blocks.length) continue;
211
+ blocks += row.msg.blocks.length - survivors.length;
212
+ if (survivors.length === 0) removals.push(row.seq);
213
+ else rewrites.push({ seq: row.seq, msg: { ...row.msg, blocks: survivors } });
214
+ }
215
+ for (const r of rewrites) {
216
+ await client.query(
217
+ `update alma_session_entries set msg = $5::jsonb
218
+ where org = $1 and uid = $2 and session_id = $3 and seq = $4`,
219
+ [scope.org, scope.uid, sessionId, r.seq, JSON.stringify(r.msg)]
220
+ );
221
+ }
222
+ if (removals.length > 0) {
223
+ await client.query(
224
+ `delete from alma_session_entries
225
+ where org = $1 and uid = $2 and session_id = $3 and seq = any($4::bigint[])`,
226
+ [scope.org, scope.uid, sessionId, removals]
227
+ );
228
+ }
229
+ return { blocks, messages: removals.length, expired: true };
230
+ });
231
+ }
232
+ async erase(scope, sessionId) {
233
+ await this.#inScope(scope, async (client) => {
234
+ if (sessionId === void 0) {
235
+ await client.query(`delete from alma_sessions where org = $1 and uid = $2`, [
236
+ scope.org,
237
+ scope.uid
238
+ ]);
239
+ } else {
240
+ await client.query(
241
+ `delete from alma_sessions where org = $1 and uid = $2 and session_id = $3`,
242
+ [scope.org, scope.uid, sessionId]
243
+ );
244
+ }
245
+ });
246
+ }
247
+ /** Shared RLS binding — see `scoped.ts`. */
248
+ async #inScope(scope, fn) {
249
+ return inScope(this.#pool, this.#role, scope, fn, this.#timeoutMs);
250
+ }
251
+ };
252
+ function instant(at) {
253
+ const ms = Date.parse(at);
254
+ if (Number.isNaN(ms)) throw new Error(`invalid ISO 8601 timestamp: ${JSON.stringify(at)}`);
255
+ return ms;
256
+ }
257
+
258
+ // src/memory-stores.ts
259
+ import {
260
+ scopePath as scopePath3
261
+ } from "@alma-harness/core";
262
+ import {
263
+ applyMemoryBudget,
264
+ assertBatchSize,
265
+ assertEpisodeInput,
266
+ assertEpisodeQuery,
267
+ assertErasureSelector,
268
+ assertFactObservation,
269
+ compareFactsForRecall,
270
+ decideObservation,
271
+ DEFAULT_CONFIDENCE,
272
+ DEFAULT_IMPORTANCE,
273
+ deriveEpisodeId,
274
+ deriveFactId,
275
+ isProtectedProfileKey,
276
+ mergeRefresh,
277
+ MEMORY_LIMITS,
278
+ rankEpisodes,
279
+ StaleWriteError,
280
+ toIsoInstant,
281
+ tokenizeMemoryText
282
+ } from "@alma-harness/memory";
283
+
284
+ // src/memory-schema.ts
285
+ var EPISODES_TABLE = "alma_memory_episodes";
286
+ var FACTS_TABLE = "alma_memory_facts";
287
+ var SCOPE_STATE_TABLE = "alma_memory_scope_state";
288
+ function memoryStoreMigrationSql(role = DEFAULT_RLS_ROLE) {
289
+ assertRoleIdentifier(role);
290
+ return `
291
+ create table if not exists ${EPISODES_TABLE} (
292
+ org text not null,
293
+ uid text not null,
294
+ id text not null,
295
+ at timestamptz not null,
296
+ kind text not null,
297
+ summary text not null,
298
+ -- Case-folded copy of summary, folded in the ADAPTER (JS toLowerCase), so
299
+ -- the candidate filter never depends on the database's ctype: under lc_ctype
300
+ -- of C, ilike folds ASCII only, and 'CAF\xC9' silently stopped matching 'caf\xE9'
301
+ -- while the in-memory reference matched it (the 011\u2013013 review). Blanked by the
302
+ -- tombstone exactly as summary is \u2014 it is the same content.
303
+ summary_fold text not null default '',
304
+ importance double precision not null,
305
+ state text not null default 'active',
306
+ source_session_id text,
307
+ source_turn_id text,
308
+ erased_at timestamptz,
309
+ created_at timestamptz not null default now(),
310
+ updated_at timestamptz not null default now(),
311
+ primary key (org, uid, id),
312
+ constraint alma_memory_episodes_state_check
313
+ check (state in ('active', 'archived', 'tombstoned'))
314
+ );
315
+
316
+ -- Migration for tables created before summary_fold existed. The SQL lower()
317
+ -- backfill is the best the database can do (exact under a folding ctype,
318
+ -- ASCII-only under C); every adapter write from then on stores the JS fold.
319
+ alter table ${EPISODES_TABLE} add column if not exists summary_fold text not null default '';
320
+ update ${EPISODES_TABLE} set summary_fold = lower(summary)
321
+ where summary_fold = '' and summary <> '';
322
+
323
+ create index if not exists alma_memory_episodes_recent
324
+ on ${EPISODES_TABLE} (org, uid, state, at desc);
325
+
326
+ create index if not exists alma_memory_episodes_session
327
+ on ${EPISODES_TABLE} (org, uid, source_session_id);
328
+
329
+ create table if not exists ${FACTS_TABLE} (
330
+ org text not null,
331
+ uid text not null,
332
+ id text not null,
333
+ key text not null,
334
+ value text not null,
335
+ confidence double precision not null,
336
+ source_episode_ids text[] not null default '{}',
337
+ observed_at timestamptz not null,
338
+ last_seen_at timestamptz not null,
339
+ superseded_at timestamptz,
340
+ superseded_by text,
341
+ invalidated_at timestamptz,
342
+ ttl_days integer,
343
+ primary key (org, uid, id)
344
+ );
345
+
346
+ -- "At most one CURRENT version per key" is a database invariant, not an
347
+ -- application hope: the confidence-gated supersession rule reads the current
348
+ -- version and writes a new one, and a lost race must fail loudly rather than
349
+ -- leave a profile with two contradictory current facts.
350
+ create unique index if not exists alma_memory_facts_current
351
+ on ${FACTS_TABLE} (org, uid, key)
352
+ where superseded_at is null and invalidated_at is null;
353
+
354
+ -- Derived invalidation walks provenance on every erasure.
355
+ create index if not exists alma_memory_facts_sources
356
+ on ${FACTS_TABLE} using gin (source_episode_ids);
357
+
358
+ create table if not exists ${SCOPE_STATE_TABLE} (
359
+ org text not null,
360
+ uid text not null,
361
+ erasure_watermark timestamptz,
362
+ primary key (org, uid)
363
+ );
364
+ ${rlsPolicySql(EPISODES_TABLE)}${rlsPolicySql(FACTS_TABLE)}${rlsPolicySql(SCOPE_STATE_TABLE)}
365
+ ${roleBootstrapSql(role)}
366
+ grant select, insert, update, delete
367
+ on ${EPISODES_TABLE}, ${FACTS_TABLE}, ${SCOPE_STATE_TABLE}
368
+ to ${role};
369
+ `;
370
+ }
371
+ async function migrateMemoryStores(pool, opts = {}) {
372
+ await pool.query(memoryStoreMigrationSql(opts.role));
373
+ }
374
+
375
+ // src/memory-stores.ts
376
+ var CANDIDATE_WINDOW = 200;
377
+ var EPISODE_COLUMNS = `id, at, kind, summary, importance, state,
378
+ source_session_id, source_turn_id, erased_at`;
379
+ var FACT_COLUMNS = `id, key, value, confidence, source_episode_ids,
380
+ observed_at, last_seen_at, superseded_at, superseded_by, invalidated_at, ttl_days`;
381
+ function toEpisode(row) {
382
+ const episode = {
383
+ id: row.id,
384
+ at: row.at.toISOString(),
385
+ kind: row.kind,
386
+ summary: row.summary,
387
+ importance: row.importance,
388
+ state: row.state
389
+ };
390
+ if (row.source_session_id !== null || row.source_turn_id !== null) {
391
+ const source = {};
392
+ if (row.source_session_id !== null) source.sessionId = row.source_session_id;
393
+ if (row.source_turn_id !== null) source.turnId = row.source_turn_id;
394
+ episode.source = source;
395
+ }
396
+ if (row.erased_at !== null) episode.erasedAt = row.erased_at.toISOString();
397
+ return episode;
398
+ }
399
+ function toFact(row) {
400
+ const fact = {
401
+ id: row.id,
402
+ key: row.key,
403
+ value: row.value,
404
+ confidence: row.confidence,
405
+ sourceEpisodeIds: row.source_episode_ids,
406
+ observedAt: row.observed_at.toISOString(),
407
+ lastSeenAt: row.last_seen_at.toISOString()
408
+ };
409
+ if (row.superseded_at !== null) fact.supersededAt = row.superseded_at.toISOString();
410
+ if (row.superseded_by !== null) fact.supersededBy = row.superseded_by;
411
+ if (row.invalidated_at !== null) fact.invalidatedAt = row.invalidated_at.toISOString();
412
+ if (row.ttl_days !== null) fact.ttlDays = row.ttl_days;
413
+ return fact;
414
+ }
415
+ var PostgresEpisodeStore = class {
416
+ copySurfaces = [{ name: EPISODES_TABLE, kind: "primary" }];
417
+ #pool;
418
+ #role;
419
+ #timeoutMs;
420
+ constructor(pool, opts = {}) {
421
+ this.#pool = pool;
422
+ this.#role = resolveRlsRole(opts);
423
+ this.#timeoutMs = resolveStatementTimeout(opts);
424
+ }
425
+ async append(scope, input) {
426
+ assertEpisodeInput(input);
427
+ const id = deriveEpisodeId(scope, input);
428
+ const at = toIsoInstant(input.at ?? (/* @__PURE__ */ new Date()).toISOString());
429
+ return this.#inScope(scope, async (client) => {
430
+ await client.query(scopeLockSql("shared"), [scopeLockKey(scope)]);
431
+ const { rows: mark } = await client.query(
432
+ `select erasure_watermark from ${SCOPE_STATE_TABLE} where org = $1 and uid = $2`,
433
+ [scope.org, scope.uid]
434
+ );
435
+ const watermark = mark[0]?.erasure_watermark?.toISOString() ?? null;
436
+ if (watermark !== null && at < watermark) {
437
+ throw new StaleWriteError(
438
+ `episode stamped ${at} precedes this scope's erasure at ${watermark}`
439
+ );
440
+ }
441
+ await client.query(
442
+ `insert into ${EPISODES_TABLE}
443
+ (org, uid, id, at, kind, summary, summary_fold, importance, source_session_id, source_turn_id)
444
+ values ($1, $2, $3, $4::timestamptz, $5, $6, $7, $8, $9, $10)
445
+ on conflict (org, uid, id) do update
446
+ set at = excluded.at,
447
+ kind = excluded.kind,
448
+ summary = excluded.summary,
449
+ summary_fold = excluded.summary_fold,
450
+ importance = excluded.importance,
451
+ source_session_id = excluded.source_session_id,
452
+ source_turn_id = excluded.source_turn_id,
453
+ updated_at = now()
454
+ where ${EPISODES_TABLE}.state <> 'tombstoned'`,
455
+ [
456
+ scope.org,
457
+ scope.uid,
458
+ id,
459
+ at,
460
+ input.kind,
461
+ input.summary,
462
+ // Folded HERE, in JS, with the same toLowerCase the tokenizer uses —
463
+ // never in SQL, where the fold depends on the database's ctype (see
464
+ // the schema comment on summary_fold).
465
+ input.summary.toLowerCase(),
466
+ input.importance ?? DEFAULT_IMPORTANCE,
467
+ input.source?.sessionId ?? null,
468
+ input.source?.turnId ?? null
469
+ ]
470
+ );
471
+ const { rows } = await client.query(
472
+ `select ${EPISODE_COLUMNS} from ${EPISODES_TABLE}
473
+ where org = $1 and uid = $2 and id = $3`,
474
+ [scope.org, scope.uid, id]
475
+ );
476
+ const row = rows[0];
477
+ if (!row) throw new Error(`episode ${id} vanished between write and read`);
478
+ return toEpisode(row);
479
+ });
480
+ }
481
+ async query(scope, q) {
482
+ scopePath3(scope);
483
+ assertEpisodeQuery(q);
484
+ if (q.limit !== void 0 && q.limit <= 0) return { episodes: [], truncated: false };
485
+ const states = q.includeArchived === true ? ["active", "archived"] : ["active"];
486
+ const terms = q.text === void 0 ? /* @__PURE__ */ new Set() : tokenizeMemoryText(q.text);
487
+ if (q.text !== void 0 && terms.size === 0) return { episodes: [], truncated: false };
488
+ const patterns = terms.size === 0 ? null : [...terms].map((t) => `%${t}%`);
489
+ const window = Math.max(CANDIDATE_WINDOW, 4 * (q.limit ?? 0));
490
+ return this.#inScope(scope, async (client) => {
491
+ const { rows } = await client.query(
492
+ `select ${EPISODE_COLUMNS} from ${EPISODES_TABLE}
493
+ where org = $1 and uid = $2
494
+ and state = any($3::text[])
495
+ and ($4::text[] is null or kind = any($4::text[]))
496
+ and ($5::timestamptz is null or at >= $5::timestamptz)
497
+ and ($6::timestamptz is null or at <= $6::timestamptz)
498
+ and ($7::text[] is null or summary_fold like any($7::text[]))
499
+ order by at desc
500
+ limit $8`,
501
+ [
502
+ scope.org,
503
+ scope.uid,
504
+ states,
505
+ q.kinds === void 0 ? null : [...q.kinds],
506
+ // Normalized like every other timestamp in the tier: raw bounds are
507
+ // read in the SERVER's timezone while the ranker reads them in the
508
+ // process's, so an offsetless bound filtered differently on each.
509
+ q.since === void 0 ? null : toIsoInstant(q.since),
510
+ q.until === void 0 ? null : toIsoInstant(q.until),
511
+ patterns,
512
+ window + 1
513
+ // one extra row is how we learn the window was saturated
514
+ ]
515
+ );
516
+ const clipped = rows.length > window;
517
+ const ranked = rankEpisodes(
518
+ rows.slice(0, window).map(toEpisode),
519
+ q,
520
+ (/* @__PURE__ */ new Date()).toISOString()
521
+ );
522
+ const limited = q.limit === void 0 ? ranked : ranked.slice(0, q.limit);
523
+ const { kept, truncated } = applyMemoryBudget(limited, q.budget, (ep) => ep.summary.length);
524
+ return {
525
+ episodes: kept,
526
+ truncated: truncated || limited.length < ranked.length || clipped
527
+ };
528
+ });
529
+ }
530
+ async get(scope, episodeIds) {
531
+ scopePath3(scope);
532
+ assertBatchSize(episodeIds.length, "episodeIds", MEMORY_LIMITS.idsPerCall);
533
+ if (episodeIds.length === 0) return [];
534
+ return this.#inScope(scope, async (client) => {
535
+ const { rows } = await client.query(
536
+ `select ${EPISODE_COLUMNS} from ${EPISODES_TABLE}
537
+ where org = $1 and uid = $2 and id = any($3::text[])`,
538
+ [scope.org, scope.uid, [...episodeIds]]
539
+ );
540
+ const byId = new Map(rows.map((r) => [r.id, toEpisode(r)]));
541
+ return episodeIds.flatMap((id) => {
542
+ const ep = byId.get(id);
543
+ return ep ? [ep] : [];
544
+ });
545
+ });
546
+ }
547
+ async tombstone(scope, selector, rawAt) {
548
+ const at = toIsoInstant(rawAt);
549
+ assertErasureSelector(selector);
550
+ const [predicate, param] = selectorPredicate(selector);
551
+ return this.#inScope(scope, async (client) => {
552
+ await client.query(scopeLockSql("exclusive"), [scopeLockKey(scope)]);
553
+ const { rows } = await client.query(
554
+ `with matched as (
555
+ select id from ${EPISODES_TABLE}
556
+ where org = $1 and uid = $2 and ${predicate}
557
+ ), blanked as (
558
+ update ${EPISODES_TABLE}
559
+ set kind = '', summary = '', summary_fold = '', state = 'tombstoned',
560
+ erased_at = $3::timestamptz, updated_at = now()
561
+ where org = $1 and uid = $2 and state <> 'tombstoned'
562
+ and id in (select id from matched)
563
+ returning id
564
+ )
565
+ select m.id, (b.id is not null) as written
566
+ from matched m left join blanked b on b.id = m.id`,
567
+ param === null ? [scope.org, scope.uid, at] : [scope.org, scope.uid, at, param]
568
+ );
569
+ return {
570
+ episodeIds: rows.map((r) => r.id),
571
+ written: rows.filter((r) => r.written).length,
572
+ surfaces: [EPISODES_TABLE]
573
+ };
574
+ });
575
+ }
576
+ async archive(scope, episodeIds) {
577
+ scopePath3(scope);
578
+ assertBatchSize(episodeIds.length, "episodeIds", MEMORY_LIMITS.idsPerCall);
579
+ if (episodeIds.length === 0) return 0;
580
+ return this.#inScope(scope, async (client) => {
581
+ const { rowCount } = await client.query(
582
+ `update ${EPISODES_TABLE} set state = 'archived', updated_at = now()
583
+ where org = $1 and uid = $2 and state = 'active' and id = any($3::text[])`,
584
+ [scope.org, scope.uid, [...episodeIds]]
585
+ );
586
+ return rowCount ?? 0;
587
+ });
588
+ }
589
+ async #inScope(scope, fn) {
590
+ return inScope(this.#pool, this.#role, scope, fn, this.#timeoutMs);
591
+ }
592
+ };
593
+ function selectorPredicate(selector) {
594
+ switch (selector.kind) {
595
+ case "all":
596
+ return ["true", null];
597
+ case "episodes":
598
+ return ["id = any($4::text[])", [...selector.episodeIds]];
599
+ case "sessions":
600
+ return ["source_session_id = any($4::text[])", [...selector.sessionIds]];
601
+ }
602
+ }
603
+ var EPOCH = "1970-01-01T00:00:00.000Z";
604
+ function scopeLockSql(mode) {
605
+ const fn = mode === "shared" ? "pg_advisory_xact_lock_shared" : "pg_advisory_xact_lock";
606
+ return `select ${fn}(hashtextextended($1::text, 0::bigint))`;
607
+ }
608
+ var scopeLockKey = (scope) => `${scope.org}/${scope.uid}`;
609
+ var PostgresProfileStore = class {
610
+ copySurfaces = [{ name: FACTS_TABLE, kind: "primary" }];
611
+ #pool;
612
+ #role;
613
+ #timeoutMs;
614
+ constructor(pool, opts = {}) {
615
+ this.#pool = pool;
616
+ this.#role = resolveRlsRole(opts);
617
+ this.#timeoutMs = resolveStatementTimeout(opts);
618
+ }
619
+ async get(scope, opts = {}) {
620
+ return this.#inScope(scope, async (client) => {
621
+ const { rows } = await client.query(
622
+ `with stamp as (
623
+ select max(last_seen_at) as updated_at from ${FACTS_TABLE}
624
+ where org = $1 and uid = $2 and invalidated_at is null
625
+ )
626
+ select updated_at, ${FACT_COLUMNS}
627
+ from stamp
628
+ left join ${FACTS_TABLE}
629
+ on ${FACTS_TABLE}.org = $1 and ${FACTS_TABLE}.uid = $2
630
+ and ($3::boolean or (${FACTS_TABLE}.superseded_at is null
631
+ and ${FACTS_TABLE}.invalidated_at is null))`,
632
+ [scope.org, scope.uid, opts.includeHistory === true]
633
+ );
634
+ const facts = rows.filter((row) => row.id !== null).map((row) => toFact(row)).sort(compareFactsForRecall);
635
+ const { kept, truncated } = applyMemoryBudget(
636
+ facts,
637
+ opts.budget,
638
+ (f) => f.key.length + f.value.length
639
+ );
640
+ return {
641
+ facts: kept,
642
+ updatedAt: rows[0]?.updated_at?.toISOString() ?? EPOCH,
643
+ truncated
644
+ };
645
+ });
646
+ }
647
+ async observe(scope, obs) {
648
+ return this.#write(scope, obs, false);
649
+ }
650
+ async setProtected(scope, facts) {
651
+ return this.#write(scope, facts, true);
652
+ }
653
+ async invalidateBySource(scope, episodeIds, rawAt) {
654
+ const at = toIsoInstant(rawAt);
655
+ return this.#inScope(scope, async (client) => {
656
+ await client.query(scopeLockSql("exclusive"), [scopeLockKey(scope)]);
657
+ const { rowCount } = await client.query(
658
+ `update ${FACTS_TABLE} set invalidated_at = $3::timestamptz, key = '', value = ''
659
+ where org = $1 and uid = $2 and invalidated_at is null
660
+ and ($4::text[] is null or source_episode_ids && $4::text[])`,
661
+ [scope.org, scope.uid, at, episodeIds === "all" ? null : [...episodeIds]]
662
+ );
663
+ return { invalidated: rowCount ?? 0, surfaces: [FACTS_TABLE] };
664
+ });
665
+ }
666
+ async #write(scope, obs, trusted) {
667
+ scopePath3(scope);
668
+ assertBatchSize(obs.length, "observations", MEMORY_LIMITS.batchItems);
669
+ for (const o of obs) assertFactObservation(o);
670
+ if (obs.length === 0) return [];
671
+ const lockKeys = [
672
+ ...new Set(
673
+ obs.filter((o) => trusted || !isProtectedProfileKey(o.key)).map((o) => o.key)
674
+ )
675
+ ].sort();
676
+ const batchAt = (/* @__PURE__ */ new Date()).toISOString();
677
+ const stamps = obs.map((o) => toIsoInstant(o.at ?? batchAt));
678
+ const candidateIds = obs.map(
679
+ (o, i) => deriveFactId(scope, { key: o.key, value: o.value, observedAt: stamps[i] })
680
+ );
681
+ return this.#inScope(scope, async (client) => {
682
+ await client.query(scopeLockSql("shared"), [scopeLockKey(scope)]);
683
+ if (lockKeys.length > 0) {
684
+ await client.query(
685
+ `select pg_advisory_xact_lock(hashtextextended(k, 0::bigint))
686
+ from unnest($1::text[]) as k`,
687
+ [lockKeys.map((key) => `${scope.org}/${scope.uid}/${key}`)]
688
+ );
689
+ }
690
+ const state = await this.#loadWriteState(client, scope, obs, lockKeys, candidateIds);
691
+ const { results, writes } = planObservations(obs, trusted, stamps, candidateIds, state);
692
+ const lost = await applyWrites(client, scope, writes);
693
+ for (const index of lost) {
694
+ results[index] = {
695
+ key: obs[index].key,
696
+ outcome: "stale",
697
+ detail: "the version this observation refreshed was closed concurrently"
698
+ };
699
+ }
700
+ return results;
701
+ });
702
+ }
703
+ /**
704
+ * Everything the batch needs to decide, read in a FIXED number of queries.
705
+ *
706
+ * This is the half of the N+1 that was pure reads: the watermark once, then
707
+ * a provenance check, a current-version read, and a replay check PER
708
+ * OBSERVATION — up to three hundred round trips for a full batch, every one
709
+ * of them holding the scope's shared lock and all its key locks (the
710
+ * 011-013 review).
711
+ */
712
+ async #loadWriteState(client, scope, obs, keys, candidateIds) {
713
+ const { rows: mark } = await client.query(
714
+ `select erasure_watermark from ${SCOPE_STATE_TABLE} where org = $1 and uid = $2`,
715
+ [scope.org, scope.uid]
716
+ );
717
+ const watermark = mark[0]?.erasure_watermark?.toISOString() ?? null;
718
+ const cited = [...new Set(obs.flatMap((o) => o.sourceEpisodeIds ?? []))];
719
+ const tombstoned = /* @__PURE__ */ new Set();
720
+ if (cited.length > 0) {
721
+ const { rows } = await client.query(
722
+ `select id from ${EPISODES_TABLE}
723
+ where org = $1 and uid = $2 and id = any($3::text[]) and state = 'tombstoned'`,
724
+ [scope.org, scope.uid, cited]
725
+ );
726
+ for (const row of rows) tombstoned.add(row.id);
727
+ }
728
+ const currentByKey = /* @__PURE__ */ new Map();
729
+ if (keys.length > 0) {
730
+ const { rows } = await client.query(
731
+ `select ${FACT_COLUMNS} from ${FACTS_TABLE}
732
+ where org = $1 and uid = $2 and key = any($3::text[])
733
+ and superseded_at is null and invalidated_at is null`,
734
+ [scope.org, scope.uid, [...keys]]
735
+ );
736
+ for (const row of rows) {
737
+ const fact = toFact(row);
738
+ currentByKey.set(fact.key, fact);
739
+ }
740
+ }
741
+ const existingIds = /* @__PURE__ */ new Set();
742
+ const { rows: replays } = await client.query(
743
+ `select id from ${FACTS_TABLE} where org = $1 and uid = $2 and id = any($3::text[])`,
744
+ [scope.org, scope.uid, [...new Set(candidateIds)]]
745
+ );
746
+ for (const row of replays) existingIds.add(row.id);
747
+ return { watermark, tombstoned, currentByKey, existingIds };
748
+ }
749
+ async #inScope(scope, fn) {
750
+ return inScope(this.#pool, this.#role, scope, fn, this.#timeoutMs);
751
+ }
752
+ };
753
+ function planObservations(obs, trusted, stamps, candidateIds, state) {
754
+ const { currentByKey, existingIds } = state;
755
+ const results = [];
756
+ const writes = [];
757
+ const rounds = /* @__PURE__ */ new Map();
758
+ for (let index = 0; index < obs.length; index++) {
759
+ const o = obs[index];
760
+ const at = stamps[index];
761
+ if (!trusted && isProtectedProfileKey(o.key)) {
762
+ results.push({
763
+ key: o.key,
764
+ outcome: "refused",
765
+ detail: `${JSON.stringify(o.key)} is in the protected profile namespace; only the product may write it`
766
+ });
767
+ continue;
768
+ }
769
+ const dead = (o.sourceEpisodeIds ?? []).filter((id2) => state.tombstoned.has(id2));
770
+ if (dead.length > 0) {
771
+ results.push({
772
+ key: o.key,
773
+ outcome: "stale",
774
+ detail: `cites erased episode(s): ${dead.join(", ")}`
775
+ });
776
+ continue;
777
+ }
778
+ if (state.watermark !== null && at < state.watermark) {
779
+ results.push({
780
+ key: o.key,
781
+ outcome: "stale",
782
+ detail: `observation stamped ${at} precedes this scope's erasure at ${state.watermark}`
783
+ });
784
+ continue;
785
+ }
786
+ const confidence = o.confidence ?? DEFAULT_CONFIDENCE;
787
+ const current = currentByKey.get(o.key);
788
+ const decision = decideObservation(current, { value: o.value, confidence }, { trusted });
789
+ const round = rounds.get(o.key) ?? 0;
790
+ if (decision.outcome === "refreshed" && current !== void 0) {
791
+ const refresh = { confidence, at };
792
+ if (o.sourceEpisodeIds !== void 0) refresh.sourceEpisodeIds = o.sourceEpisodeIds;
793
+ if (o.ttlDays !== void 0) refresh.ttlDays = o.ttlDays;
794
+ const merged = mergeRefresh(current, refresh);
795
+ currentByKey.set(o.key, { ...current, ...merged });
796
+ rounds.set(o.key, round + 1);
797
+ writes.push({ kind: "refresh", round, index, id: current.id, merged });
798
+ results.push({ key: o.key, outcome: "refreshed", factId: current.id });
799
+ continue;
800
+ }
801
+ const id = candidateIds[index];
802
+ if (existingIds.has(id)) {
803
+ results.push({
804
+ key: o.key,
805
+ outcome: "replayed",
806
+ factId: id,
807
+ detail: "this exact observation is already a stored version; nothing was written"
808
+ });
809
+ continue;
810
+ }
811
+ const version = {
812
+ id,
813
+ key: o.key,
814
+ value: o.value,
815
+ confidence,
816
+ sourceEpisodeIds: [...o.sourceEpisodeIds ?? []],
817
+ observedAt: at,
818
+ lastSeenAt: at
819
+ };
820
+ if (o.ttlDays !== void 0) version.ttlDays = o.ttlDays;
821
+ existingIds.add(id);
822
+ rounds.set(o.key, round + 1);
823
+ if (decision.outcome === "conflict" && current !== void 0) {
824
+ version.supersededAt = at;
825
+ version.supersededBy = current.id;
826
+ writes.push({ kind: "insert", round, index, version });
827
+ const conflict = { key: o.key, outcome: "conflict", factId: id };
828
+ if (decision.detail !== void 0) conflict.detail = decision.detail;
829
+ results.push(conflict);
830
+ continue;
831
+ }
832
+ currentByKey.set(o.key, version);
833
+ writes.push(
834
+ decision.outcome === "superseded" && current !== void 0 ? { kind: "insert", round, index, version, closes: current.id } : { kind: "insert", round, index, version }
835
+ );
836
+ const result = { key: o.key, outcome: decision.outcome, factId: id };
837
+ if (decision.detail !== void 0) result.detail = decision.detail;
838
+ results.push(result);
839
+ }
840
+ return { results, writes };
841
+ }
842
+ async function applyWrites(client, scope, writes) {
843
+ const lost = /* @__PURE__ */ new Set();
844
+ const lastRound = writes.reduce((max, w) => Math.max(max, w.round), -1);
845
+ for (let round = 0; round <= lastRound; round++) {
846
+ const refreshes = writes.filter(
847
+ (w) => w.round === round && w.kind === "refresh"
848
+ );
849
+ const inserts = writes.filter(
850
+ (w) => w.round === round && w.kind === "insert"
851
+ );
852
+ if (refreshes.length > 0) {
853
+ const updated = await refreshVersions(client, scope, refreshes);
854
+ for (const w of refreshes) if (!updated.has(w.id)) lost.add(w.index);
855
+ }
856
+ const closes = inserts.flatMap(
857
+ (w) => w.closes === void 0 ? [] : [{ id: w.closes, at: w.version.observedAt, by: w.version.id }]
858
+ );
859
+ if (closes.length > 0) await closeVersions(client, scope, closes);
860
+ if (inserts.length > 0) await insertVersions(client, scope, inserts.map((w) => w.version));
861
+ }
862
+ return lost;
863
+ }
864
+ function placeholderList(params, cells) {
865
+ return cells.map(([value, type]) => {
866
+ params.push(value);
867
+ return `$${params.length}::${type}`;
868
+ }).join(", ");
869
+ }
870
+ async function refreshVersions(client, scope, writes) {
871
+ const params = [scope.org, scope.uid];
872
+ const rows = writes.map(
873
+ (w) => `(${placeholderList(params, [
874
+ [w.id, "text"],
875
+ [w.merged.confidence, "double precision"],
876
+ [w.merged.lastSeenAt, "timestamptz"],
877
+ [[...w.merged.sourceEpisodeIds], "text[]"],
878
+ [w.merged.ttlDays ?? null, "integer"]
879
+ ])})`
880
+ );
881
+ const { rows: updated } = await client.query(
882
+ `update ${FACTS_TABLE}
883
+ set confidence = v.confidence, last_seen_at = v.last_seen_at,
884
+ source_episode_ids = v.source_episode_ids, ttl_days = v.ttl_days
885
+ from (values ${rows.join(", ")}) as v(id, confidence, last_seen_at, source_episode_ids, ttl_days)
886
+ where ${FACTS_TABLE}.org = $1 and ${FACTS_TABLE}.uid = $2
887
+ and ${FACTS_TABLE}.id = v.id
888
+ and ${FACTS_TABLE}.superseded_at is null and ${FACTS_TABLE}.invalidated_at is null
889
+ returning ${FACTS_TABLE}.id`,
890
+ params
891
+ );
892
+ return new Set(updated.map((row) => row.id));
893
+ }
894
+ async function closeVersions(client, scope, closes) {
895
+ const params = [scope.org, scope.uid];
896
+ const rows = closes.map(
897
+ (c) => `(${placeholderList(params, [
898
+ [c.id, "text"],
899
+ [c.at, "timestamptz"],
900
+ [c.by, "text"]
901
+ ])})`
902
+ );
903
+ await client.query(
904
+ `update ${FACTS_TABLE} set superseded_at = v.at, superseded_by = v.by
905
+ from (values ${rows.join(", ")}) as v(id, at, by)
906
+ where ${FACTS_TABLE}.org = $1 and ${FACTS_TABLE}.uid = $2 and ${FACTS_TABLE}.id = v.id`,
907
+ params
908
+ );
909
+ }
910
+ async function insertVersions(client, scope, versions) {
911
+ const params = [scope.org, scope.uid];
912
+ const rows = versions.map(
913
+ (f) => `($1, $2, ${placeholderList(params, [
914
+ [f.id, "text"],
915
+ [f.key, "text"],
916
+ [f.value, "text"],
917
+ [f.confidence, "double precision"],
918
+ [[...f.sourceEpisodeIds], "text[]"],
919
+ [f.observedAt, "timestamptz"],
920
+ [f.lastSeenAt, "timestamptz"],
921
+ [f.supersededAt ?? null, "timestamptz"],
922
+ [f.supersededBy ?? null, "text"],
923
+ [f.ttlDays ?? null, "integer"]
924
+ ])})`
925
+ );
926
+ await client.query(
927
+ `insert into ${FACTS_TABLE}
928
+ (org, uid, id, key, value, confidence, source_episode_ids,
929
+ observed_at, last_seen_at, superseded_at, superseded_by, ttl_days)
930
+ values ${rows.join(", ")}`,
931
+ params
932
+ );
933
+ }
934
+ var PostgresErasureWatermarks = class {
935
+ #pool;
936
+ #role;
937
+ #timeoutMs;
938
+ constructor(pool, opts = {}) {
939
+ this.#pool = pool;
940
+ this.#role = resolveRlsRole(opts);
941
+ this.#timeoutMs = resolveStatementTimeout(opts);
942
+ }
943
+ async get(scope) {
944
+ return inScope(
945
+ this.#pool,
946
+ this.#role,
947
+ scope,
948
+ async (client) => {
949
+ const { rows } = await client.query(
950
+ `select erasure_watermark from ${SCOPE_STATE_TABLE} where org = $1 and uid = $2`,
951
+ [scope.org, scope.uid]
952
+ );
953
+ return rows[0]?.erasure_watermark?.toISOString() ?? null;
954
+ },
955
+ this.#timeoutMs
956
+ );
957
+ }
958
+ async set(scope, rawAt) {
959
+ const at = toIsoInstant(rawAt);
960
+ await inScope(
961
+ this.#pool,
962
+ this.#role,
963
+ scope,
964
+ async (client) => {
965
+ await client.query(
966
+ `insert into ${SCOPE_STATE_TABLE} (org, uid, erasure_watermark)
967
+ values ($1, $2, $3::timestamptz)
968
+ on conflict (org, uid) do update
969
+ set erasure_watermark =
970
+ greatest(${SCOPE_STATE_TABLE}.erasure_watermark, excluded.erasure_watermark)`,
971
+ [scope.org, scope.uid, at]
972
+ );
973
+ },
974
+ this.#timeoutMs
975
+ );
976
+ }
977
+ };
978
+
979
+ // src/spend-schema.ts
980
+ var SPEND_SESSIONS_TABLE = "alma_spend_sessions";
981
+ var SPEND_TENANT_DAYS_TABLE = "alma_spend_tenant_days";
982
+ function spendStoreMigrationSql(role = DEFAULT_RLS_ROLE) {
983
+ assertRoleIdentifier(role);
984
+ return `
985
+ create table if not exists ${SPEND_SESSIONS_TABLE} (
986
+ org text not null,
987
+ uid text not null,
988
+ session_id text not null,
989
+ usd double precision not null default 0,
990
+ updated_at timestamptz not null default now(),
991
+ primary key (org, uid, session_id)
992
+ );
993
+
994
+ create table if not exists ${SPEND_TENANT_DAYS_TABLE} (
995
+ org text not null,
996
+ day date not null,
997
+ usd double precision not null default 0,
998
+ updated_at timestamptz not null default now(),
999
+ primary key (org, day)
1000
+ );
1001
+ ${rlsPolicySql(SPEND_SESSIONS_TABLE)}${rlsPolicySql(SPEND_TENANT_DAYS_TABLE, ["org"])}
1002
+ ${roleBootstrapSql(role)}
1003
+ grant select, insert, update
1004
+ on ${SPEND_SESSIONS_TABLE}, ${SPEND_TENANT_DAYS_TABLE}
1005
+ to ${role};
1006
+ `;
1007
+ }
1008
+ async function migrateSpendStore(pool, opts = {}) {
1009
+ await pool.query(spendStoreMigrationSql(opts.role));
1010
+ }
1011
+
1012
+ // src/spend-store.ts
1013
+ var PostgresSpendStore = class {
1014
+ #pool;
1015
+ #role;
1016
+ #timeoutMs;
1017
+ constructor(pool, opts = {}) {
1018
+ this.#pool = pool;
1019
+ this.#role = resolveRlsRole(opts);
1020
+ this.#timeoutMs = resolveStatementTimeout(opts);
1021
+ }
1022
+ async add(entry) {
1023
+ if (!Number.isFinite(entry.usd) || entry.usd < 0) {
1024
+ throw new Error(`spend must be a non-negative finite number, got ${entry.usd}`);
1025
+ }
1026
+ const day = utcDayBucket(entry.at);
1027
+ return this.#inScope(entry.scope, async (client) => {
1028
+ const { rows: session } = await client.query(
1029
+ `insert into ${SPEND_SESSIONS_TABLE} (org, uid, session_id, usd)
1030
+ values ($1, $2, $3, $4)
1031
+ on conflict (org, uid, session_id)
1032
+ do update set usd = ${SPEND_SESSIONS_TABLE}.usd + excluded.usd, updated_at = now()
1033
+ returning usd`,
1034
+ [entry.scope.org, entry.scope.uid, entry.sessionId, entry.usd]
1035
+ );
1036
+ const { rows: dayRows } = await client.query(
1037
+ `insert into ${SPEND_TENANT_DAYS_TABLE} (org, day, usd)
1038
+ values ($1, $2::date, $3)
1039
+ on conflict (org, day)
1040
+ do update set usd = ${SPEND_TENANT_DAYS_TABLE}.usd + excluded.usd, updated_at = now()
1041
+ returning usd`,
1042
+ [entry.scope.org, day, entry.usd]
1043
+ );
1044
+ return { sessionUsd: session[0].usd, tenantDayUsd: dayRows[0].usd };
1045
+ });
1046
+ }
1047
+ async peek(key) {
1048
+ const day = utcDayBucket(key.at);
1049
+ return this.#inScope(key.scope, async (client) => {
1050
+ const { rows } = await client.query(
1051
+ `select
1052
+ coalesce((select usd from ${SPEND_SESSIONS_TABLE}
1053
+ where org = $1 and uid = $2 and session_id = $3), 0) as session_usd,
1054
+ coalesce((select usd from ${SPEND_TENANT_DAYS_TABLE}
1055
+ where org = $1 and day = $4::date), 0) as tenant_day_usd`,
1056
+ [key.scope.org, key.scope.uid, key.sessionId, day]
1057
+ );
1058
+ return { sessionUsd: rows[0].session_usd, tenantDayUsd: rows[0].tenant_day_usd };
1059
+ });
1060
+ }
1061
+ /** Shared RLS binding — see `scoped.ts`. */
1062
+ async #inScope(scope, fn) {
1063
+ return inScope(this.#pool, this.#role, scope, fn, this.#timeoutMs);
1064
+ }
1065
+ };
1066
+ function utcDayBucket(at) {
1067
+ const ms = Date.parse(at);
1068
+ if (Number.isNaN(ms)) throw new Error(`invalid ISO 8601 timestamp: ${JSON.stringify(at)}`);
1069
+ return new Date(ms).toISOString().slice(0, 10);
1070
+ }
1071
+
1072
+ // src/audit-schema.ts
1073
+ var AUDIT_ACCESS_TABLE = "alma_audit_access";
1074
+ var AUDIT_ROUTING_TABLE = "alma_audit_routing";
1075
+ var AUDIT_COST_TABLE = "alma_audit_cost";
1076
+ var AUDIT_RECALL_TABLE = "alma_audit_recall";
1077
+ var AUDIT_CONTEXT_TABLE = "alma_audit_context";
1078
+ var DEFAULT_RETENTION_ROLE = "alma_retention";
1079
+ var AUDIT_TABLES = [
1080
+ AUDIT_ACCESS_TABLE,
1081
+ AUDIT_ROUTING_TABLE,
1082
+ AUDIT_COST_TABLE,
1083
+ AUDIT_RECALL_TABLE,
1084
+ AUDIT_CONTEXT_TABLE
1085
+ ];
1086
+ function auditLogMigrationSql(role = DEFAULT_RLS_ROLE, retentionRole = DEFAULT_RETENTION_ROLE) {
1087
+ assertRoleIdentifier(role);
1088
+ assertRoleIdentifier(retentionRole);
1089
+ return `
1090
+ create table if not exists ${AUDIT_ACCESS_TABLE} (
1091
+ id uuid not null default gen_random_uuid(),
1092
+ org text not null,
1093
+ uid text not null,
1094
+ at timestamptz not null,
1095
+ tool text not null,
1096
+ action text not null,
1097
+ -- Identifier of the touched resource (id/path) \u2014 NEVER its content.
1098
+ resource text,
1099
+ session_id text,
1100
+ turn_id text,
1101
+ primary key (id),
1102
+ constraint alma_audit_access_action_check
1103
+ check (action in ('read', 'write', 'delete', 'export'))
1104
+ );
1105
+
1106
+ create index if not exists alma_audit_access_turn
1107
+ on ${AUDIT_ACCESS_TABLE} (org, uid, turn_id);
1108
+
1109
+ create index if not exists alma_audit_access_recent
1110
+ on ${AUDIT_ACCESS_TABLE} (org, uid, at desc);
1111
+
1112
+ create table if not exists ${AUDIT_ROUTING_TABLE} (
1113
+ id uuid not null default gen_random_uuid(),
1114
+ org text not null,
1115
+ uid text not null,
1116
+ at timestamptz not null,
1117
+ tier text not null,
1118
+ sensitivity text not null,
1119
+ model_provider text not null,
1120
+ model_id text not null,
1121
+ -- Carried verbatim from ModelChoice.rationale: a policy must explain itself.
1122
+ rationale text not null,
1123
+ session_id text,
1124
+ turn_id text,
1125
+ primary key (id)
1126
+ );
1127
+
1128
+ create index if not exists alma_audit_routing_turn
1129
+ on ${AUDIT_ROUTING_TABLE} (org, uid, turn_id);
1130
+
1131
+ create table if not exists ${AUDIT_COST_TABLE} (
1132
+ id uuid not null default gen_random_uuid(),
1133
+ org text not null,
1134
+ uid text not null,
1135
+ at timestamptz not null,
1136
+ model_provider text not null,
1137
+ model_id text not null,
1138
+ input_tokens bigint not null,
1139
+ output_tokens bigint not null,
1140
+ cache_read_input_tokens bigint,
1141
+ cache_write_input_tokens bigint,
1142
+ -- double precision, like every fractional number in this package: the whole
1143
+ -- pricing pipeline computes in JS floats, and NUMERIC round-trips as a
1144
+ -- string the driver does not sum (spec: spend-store).
1145
+ cost_usd double precision not null,
1146
+ caps_crossed text[] not null default '{}',
1147
+ session_id text,
1148
+ turn_id text,
1149
+ primary key (id)
1150
+ );
1151
+
1152
+ create index if not exists alma_audit_cost_turn
1153
+ on ${AUDIT_COST_TABLE} (org, uid, turn_id);
1154
+
1155
+ create index if not exists alma_audit_cost_recent
1156
+ on ${AUDIT_COST_TABLE} (org, uid, at desc);
1157
+
1158
+ create table if not exists ${AUDIT_RECALL_TABLE} (
1159
+ id uuid not null default gen_random_uuid(),
1160
+ org text not null,
1161
+ uid text not null,
1162
+ at timestamptz not null,
1163
+ session_id text not null,
1164
+ turn_id text not null,
1165
+ -- Provenance, never the recalled text: a verbatim copy would be a surface
1166
+ -- erasure cannot reach (spec 012).
1167
+ fact_ids text[] not null default '{}',
1168
+ episode_ids text[] not null default '{}',
1169
+ budget_tokens bigint not null,
1170
+ estimated_tokens bigint not null,
1171
+ dropped_tokens bigint,
1172
+ truncated boolean not null,
1173
+ degraded_tiers text[] not null default '{}',
1174
+ primary key (id)
1175
+ );
1176
+
1177
+ create index if not exists alma_audit_recall_turn
1178
+ on ${AUDIT_RECALL_TABLE} (org, uid, turn_id);
1179
+
1180
+ create table if not exists ${AUDIT_CONTEXT_TABLE} (
1181
+ id uuid not null default gen_random_uuid(),
1182
+ org text not null,
1183
+ uid text not null,
1184
+ at timestamptz not null,
1185
+ session_id text not null,
1186
+ turn_id text not null,
1187
+ step integer not null,
1188
+ delegate boolean not null default false,
1189
+ changed text[] not null,
1190
+ refused text[] not null default '{}',
1191
+ -- The two shapes, flattened. Six numbers should be six numbers (spec 038).
1192
+ before_system_blocks bigint not null,
1193
+ before_system_chars bigint not null,
1194
+ before_messages bigint not null,
1195
+ before_message_blocks bigint not null,
1196
+ before_message_chars bigint not null,
1197
+ before_max_tokens bigint not null,
1198
+ after_system_blocks bigint not null,
1199
+ after_system_chars bigint not null,
1200
+ after_messages bigint not null,
1201
+ after_message_blocks bigint not null,
1202
+ after_message_chars bigint not null,
1203
+ after_max_tokens bigint not null,
1204
+ primary key (id)
1205
+ );
1206
+
1207
+ create index if not exists alma_audit_context_turn
1208
+ on ${AUDIT_CONTEXT_TABLE} (org, uid, turn_id);
1209
+ ${AUDIT_TABLES.map((t) => rlsPolicySql(t)).join("")}
1210
+ ${roleBootstrapSql(role)}
1211
+ ${roleBootstrapSql(retentionRole)}
1212
+ grant select, insert
1213
+ on ${AUDIT_TABLES.join(", ")}
1214
+ to ${role};
1215
+
1216
+ -- Retention is a DIFFERENT ACTOR from erasure (spec 039). The app role above
1217
+ -- cannot delete, so a scoped purge can never remove the proof an erasure
1218
+ -- happened; this role can delete any row, and only that \u2014 the grant below
1219
+ -- withholds insert and update from it.
1220
+ --
1221
+ -- These policies are not optional decoration. Every audit table carries FORCE
1222
+ -- ROW LEVEL SECURITY, which subjects even the table OWNER to the predicate, so
1223
+ -- a sweep running as any non-superuser matched the scope-keyed policy, found
1224
+ -- nothing, and deleted zero rows while reporting success. That is what the spec
1225
+ -- 039 review caught: a retention mechanism that silently retains forever, in
1226
+ -- exactly the deployments careful enough not to connect as a superuser.
1227
+ --
1228
+ -- TWO policies, not one. A FOR DELETE policy alone still deleted nothing,
1229
+ -- because a DELETE with a WHERE clause must SCAN the rows to filter them, and
1230
+ -- the scope-keyed policy is FOR ALL -- which governs SELECT too. That second
1231
+ -- step surfaced only by running the statement; it does not follow from reading
1232
+ -- the first fix.
1233
+ ${AUDIT_TABLES.map(
1234
+ (t) => `
1235
+ drop policy if exists alma_audit_retention_read on ${t};
1236
+ create policy alma_audit_retention_read on ${t}
1237
+ for select
1238
+ to ${retentionRole}
1239
+ using (true);
1240
+
1241
+ drop policy if exists alma_audit_retention on ${t};
1242
+ create policy alma_audit_retention on ${t}
1243
+ for delete
1244
+ to ${retentionRole}
1245
+ using (true);`
1246
+ ).join("")}
1247
+
1248
+ grant select, delete
1249
+ on ${AUDIT_TABLES.join(", ")}
1250
+ to ${retentionRole};
1251
+ `;
1252
+ }
1253
+ async function migrateAuditLog(pool, opts = {}) {
1254
+ await pool.query(auditLogMigrationSql(opts.role, opts.retentionRole));
1255
+ }
1256
+ async function purgeAuditBefore(pool, windows, opts = {}) {
1257
+ const retentionRole = opts.retentionRole ?? DEFAULT_RETENTION_ROLE;
1258
+ assertRoleIdentifier(retentionRole);
1259
+ for (const table of AUDIT_TABLES) {
1260
+ const before = windows[table];
1261
+ if (before !== void 0 && Number.isNaN(Date.parse(before))) {
1262
+ throw new Error(`invalid ISO 8601 timestamp for ${table}: ${JSON.stringify(before)}`);
1263
+ }
1264
+ }
1265
+ const purged = {};
1266
+ const client = await pool.connect();
1267
+ try {
1268
+ await client.query("begin");
1269
+ await client.query(`set local role ${retentionRole}`);
1270
+ for (const table of AUDIT_TABLES) {
1271
+ const before = windows[table];
1272
+ if (before === void 0) continue;
1273
+ const { rowCount } = await client.query(
1274
+ `delete from ${table} where at < $1::timestamptz`,
1275
+ [before]
1276
+ );
1277
+ purged[table] = rowCount ?? 0;
1278
+ }
1279
+ await client.query("commit");
1280
+ return purged;
1281
+ } catch (err) {
1282
+ await client.query("rollback").catch(() => void 0);
1283
+ throw err;
1284
+ } finally {
1285
+ client.release();
1286
+ }
1287
+ }
1288
+
1289
+ // src/audit-store.ts
1290
+ var PostgresAuditLog = class {
1291
+ #pool;
1292
+ #role;
1293
+ #timeoutMs;
1294
+ constructor(pool, opts = {}) {
1295
+ this.#pool = pool;
1296
+ this.#role = resolveRlsRole(opts);
1297
+ this.#timeoutMs = resolveStatementTimeout(opts);
1298
+ }
1299
+ async access(e) {
1300
+ await this.#write(
1301
+ e.scope,
1302
+ (client) => client.query(
1303
+ `insert into ${AUDIT_ACCESS_TABLE}
1304
+ (org, uid, at, tool, action, resource, session_id, turn_id)
1305
+ values ($1, $2, $3::timestamptz, $4, $5, $6, $7, $8)`,
1306
+ [
1307
+ e.scope.org,
1308
+ e.scope.uid,
1309
+ instant2(e.at),
1310
+ e.tool,
1311
+ e.action,
1312
+ e.resource ?? null,
1313
+ e.sessionId ?? null,
1314
+ e.turnId ?? null
1315
+ ]
1316
+ )
1317
+ );
1318
+ }
1319
+ async routing(e) {
1320
+ await this.#write(
1321
+ e.scope,
1322
+ (client) => client.query(
1323
+ `insert into ${AUDIT_ROUTING_TABLE}
1324
+ (org, uid, at, tier, sensitivity, model_provider, model_id, rationale,
1325
+ session_id, turn_id)
1326
+ values ($1, $2, $3::timestamptz, $4, $5, $6, $7, $8, $9, $10)`,
1327
+ [
1328
+ e.scope.org,
1329
+ e.scope.uid,
1330
+ instant2(e.at),
1331
+ e.tier,
1332
+ e.sensitivity,
1333
+ e.model.provider,
1334
+ e.model.id,
1335
+ e.rationale,
1336
+ e.sessionId ?? null,
1337
+ e.turnId ?? null
1338
+ ]
1339
+ )
1340
+ );
1341
+ }
1342
+ async cost(e) {
1343
+ await this.#write(
1344
+ e.scope,
1345
+ (client) => client.query(
1346
+ `insert into ${AUDIT_COST_TABLE}
1347
+ (org, uid, at, model_provider, model_id, input_tokens, output_tokens,
1348
+ cache_read_input_tokens, cache_write_input_tokens, cost_usd,
1349
+ caps_crossed, session_id, turn_id)
1350
+ values ($1, $2, $3::timestamptz, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
1351
+ [
1352
+ e.scope.org,
1353
+ e.scope.uid,
1354
+ instant2(e.at),
1355
+ e.model.provider,
1356
+ e.model.id,
1357
+ e.usage.inputTokens,
1358
+ e.usage.outputTokens,
1359
+ // Absent stays absent rather than becoming a reported zero — the
1360
+ // distinction `addUsage` is careful about, preserved at the boundary.
1361
+ e.usage.cacheReadInputTokens ?? null,
1362
+ e.usage.cacheWriteInputTokens ?? null,
1363
+ e.costUsd,
1364
+ [...e.capsCrossed ?? []],
1365
+ e.sessionId ?? null,
1366
+ e.turnId ?? null
1367
+ ]
1368
+ )
1369
+ );
1370
+ }
1371
+ async recall(e) {
1372
+ await this.#write(
1373
+ e.scope,
1374
+ (client) => client.query(
1375
+ `insert into ${AUDIT_RECALL_TABLE}
1376
+ (org, uid, at, session_id, turn_id, fact_ids, episode_ids,
1377
+ budget_tokens, estimated_tokens, dropped_tokens, truncated,
1378
+ degraded_tiers)
1379
+ values ($1, $2, $3::timestamptz, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
1380
+ [
1381
+ e.scope.org,
1382
+ e.scope.uid,
1383
+ instant2(e.at),
1384
+ e.sessionId,
1385
+ e.turnId,
1386
+ [...e.factIds],
1387
+ [...e.episodeIds],
1388
+ e.budgetTokens,
1389
+ e.estimatedTokens,
1390
+ e.droppedTokens ?? null,
1391
+ e.truncated,
1392
+ [...e.degradedTiers ?? []]
1393
+ ]
1394
+ )
1395
+ );
1396
+ }
1397
+ async context(e) {
1398
+ await this.#write(
1399
+ e.scope,
1400
+ (client) => client.query(
1401
+ `insert into ${AUDIT_CONTEXT_TABLE}
1402
+ (org, uid, at, session_id, turn_id, step, delegate, changed, refused,
1403
+ before_system_blocks, before_system_chars, before_messages,
1404
+ before_message_blocks, before_message_chars, before_max_tokens,
1405
+ after_system_blocks, after_system_chars, after_messages,
1406
+ after_message_blocks, after_message_chars, after_max_tokens)
1407
+ values ($1, $2, $3::timestamptz, $4, $5, $6, $7, $8, $9,
1408
+ $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)`,
1409
+ [
1410
+ e.scope.org,
1411
+ e.scope.uid,
1412
+ instant2(e.at),
1413
+ e.sessionId,
1414
+ e.turnId,
1415
+ e.step,
1416
+ e.delegate ?? false,
1417
+ [...e.changed],
1418
+ [...e.refused ?? []],
1419
+ e.before.systemBlocks,
1420
+ e.before.systemChars,
1421
+ e.before.messages,
1422
+ e.before.messageBlocks,
1423
+ e.before.messageChars,
1424
+ e.before.maxTokens,
1425
+ e.after.systemBlocks,
1426
+ e.after.systemChars,
1427
+ e.after.messages,
1428
+ e.after.messageBlocks,
1429
+ e.after.messageChars,
1430
+ e.after.maxTokens
1431
+ ]
1432
+ )
1433
+ );
1434
+ }
1435
+ /** Shared RLS binding — see `scoped.ts`. */
1436
+ async #write(scope, run) {
1437
+ await inScope(this.#pool, this.#role, scope, async (client) => {
1438
+ await run(client);
1439
+ }, this.#timeoutMs);
1440
+ }
1441
+ };
1442
+ function instant2(at) {
1443
+ if (Number.isNaN(Date.parse(at))) {
1444
+ throw new Error(`invalid ISO 8601 timestamp: ${JSON.stringify(at)}`);
1445
+ }
1446
+ return at;
1447
+ }
1448
+
1449
+ // src/turn-store.ts
1450
+ import {
1451
+ assertWellFormed as assertWellFormed2,
1452
+ scopePath as scopePath4
1453
+ } from "@alma-harness/core";
1454
+
1455
+ // src/turn-schema.ts
1456
+ var TURN_LEASES_TABLE = "alma_turn_leases";
1457
+ var TURN_CLAIMS_TABLE = "alma_turn_claims";
1458
+ function turnStoreMigrationSql(role = DEFAULT_RLS_ROLE) {
1459
+ assertRoleIdentifier(role);
1460
+ return `
1461
+ create table if not exists ${TURN_LEASES_TABLE} (
1462
+ org text not null,
1463
+ uid text not null,
1464
+ session_id text not null,
1465
+ token text not null,
1466
+ expires_at timestamptz not null,
1467
+ primary key (org, uid, session_id)
1468
+ );
1469
+
1470
+ create table if not exists ${TURN_CLAIMS_TABLE} (
1471
+ org text not null,
1472
+ uid text not null,
1473
+ session_id text not null,
1474
+ idempotency_key text not null,
1475
+ completed jsonb,
1476
+ created_at timestamptz not null default now(),
1477
+ primary key (org, uid, session_id, idempotency_key)
1478
+ );
1479
+ ${rlsPolicySql(TURN_LEASES_TABLE)}${rlsPolicySql(TURN_CLAIMS_TABLE)}
1480
+ ${roleBootstrapSql(role)}
1481
+ grant select, insert, update, delete
1482
+ on ${TURN_LEASES_TABLE}, ${TURN_CLAIMS_TABLE}
1483
+ to ${role};
1484
+ `;
1485
+ }
1486
+ async function migrateTurnStore(pool, opts = {}) {
1487
+ await pool.query(turnStoreMigrationSql(opts.role));
1488
+ }
1489
+
1490
+ // src/turn-store.ts
1491
+ var PostgresTurnStore = class {
1492
+ #pool;
1493
+ #role;
1494
+ #timeoutMs;
1495
+ constructor(pool, opts = {}) {
1496
+ this.#pool = pool;
1497
+ this.#role = resolveRlsRole(opts);
1498
+ this.#timeoutMs = resolveStatementTimeout(opts);
1499
+ }
1500
+ async acquire(scope, sessionId, opts) {
1501
+ assertLeaseOpts(opts);
1502
+ scopePath4(scope);
1503
+ const deadline = Date.now() + opts.waitMs;
1504
+ let backoffMs = 25;
1505
+ for (; ; ) {
1506
+ const lease = await this.#tryAcquire(scope, sessionId, opts.ttlMs);
1507
+ if (lease !== null) return lease;
1508
+ const remaining = deadline - Date.now();
1509
+ if (remaining <= 0) return null;
1510
+ await new Promise((resolve) => setTimeout(resolve, Math.min(backoffMs, remaining)));
1511
+ backoffMs = Math.min(backoffMs * 2, 250);
1512
+ }
1513
+ }
1514
+ async #tryAcquire(scope, sessionId, ttlMs) {
1515
+ const token = crypto.randomUUID();
1516
+ return this.#inScope(scope, async (client) => {
1517
+ const { rows } = await client.query(
1518
+ // The `where` on the conflict target is what makes this atomic: a live
1519
+ // lease makes the update match nothing, so no row comes back and the
1520
+ // caller lost. Expiry is compared against the SERVER's clock, so two
1521
+ // app instances with drifting clocks cannot disagree about whether a
1522
+ // lease is live — which is how the same session reaches two holders.
1523
+ `insert into ${TURN_LEASES_TABLE} (org, uid, session_id, token, expires_at)
1524
+ values ($1, $2, $3, $4, now() + make_interval(secs => $5::double precision))
1525
+ on conflict (org, uid, session_id) do update
1526
+ set token = excluded.token, expires_at = excluded.expires_at
1527
+ where ${TURN_LEASES_TABLE}.expires_at <= now()
1528
+ returning token, expires_at`,
1529
+ [scope.org, scope.uid, sessionId, token, ttlMs / 1e3]
1530
+ );
1531
+ const row = rows[0];
1532
+ return row === void 0 ? null : { token: row.token, expiresAt: row.expires_at.toISOString() };
1533
+ });
1534
+ }
1535
+ async release(scope, sessionId, lease) {
1536
+ await this.#inScope(scope, async (client) => {
1537
+ await client.query(
1538
+ `delete from ${TURN_LEASES_TABLE}
1539
+ where org = $1 and uid = $2 and session_id = $3 and token = $4`,
1540
+ [scope.org, scope.uid, sessionId, lease.token]
1541
+ );
1542
+ });
1543
+ }
1544
+ async claim(key) {
1545
+ return this.#inScope(key.scope, async (client) => {
1546
+ await client.query(
1547
+ `insert into ${TURN_CLAIMS_TABLE} (org, uid, session_id, idempotency_key)
1548
+ values ($1, $2, $3, $4)
1549
+ on conflict (org, uid, session_id, idempotency_key) do nothing`,
1550
+ [key.scope.org, key.scope.uid, key.sessionId, key.idempotencyKey]
1551
+ );
1552
+ const { rows } = await client.query(
1553
+ `select completed from ${TURN_CLAIMS_TABLE}
1554
+ where org = $1 and uid = $2 and session_id = $3 and idempotency_key = $4`,
1555
+ [key.scope.org, key.scope.uid, key.sessionId, key.idempotencyKey]
1556
+ );
1557
+ const completed = rows[0]?.completed ?? null;
1558
+ return completed === null ? { status: "fresh" } : { status: "replay", completed };
1559
+ });
1560
+ }
1561
+ /**
1562
+ * PRECONDITION: every string in `completed`, keys included, is well-formed
1563
+ * UTF-16 — `jsonb` refuses a lone surrogate and the write fails (spec:
1564
+ * well-formed-text).
1565
+ *
1566
+ * The loop repairs it on the way in: `reply` is drawn from messages
1567
+ * `record()` passed through `toWellFormedDeep`. But that repair is
1568
+ * BEST-EFFORT by design — its `catch` keeps the unrepaired message, because
1569
+ * failing to repair must never cost more than not having tried — so a
1570
+ * pathologically nested payload can still arrive malformed.
1571
+ *
1572
+ * Now ENFORCED here and in the in-memory reference alike, by the same guard
1573
+ * `SessionStore.append` uses (spec 040). The adapters used to differ — that
1574
+ * store kept the lone surrogate, this one refused the write — which is the
1575
+ * gap spec 025's review recorded and two contracts then documented instead
1576
+ * of closing. The check runs BEFORE the UPDATE and regardless of whether a
1577
+ * row matches, because `$5::jsonb` is parsed either way; the in-memory
1578
+ * reference orders it the same for that reason. The first version of this
1579
+ * comment claimed the loop simply guaranteed it (spec 033).
1580
+ */
1581
+ async complete(key, completed) {
1582
+ assertWellFormed2(completed, "completed");
1583
+ await this.#inScope(key.scope, async (client) => {
1584
+ await client.query(
1585
+ `update ${TURN_CLAIMS_TABLE} set completed = $5::jsonb
1586
+ where org = $1 and uid = $2 and session_id = $3 and idempotency_key = $4`,
1587
+ [
1588
+ key.scope.org,
1589
+ key.scope.uid,
1590
+ key.sessionId,
1591
+ key.idempotencyKey,
1592
+ JSON.stringify(completed)
1593
+ ]
1594
+ );
1595
+ });
1596
+ }
1597
+ async abandon(key) {
1598
+ await this.#inScope(key.scope, async (client) => {
1599
+ await client.query(
1600
+ `delete from ${TURN_CLAIMS_TABLE}
1601
+ where org = $1 and uid = $2 and session_id = $3 and idempotency_key = $4`,
1602
+ [key.scope.org, key.scope.uid, key.sessionId, key.idempotencyKey]
1603
+ );
1604
+ });
1605
+ }
1606
+ async erase(scope, sessionId) {
1607
+ await this.#inScope(scope, async (client) => {
1608
+ const params = sessionId === void 0 ? [scope.org, scope.uid] : [scope.org, scope.uid, sessionId];
1609
+ const bySession = sessionId === void 0 ? "" : " and session_id = $3";
1610
+ for (const table of [TURN_CLAIMS_TABLE, TURN_LEASES_TABLE]) {
1611
+ await client.query(
1612
+ `delete from ${table} where org = $1 and uid = $2${bySession}`,
1613
+ params
1614
+ );
1615
+ }
1616
+ });
1617
+ }
1618
+ /** Shared RLS binding — see `scoped.ts`. */
1619
+ async #inScope(scope, fn) {
1620
+ return inScope(this.#pool, this.#role, scope, fn, this.#timeoutMs);
1621
+ }
1622
+ };
1623
+ function assertLeaseOpts(opts) {
1624
+ for (const [name, value] of [
1625
+ ["ttlMs", opts.ttlMs],
1626
+ ["waitMs", opts.waitMs]
1627
+ ]) {
1628
+ if (!Number.isFinite(value) || value < 0) {
1629
+ throw new Error(`${name} must be a non-negative finite number, got ${value}`);
1630
+ }
1631
+ }
1632
+ }
1633
+ export {
1634
+ AUDIT_ACCESS_TABLE,
1635
+ AUDIT_CONTEXT_TABLE,
1636
+ AUDIT_COST_TABLE,
1637
+ AUDIT_RECALL_TABLE,
1638
+ AUDIT_ROUTING_TABLE,
1639
+ AUDIT_TABLES,
1640
+ DEFAULT_RETENTION_ROLE,
1641
+ DEFAULT_RLS_ROLE,
1642
+ EPISODES_TABLE,
1643
+ FACTS_TABLE,
1644
+ PostgresAuditLog,
1645
+ PostgresEpisodeStore,
1646
+ PostgresErasureWatermarks,
1647
+ PostgresProfileStore,
1648
+ PostgresSessionStore,
1649
+ PostgresSpendStore,
1650
+ PostgresTurnStore,
1651
+ SCOPE_STATE_TABLE,
1652
+ SPEND_SESSIONS_TABLE,
1653
+ SPEND_TENANT_DAYS_TABLE,
1654
+ TURN_CLAIMS_TABLE,
1655
+ TURN_LEASES_TABLE,
1656
+ assertRoleIdentifier,
1657
+ auditLogMigrationSql,
1658
+ memoryStoreMigrationSql,
1659
+ migrateAuditLog,
1660
+ migrateMemoryStores,
1661
+ migrateSessionStore,
1662
+ migrateSpendStore,
1663
+ migrateTurnStore,
1664
+ purgeAuditBefore,
1665
+ rlsPolicySql,
1666
+ roleBootstrapSql,
1667
+ sessionStoreMigrationSql,
1668
+ spendStoreMigrationSql,
1669
+ turnStoreMigrationSql
1670
+ };
1671
+ //# sourceMappingURL=index.js.map