@nightowlsdev/storage-supabase 0.3.0 → 1.0.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 CHANGED
@@ -112,6 +112,19 @@ function makeEventStore(ctx) {
112
112
  const rows = await many(ctx.pool, "select run_id, seq, payload from events where org_id=$1 and run_id=$2 and seq>$3 order by seq", [tenantId, runId, sinceSeq]);
113
113
  return rows.map(fromRow);
114
114
  },
115
+ // The full ordered log for a CONTAINER: join events → runs and match the container (the root thread_id, OR a
116
+ // lane sub-thread `<container>:<slug>`). `seq` is GENERATED ALWAYS (global, monotonic across all runs), so a
117
+ // plain `order by seq` interleaves every run correctly. Org-scoped (service connection bypasses RLS, so we
118
+ // enforce tenancy here). split_part is injection-safe; a container id never contains ':'.
119
+ async listForContainer(tenantId, container) {
120
+ const rows = await many(
121
+ ctx.pool,
122
+ `select e.run_id, e.seq, e.payload, r.thread_id from events e join runs r on r.id = e.run_id
123
+ where e.org_id = $1 and split_part(r.thread_id, ':', 1) = $2 order by e.seq`,
124
+ [tenantId, container]
125
+ );
126
+ return rows.map((r) => ({ ...fromRow(r), threadId: r.thread_id }));
127
+ },
115
128
  subscribe: makeSubscribe(ctx)
116
129
  };
117
130
  }
@@ -258,6 +271,22 @@ function makeScratchpadStore(ctx) {
258
271
  };
259
272
  }
260
273
 
274
+ // src/agents.ts
275
+ import { assertActorMayMutateDefinition } from "@nightowlsdev/core";
276
+
277
+ // src/versioning.ts
278
+ async function appendVersion(client, entity, headId, tenantId, insertVersionRow, action, actor, auditAfter) {
279
+ await client.query("select pg_advisory_xact_lock(hashtext($1), hashtext($2))", [entity.versionTable, headId]);
280
+ const nextV = (await client.query(`select coalesce(max(version),0)+1 v from ${entity.versionTable} where ${entity.fkColumn}=$1`, [headId])).rows[0].v;
281
+ const versionRowId = await insertVersionRow(nextV);
282
+ await client.query(`update ${entity.headTable} set current_version_id=$2 where id=$1`, [headId, versionRowId]);
283
+ await client.query(
284
+ "insert into audit_log(org_id, actor, action, entity, entity_id, after) values($1,$2,$3,$4,$5,$6)",
285
+ [tenantId, actor, action, entity.kind, headId, JSON.stringify({ version: nextV, ...auditAfter })]
286
+ );
287
+ return nextV;
288
+ }
289
+
261
290
  // src/subscribe-invalidations.ts
262
291
  var INVALIDATE_CHANNEL = "nightowls_agent_invalidate";
263
292
  async function listenForInvalidations(ctx, onInvalidate) {
@@ -269,7 +298,7 @@ async function listenForInvalidations(ctx, onInvalidate) {
269
298
  client.on("error", (e) => {
270
299
  client.removeListener("notification", onNotification);
271
300
  console.warn(
272
- "[nightowls] agent-cache invalidation LISTEN connection errored \u2014 cross-process eviction paused until restart; the 30s cache TTL still bounds staleness:",
301
+ "[@nightowlsdev/storage-supabase] agent-cache invalidation LISTEN connection errored \u2014 cross-process eviction paused until restart; the 30s cache TTL still bounds staleness:",
273
302
  e instanceof Error ? e.message : e
274
303
  );
275
304
  });
@@ -293,6 +322,7 @@ async function listenForInvalidations(ctx, onInvalidate) {
293
322
  }
294
323
 
295
324
  // src/agents.ts
325
+ var AGENT_ENTITY = { kind: "agent", versionTable: "agent_versions", headTable: "agents", fkColumn: "agent_id" };
296
326
  function rowToVersion(r) {
297
327
  return {
298
328
  slug: r.slug,
@@ -305,6 +335,18 @@ function rowToVersion(r) {
305
335
  modelId: r.model_id
306
336
  };
307
337
  }
338
+ function auditActor(actor) {
339
+ switch (actor.type) {
340
+ case "human":
341
+ return `user:${actor.userId}`;
342
+ case "service":
343
+ return `service:${actor.serviceId}`;
344
+ case "system":
345
+ return `system:${actor.reason}`;
346
+ case "agent":
347
+ return `agent:${actor.agentSlug}`;
348
+ }
349
+ }
308
350
  function makeAgentRepo(ctx) {
309
351
  return {
310
352
  async head(tenantId, slug) {
@@ -333,19 +375,32 @@ function makeAgentRepo(ctx) {
333
375
  }
334
376
  };
335
377
  }
378
+ function makeVersionedRepo(ctx) {
379
+ const read = makeAgentRepo(ctx);
380
+ return {
381
+ ...read,
382
+ async publish(tenantId, slug, content, actor) {
383
+ const { version } = await publishAgentVersion(ctx, { tenantId, ...content, slug, actor });
384
+ return { version };
385
+ },
386
+ async rollback(tenantId, slug, toVersion, actor) {
387
+ return rollbackAgentVersion(ctx, { tenantId, slug, toVersion, actor });
388
+ },
389
+ async listVersions(tenantId, slug, actor) {
390
+ assertActorMayMutateDefinition(actor);
391
+ return listAgentVersions(ctx, tenantId, slug);
392
+ }
393
+ };
394
+ }
336
395
  async function commitVersion(client, agentId, tenantId, content, action, actor, auditAfter) {
337
- const nextV = (await client.query("select coalesce(max(version),0)+1 v from agent_versions where agent_id=$1", [agentId])).rows[0].v;
338
- const ver = (await client.query(
339
- `insert into agent_versions(agent_id, org_id, version, role, personality, capabilities, skill_names, delegate_slugs, model_id, status)
340
- values($1,$2,$3,$4,$5,$6,$7,$8,$9,'published') returning id`,
341
- [agentId, tenantId, nextV, content.role, content.personality, JSON.stringify(content.capabilities), JSON.stringify(content.skillNames), JSON.stringify(content.delegateSlugs), content.modelId]
342
- )).rows[0];
343
- await client.query("update agents set current_version_id=$2 where id=$1", [agentId, ver.id]);
344
- await client.query(
345
- "insert into audit_log(org_id, actor, action, entity, entity_id, after) values($1,$2,$3,'agent',$4,$5)",
346
- [tenantId, actor, action, agentId, JSON.stringify({ version: nextV, ...auditAfter })]
347
- );
348
- return nextV;
396
+ return appendVersion(client, AGENT_ENTITY, agentId, tenantId, async (version) => {
397
+ const ver = (await client.query(
398
+ `insert into agent_versions(agent_id, org_id, version, role, personality, capabilities, skill_names, delegate_slugs, model_id, status)
399
+ values($1,$2,$3,$4,$5,$6,$7,$8,$9,'published') returning id`,
400
+ [agentId, tenantId, version, content.role, content.personality, JSON.stringify(content.capabilities), JSON.stringify(content.skillNames), JSON.stringify(content.delegateSlugs), content.modelId]
401
+ )).rows[0];
402
+ return ver.id;
403
+ }, action, actor, auditAfter);
349
404
  }
350
405
  async function notifyInvalidate(client, tenantId, slug) {
351
406
  try {
@@ -353,7 +408,10 @@ async function notifyInvalidate(client, tenantId, slug) {
353
408
  } catch {
354
409
  }
355
410
  }
411
+ var SEED_ACTOR = { type: "system", reason: "seed" };
356
412
  async function publishAgentVersion(ctx, def) {
413
+ const actor = def.actor ?? SEED_ACTOR;
414
+ assertActorMayMutateDefinition(actor);
357
415
  const client = await ctx.pool.connect();
358
416
  try {
359
417
  await client.query("begin");
@@ -362,9 +420,10 @@ async function publishAgentVersion(ctx, def) {
362
420
  on conflict (org_id, project_id, slug) do update set slug=excluded.slug returning id`,
363
421
  [def.tenantId, def.slug, def.role === "orchestrator"]
364
422
  )).rows[0];
365
- await commitVersion(client, agent.id, def.tenantId, def, "publish", def.actor ?? "seed", { slug: def.slug });
423
+ const version = await commitVersion(client, agent.id, def.tenantId, def, "publish", auditActor(actor), { slug: def.slug });
366
424
  await client.query("commit");
367
425
  await notifyInvalidate(client, def.tenantId, def.slug);
426
+ return { version };
368
427
  } catch (e) {
369
428
  await client.query("rollback");
370
429
  throw e;
@@ -383,6 +442,8 @@ async function listAgentVersions(ctx, tenantId, slug) {
383
442
  return rows.map((r) => ({ version: Number(r.version), role: r.role, modelId: r.model_id, status: r.status, isCurrent: r.is_current }));
384
443
  }
385
444
  async function rollbackAgentVersion(ctx, args) {
445
+ const actor = args.actor ?? { type: "system", reason: "rollback" };
446
+ assertActorMayMutateDefinition(actor);
386
447
  const client = await ctx.pool.connect();
387
448
  try {
388
449
  await client.query("begin");
@@ -401,7 +462,7 @@ async function rollbackAgentVersion(ctx, args) {
401
462
  delegateSlugs: target.delegate_slugs ?? [],
402
463
  modelId: target.model_id
403
464
  };
404
- const version = await commitVersion(client, target.agent_id, args.tenantId, content, "rollback", args.actor ?? "rollback", {
465
+ const version = await commitVersion(client, target.agent_id, args.tenantId, content, "rollback", auditActor(actor), {
405
466
  slug: args.slug,
406
467
  restoredFrom: args.toVersion
407
468
  });
@@ -416,6 +477,114 @@ async function rollbackAgentVersion(ctx, args) {
416
477
  }
417
478
  }
418
479
 
480
+ // src/bundles.ts
481
+ import { assertActorMayMutateDefinition as assertActorMayMutateDefinition2 } from "@nightowlsdev/core";
482
+ var BUNDLE_ENTITY = { kind: "bundle", versionTable: "bundle_versions", headTable: "bundles", fkColumn: "bundle_id" };
483
+ function auditActor2(actor) {
484
+ switch (actor.type) {
485
+ case "human":
486
+ return `user:${actor.userId}`;
487
+ case "service":
488
+ return `service:${actor.serviceId}`;
489
+ case "system":
490
+ return `system:${actor.reason}`;
491
+ case "agent":
492
+ return `agent:${actor.agentSlug}`;
493
+ }
494
+ }
495
+ function rowToBundleVersion(r) {
496
+ return { version: Number(r.version), ...r.content };
497
+ }
498
+ function makeBundleRepo(ctx) {
499
+ return {
500
+ async head(tenantId, slug) {
501
+ const r = await one(
502
+ ctx.pool,
503
+ `select v.version, v.content from bundles b join bundle_versions v on v.id = b.current_version_id
504
+ where b.org_id=$1 and b.slug=$2`,
505
+ [tenantId, slug]
506
+ );
507
+ return r ? rowToBundleVersion(r) : null;
508
+ },
509
+ async getVersion(tenantId, slug, version) {
510
+ const r = await one(
511
+ ctx.pool,
512
+ `select v.version, v.content from bundles b join bundle_versions v on v.bundle_id = b.id
513
+ where b.org_id=$1 and b.slug=$2 and v.version=$3`,
514
+ [tenantId, slug, version]
515
+ );
516
+ return r ? rowToBundleVersion(r) : null;
517
+ },
518
+ async listSlugs(tenantId) {
519
+ const rows = await many(ctx.pool, "select slug from bundles where org_id=$1", [tenantId]);
520
+ return rows.map((r) => r.slug);
521
+ }
522
+ };
523
+ }
524
+ async function commitBundle(ctx, tenantId, slug, content, action, actor, auditAfter) {
525
+ const client = await ctx.pool.connect();
526
+ try {
527
+ await client.query("begin");
528
+ const bundle = (await client.query(
529
+ `insert into bundles(org_id, slug) values($1,$2)
530
+ on conflict (org_id, project_id, slug) do update set slug=excluded.slug returning id`,
531
+ [tenantId, slug]
532
+ )).rows[0];
533
+ const version = await appendVersion(client, BUNDLE_ENTITY, bundle.id, tenantId, async (v) => {
534
+ const row = (await client.query(
535
+ `insert into bundle_versions(bundle_id, org_id, version, content, status) values($1,$2,$3,$4,'published') returning id`,
536
+ [bundle.id, tenantId, v, JSON.stringify(content)]
537
+ )).rows[0];
538
+ return row.id;
539
+ }, action, auditActor2(actor), auditAfter);
540
+ await client.query("commit");
541
+ return { version };
542
+ } catch (e) {
543
+ await client.query("rollback");
544
+ throw e;
545
+ } finally {
546
+ client.release();
547
+ }
548
+ }
549
+ async function listBundleVersions(ctx, tenantId, slug) {
550
+ const rows = await many(
551
+ ctx.pool,
552
+ `select v.version, v.content, v.status, (v.id = b.current_version_id) as is_current
553
+ from bundles b join bundle_versions v on v.bundle_id = b.id
554
+ where b.org_id=$1 and b.slug=$2 order by v.version`,
555
+ [tenantId, slug]
556
+ );
557
+ return rows.map((r) => ({
558
+ version: Number(r.version),
559
+ title: r.content?.title ?? "",
560
+ status: r.status,
561
+ isCurrent: r.is_current,
562
+ memberCount: Array.isArray(r.content?.agents) ? r.content.agents.length : 0
563
+ }));
564
+ }
565
+ function makeBundleWritableRepo(ctx) {
566
+ const read = makeBundleRepo(ctx);
567
+ return {
568
+ ...read,
569
+ async publish(tenantId, slug, content, actor) {
570
+ assertActorMayMutateDefinition2(actor);
571
+ return commitBundle(ctx, tenantId, slug, content, "publish", actor, { slug });
572
+ },
573
+ async rollback(tenantId, slug, toVersion, actor) {
574
+ assertActorMayMutateDefinition2(actor);
575
+ const target = await read.getVersion(tenantId, slug, toVersion);
576
+ if (!target) throw new Error(`cannot roll back bundle ${slug} to v${toVersion}: no such version for this tenant`);
577
+ const { version: _v, ...content } = target;
578
+ const { version } = await commitBundle(ctx, tenantId, slug, content, "rollback", actor, { slug, restoredFrom: toVersion });
579
+ return { version, restoredFrom: toVersion };
580
+ },
581
+ async listVersions(tenantId, slug, actor) {
582
+ assertActorMayMutateDefinition2(actor);
583
+ return listBundleVersions(ctx, tenantId, slug);
584
+ }
585
+ };
586
+ }
587
+
419
588
  // src/mastra-store.ts
420
589
  import { PostgresStore, PgVector } from "@mastra/pg";
421
590
  function createMastraPgStore(opts) {
@@ -1553,12 +1722,171 @@ begin
1553
1722
  execute format('alter index nightowls.%I rename to %I', r.indexname, 'nightowls_' || substring(r.indexname from 8));
1554
1723
  end loop;
1555
1724
  end $$;
1725
+
1726
+ -- Repoint any function BODY in the (now) nightowls schema that still hardcodes the old 'corale.' schema \u2014
1727
+ -- ALTER SCHEMA RENAME does not rewrite dollar-quoted bodies (see the header note). Recreate each via its own
1728
+ -- definition with 'corale.' \u2192 'nightowls.'. Generic + idempotent: once rewritten, prosrc no longer matches.
1729
+ do $$
1730
+ declare r record;
1731
+ begin
1732
+ for r in
1733
+ select p.oid
1734
+ from pg_proc p
1735
+ join pg_namespace n on n.oid = p.pronamespace
1736
+ where n.nspname = 'nightowls' and p.prosrc like '%corale.%'
1737
+ loop
1738
+ execute replace(pg_get_functiondef(r.oid), 'corale.', 'nightowls.');
1739
+ end loop;
1740
+ end $$;
1741
+ `
1742
+ )
1743
+ };
1744
+
1745
+ // src/migrations/0014_agent_versions_immutable.ts
1746
+ var M0014_AGENT_VERSIONS_IMMUTABLE = {
1747
+ version: "0014_agent_versions_immutable",
1748
+ name: "agent_versions: published rows are append-only (no content UPDATE, no DELETE)",
1749
+ sql: (
1750
+ /* sql */
1751
+ `
1752
+ -- Guard function: reject any mutation that would rewrite or remove a PUBLISHED agent_versions row.
1753
+ create or replace function nightowls.agent_versions_enforce_immutable()
1754
+ returns trigger language plpgsql as $$
1755
+ begin
1756
+ if (tg_op = 'DELETE') then
1757
+ if (old.status = 'published') then
1758
+ raise exception 'agent_versions: published version v% of agent % is immutable (DELETE forbidden \u2014 append-only)',
1759
+ old.version, old.agent_id
1760
+ using errcode = 'restrict_violation';
1761
+ end if;
1762
+ return old;
1763
+ end if;
1764
+
1765
+ -- UPDATE: a published row's CONTENT is frozen. Allow the lifecycle status to advance (e.g. archive), but
1766
+ -- forbid changing any identifying/behavior column once published.
1767
+ if (old.status = 'published') then
1768
+ if ( new.agent_id is distinct from old.agent_id
1769
+ or new.org_id is distinct from old.org_id
1770
+ or new.version is distinct from old.version
1771
+ or new.role is distinct from old.role
1772
+ or new.personality is distinct from old.personality
1773
+ or new.capabilities is distinct from old.capabilities
1774
+ or new.skill_names is distinct from old.skill_names
1775
+ or new.delegate_slugs is distinct from old.delegate_slugs
1776
+ or new.model_id is distinct from old.model_id
1777
+ or new.model_settings is distinct from old.model_settings
1778
+ or new.guardrails_override is distinct from old.guardrails_override
1779
+ or new.created_at is distinct from old.created_at ) then
1780
+ raise exception 'agent_versions: published version v% of agent % is immutable (content UPDATE forbidden \u2014 append-only; republish a new version instead)',
1781
+ old.version, old.agent_id
1782
+ using errcode = 'restrict_violation';
1783
+ end if;
1784
+ end if;
1785
+ return new;
1786
+ end; $$;
1787
+
1788
+ -- Fire BEFORE every UPDATE/DELETE on agent_versions. (No INSERT trigger \u2014 appends are the whole point.)
1789
+ drop trigger if exists agent_versions_immutable on nightowls.agent_versions;
1790
+ create trigger agent_versions_immutable
1791
+ before update or delete on nightowls.agent_versions
1792
+ for each row execute function nightowls.agent_versions_enforce_immutable();
1793
+ `
1794
+ )
1795
+ };
1796
+
1797
+ // src/migrations/0015_drop_saas_tables.ts
1798
+ var M0015_DROP_SAAS_TABLES = {
1799
+ version: "0015_drop_saas_tables",
1800
+ name: "drop SaaS-only tenant_policies + eval_runs (host-owned, not engine schema)",
1801
+ sql: (
1802
+ /* sql */
1803
+ `
1804
+ drop table if exists nightowls.tenant_policies cascade;
1805
+ drop table if exists nightowls.eval_runs cascade;
1806
+ `
1807
+ )
1808
+ };
1809
+
1810
+ // src/migrations/0016_bundle_versions.ts
1811
+ var M0016_BUNDLE_VERSIONS = {
1812
+ version: "0016_bundle_versions",
1813
+ name: "bundles + bundle_versions (append-only bundle definition versioning) + immutability trigger",
1814
+ sql: (
1815
+ /* sql */
1816
+ `
1817
+ -- Head pointer (mirrors nightowls.agents). NULLS NOT DISTINCT so the common project_id = NULL case still
1818
+ -- collides on (org, slug) \u2014 required for the publish upsert's ON CONFLICT to fire.
1819
+ create table nightowls.bundles (
1820
+ id uuid primary key default gen_random_uuid(),
1821
+ org_id uuid not null references nightowls.orgs(id) on delete cascade,
1822
+ project_id uuid, slug text not null, current_version_id uuid,
1823
+ created_at timestamptz not null default now(),
1824
+ unique nulls not distinct (org_id, project_id, slug)
1825
+ );
1826
+
1827
+ -- Append-only versions (mirrors nightowls.agent_versions; the whole bundle content is a single jsonb snapshot).
1828
+ create table nightowls.bundle_versions (
1829
+ id uuid primary key default gen_random_uuid(),
1830
+ bundle_id uuid not null references nightowls.bundles(id) on delete cascade,
1831
+ org_id uuid not null references nightowls.orgs(id) on delete cascade,
1832
+ version integer not null,
1833
+ content jsonb not null,
1834
+ status text not null default 'draft' check (status in ('draft','published','archived')),
1835
+ created_by uuid, created_at timestamptz not null default now(),
1836
+ unique (bundle_id, version)
1837
+ );
1838
+ alter table nightowls.bundles add constraint bundles_current_version_fk
1839
+ foreign key (current_version_id) references nightowls.bundle_versions(id);
1840
+
1841
+ -- RLS + org indexes (mirrors 0001's per-table pattern for the new tables). The adapter uses the service role
1842
+ -- (RLS-exempt), so this is defense-in-depth for any future direct authenticated-client read: enable RLS, add an
1843
+ -- org-scoped SELECT policy (writes stay service-role-only, like every other definition table), and index org_id.
1844
+ alter table nightowls.bundles enable row level security;
1845
+ alter table nightowls.bundle_versions enable row level security;
1846
+ create policy org_read_bundles on nightowls.bundles
1847
+ for select to authenticated using ((select nightowls.is_org_member(org_id)));
1848
+ create policy org_read_bundle_versions on nightowls.bundle_versions
1849
+ for select to authenticated using ((select nightowls.is_org_member(org_id)));
1850
+ create index bundles_org_idx on nightowls.bundles (org_id);
1851
+ create index bundle_versions_org_idx on nightowls.bundle_versions (org_id);
1852
+
1853
+ -- Published-row immutability (mirrors 0014): once status='published', the content + identity columns are frozen
1854
+ -- and the row may not be DELETEd. The lifecycle status may still advance (e.g. \u2192 'archived'), which doesn't
1855
+ -- rewrite history. No INSERT trigger \u2014 appends are the whole point.
1856
+ create or replace function nightowls.bundle_versions_enforce_immutable()
1857
+ returns trigger language plpgsql as $$
1858
+ begin
1859
+ if (tg_op = 'DELETE') then
1860
+ if (old.status = 'published') then
1861
+ raise exception 'bundle_versions: published version v% of bundle % is immutable (DELETE forbidden \u2014 append-only)',
1862
+ old.version, old.bundle_id using errcode = 'restrict_violation';
1863
+ end if;
1864
+ return old;
1865
+ end if;
1866
+
1867
+ -- UPDATE: a published row's content + identity is frozen; allow only the lifecycle status to advance.
1868
+ if (old.status = 'published') then
1869
+ if ( new.bundle_id is distinct from old.bundle_id
1870
+ or new.org_id is distinct from old.org_id
1871
+ or new.version is distinct from old.version
1872
+ or new.content is distinct from old.content
1873
+ or new.created_at is distinct from old.created_at ) then
1874
+ raise exception 'bundle_versions: published version v% of bundle % is immutable (content UPDATE forbidden \u2014 append-only; republish a new version instead)',
1875
+ old.version, old.bundle_id using errcode = 'restrict_violation';
1876
+ end if;
1877
+ end if;
1878
+ return new;
1879
+ end; $$;
1880
+
1881
+ drop trigger if exists bundle_versions_immutable on nightowls.bundle_versions;
1882
+ create trigger bundle_versions_immutable before update or delete on nightowls.bundle_versions
1883
+ for each row execute function nightowls.bundle_versions_enforce_immutable();
1556
1884
  `
1557
1885
  )
1558
1886
  };
1559
1887
 
1560
1888
  // src/migrations/index.ts
1561
- var MIGRATIONS = [M0001_CORE, M0002_MASTRA, M0003_FOLLOWUPS, M0004_MEMORY_VECTOR, M0005_THREAD_TEXT_IDS, M0006_SCRATCHPAD, M0007_SCRATCHPAD_ENTRIES, M0008_FLOOR, M0009_SCRATCHPAD_REALTIME, M0010_PRESENCE_REALTIME, M0011_BROADCAST_ALLOWLIST, M0012_THREAD_SCOPED_RESOURCE, M0013_RENAME_SCHEMA];
1889
+ var MIGRATIONS = [M0001_CORE, M0002_MASTRA, M0003_FOLLOWUPS, M0004_MEMORY_VECTOR, M0005_THREAD_TEXT_IDS, M0006_SCRATCHPAD, M0007_SCRATCHPAD_ENTRIES, M0008_FLOOR, M0009_SCRATCHPAD_REALTIME, M0010_PRESENCE_REALTIME, M0011_BROADCAST_ALLOWLIST, M0012_THREAD_SCOPED_RESOURCE, M0013_RENAME_SCHEMA, M0014_AGENT_VERSIONS_IMMUTABLE, M0015_DROP_SAAS_TABLES, M0016_BUNDLE_VERSIONS];
1562
1890
 
1563
1891
  // src/plugin.ts
1564
1892
  var nightOwlsPlugin = {
@@ -1710,8 +2038,13 @@ function createPostgresFloor(pool, opts = {}) {
1710
2038
  function createSupabaseStorage(opts) {
1711
2039
  const ctx = makeCtx(opts);
1712
2040
  let invalidationSub = null;
2041
+ const versionedAgents = makeVersionedRepo(ctx);
2042
+ const writableBundles = makeBundleWritableRepo(ctx);
1713
2043
  return {
1714
- agents: makeAgentRepo(ctx),
2044
+ agents: versionedAgents,
2045
+ agentsWritable: versionedAgents,
2046
+ bundles: writableBundles,
2047
+ bundlesWritable: writableBundles,
1715
2048
  runs: makeRunStore(ctx),
1716
2049
  events: makeEventStore(ctx),
1717
2050
  messages: makeMessageStore(ctx),
@@ -1732,10 +2065,11 @@ function createSupabaseStorage(opts) {
1732
2065
  // Mark a followup answered so `findSuspended` (which filters `answered_at is null`) stops returning
1733
2066
  // it — closes the replay window once a resume begins. Tenant-scoped + idempotent.
1734
2067
  markFollowupAnswered: async (followupId, tenantId) => {
1735
- await ctx.pool.query("update followups set answered_at = now() where id=$1 and org_id=$2 and answered_at is null", [
2068
+ const r = await ctx.pool.query("update followups set answered_at = now() where id=$1 and org_id=$2 and answered_at is null", [
1736
2069
  followupId,
1737
2070
  tenantId
1738
2071
  ]);
2072
+ return (r.rowCount ?? 0) > 0;
1739
2073
  },
1740
2074
  // R12: cross-process cache invalidation via Postgres LISTEN. The engine wires this to
1741
2075
  // `rowCache.invalidate`; a `publishAgentVersion` elsewhere NOTIFYs the key and every instance evicts
@@ -1784,6 +2118,9 @@ export {
1784
2118
  createPostgresFloor,
1785
2119
  createSupabaseStorage,
1786
2120
  listAgentVersions,
2121
+ makeBundleRepo,
2122
+ makeBundleWritableRepo,
2123
+ makeVersionedRepo,
1787
2124
  nightOwlsPlugin,
1788
2125
  publishAgentVersion,
1789
2126
  rollbackAgentVersion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nightowlsdev/storage-supabase",
3
- "version": "0.3.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -33,7 +33,7 @@
33
33
  "peerDependencies": {
34
34
  "@mastra/core": "^1.38.0",
35
35
  "@mastra/pg": "^1.12.0",
36
- "@nightowlsdev/core": "0.3.0"
36
+ "@nightowlsdev/core": "0.4.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@mastra/core": "^1.38.0",
@@ -47,7 +47,7 @@
47
47
  "vitest": "^3.2.0",
48
48
  "zod": "^4.0.0",
49
49
  "@nightowlsdev/tsconfig": "0.0.0",
50
- "@nightowlsdev/core": "0.3.0",
50
+ "@nightowlsdev/core": "0.4.0",
51
51
  "@nightowlsdev/eslint-config": "0.0.0"
52
52
  },
53
53
  "scripts": {