@nightowlsdev/storage-supabase 0.3.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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.cjs +1825 -0
- package/dist/index.d.cts +165 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +1790 -0
- package/package.json +59 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1825 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
MIGRATIONS: () => MIGRATIONS,
|
|
24
|
+
createMastraPgStore: () => createMastraPgStore,
|
|
25
|
+
createMastraVectorStore: () => createMastraVectorStore,
|
|
26
|
+
createPostgresFloor: () => createPostgresFloor,
|
|
27
|
+
createSupabaseStorage: () => createSupabaseStorage,
|
|
28
|
+
listAgentVersions: () => listAgentVersions,
|
|
29
|
+
nightOwlsPlugin: () => nightOwlsPlugin,
|
|
30
|
+
publishAgentVersion: () => publishAgentVersion,
|
|
31
|
+
rollbackAgentVersion: () => rollbackAgentVersion
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/client.ts
|
|
36
|
+
var import_pg = require("pg");
|
|
37
|
+
var import_supabase_js = require("@supabase/supabase-js");
|
|
38
|
+
function makeCtx(o) {
|
|
39
|
+
if (/:6543\b/.test(o.dbUrl)) throw new Error("dbUrl must be the Session/Direct port (5432), not the Transaction pooler (6543) \u2014 @mastra/pg/pg prepared statements break on 6543");
|
|
40
|
+
const isLocal = o.dbUrl.includes("127.0.0.1") || o.dbUrl.includes("localhost");
|
|
41
|
+
const ssl = isLocal ? void 0 : o.ssl ?? true;
|
|
42
|
+
const pool = new import_pg.Pool({ connectionString: o.dbUrl, max: o.max ?? 10, ssl, options: "-c search_path=nightowls,public" });
|
|
43
|
+
const sb = (0, import_supabase_js.createClient)(o.url, o.secretKey, { auth: { persistSession: false } });
|
|
44
|
+
return { pool, sb };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/events.ts
|
|
48
|
+
var import_core = require("@nightowlsdev/core");
|
|
49
|
+
|
|
50
|
+
// src/sql.ts
|
|
51
|
+
async function one(pool, text, params = []) {
|
|
52
|
+
const r = await pool.query(text, params);
|
|
53
|
+
return r.rows[0] ?? null;
|
|
54
|
+
}
|
|
55
|
+
async function many(pool, text, params = []) {
|
|
56
|
+
const r = await pool.query(text, params);
|
|
57
|
+
return r.rows;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/subscribe.ts
|
|
61
|
+
var fromWire = (w) => ({ ...w.payload, seq: w.seq, runId: w.run_id });
|
|
62
|
+
function makeSubscribe(ctx, accessToken) {
|
|
63
|
+
return function subscribe(runId) {
|
|
64
|
+
return { [Symbol.asyncIterator]() {
|
|
65
|
+
return iterator(ctx, runId, accessToken);
|
|
66
|
+
} };
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function iterator(ctx, runId, accessToken) {
|
|
70
|
+
const buffer = [];
|
|
71
|
+
const waiters = [];
|
|
72
|
+
let failure = null, closed = false, channel = null;
|
|
73
|
+
const push = (ev) => {
|
|
74
|
+
const w = waiters.shift();
|
|
75
|
+
if (w) w({ value: ev, done: false });
|
|
76
|
+
else buffer.push(ev);
|
|
77
|
+
};
|
|
78
|
+
const fail = (err) => {
|
|
79
|
+
failure = err;
|
|
80
|
+
while (waiters.length) waiters.shift()({ value: void 0, done: true });
|
|
81
|
+
};
|
|
82
|
+
const ready = (async () => {
|
|
83
|
+
await ctx.sb.realtime.setAuth(accessToken);
|
|
84
|
+
channel = ctx.sb.channel(`run:${runId}`, { config: { private: true } }).on("broadcast", { event: "*" }, ({ payload }) => push(fromWire(payload))).subscribe((status, err) => {
|
|
85
|
+
if (closed) return;
|
|
86
|
+
if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") fail(err ?? new Error(`realtime ${status} for run:${runId}`));
|
|
87
|
+
});
|
|
88
|
+
})();
|
|
89
|
+
const teardown = async () => {
|
|
90
|
+
closed = true;
|
|
91
|
+
if (channel) await ctx.sb.removeChannel(channel);
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
async next() {
|
|
95
|
+
await ready;
|
|
96
|
+
if (buffer.length) return { value: buffer.shift(), done: false };
|
|
97
|
+
if (failure) {
|
|
98
|
+
await teardown();
|
|
99
|
+
throw failure;
|
|
100
|
+
}
|
|
101
|
+
if (closed) return { value: void 0, done: true };
|
|
102
|
+
const r = await new Promise((res) => waiters.push(res));
|
|
103
|
+
if (r.done) {
|
|
104
|
+
if (failure) {
|
|
105
|
+
await teardown();
|
|
106
|
+
throw failure;
|
|
107
|
+
}
|
|
108
|
+
return { value: void 0, done: true };
|
|
109
|
+
}
|
|
110
|
+
return r;
|
|
111
|
+
},
|
|
112
|
+
async return() {
|
|
113
|
+
await teardown();
|
|
114
|
+
return { value: void 0, done: true };
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/events.ts
|
|
120
|
+
var orgCache = new import_core.RowCache({ max: 1e3, ttlMs: 6e4 });
|
|
121
|
+
async function orgOf(ctx, runId) {
|
|
122
|
+
return orgCache.get(runId, async () => {
|
|
123
|
+
const row = await one(ctx.pool, "select org_id from runs where id = $1", [runId]);
|
|
124
|
+
if (!row) throw new Error(`unknown run: ${runId}`);
|
|
125
|
+
return row.org_id;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function fromRow(r) {
|
|
129
|
+
return { ...r.payload, seq: Number(r.seq), runId: r.run_id };
|
|
130
|
+
}
|
|
131
|
+
function makeEventStore(ctx) {
|
|
132
|
+
return {
|
|
133
|
+
async append(e) {
|
|
134
|
+
const orgId = await orgOf(ctx, e.runId);
|
|
135
|
+
const row = await one(
|
|
136
|
+
ctx.pool,
|
|
137
|
+
`insert into events(org_id, run_id, type, agent_slug, from_agent, payload, schema_version, ts)
|
|
138
|
+
values($1,$2,$3,$4,$5,$6,$7,$8) returning seq`,
|
|
139
|
+
[orgId, e.runId, e.type, e.agentSlug, null, e, e.schemaVersion, e.ts]
|
|
140
|
+
);
|
|
141
|
+
return Number(row.seq);
|
|
142
|
+
},
|
|
143
|
+
// R11: org-scoped — a forged cross-org runId returns [] at the store (the service connection bypasses RLS,
|
|
144
|
+
// so tenancy is enforced in code, not just by the caller). This closes the events-hydrate cross-org read.
|
|
145
|
+
async list(tenantId, runId, sinceSeq) {
|
|
146
|
+
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]);
|
|
147
|
+
return rows.map(fromRow);
|
|
148
|
+
},
|
|
149
|
+
subscribe: makeSubscribe(ctx)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/runs.ts
|
|
154
|
+
var READ_STATUS = { running: "running", suspended: "suspended", waiting: "suspended", done: "done", success: "done", failed: "failed", bailed: "failed" };
|
|
155
|
+
function makeRunStore(ctx) {
|
|
156
|
+
return {
|
|
157
|
+
async create(run) {
|
|
158
|
+
await ctx.pool.query(
|
|
159
|
+
`insert into runs(id, org_id, user_id, agent_slug, thread_id, resource_id, runner, status)
|
|
160
|
+
values($1,$2,$3,$4,$5,$6,'nextjs','running') on conflict (id) do nothing`,
|
|
161
|
+
[run.runId, run.tenantId, run.userId, run.agentSlug, run.threadId, `${run.tenantId}:${run.userId}`]
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
async setStatus(runId, status, _patch) {
|
|
165
|
+
await ctx.pool.query("update runs set status=$2, updated_at=now() where id=$1", [runId, status]);
|
|
166
|
+
},
|
|
167
|
+
async saveSnapshot(runId, snapshot) {
|
|
168
|
+
await ctx.pool.query("update runs set snapshot=$2, updated_at=now() where id=$1", [runId, JSON.stringify(snapshot)]);
|
|
169
|
+
},
|
|
170
|
+
// R11: org-scoped — a cross-org runId returns null at the store (the service connection bypasses RLS, so
|
|
171
|
+
// tenancy is enforced in code). Resume also gates via findSuspended; this is defense-in-depth.
|
|
172
|
+
async loadSnapshot(tenantId, runId) {
|
|
173
|
+
const r = await one(ctx.pool, "select snapshot from runs where id=$1 and org_id=$2", [runId, tenantId]);
|
|
174
|
+
return r?.snapshot ?? null;
|
|
175
|
+
},
|
|
176
|
+
async findSuspended(tenantId, followupId) {
|
|
177
|
+
const r = await one(
|
|
178
|
+
ctx.pool,
|
|
179
|
+
"select run_id, tool_call_id from followups where id=$1 and org_id=$2 and answered_at is null",
|
|
180
|
+
[followupId, tenantId]
|
|
181
|
+
);
|
|
182
|
+
return r ? { runId: r.run_id, toolCallId: r.tool_call_id } : null;
|
|
183
|
+
},
|
|
184
|
+
async get(runId) {
|
|
185
|
+
const r = await one(
|
|
186
|
+
ctx.pool,
|
|
187
|
+
"select id, org_id, user_id, thread_id, agent_slug, status from runs where id=$1",
|
|
188
|
+
[runId]
|
|
189
|
+
);
|
|
190
|
+
return r ? { runId: r.id, tenantId: r.org_id, userId: r.user_id, threadId: r.thread_id, agentSlug: r.agent_slug, status: READ_STATUS[r.status] ?? "running" } : null;
|
|
191
|
+
},
|
|
192
|
+
async listActive(tenantId, container) {
|
|
193
|
+
const { rows } = await ctx.pool.query(
|
|
194
|
+
`select id, thread_id, agent_slug, status from runs
|
|
195
|
+
where org_id=$1 and (thread_id=$2 or left(thread_id, length($2)+1) = $2 || ':')
|
|
196
|
+
and (status='suspended' or (status='running' and updated_at > now() - interval '10 minutes'))`,
|
|
197
|
+
[tenantId, container]
|
|
198
|
+
);
|
|
199
|
+
return rows.map((r) => ({ runId: r.id, threadId: r.thread_id, agentSlug: r.agent_slug, status: READ_STATUS[r.status] ?? "running" }));
|
|
200
|
+
},
|
|
201
|
+
// R14: the user's distinct containers (newest-active first) for participation-based listThreads. One query
|
|
202
|
+
// over the runs the user owns; container = thread_id before any ':lane' (split_part is injection-safe).
|
|
203
|
+
async listUserContainers(tenantId, userId, limit = 50) {
|
|
204
|
+
const { rows } = await ctx.pool.query(
|
|
205
|
+
`select split_part(thread_id, ':', 1) as container, max(updated_at) as last
|
|
206
|
+
from runs where org_id=$1 and user_id=$2
|
|
207
|
+
group by 1 order by last desc limit $3`,
|
|
208
|
+
[tenantId, userId, limit]
|
|
209
|
+
);
|
|
210
|
+
return rows.map((r) => r.container);
|
|
211
|
+
},
|
|
212
|
+
// R14 security gate: may this user read this container? True iff it has NO runs (empty/new — nothing to
|
|
213
|
+
// protect) OR the user has a run in it (participant). One query; split_part is injection-safe.
|
|
214
|
+
async canAccessContainer(tenantId, userId, container) {
|
|
215
|
+
const { rows } = await ctx.pool.query(
|
|
216
|
+
`select (not exists(select 1 from runs where org_id=$1 and split_part(thread_id, ':', 1)=$3))
|
|
217
|
+
or exists(select 1 from runs where org_id=$1 and user_id=$2 and split_part(thread_id, ':', 1)=$3) as ok`,
|
|
218
|
+
[tenantId, userId, container]
|
|
219
|
+
);
|
|
220
|
+
return rows[0]?.ok ?? true;
|
|
221
|
+
},
|
|
222
|
+
// Durable runner (Plan 4): persist/read the vendor waitpoint token id on the followup row.
|
|
223
|
+
async attachWaitpoint(followupId, token, kind) {
|
|
224
|
+
await ctx.pool.query("update followups set waitpoint_token=$2, runner_kind=$3 where id=$1", [followupId, token, kind]);
|
|
225
|
+
},
|
|
226
|
+
async getWaitpoint(followupId) {
|
|
227
|
+
const r = await one(ctx.pool, "select waitpoint_token from followups where id=$1", [followupId]);
|
|
228
|
+
return r?.waitpoint_token ?? null;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/messages.ts
|
|
234
|
+
function makeMessageStore(ctx) {
|
|
235
|
+
return {
|
|
236
|
+
async append(m) {
|
|
237
|
+
const org = await one(ctx.pool, "select org_id from threads where id=$1", [m.threadId]);
|
|
238
|
+
if (!org) throw new Error(`unknown thread: ${m.threadId}`);
|
|
239
|
+
await ctx.pool.query(
|
|
240
|
+
"insert into messages(org_id, thread_id, role, text, ts) values($1,$2,$3,$4, to_timestamp($5/1000.0))",
|
|
241
|
+
[org.org_id, m.threadId, m.role, m.text, m.ts]
|
|
242
|
+
);
|
|
243
|
+
},
|
|
244
|
+
// R11: org-scoped (defense-in-depth). NOTE: no production caller — the runner's history endpoint uses
|
|
245
|
+
// engine.history (Mastra recall, scoped by resourceId=tenant:user). Kept consistent with the tenant contract.
|
|
246
|
+
async history(tenantId, threadId, limit = 50) {
|
|
247
|
+
const rows = await many(ctx.pool, "select thread_id, role, text, extract(epoch from ts)*1000 as ts from messages where org_id=$1 and thread_id=$2 order by ts asc limit $3", [tenantId, threadId, limit]);
|
|
248
|
+
return rows.map((r) => ({ threadId: r.thread_id, role: r.role, text: r.text, ts: Number(r.ts) }));
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/scratchpad.ts
|
|
254
|
+
var import_core2 = require("@nightowlsdev/core");
|
|
255
|
+
function makeScratchpadStore(ctx) {
|
|
256
|
+
return {
|
|
257
|
+
async write(tenantId, container, section, key, content, by) {
|
|
258
|
+
const col = section === "public" ? "public_entries" : "meta_entries";
|
|
259
|
+
const entry = JSON.stringify({ author: by.agentSlug, user: by.userId, requestedBy: by.requestedBy, content, ts: Date.now() });
|
|
260
|
+
await ctx.pool.query(
|
|
261
|
+
`insert into nightowls.scratchpad(org_id, container, ${col}, updated_by, updated_by_user, updated_at)
|
|
262
|
+
values($1,$2, jsonb_build_object($3::text, $4::jsonb), $5, $6, now())
|
|
263
|
+
on conflict (org_id, container) do update
|
|
264
|
+
set ${col} = (
|
|
265
|
+
select coalesce(jsonb_object_agg(key, value), '{}'::jsonb)
|
|
266
|
+
from (select key, value
|
|
267
|
+
from jsonb_each(jsonb_set(coalesce(nightowls.scratchpad.${col}, '{}'::jsonb), array[$3::text], $4::jsonb, true))
|
|
268
|
+
order by (value->>'ts')::bigint desc nulls last
|
|
269
|
+
limit $7) top
|
|
270
|
+
),
|
|
271
|
+
updated_by = $5, updated_by_user = $6, updated_at = now()`,
|
|
272
|
+
[tenantId, container, key, entry, by.agentSlug, by.userId, by.maxKeys ?? import_core2.SCRATCHPAD_MAX_KEYS]
|
|
273
|
+
// L3: per-write cap
|
|
274
|
+
);
|
|
275
|
+
},
|
|
276
|
+
async list(tenantId, container) {
|
|
277
|
+
const row = await one(
|
|
278
|
+
ctx.pool,
|
|
279
|
+
"select public_entries, meta_entries from nightowls.scratchpad where org_id=$1 and container=$2",
|
|
280
|
+
[tenantId, container]
|
|
281
|
+
);
|
|
282
|
+
if (!row) return [];
|
|
283
|
+
const out = [];
|
|
284
|
+
for (const section of ["public", "meta"]) {
|
|
285
|
+
const map = section === "public" ? row.public_entries : row.meta_entries;
|
|
286
|
+
for (const [key, e] of Object.entries(map ?? {})) {
|
|
287
|
+
out.push({ section, key, author: e.author, requestedBy: e.requestedBy ?? "unknown", content: e.content, updatedAt: Number(e.ts) });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/subscribe-invalidations.ts
|
|
296
|
+
var INVALIDATE_CHANNEL = "nightowls_agent_invalidate";
|
|
297
|
+
async function listenForInvalidations(ctx, onInvalidate) {
|
|
298
|
+
const client = await ctx.pool.connect();
|
|
299
|
+
const onNotification = (msg) => {
|
|
300
|
+
if (msg.channel === INVALIDATE_CHANNEL && msg.payload) onInvalidate(msg.payload);
|
|
301
|
+
};
|
|
302
|
+
client.on("notification", onNotification);
|
|
303
|
+
client.on("error", (e) => {
|
|
304
|
+
client.removeListener("notification", onNotification);
|
|
305
|
+
console.warn(
|
|
306
|
+
"[nightowls] agent-cache invalidation LISTEN connection errored \u2014 cross-process eviction paused until restart; the 30s cache TTL still bounds staleness:",
|
|
307
|
+
e instanceof Error ? e.message : e
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
await client.query(`LISTEN ${INVALIDATE_CHANNEL}`);
|
|
311
|
+
let released = false;
|
|
312
|
+
return {
|
|
313
|
+
unsubscribe: async () => {
|
|
314
|
+
if (released) return;
|
|
315
|
+
released = true;
|
|
316
|
+
client.removeListener("notification", onNotification);
|
|
317
|
+
try {
|
|
318
|
+
await client.query(`UNLISTEN ${INVALIDATE_CHANNEL}`);
|
|
319
|
+
} catch {
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
client.release();
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/agents.ts
|
|
330
|
+
function rowToVersion(r) {
|
|
331
|
+
return {
|
|
332
|
+
slug: r.slug,
|
|
333
|
+
version: r.version,
|
|
334
|
+
role: r.role,
|
|
335
|
+
personality: r.personality,
|
|
336
|
+
capabilities: r.capabilities ?? [],
|
|
337
|
+
skillNames: r.skill_names ?? [],
|
|
338
|
+
delegateSlugs: r.delegate_slugs ?? [],
|
|
339
|
+
modelId: r.model_id
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function makeAgentRepo(ctx) {
|
|
343
|
+
return {
|
|
344
|
+
async head(tenantId, slug) {
|
|
345
|
+
const r = await one(
|
|
346
|
+
ctx.pool,
|
|
347
|
+
`select a.slug, v.version, v.role, v.personality, v.capabilities, v.skill_names, v.delegate_slugs, v.model_id
|
|
348
|
+
from agents a join agent_versions v on v.id = a.current_version_id
|
|
349
|
+
where a.org_id=$1 and a.slug=$2`,
|
|
350
|
+
[tenantId, slug]
|
|
351
|
+
);
|
|
352
|
+
return r ? rowToVersion(r) : null;
|
|
353
|
+
},
|
|
354
|
+
async getVersion(tenantId, slug, version) {
|
|
355
|
+
const r = await one(
|
|
356
|
+
ctx.pool,
|
|
357
|
+
`select a.slug, v.version, v.role, v.personality, v.capabilities, v.skill_names, v.delegate_slugs, v.model_id
|
|
358
|
+
from agents a join agent_versions v on v.agent_id = a.id
|
|
359
|
+
where a.org_id=$1 and a.slug=$2 and v.version=$3`,
|
|
360
|
+
[tenantId, slug, version]
|
|
361
|
+
);
|
|
362
|
+
return r ? rowToVersion(r) : null;
|
|
363
|
+
},
|
|
364
|
+
async listSlugs(tenantId) {
|
|
365
|
+
const rows = await many(ctx.pool, "select slug from agents where org_id=$1", [tenantId]);
|
|
366
|
+
return rows.map((r) => r.slug);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async function commitVersion(client, agentId, tenantId, content, action, actor, auditAfter) {
|
|
371
|
+
const nextV = (await client.query("select coalesce(max(version),0)+1 v from agent_versions where agent_id=$1", [agentId])).rows[0].v;
|
|
372
|
+
const ver = (await client.query(
|
|
373
|
+
`insert into agent_versions(agent_id, org_id, version, role, personality, capabilities, skill_names, delegate_slugs, model_id, status)
|
|
374
|
+
values($1,$2,$3,$4,$5,$6,$7,$8,$9,'published') returning id`,
|
|
375
|
+
[agentId, tenantId, nextV, content.role, content.personality, JSON.stringify(content.capabilities), JSON.stringify(content.skillNames), JSON.stringify(content.delegateSlugs), content.modelId]
|
|
376
|
+
)).rows[0];
|
|
377
|
+
await client.query("update agents set current_version_id=$2 where id=$1", [agentId, ver.id]);
|
|
378
|
+
await client.query(
|
|
379
|
+
"insert into audit_log(org_id, actor, action, entity, entity_id, after) values($1,$2,$3,'agent',$4,$5)",
|
|
380
|
+
[tenantId, actor, action, agentId, JSON.stringify({ version: nextV, ...auditAfter })]
|
|
381
|
+
);
|
|
382
|
+
return nextV;
|
|
383
|
+
}
|
|
384
|
+
async function notifyInvalidate(client, tenantId, slug) {
|
|
385
|
+
try {
|
|
386
|
+
await client.query("select pg_notify($1, $2)", [INVALIDATE_CHANNEL, `${tenantId}:${slug}`]);
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async function publishAgentVersion(ctx, def) {
|
|
391
|
+
const client = await ctx.pool.connect();
|
|
392
|
+
try {
|
|
393
|
+
await client.query("begin");
|
|
394
|
+
const agent = (await client.query(
|
|
395
|
+
`insert into agents(org_id, slug, is_orchestrator) values($1,$2,$3)
|
|
396
|
+
on conflict (org_id, project_id, slug) do update set slug=excluded.slug returning id`,
|
|
397
|
+
[def.tenantId, def.slug, def.role === "orchestrator"]
|
|
398
|
+
)).rows[0];
|
|
399
|
+
await commitVersion(client, agent.id, def.tenantId, def, "publish", def.actor ?? "seed", { slug: def.slug });
|
|
400
|
+
await client.query("commit");
|
|
401
|
+
await notifyInvalidate(client, def.tenantId, def.slug);
|
|
402
|
+
} catch (e) {
|
|
403
|
+
await client.query("rollback");
|
|
404
|
+
throw e;
|
|
405
|
+
} finally {
|
|
406
|
+
client.release();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async function listAgentVersions(ctx, tenantId, slug) {
|
|
410
|
+
const rows = await many(
|
|
411
|
+
ctx.pool,
|
|
412
|
+
`select v.version, v.role, v.model_id, v.status, (v.id = a.current_version_id) as is_current
|
|
413
|
+
from agents a join agent_versions v on v.agent_id = a.id
|
|
414
|
+
where a.org_id=$1 and a.slug=$2 order by v.version`,
|
|
415
|
+
[tenantId, slug]
|
|
416
|
+
);
|
|
417
|
+
return rows.map((r) => ({ version: Number(r.version), role: r.role, modelId: r.model_id, status: r.status, isCurrent: r.is_current }));
|
|
418
|
+
}
|
|
419
|
+
async function rollbackAgentVersion(ctx, args) {
|
|
420
|
+
const client = await ctx.pool.connect();
|
|
421
|
+
try {
|
|
422
|
+
await client.query("begin");
|
|
423
|
+
const target = (await client.query(
|
|
424
|
+
`select a.id as agent_id, v.role, v.personality, v.capabilities, v.skill_names, v.delegate_slugs, v.model_id
|
|
425
|
+
from agents a join agent_versions v on v.agent_id = a.id
|
|
426
|
+
where a.org_id=$1 and a.slug=$2 and v.version=$3`,
|
|
427
|
+
[args.tenantId, args.slug, args.toVersion]
|
|
428
|
+
)).rows[0];
|
|
429
|
+
if (!target) throw new Error(`cannot roll back ${args.slug} to v${args.toVersion}: no such version for this tenant`);
|
|
430
|
+
const content = {
|
|
431
|
+
role: target.role,
|
|
432
|
+
personality: target.personality,
|
|
433
|
+
capabilities: target.capabilities ?? [],
|
|
434
|
+
skillNames: target.skill_names ?? [],
|
|
435
|
+
delegateSlugs: target.delegate_slugs ?? [],
|
|
436
|
+
modelId: target.model_id
|
|
437
|
+
};
|
|
438
|
+
const version = await commitVersion(client, target.agent_id, args.tenantId, content, "rollback", args.actor ?? "rollback", {
|
|
439
|
+
slug: args.slug,
|
|
440
|
+
restoredFrom: args.toVersion
|
|
441
|
+
});
|
|
442
|
+
await client.query("commit");
|
|
443
|
+
await notifyInvalidate(client, args.tenantId, args.slug);
|
|
444
|
+
return { version, restoredFrom: args.toVersion };
|
|
445
|
+
} catch (e) {
|
|
446
|
+
await client.query("rollback");
|
|
447
|
+
throw e;
|
|
448
|
+
} finally {
|
|
449
|
+
client.release();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/mastra-store.ts
|
|
454
|
+
var import_pg2 = require("@mastra/pg");
|
|
455
|
+
function createMastraPgStore(opts) {
|
|
456
|
+
if (/:6543\b/.test(opts.dbUrl)) throw new Error("use Session/Direct port (5432), not the Transaction pooler (6543)");
|
|
457
|
+
return new import_pg2.PostgresStore({ id: "nightowls-mastra", connectionString: opts.dbUrl, disableInit: opts.disableInit ?? true, schemaName: "nightowls" });
|
|
458
|
+
}
|
|
459
|
+
function createMastraVectorStore(opts) {
|
|
460
|
+
if (/:6543\b/.test(opts.dbUrl)) throw new Error("use Session/Direct port (5432), not the Transaction pooler (6543)");
|
|
461
|
+
return new import_pg2.PgVector({ id: "nightowls-vector", connectionString: opts.dbUrl, schemaName: "nightowls" });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/migrations/0001_core.ts
|
|
465
|
+
var M0001_CORE = {
|
|
466
|
+
version: "0001_core",
|
|
467
|
+
name: "corale core schema (orgs/agents/runs/events/... + RLS + realtime broadcast + grants)",
|
|
468
|
+
sql: (
|
|
469
|
+
/* sql */
|
|
470
|
+
`
|
|
471
|
+
create schema if not exists corale;
|
|
472
|
+
|
|
473
|
+
create table corale.orgs (
|
|
474
|
+
id uuid primary key default gen_random_uuid(), slug text not null, name text,
|
|
475
|
+
created_at timestamptz not null default now(), unique (slug)
|
|
476
|
+
);
|
|
477
|
+
create table corale.org_members (
|
|
478
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
479
|
+
user_id uuid not null, role text not null default 'member',
|
|
480
|
+
created_at timestamptz not null default now(), primary key (org_id, user_id)
|
|
481
|
+
);
|
|
482
|
+
create index org_members_user_idx on corale.org_members (user_id);
|
|
483
|
+
|
|
484
|
+
create table corale.agents (
|
|
485
|
+
id uuid primary key default gen_random_uuid(),
|
|
486
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
487
|
+
project_id uuid, slug text not null, current_version_id uuid,
|
|
488
|
+
is_orchestrator boolean not null default false,
|
|
489
|
+
-- NULLS NOT DISTINCT (PG15+) so the common project_id = NULL case still collides on
|
|
490
|
+
-- (org, slug); without it \`ON CONFLICT (org_id, project_id, slug)\` in publishAgentVersion
|
|
491
|
+
-- never fires and a re-publish inserts a duplicate agent row (head then breaks).
|
|
492
|
+
created_at timestamptz not null default now(), unique nulls not distinct (org_id, project_id, slug)
|
|
493
|
+
);
|
|
494
|
+
create table corale.agent_versions (
|
|
495
|
+
id uuid primary key default gen_random_uuid(),
|
|
496
|
+
agent_id uuid not null references corale.agents(id) on delete cascade,
|
|
497
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
498
|
+
version integer not null,
|
|
499
|
+
role text not null default 'specialist' check (role in ('orchestrator','specialist')),
|
|
500
|
+
personality text not null, capabilities jsonb not null default '[]',
|
|
501
|
+
skill_names jsonb not null default '[]', delegate_slugs jsonb not null default '[]',
|
|
502
|
+
model_id text not null, model_settings jsonb not null default '{}', guardrails_override jsonb,
|
|
503
|
+
status text not null default 'draft' check (status in ('draft','published','archived')),
|
|
504
|
+
created_by uuid, created_at timestamptz not null default now(), unique (agent_id, version)
|
|
505
|
+
);
|
|
506
|
+
alter table corale.agents add constraint agents_current_version_fk
|
|
507
|
+
foreign key (current_version_id) references corale.agent_versions(id);
|
|
508
|
+
|
|
509
|
+
create table corale.mcp_servers (
|
|
510
|
+
id uuid primary key default gen_random_uuid(),
|
|
511
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
512
|
+
slug text not null, transport text not null check (transport in ('stdio','http')),
|
|
513
|
+
config jsonb not null default '{}', auth_ref text,
|
|
514
|
+
auth_scope text not null default 'org' check (auth_scope in ('user','org')),
|
|
515
|
+
current_version_id uuid, created_at timestamptz not null default now(), unique (org_id, slug)
|
|
516
|
+
);
|
|
517
|
+
create table corale.api_connections (
|
|
518
|
+
id uuid primary key default gen_random_uuid(),
|
|
519
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
520
|
+
slug text not null, base_url text, auth_ref text,
|
|
521
|
+
auth_scope text not null default 'org' check (auth_scope in ('user','org')),
|
|
522
|
+
config jsonb not null default '{}', current_version_id uuid,
|
|
523
|
+
created_at timestamptz not null default now(), unique (org_id, slug)
|
|
524
|
+
);
|
|
525
|
+
create table corale.tools (
|
|
526
|
+
id uuid primary key default gen_random_uuid(),
|
|
527
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
528
|
+
slug text not null, kind text not null check (kind in ('predefined','user','mcp','rest')),
|
|
529
|
+
origin_mcp_server_id uuid references corale.mcp_servers(id),
|
|
530
|
+
origin_api_connection_id uuid references corale.api_connections(id),
|
|
531
|
+
current_version_id uuid, created_at timestamptz not null default now(), unique (org_id, slug)
|
|
532
|
+
);
|
|
533
|
+
create table corale.tool_versions (
|
|
534
|
+
id uuid primary key default gen_random_uuid(),
|
|
535
|
+
tool_id uuid not null references corale.tools(id) on delete cascade,
|
|
536
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
537
|
+
version integer not null, input_schema jsonb not null, output_schema jsonb,
|
|
538
|
+
config jsonb not null default '{}', needs_approval boolean not null default false,
|
|
539
|
+
created_at timestamptz not null default now(), unique (tool_id, version)
|
|
540
|
+
);
|
|
541
|
+
alter table corale.tools add constraint tools_current_version_fk
|
|
542
|
+
foreign key (current_version_id) references corale.tool_versions(id);
|
|
543
|
+
create table corale.skills (
|
|
544
|
+
id uuid primary key default gen_random_uuid(),
|
|
545
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
546
|
+
tool_id uuid not null references corale.tools(id), slug text not null, description text not null,
|
|
547
|
+
current_version_id uuid, created_at timestamptz not null default now(), unique (org_id, slug)
|
|
548
|
+
);
|
|
549
|
+
create table corale.skill_versions (
|
|
550
|
+
id uuid primary key default gen_random_uuid(),
|
|
551
|
+
skill_id uuid not null references corale.skills(id) on delete cascade,
|
|
552
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
553
|
+
version integer not null, description text not null, config jsonb not null default '{}',
|
|
554
|
+
created_at timestamptz not null default now(), unique (skill_id, version)
|
|
555
|
+
);
|
|
556
|
+
alter table corale.skills add constraint skills_current_version_fk
|
|
557
|
+
foreign key (current_version_id) references corale.skill_versions(id);
|
|
558
|
+
create table corale.agent_skills (
|
|
559
|
+
agent_version_id uuid not null references corale.agent_versions(id) on delete cascade,
|
|
560
|
+
skill_version_id uuid not null references corale.skill_versions(id),
|
|
561
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
562
|
+
enabled boolean not null default true, primary key (agent_version_id, skill_version_id)
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
create table corale.swarms (
|
|
566
|
+
id uuid primary key default gen_random_uuid(),
|
|
567
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
568
|
+
project_id uuid, slug text not null, current_version_id uuid,
|
|
569
|
+
-- NULLS NOT DISTINCT (PG15+): same rationale as corale.agents \u2014 keep (org, NULL project, slug) unique.
|
|
570
|
+
created_at timestamptz not null default now(), unique nulls not distinct (org_id, project_id, slug)
|
|
571
|
+
);
|
|
572
|
+
create table corale.swarm_members (
|
|
573
|
+
swarm_id uuid not null references corale.swarms(id) on delete cascade,
|
|
574
|
+
agent_id uuid not null references corale.agents(id),
|
|
575
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
576
|
+
role text not null default 'specialist' check (role in ('orchestrator','specialist')),
|
|
577
|
+
can_delegate_to uuid[] default '{}', primary key (swarm_id, agent_id)
|
|
578
|
+
);
|
|
579
|
+
create unique index one_orchestrator_per_swarm on corale.swarm_members (swarm_id) where role = 'orchestrator';
|
|
580
|
+
|
|
581
|
+
create table corale.threads (
|
|
582
|
+
id uuid primary key default gen_random_uuid(),
|
|
583
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
584
|
+
project_id uuid, user_id text not null, title text, created_at timestamptz not null default now()
|
|
585
|
+
);
|
|
586
|
+
create table corale.runs (
|
|
587
|
+
id uuid primary key default gen_random_uuid(),
|
|
588
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
589
|
+
project_id uuid, user_id text not null, swarm_id uuid references corale.swarms(id),
|
|
590
|
+
agent_slug text not null, thread_id uuid not null references corale.threads(id),
|
|
591
|
+
mastra_run_id text, resource_id text not null,
|
|
592
|
+
runner text not null check (runner in ('nextjs','background')),
|
|
593
|
+
status text not null default 'running'
|
|
594
|
+
check (status in ('running','suspended','waiting','done','success','failed','bailed')),
|
|
595
|
+
agent_version_pins jsonb not null default '{}', usage jsonb, cost_usd numeric, trace_id text,
|
|
596
|
+
model_id text, snapshot jsonb, error jsonb,
|
|
597
|
+
created_at timestamptz not null default now(), updated_at timestamptz not null default now()
|
|
598
|
+
);
|
|
599
|
+
create index runs_thread_idx on corale.runs (thread_id);
|
|
600
|
+
create index runs_status_idx on corale.runs (org_id, status);
|
|
601
|
+
create index runs_user_idx on corale.runs (user_id);
|
|
602
|
+
create table corale.run_steps (
|
|
603
|
+
id uuid primary key default gen_random_uuid(),
|
|
604
|
+
run_id uuid not null references corale.runs(id) on delete cascade,
|
|
605
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
606
|
+
agent_slug text not null, step_index integer not null,
|
|
607
|
+
type text not null check (type in ('delegation','tool','suspend','resume','finish')),
|
|
608
|
+
status text, tool_call_id text, followup_id text, suspend_payload jsonb, resume_payload jsonb,
|
|
609
|
+
finish_reason text, created_at timestamptz not null default now()
|
|
610
|
+
);
|
|
611
|
+
create index run_steps_run_idx on corale.run_steps (run_id, step_index);
|
|
612
|
+
create index run_steps_suspended_idx on corale.run_steps (org_id, followup_id) where status = 'suspended';
|
|
613
|
+
create table corale.run_usage (
|
|
614
|
+
id uuid primary key default gen_random_uuid(),
|
|
615
|
+
run_id uuid not null references corale.runs(id) on delete cascade,
|
|
616
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
617
|
+
step_index integer, model_id text, input_tokens integer, output_tokens integer, cost_usd numeric,
|
|
618
|
+
created_at timestamptz not null default now()
|
|
619
|
+
);
|
|
620
|
+
create index run_usage_run_idx on corale.run_usage (run_id);
|
|
621
|
+
create table corale.followups (
|
|
622
|
+
id text primary key, run_id uuid not null references corale.runs(id) on delete cascade,
|
|
623
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
624
|
+
tool_call_id text not null, to_target text, answered_at timestamptz,
|
|
625
|
+
created_at timestamptz not null default now()
|
|
626
|
+
);
|
|
627
|
+
create index followups_run_idx on corale.followups (run_id);
|
|
628
|
+
create table corale.messages (
|
|
629
|
+
id uuid primary key default gen_random_uuid(),
|
|
630
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
631
|
+
thread_id uuid not null references corale.threads(id) on delete cascade,
|
|
632
|
+
run_id uuid references corale.runs(id) on delete cascade,
|
|
633
|
+
role text not null check (role in ('user','assistant')), text text not null,
|
|
634
|
+
ts timestamptz not null default now()
|
|
635
|
+
);
|
|
636
|
+
create index messages_thread_idx on corale.messages (thread_id, ts);
|
|
637
|
+
create table corale.events (
|
|
638
|
+
id uuid primary key default gen_random_uuid(),
|
|
639
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
640
|
+
run_id uuid not null references corale.runs(id) on delete cascade,
|
|
641
|
+
seq bigint generated always as identity, type text not null, agent_slug text,
|
|
642
|
+
from_agent text, to_agent text, payload jsonb not null,
|
|
643
|
+
schema_version smallint not null default 1, ts bigint, created_at timestamptz not null default now()
|
|
644
|
+
);
|
|
645
|
+
create index events_run_seq on corale.events (run_id, seq);
|
|
646
|
+
create table corale.tenant_policies (
|
|
647
|
+
org_id uuid primary key references corale.orgs(id) on delete cascade,
|
|
648
|
+
allowed_models text[], max_cost_per_run_usd numeric, monthly_budget_usd numeric,
|
|
649
|
+
default_max_steps int, can_self_evolve boolean not null default false
|
|
650
|
+
);
|
|
651
|
+
create table corale.eval_runs (
|
|
652
|
+
id uuid primary key default gen_random_uuid(),
|
|
653
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
654
|
+
suite text not null, agent_slug text not null, score jsonb, created_at timestamptz not null default now()
|
|
655
|
+
);
|
|
656
|
+
create table corale.audit_log (
|
|
657
|
+
id uuid primary key default gen_random_uuid(),
|
|
658
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
659
|
+
actor text, action text not null, entity text not null, entity_id uuid,
|
|
660
|
+
before jsonb, after jsonb, created_at timestamptz not null default now()
|
|
661
|
+
);
|
|
662
|
+
create index audit_log_entity_idx on corale.audit_log (org_id, entity, entity_id);
|
|
663
|
+
|
|
664
|
+
-- Redacted Broadcast-from-Database (CONTRACTS \xA74): forward stored SwarmEvent + authoritative seq.
|
|
665
|
+
-- NOTE (m2): this original forwards new.payload verbatim \u2014 secret/PII scrubbing is the UPSTREAM emitter's
|
|
666
|
+
-- job (the engine writes already-redacted SwarmEvents). SUPERSEDED by 0011_broadcast_allowlist, which
|
|
667
|
+
-- create-or-replaces this function to forward only an explicit SwarmEvent field allowlist (R2,
|
|
668
|
+
-- defense-in-depth). This definition is kept as-is for migration-history fidelity; 0011 is the live one.
|
|
669
|
+
create or replace function corale.broadcast_event()
|
|
670
|
+
returns trigger language plpgsql security definer set search_path = '' as $$
|
|
671
|
+
begin
|
|
672
|
+
perform realtime.send(
|
|
673
|
+
jsonb_build_object('type', new.type, 'seq', new.seq, 'run_id', new.run_id,
|
|
674
|
+
'from_agent', new.from_agent, 'to_agent', new.to_agent, 'payload', new.payload),
|
|
675
|
+
new.type, 'run:' || new.run_id::text, true);
|
|
676
|
+
return new;
|
|
677
|
+
end; $$;
|
|
678
|
+
create trigger trg_broadcast_event after insert on corale.events
|
|
679
|
+
for each row execute function corale.broadcast_event();
|
|
680
|
+
|
|
681
|
+
-- RLS (defense-in-depth). corale.is_org_member drives reads; service_role bypasses everything.
|
|
682
|
+
create or replace function corale.is_org_member(p_org uuid)
|
|
683
|
+
returns boolean language sql stable set search_path = '' as $$
|
|
684
|
+
select exists (select 1 from corale.org_members m
|
|
685
|
+
where m.org_id = p_org and m.user_id = (select auth.uid()));
|
|
686
|
+
$$;
|
|
687
|
+
do $$ declare t text; begin
|
|
688
|
+
foreach t in array array['orgs','org_members','agents','agent_versions',
|
|
689
|
+
'skills','skill_versions','tools','tool_versions','agent_skills',
|
|
690
|
+
'swarms','swarm_members','threads','runs','run_steps',
|
|
691
|
+
'run_usage','followups','messages','events','mcp_servers',
|
|
692
|
+
'api_connections','tenant_policies','eval_runs','audit_log']
|
|
693
|
+
loop execute format('alter table corale.%I enable row level security;', t); end loop; end $$;
|
|
694
|
+
create policy org_read_orgs on corale.orgs for select to authenticated using ((select corale.is_org_member(id)));
|
|
695
|
+
create policy self_read_membership on corale.org_members for select to authenticated using (user_id = (select auth.uid()));
|
|
696
|
+
do $$ declare t text; begin
|
|
697
|
+
foreach t in array array['agents','agent_versions','skills','skill_versions',
|
|
698
|
+
'tools','tool_versions','agent_skills','swarms','swarm_members',
|
|
699
|
+
'threads','runs','run_steps','run_usage','followups','messages',
|
|
700
|
+
'events','tenant_policies','eval_runs','audit_log']
|
|
701
|
+
loop execute format('create policy org_read_%1$s on corale.%1$I for select to authenticated using ((select corale.is_org_member(org_id)));', t);
|
|
702
|
+
execute format('create index %1$s_org_idx on corale.%1$I (org_id);', t); end loop; end $$;
|
|
703
|
+
-- corale.mcp_servers / corale.api_connections: no client read policy (secret refs). Server adapter only.
|
|
704
|
+
create index mcp_servers_org_idx on corale.mcp_servers (org_id);
|
|
705
|
+
create index api_connections_org_idx on corale.api_connections (org_id);
|
|
706
|
+
|
|
707
|
+
-- Private Realtime read gate: only members of the run's org receive run:<id> broadcasts.
|
|
708
|
+
create policy members_receive_run_events on realtime.messages for select to authenticated using (
|
|
709
|
+
realtime.messages.extension = 'broadcast' and (select realtime.topic()) like 'run:%'
|
|
710
|
+
and exists (select 1 from corale.runs r
|
|
711
|
+
where ('run:' || r.id::text) = (select realtime.topic()) and (select corale.is_org_member(r.org_id))));
|
|
712
|
+
|
|
713
|
+
-- GRANTs (CRITICAL): a fresh schema is granted to nobody. Without these the JWT-scoped Realtime gate
|
|
714
|
+
-- (is_org_member reading corale.org_members during a private subscribe) fails permission-denied.
|
|
715
|
+
-- RLS still gates ROWS (default-deny on policy-less tables); these only open the schema + read path.
|
|
716
|
+
grant usage on schema corale to anon, authenticated;
|
|
717
|
+
grant select on all tables in schema corale to authenticated;
|
|
718
|
+
alter default privileges in schema corale grant select on tables to authenticated;
|
|
719
|
+
`
|
|
720
|
+
)
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// src/migrations/0002_mastra.ts
|
|
724
|
+
var M0002_MASTRA = {
|
|
725
|
+
version: "0002_mastra",
|
|
726
|
+
name: "mastra storage tables (corale schema, via @mastra/pg exportSchemas)",
|
|
727
|
+
sql: (
|
|
728
|
+
/* sql */
|
|
729
|
+
`
|
|
730
|
+
CREATE SCHEMA IF NOT EXISTS "corale";
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_threads" (
|
|
734
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
735
|
+
"resourceId" TEXT NOT NULL,
|
|
736
|
+
"title" TEXT NOT NULL,
|
|
737
|
+
"metadata" JSONB ,
|
|
738
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
739
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
740
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
741
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_messages" (
|
|
748
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
749
|
+
"thread_id" TEXT NOT NULL,
|
|
750
|
+
"content" TEXT NOT NULL,
|
|
751
|
+
"role" TEXT NOT NULL,
|
|
752
|
+
"type" TEXT NOT NULL,
|
|
753
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
754
|
+
"resourceId" TEXT ,
|
|
755
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_resources" (
|
|
762
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
763
|
+
"workingMemory" TEXT ,
|
|
764
|
+
"metadata" JSONB ,
|
|
765
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
766
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
767
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
768
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_observational_memory" (
|
|
775
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
776
|
+
"lookupKey" TEXT NOT NULL,
|
|
777
|
+
"scope" TEXT NOT NULL,
|
|
778
|
+
"resourceId" TEXT ,
|
|
779
|
+
"threadId" TEXT ,
|
|
780
|
+
"activeObservations" TEXT NOT NULL,
|
|
781
|
+
"activeObservationsPendingUpdate" TEXT ,
|
|
782
|
+
"originType" TEXT NOT NULL,
|
|
783
|
+
"config" TEXT NOT NULL,
|
|
784
|
+
"generationCount" INTEGER NOT NULL,
|
|
785
|
+
"lastObservedAt" TIMESTAMP ,
|
|
786
|
+
"lastReflectionAt" TIMESTAMP ,
|
|
787
|
+
"pendingMessageTokens" INTEGER NOT NULL,
|
|
788
|
+
"totalTokensObserved" INTEGER NOT NULL,
|
|
789
|
+
"observationTokenCount" INTEGER NOT NULL,
|
|
790
|
+
"isObserving" BOOLEAN NOT NULL,
|
|
791
|
+
"isReflecting" BOOLEAN NOT NULL,
|
|
792
|
+
"observedMessageIds" JSONB ,
|
|
793
|
+
"observedTimezone" TEXT ,
|
|
794
|
+
"bufferedObservations" TEXT ,
|
|
795
|
+
"bufferedObservationTokens" INTEGER ,
|
|
796
|
+
"bufferedMessageIds" JSONB ,
|
|
797
|
+
"bufferedReflection" TEXT ,
|
|
798
|
+
"bufferedReflectionTokens" INTEGER ,
|
|
799
|
+
"bufferedReflectionInputTokens" INTEGER ,
|
|
800
|
+
"reflectedObservationLineCount" INTEGER ,
|
|
801
|
+
"bufferedObservationChunks" JSONB ,
|
|
802
|
+
"isBufferingObservation" BOOLEAN NOT NULL,
|
|
803
|
+
"isBufferingReflection" BOOLEAN NOT NULL,
|
|
804
|
+
"lastBufferedAtTokens" INTEGER NOT NULL,
|
|
805
|
+
"lastBufferedAtTime" TIMESTAMP ,
|
|
806
|
+
"metadata" JSONB ,
|
|
807
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
808
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
809
|
+
"lastObservedAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
810
|
+
"lastReflectionAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
811
|
+
"lastBufferedAtTimeZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
812
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
813
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
CREATE INDEX IF NOT EXISTS "corale_idx_om_lookup_key" ON "corale"."mastra_observational_memory" ("lookupKey");
|
|
819
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_threads_resourceid_createdat_idx" ON "corale"."mastra_threads" ("resourceId", "createdAt" DESC);
|
|
820
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_messages_thread_id_createdat_idx" ON "corale"."mastra_messages" ("thread_id", "createdAt" DESC);
|
|
821
|
+
|
|
822
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_ai_spans" (
|
|
823
|
+
"traceId" TEXT NOT NULL,
|
|
824
|
+
"spanId" TEXT NOT NULL,
|
|
825
|
+
"name" TEXT NOT NULL,
|
|
826
|
+
"spanType" TEXT NOT NULL,
|
|
827
|
+
"isEvent" BOOLEAN NOT NULL,
|
|
828
|
+
"startedAt" TIMESTAMP NOT NULL,
|
|
829
|
+
"parentSpanId" TEXT ,
|
|
830
|
+
"entityType" TEXT ,
|
|
831
|
+
"entityId" TEXT ,
|
|
832
|
+
"entityName" TEXT ,
|
|
833
|
+
"parentEntityType" TEXT ,
|
|
834
|
+
"parentEntityId" TEXT ,
|
|
835
|
+
"parentEntityName" TEXT ,
|
|
836
|
+
"rootEntityType" TEXT ,
|
|
837
|
+
"rootEntityId" TEXT ,
|
|
838
|
+
"rootEntityName" TEXT ,
|
|
839
|
+
"userId" TEXT ,
|
|
840
|
+
"organizationId" TEXT ,
|
|
841
|
+
"resourceId" TEXT ,
|
|
842
|
+
"runId" TEXT ,
|
|
843
|
+
"sessionId" TEXT ,
|
|
844
|
+
"threadId" TEXT ,
|
|
845
|
+
"requestId" TEXT ,
|
|
846
|
+
"environment" TEXT ,
|
|
847
|
+
"serviceName" TEXT ,
|
|
848
|
+
"scope" JSONB ,
|
|
849
|
+
"entityVersionId" TEXT ,
|
|
850
|
+
"parentEntityVersionId" TEXT ,
|
|
851
|
+
"rootEntityVersionId" TEXT ,
|
|
852
|
+
"experimentId" TEXT ,
|
|
853
|
+
"source" TEXT ,
|
|
854
|
+
"metadata" JSONB ,
|
|
855
|
+
"tags" JSONB ,
|
|
856
|
+
"attributes" JSONB ,
|
|
857
|
+
"links" JSONB ,
|
|
858
|
+
"input" JSONB ,
|
|
859
|
+
"output" JSONB ,
|
|
860
|
+
"error" JSONB ,
|
|
861
|
+
"endedAt" TIMESTAMP ,
|
|
862
|
+
"requestContext" JSONB ,
|
|
863
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
864
|
+
"updatedAt" TIMESTAMP ,
|
|
865
|
+
"startedAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
866
|
+
"endedAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
867
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
868
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
DO $$ BEGIN
|
|
873
|
+
IF NOT EXISTS (
|
|
874
|
+
SELECT 1 FROM pg_constraint WHERE conname = lower('corale_mastra_ai_spans_traceid_spanid_pk') AND connamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'corale')
|
|
875
|
+
) THEN
|
|
876
|
+
ALTER TABLE "corale"."mastra_ai_spans"
|
|
877
|
+
ADD CONSTRAINT corale_mastra_ai_spans_traceid_spanid_pk
|
|
878
|
+
PRIMARY KEY ("traceId", "spanId");
|
|
879
|
+
END IF;
|
|
880
|
+
END $$;
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
CREATE OR REPLACE FUNCTION "corale".trigger_set_timestamps()
|
|
884
|
+
RETURNS TRIGGER AS $$
|
|
885
|
+
BEGIN
|
|
886
|
+
IF TG_OP = 'INSERT' THEN
|
|
887
|
+
NEW."createdAt" = NOW();
|
|
888
|
+
NEW."updatedAt" = NOW();
|
|
889
|
+
NEW."createdAtZ" = NOW();
|
|
890
|
+
NEW."updatedAtZ" = NOW();
|
|
891
|
+
ELSIF TG_OP = 'UPDATE' THEN
|
|
892
|
+
NEW."updatedAt" = NOW();
|
|
893
|
+
NEW."updatedAtZ" = NOW();
|
|
894
|
+
NEW."createdAt" = OLD."createdAt";
|
|
895
|
+
NEW."createdAtZ" = OLD."createdAtZ";
|
|
896
|
+
END IF;
|
|
897
|
+
RETURN NEW;
|
|
898
|
+
END;
|
|
899
|
+
$$ LANGUAGE plpgsql;
|
|
900
|
+
|
|
901
|
+
DROP TRIGGER IF EXISTS "mastra_ai_spans_timestamps" ON "corale"."mastra_ai_spans";
|
|
902
|
+
|
|
903
|
+
CREATE TRIGGER "mastra_ai_spans_timestamps"
|
|
904
|
+
BEFORE INSERT OR UPDATE ON "corale"."mastra_ai_spans"
|
|
905
|
+
FOR EACH ROW
|
|
906
|
+
EXECUTE FUNCTION "corale".trigger_set_timestamps();
|
|
907
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_ai_spans_traceid_startedat_idx" ON "corale"."mastra_ai_spans" ("traceId", "startedAt" DESC);
|
|
908
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_ai_spans_parentspanid_startedat_idx" ON "corale"."mastra_ai_spans" ("parentSpanId", "startedAt" DESC);
|
|
909
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_ai_spans_name_idx" ON "corale"."mastra_ai_spans" ("name");
|
|
910
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_ai_spans_spantype_startedat_idx" ON "corale"."mastra_ai_spans" ("spanType", "startedAt" DESC);
|
|
911
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_ai_spans_root_spans_idx" ON "corale"."mastra_ai_spans" ("startedAt" DESC) WHERE "parentSpanId" IS NULL;
|
|
912
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_ai_spans_entitytype_entityid_idx" ON "corale"."mastra_ai_spans" ("entityType", "entityId");
|
|
913
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_ai_spans_entitytype_entityname_idx" ON "corale"."mastra_ai_spans" ("entityType", "entityName");
|
|
914
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_ai_spans_orgid_userid_idx" ON "corale"."mastra_ai_spans" ("organizationId", "userId");
|
|
915
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_ai_spans_metadata_gin_idx" ON "corale"."mastra_ai_spans" USING gin ("metadata");
|
|
916
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_ai_spans_tags_gin_idx" ON "corale"."mastra_ai_spans" USING gin ("tags");
|
|
917
|
+
|
|
918
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_scorers" (
|
|
919
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
920
|
+
"scorerId" TEXT NOT NULL,
|
|
921
|
+
"traceId" TEXT ,
|
|
922
|
+
"spanId" TEXT ,
|
|
923
|
+
"runId" TEXT NOT NULL,
|
|
924
|
+
"scorer" JSONB NOT NULL,
|
|
925
|
+
"preprocessStepResult" JSONB ,
|
|
926
|
+
"extractStepResult" JSONB ,
|
|
927
|
+
"analyzeStepResult" JSONB ,
|
|
928
|
+
"score" FLOAT NOT NULL,
|
|
929
|
+
"reason" TEXT ,
|
|
930
|
+
"metadata" JSONB ,
|
|
931
|
+
"preprocessPrompt" TEXT ,
|
|
932
|
+
"extractPrompt" TEXT ,
|
|
933
|
+
"generateScorePrompt" TEXT ,
|
|
934
|
+
"generateReasonPrompt" TEXT ,
|
|
935
|
+
"analyzePrompt" TEXT ,
|
|
936
|
+
"reasonPrompt" TEXT ,
|
|
937
|
+
"input" JSONB NOT NULL,
|
|
938
|
+
"output" JSONB NOT NULL,
|
|
939
|
+
"additionalContext" JSONB ,
|
|
940
|
+
"requestContext" JSONB ,
|
|
941
|
+
"entityType" TEXT ,
|
|
942
|
+
"entity" JSONB ,
|
|
943
|
+
"entityId" TEXT ,
|
|
944
|
+
"source" TEXT NOT NULL,
|
|
945
|
+
"resourceId" TEXT ,
|
|
946
|
+
"threadId" TEXT ,
|
|
947
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
948
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
949
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
950
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_scores_trace_id_span_id_created_at_idx" ON "corale"."mastra_scorers" ("traceId", "spanId", "createdAt" DESC);
|
|
956
|
+
|
|
957
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_scorer_definitions" (
|
|
958
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
959
|
+
"status" TEXT NOT NULL,
|
|
960
|
+
"activeVersionId" TEXT ,
|
|
961
|
+
"authorId" TEXT ,
|
|
962
|
+
"metadata" JSONB ,
|
|
963
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
964
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
965
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
966
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_scorer_definition_versions" (
|
|
973
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
974
|
+
"scorerDefinitionId" TEXT NOT NULL,
|
|
975
|
+
"versionNumber" INTEGER NOT NULL,
|
|
976
|
+
"name" TEXT NOT NULL,
|
|
977
|
+
"description" TEXT ,
|
|
978
|
+
"type" TEXT NOT NULL,
|
|
979
|
+
"model" JSONB ,
|
|
980
|
+
"instructions" TEXT ,
|
|
981
|
+
"scoreRange" JSONB ,
|
|
982
|
+
"presetConfig" JSONB ,
|
|
983
|
+
"defaultSampling" JSONB ,
|
|
984
|
+
"changedFields" JSONB ,
|
|
985
|
+
"changeMessage" TEXT ,
|
|
986
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
987
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "corale_idx_scorer_definition_versions_def_version" ON "corale"."mastra_scorer_definition_versions" ("scorerDefinitionId", "versionNumber");
|
|
993
|
+
|
|
994
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_prompt_blocks" (
|
|
995
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
996
|
+
"status" TEXT NOT NULL,
|
|
997
|
+
"activeVersionId" TEXT ,
|
|
998
|
+
"authorId" TEXT ,
|
|
999
|
+
"metadata" JSONB ,
|
|
1000
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1001
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
1002
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1003
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_prompt_block_versions" (
|
|
1010
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
1011
|
+
"blockId" TEXT NOT NULL,
|
|
1012
|
+
"versionNumber" INTEGER NOT NULL,
|
|
1013
|
+
"name" TEXT NOT NULL,
|
|
1014
|
+
"description" TEXT ,
|
|
1015
|
+
"content" TEXT NOT NULL,
|
|
1016
|
+
"rules" JSONB ,
|
|
1017
|
+
"requestContextSchema" JSONB ,
|
|
1018
|
+
"changedFields" JSONB ,
|
|
1019
|
+
"changeMessage" TEXT ,
|
|
1020
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1021
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
1022
|
+
);
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "corale_idx_prompt_block_versions_block_version" ON "corale"."mastra_prompt_block_versions" ("blockId", "versionNumber");
|
|
1027
|
+
|
|
1028
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_agents" (
|
|
1029
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
1030
|
+
"status" TEXT NOT NULL,
|
|
1031
|
+
"activeVersionId" TEXT ,
|
|
1032
|
+
"authorId" TEXT ,
|
|
1033
|
+
"visibility" TEXT ,
|
|
1034
|
+
"metadata" JSONB ,
|
|
1035
|
+
"favoriteCount" INTEGER ,
|
|
1036
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1037
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
1038
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1039
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_agent_versions" (
|
|
1046
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
1047
|
+
"agentId" TEXT NOT NULL,
|
|
1048
|
+
"versionNumber" INTEGER NOT NULL,
|
|
1049
|
+
"name" TEXT NOT NULL,
|
|
1050
|
+
"description" TEXT ,
|
|
1051
|
+
"instructions" TEXT NOT NULL,
|
|
1052
|
+
"model" JSONB NOT NULL,
|
|
1053
|
+
"tools" JSONB ,
|
|
1054
|
+
"defaultOptions" JSONB ,
|
|
1055
|
+
"workflows" JSONB ,
|
|
1056
|
+
"agents" JSONB ,
|
|
1057
|
+
"integrationTools" JSONB ,
|
|
1058
|
+
"toolProviders" JSONB ,
|
|
1059
|
+
"inputProcessors" JSONB ,
|
|
1060
|
+
"outputProcessors" JSONB ,
|
|
1061
|
+
"memory" JSONB ,
|
|
1062
|
+
"scorers" JSONB ,
|
|
1063
|
+
"mcpClients" JSONB ,
|
|
1064
|
+
"requestContextSchema" JSONB ,
|
|
1065
|
+
"workspace" JSONB ,
|
|
1066
|
+
"skills" JSONB ,
|
|
1067
|
+
"skillsFormat" TEXT ,
|
|
1068
|
+
"browser" JSONB ,
|
|
1069
|
+
"changedFields" JSONB ,
|
|
1070
|
+
"changeMessage" TEXT ,
|
|
1071
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1072
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_workflow_snapshot" (
|
|
1079
|
+
"workflow_name" TEXT NOT NULL,
|
|
1080
|
+
"run_id" TEXT NOT NULL,
|
|
1081
|
+
"resourceId" TEXT ,
|
|
1082
|
+
"snapshot" JSONB NOT NULL,
|
|
1083
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1084
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
1085
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1086
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
DO $$ BEGIN
|
|
1090
|
+
IF NOT EXISTS (
|
|
1091
|
+
SELECT 1 FROM pg_constraint WHERE conname = lower('corale_mastra_workflow_snapshot_workflow_name_run_id_key') AND connamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'corale')
|
|
1092
|
+
) AND NOT EXISTS (
|
|
1093
|
+
SELECT 1 FROM pg_indexes WHERE indexname = lower('corale_mastra_workflow_snapshot_workflow_name_run_id_key') AND schemaname = 'corale'
|
|
1094
|
+
) THEN
|
|
1095
|
+
ALTER TABLE "corale"."mastra_workflow_snapshot"
|
|
1096
|
+
ADD CONSTRAINT corale_mastra_workflow_snapshot_workflow_name_run_id_key
|
|
1097
|
+
UNIQUE (workflow_name, run_id);
|
|
1098
|
+
END IF;
|
|
1099
|
+
IF EXISTS (
|
|
1100
|
+
SELECT 1 FROM pg_index i
|
|
1101
|
+
JOIN pg_class c ON i.indexrelid = c.oid
|
|
1102
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
1103
|
+
WHERE c.relname = lower('corale_mastra_workflow_snapshot_workflow_name_run_id_key')
|
|
1104
|
+
AND n.nspname = 'corale'
|
|
1105
|
+
AND i.indisreplident = false
|
|
1106
|
+
) THEN
|
|
1107
|
+
ALTER TABLE "corale"."mastra_workflow_snapshot"
|
|
1108
|
+
REPLICA IDENTITY USING INDEX corale_mastra_workflow_snapshot_workflow_name_run_id_key;
|
|
1109
|
+
END IF;
|
|
1110
|
+
END $$;
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_datasets" (
|
|
1116
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
1117
|
+
"name" TEXT NOT NULL,
|
|
1118
|
+
"description" TEXT ,
|
|
1119
|
+
"metadata" JSONB ,
|
|
1120
|
+
"inputSchema" JSONB ,
|
|
1121
|
+
"groundTruthSchema" JSONB ,
|
|
1122
|
+
"requestContextSchema" JSONB ,
|
|
1123
|
+
"tags" JSONB ,
|
|
1124
|
+
"targetType" TEXT ,
|
|
1125
|
+
"targetIds" JSONB ,
|
|
1126
|
+
"scorerIds" JSONB ,
|
|
1127
|
+
"version" INTEGER NOT NULL,
|
|
1128
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1129
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
1130
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1131
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_dataset_items" (
|
|
1138
|
+
"id" TEXT NOT NULL,
|
|
1139
|
+
"datasetId" TEXT NOT NULL,
|
|
1140
|
+
"datasetVersion" INTEGER NOT NULL,
|
|
1141
|
+
"validTo" INTEGER ,
|
|
1142
|
+
"isDeleted" BOOLEAN NOT NULL,
|
|
1143
|
+
"input" JSONB NOT NULL,
|
|
1144
|
+
"groundTruth" JSONB ,
|
|
1145
|
+
"requestContext" JSONB ,
|
|
1146
|
+
"metadata" JSONB ,
|
|
1147
|
+
"source" JSONB ,
|
|
1148
|
+
"expectedTrajectory" JSONB ,
|
|
1149
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1150
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
1151
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1152
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1153
|
+
PRIMARY KEY ("id", "datasetVersion")
|
|
1154
|
+
);
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_dataset_versions" (
|
|
1160
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
1161
|
+
"datasetId" TEXT NOT NULL,
|
|
1162
|
+
"version" INTEGER NOT NULL,
|
|
1163
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1164
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
1165
|
+
);
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_experiments" (
|
|
1171
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
1172
|
+
"name" TEXT ,
|
|
1173
|
+
"description" TEXT ,
|
|
1174
|
+
"metadata" JSONB ,
|
|
1175
|
+
"datasetId" TEXT ,
|
|
1176
|
+
"datasetVersion" INTEGER ,
|
|
1177
|
+
"targetType" TEXT NOT NULL,
|
|
1178
|
+
"targetId" TEXT NOT NULL,
|
|
1179
|
+
"status" TEXT NOT NULL,
|
|
1180
|
+
"totalItems" INTEGER NOT NULL,
|
|
1181
|
+
"succeededCount" INTEGER NOT NULL,
|
|
1182
|
+
"failedCount" INTEGER NOT NULL,
|
|
1183
|
+
"skippedCount" INTEGER NOT NULL,
|
|
1184
|
+
"startedAt" TIMESTAMP ,
|
|
1185
|
+
"completedAt" TIMESTAMP ,
|
|
1186
|
+
"agentVersion" TEXT ,
|
|
1187
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1188
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
1189
|
+
"startedAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1190
|
+
"completedAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1191
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1192
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
1193
|
+
);
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_experiment_results" (
|
|
1199
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
1200
|
+
"experimentId" TEXT NOT NULL,
|
|
1201
|
+
"itemId" TEXT NOT NULL,
|
|
1202
|
+
"itemDatasetVersion" INTEGER ,
|
|
1203
|
+
"input" JSONB NOT NULL,
|
|
1204
|
+
"output" JSONB ,
|
|
1205
|
+
"groundTruth" JSONB ,
|
|
1206
|
+
"error" JSONB ,
|
|
1207
|
+
"startedAt" TIMESTAMP NOT NULL,
|
|
1208
|
+
"completedAt" TIMESTAMP NOT NULL,
|
|
1209
|
+
"retryCount" INTEGER NOT NULL,
|
|
1210
|
+
"traceId" TEXT ,
|
|
1211
|
+
"status" TEXT ,
|
|
1212
|
+
"tags" JSONB ,
|
|
1213
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1214
|
+
"startedAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1215
|
+
"completedAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1216
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
1217
|
+
);
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_background_tasks" (
|
|
1223
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
1224
|
+
"tool_call_id" TEXT NOT NULL,
|
|
1225
|
+
"tool_name" TEXT NOT NULL,
|
|
1226
|
+
"agent_id" TEXT NOT NULL,
|
|
1227
|
+
"run_id" TEXT NOT NULL,
|
|
1228
|
+
"thread_id" TEXT ,
|
|
1229
|
+
"resource_id" TEXT ,
|
|
1230
|
+
"status" TEXT NOT NULL,
|
|
1231
|
+
"args" JSONB NOT NULL,
|
|
1232
|
+
"result" JSONB ,
|
|
1233
|
+
"error" JSONB ,
|
|
1234
|
+
"suspend_payload" JSONB ,
|
|
1235
|
+
"retry_count" INTEGER NOT NULL,
|
|
1236
|
+
"max_retries" INTEGER NOT NULL,
|
|
1237
|
+
"timeout_ms" INTEGER NOT NULL,
|
|
1238
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1239
|
+
"startedAt" TIMESTAMP ,
|
|
1240
|
+
"suspendedAt" TIMESTAMP ,
|
|
1241
|
+
"completedAt" TIMESTAMP ,
|
|
1242
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1243
|
+
"startedAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1244
|
+
"suspendedAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1245
|
+
"completedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
1246
|
+
);
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_bg_tasks_status_created_at_idx" ON "corale"."mastra_background_tasks" ("status", "createdAt");
|
|
1251
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_bg_tasks_agent_status_idx" ON "corale"."mastra_background_tasks" ("agent_id", "status");
|
|
1252
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_bg_tasks_thread_idx" ON "corale"."mastra_background_tasks" ("thread_id", "createdAt");
|
|
1253
|
+
CREATE INDEX IF NOT EXISTS "corale_mastra_bg_tasks_tool_call_idx" ON "corale"."mastra_background_tasks" ("tool_call_id");
|
|
1254
|
+
|
|
1255
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_favorites" (
|
|
1256
|
+
"userId" TEXT NOT NULL,
|
|
1257
|
+
"entityType" TEXT NOT NULL,
|
|
1258
|
+
"entityId" TEXT NOT NULL,
|
|
1259
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1260
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1261
|
+
PRIMARY KEY ("userId", "entityType", "entityId")
|
|
1262
|
+
);
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
CREATE INDEX IF NOT EXISTS idx_favorites_entity ON "corale"."mastra_favorites" ("entityType", "entityId");
|
|
1267
|
+
|
|
1268
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_channel_installations" (
|
|
1269
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
1270
|
+
"platform" TEXT NOT NULL,
|
|
1271
|
+
"agentId" TEXT NOT NULL,
|
|
1272
|
+
"status" TEXT NOT NULL,
|
|
1273
|
+
"webhookId" TEXT ,
|
|
1274
|
+
"data" JSONB NOT NULL,
|
|
1275
|
+
"configHash" TEXT ,
|
|
1276
|
+
"error" TEXT ,
|
|
1277
|
+
"createdAt" TIMESTAMP NOT NULL,
|
|
1278
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
1279
|
+
"createdAtZ" TIMESTAMPTZ DEFAULT NOW(),
|
|
1280
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
1281
|
+
);
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_channel_config" (
|
|
1287
|
+
"platform" TEXT PRIMARY KEY NOT NULL,
|
|
1288
|
+
"data" JSONB NOT NULL,
|
|
1289
|
+
"updatedAt" TIMESTAMP NOT NULL,
|
|
1290
|
+
"updatedAtZ" TIMESTAMPTZ DEFAULT NOW()
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "corale_idx_channel_installations_webhook" ON "corale"."mastra_channel_installations" ("webhookId");
|
|
1296
|
+
CREATE INDEX IF NOT EXISTS "corale_idx_channel_installations_platform_agent" ON "corale"."mastra_channel_installations" ("platform", "agentId");
|
|
1297
|
+
|
|
1298
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_schedules" (
|
|
1299
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
1300
|
+
"target" JSONB NOT NULL,
|
|
1301
|
+
"cron" TEXT NOT NULL,
|
|
1302
|
+
"timezone" TEXT ,
|
|
1303
|
+
"status" TEXT NOT NULL,
|
|
1304
|
+
"next_fire_at" BIGINT NOT NULL,
|
|
1305
|
+
"last_fire_at" BIGINT ,
|
|
1306
|
+
"last_run_id" TEXT ,
|
|
1307
|
+
"created_at" BIGINT NOT NULL,
|
|
1308
|
+
"updated_at" BIGINT NOT NULL,
|
|
1309
|
+
"metadata" JSONB ,
|
|
1310
|
+
"owner_type" TEXT ,
|
|
1311
|
+
"owner_id" TEXT
|
|
1312
|
+
);
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
CREATE TABLE IF NOT EXISTS "corale"."mastra_schedule_triggers" (
|
|
1318
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
1319
|
+
"schedule_id" TEXT NOT NULL,
|
|
1320
|
+
"run_id" TEXT ,
|
|
1321
|
+
"scheduled_fire_at" BIGINT NOT NULL,
|
|
1322
|
+
"actual_fire_at" BIGINT NOT NULL,
|
|
1323
|
+
"outcome" TEXT NOT NULL,
|
|
1324
|
+
"error" TEXT ,
|
|
1325
|
+
"trigger_kind" TEXT NOT NULL,
|
|
1326
|
+
"parent_trigger_id" TEXT ,
|
|
1327
|
+
"metadata" JSONB
|
|
1328
|
+
);
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
CREATE INDEX IF NOT EXISTS "corale_idx_mastra_schedules_status_next_fire" ON "corale"."mastra_schedules" ("status", "next_fire_at");
|
|
1333
|
+
CREATE INDEX IF NOT EXISTS "corale_idx_mastra_schedule_triggers_schedule_fire" ON "corale"."mastra_schedule_triggers" ("schedule_id", "actual_fire_at" DESC);
|
|
1334
|
+
`
|
|
1335
|
+
)
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
// src/migrations/0003_followups.ts
|
|
1339
|
+
var M0003_FOLLOWUPS = {
|
|
1340
|
+
version: "0003_followups",
|
|
1341
|
+
name: "corale.followups waitpoint columns (durable runner)",
|
|
1342
|
+
sql: (
|
|
1343
|
+
/* sql */
|
|
1344
|
+
`
|
|
1345
|
+
alter table corale.followups
|
|
1346
|
+
add column if not exists waitpoint_token text, -- Trigger createToken id (null for vercel/interactive)
|
|
1347
|
+
add column if not exists runner_kind text; -- 'trigger' | 'vercel'
|
|
1348
|
+
`
|
|
1349
|
+
)
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
// src/migrations/0004_memory_vector.ts
|
|
1353
|
+
var M0004_MEMORY_VECTOR = {
|
|
1354
|
+
version: "0004",
|
|
1355
|
+
name: "memory_vector",
|
|
1356
|
+
sql: `create extension if not exists vector;`
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
// src/migrations/0005_thread_text_ids.ts
|
|
1360
|
+
var M0005_THREAD_TEXT_IDS = {
|
|
1361
|
+
version: "0005",
|
|
1362
|
+
name: "thread_text_ids",
|
|
1363
|
+
sql: `
|
|
1364
|
+
alter table corale.runs drop constraint if exists runs_thread_id_fkey;
|
|
1365
|
+
alter table corale.messages drop constraint if exists messages_thread_id_fkey;
|
|
1366
|
+
alter table corale.threads alter column id drop default;
|
|
1367
|
+
alter table corale.threads alter column id type text using id::text;
|
|
1368
|
+
alter table corale.threads alter column id set default gen_random_uuid()::text;
|
|
1369
|
+
alter table corale.runs alter column thread_id type text using thread_id::text;
|
|
1370
|
+
alter table corale.messages alter column thread_id type text using thread_id::text;
|
|
1371
|
+
alter table corale.runs add constraint runs_thread_id_fkey foreign key (thread_id) references corale.threads(id);
|
|
1372
|
+
alter table corale.messages add constraint messages_thread_id_fkey foreign key (thread_id) references corale.threads(id) on delete cascade;
|
|
1373
|
+
`.trim()
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
// src/migrations/0006_scratchpad.ts
|
|
1377
|
+
var M0006_SCRATCHPAD = {
|
|
1378
|
+
version: "0006_scratchpad",
|
|
1379
|
+
name: "corale container scratchpad (mutable shared doc per conversation)",
|
|
1380
|
+
sql: (
|
|
1381
|
+
/* sql */
|
|
1382
|
+
`
|
|
1383
|
+
create table corale.scratchpad (
|
|
1384
|
+
org_id uuid not null references corale.orgs(id) on delete cascade,
|
|
1385
|
+
container text not null,
|
|
1386
|
+
public_content text not null default '',
|
|
1387
|
+
meta_content text not null default '',
|
|
1388
|
+
updated_by text,
|
|
1389
|
+
updated_by_user text,
|
|
1390
|
+
updated_at timestamptz not null default now(),
|
|
1391
|
+
primary key (org_id, container)
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
-- RLS (defense-in-depth), copied from 0001_core's corale.messages setup: enable RLS, an
|
|
1395
|
+
-- org-membership read policy via corale.is_org_member(org_id), and the org_id index. service_role
|
|
1396
|
+
-- (the server adapter) bypasses RLS; this only gates the browser/Realtime read path.
|
|
1397
|
+
alter table corale.scratchpad enable row level security;
|
|
1398
|
+
create policy org_read_scratchpad on corale.scratchpad for select to authenticated using ((select corale.is_org_member(org_id)));
|
|
1399
|
+
create index scratchpad_org_idx on corale.scratchpad (org_id);
|
|
1400
|
+
|
|
1401
|
+
-- GRANT (CRITICAL): 0001_core's schema-wide \`grant select on all tables\` only covered tables that
|
|
1402
|
+
-- existed then; a new table needs its own grant so the JWT-scoped Realtime/browser read path can see
|
|
1403
|
+
-- it. RLS still gates ROWS. Mirrors 0001's \`grant select on all tables in schema corale\` intent.
|
|
1404
|
+
grant select on corale.scratchpad to authenticated;
|
|
1405
|
+
`
|
|
1406
|
+
)
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
// src/migrations/0007_scratchpad_entries.ts
|
|
1410
|
+
var M0007_SCRATCHPAD_ENTRIES = {
|
|
1411
|
+
version: "0007_scratchpad_entries",
|
|
1412
|
+
name: "corale scratchpad \u2192 keyed jsonb entries (concurrent-safe writes)",
|
|
1413
|
+
sql: (
|
|
1414
|
+
/* sql */
|
|
1415
|
+
`
|
|
1416
|
+
alter table corale.scratchpad
|
|
1417
|
+
drop column public_content,
|
|
1418
|
+
drop column meta_content,
|
|
1419
|
+
add column public_entries jsonb not null default '{}'::jsonb,
|
|
1420
|
+
add column meta_entries jsonb not null default '{}'::jsonb;
|
|
1421
|
+
`
|
|
1422
|
+
)
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
// src/migrations/0008_floor.ts
|
|
1426
|
+
var M0008_FLOOR = {
|
|
1427
|
+
version: "0008_floor",
|
|
1428
|
+
name: "corale container floor (serverless mutual-exclusion + FIFO queue)",
|
|
1429
|
+
sql: (
|
|
1430
|
+
/* sql */
|
|
1431
|
+
`
|
|
1432
|
+
create table corale.floor_lock (
|
|
1433
|
+
container text primary key,
|
|
1434
|
+
holder_label text not null,
|
|
1435
|
+
holder_run_id text not null,
|
|
1436
|
+
acquired_at timestamptz not null default now()
|
|
1437
|
+
);
|
|
1438
|
+
alter table corale.floor_lock enable row level security;
|
|
1439
|
+
|
|
1440
|
+
create table corale.floor_waiter (
|
|
1441
|
+
container text not null,
|
|
1442
|
+
run_id text not null,
|
|
1443
|
+
label text not null,
|
|
1444
|
+
enqueued_at timestamptz not null default now(),
|
|
1445
|
+
last_seen timestamptz not null default now(),
|
|
1446
|
+
primary key (container, run_id)
|
|
1447
|
+
);
|
|
1448
|
+
create index floor_waiter_order on corale.floor_waiter (container, enqueued_at);
|
|
1449
|
+
alter table corale.floor_waiter enable row level security;
|
|
1450
|
+
|
|
1451
|
+
revoke all on corale.floor_lock, corale.floor_waiter from anon, authenticated;
|
|
1452
|
+
`
|
|
1453
|
+
)
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
// src/migrations/0009_scratchpad_realtime.ts
|
|
1457
|
+
var M0009_SCRATCHPAD_REALTIME = {
|
|
1458
|
+
version: "0009_scratchpad_realtime",
|
|
1459
|
+
name: "corale scratchpad realtime broadcast + member read gate",
|
|
1460
|
+
sql: (
|
|
1461
|
+
/* sql */
|
|
1462
|
+
`
|
|
1463
|
+
create or replace function corale.broadcast_scratchpad()
|
|
1464
|
+
returns trigger language plpgsql security definer set search_path = '' as $$
|
|
1465
|
+
begin
|
|
1466
|
+
perform realtime.send(
|
|
1467
|
+
jsonb_build_object('container', new.container, 'updated_at', new.updated_at),
|
|
1468
|
+
'scratchpad', 'scratchpad:' || new.container, true);
|
|
1469
|
+
return new;
|
|
1470
|
+
end; $$;
|
|
1471
|
+
create trigger trg_broadcast_scratchpad after insert or update on corale.scratchpad
|
|
1472
|
+
for each row execute function corale.broadcast_scratchpad();
|
|
1473
|
+
|
|
1474
|
+
create policy members_receive_scratchpad on realtime.messages for select to authenticated using (
|
|
1475
|
+
realtime.messages.extension = 'broadcast' and (select realtime.topic()) like 'scratchpad:%'
|
|
1476
|
+
and exists (select 1 from corale.scratchpad s
|
|
1477
|
+
where ('scratchpad:' || s.container) = (select realtime.topic()) and (select corale.is_org_member(s.org_id))));
|
|
1478
|
+
`
|
|
1479
|
+
)
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
// src/migrations/0010_presence_realtime.ts
|
|
1483
|
+
var M0010_PRESENCE_REALTIME = {
|
|
1484
|
+
version: "0010_presence_realtime",
|
|
1485
|
+
name: "corale run-status presence broadcast + member read gate",
|
|
1486
|
+
sql: (
|
|
1487
|
+
/* sql */
|
|
1488
|
+
`
|
|
1489
|
+
create or replace function corale.broadcast_presence()
|
|
1490
|
+
returns trigger language plpgsql security definer set search_path = '' as $$
|
|
1491
|
+
begin
|
|
1492
|
+
perform realtime.send(
|
|
1493
|
+
jsonb_build_object('container', split_part(new.thread_id, ':', 1), 'status', new.status),
|
|
1494
|
+
'presence', 'presence:' || split_part(new.thread_id, ':', 1), true);
|
|
1495
|
+
return new;
|
|
1496
|
+
end; $$;
|
|
1497
|
+
create trigger trg_broadcast_presence after insert or update of status on corale.runs
|
|
1498
|
+
for each row execute function corale.broadcast_presence();
|
|
1499
|
+
|
|
1500
|
+
create policy members_receive_presence on realtime.messages for select to authenticated using (
|
|
1501
|
+
realtime.messages.extension = 'broadcast' and (select realtime.topic()) like 'presence:%'
|
|
1502
|
+
and exists (select 1 from corale.runs r
|
|
1503
|
+
where ('presence:' || split_part(r.thread_id, ':', 1)) = (select realtime.topic()) and (select corale.is_org_member(r.org_id))));
|
|
1504
|
+
`
|
|
1505
|
+
)
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
// src/migrations/0011_broadcast_allowlist.ts
|
|
1509
|
+
var M0011_BROADCAST_ALLOWLIST = {
|
|
1510
|
+
version: "0011_broadcast_allowlist",
|
|
1511
|
+
name: "corale.broadcast_event: scrub forwarded payload to a SwarmEvent field allowlist",
|
|
1512
|
+
sql: (
|
|
1513
|
+
/* sql */
|
|
1514
|
+
`
|
|
1515
|
+
create or replace function corale.broadcast_event()
|
|
1516
|
+
returns trigger language plpgsql security definer set search_path = '' as $$
|
|
1517
|
+
begin
|
|
1518
|
+
perform realtime.send(
|
|
1519
|
+
jsonb_build_object('type', new.type, 'seq', new.seq, 'run_id', new.run_id,
|
|
1520
|
+
'from_agent', new.from_agent, 'to_agent', new.to_agent,
|
|
1521
|
+
-- R2: allowlist \u2014 forward only known SwarmEvent fields from new.payload, never the column verbatim.
|
|
1522
|
+
'payload', jsonb_build_object(
|
|
1523
|
+
'type', new.payload->'type',
|
|
1524
|
+
'runId', new.payload->'runId',
|
|
1525
|
+
'agentSlug', new.payload->'agentSlug',
|
|
1526
|
+
'ts', new.payload->'ts',
|
|
1527
|
+
'schemaVersion', new.payload->'schemaVersion',
|
|
1528
|
+
'data', new.payload->'data')),
|
|
1529
|
+
new.type, 'run:' || new.run_id::text, true);
|
|
1530
|
+
return new;
|
|
1531
|
+
end; $$;
|
|
1532
|
+
`
|
|
1533
|
+
)
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
// src/migrations/0012_thread_scoped_resource.ts
|
|
1537
|
+
var M0012_THREAD_SCOPED_RESOURCE = {
|
|
1538
|
+
version: "0012_thread_scoped_resource",
|
|
1539
|
+
name: "R14: re-key Mastra resourceId tenant:user \u2192 tenant:container (thread-scoped)",
|
|
1540
|
+
sql: (
|
|
1541
|
+
/* sql */
|
|
1542
|
+
`
|
|
1543
|
+
do $$ begin
|
|
1544
|
+
if exists (select 1 from corale.mastra_resources limit 1) then
|
|
1545
|
+
raise exception 'corale 0012: corale.mastra_resources has rows (Mastra working memory is in use). Its resourceId (tenant:user) cannot be auto-re-keyed to tenant:container \u2014 one user-resource maps to many containers. Migrate or clear corale.mastra_resources by hand, then re-run.';
|
|
1546
|
+
end if;
|
|
1547
|
+
end $$;
|
|
1548
|
+
|
|
1549
|
+
update corale.mastra_messages
|
|
1550
|
+
set "resourceId" = split_part("resourceId", ':', 1) || ':' || split_part(thread_id, ':', 1)
|
|
1551
|
+
where "resourceId" is not null and position(':' in "resourceId") > 0;
|
|
1552
|
+
|
|
1553
|
+
update corale.mastra_threads
|
|
1554
|
+
set "resourceId" = split_part("resourceId", ':', 1) || ':' || split_part(id, ':', 1)
|
|
1555
|
+
where position(':' in "resourceId") > 0;
|
|
1556
|
+
|
|
1557
|
+
update corale.mastra_observational_memory
|
|
1558
|
+
set "resourceId" = split_part("resourceId", ':', 1) || ':' || split_part("threadId", ':', 1)
|
|
1559
|
+
where "resourceId" is not null and "threadId" is not null and position(':' in "resourceId") > 0;
|
|
1560
|
+
`
|
|
1561
|
+
)
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1564
|
+
// src/migrations/0013_rename_schema.ts
|
|
1565
|
+
var M0013_RENAME_SCHEMA = {
|
|
1566
|
+
version: "0013_rename_schema",
|
|
1567
|
+
name: "Phase-00: rename schema corale \u2192 nightowls (+ corale_* index identifiers)",
|
|
1568
|
+
sql: (
|
|
1569
|
+
/* sql */
|
|
1570
|
+
`
|
|
1571
|
+
do $$
|
|
1572
|
+
begin
|
|
1573
|
+
if exists (select 1 from information_schema.schemata where schema_name = 'corale')
|
|
1574
|
+
and not exists (select 1 from information_schema.schemata where schema_name = 'nightowls') then
|
|
1575
|
+
execute 'alter schema corale rename to nightowls';
|
|
1576
|
+
end if;
|
|
1577
|
+
end $$;
|
|
1578
|
+
|
|
1579
|
+
-- Rename any index in the (now) nightowls schema still carrying the legacy 'corale_' prefix.
|
|
1580
|
+
do $$
|
|
1581
|
+
declare r record;
|
|
1582
|
+
begin
|
|
1583
|
+
for r in
|
|
1584
|
+
select indexname from pg_indexes
|
|
1585
|
+
where schemaname = 'nightowls' and indexname like 'corale\\_%'
|
|
1586
|
+
loop
|
|
1587
|
+
execute format('alter index nightowls.%I rename to %I', r.indexname, 'nightowls_' || substring(r.indexname from 8));
|
|
1588
|
+
end loop;
|
|
1589
|
+
end $$;
|
|
1590
|
+
`
|
|
1591
|
+
)
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
// src/migrations/index.ts
|
|
1595
|
+
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];
|
|
1596
|
+
|
|
1597
|
+
// src/plugin.ts
|
|
1598
|
+
var nightOwlsPlugin = {
|
|
1599
|
+
name: "storage-supabase",
|
|
1600
|
+
version: "0.0.0",
|
|
1601
|
+
kind: "storage",
|
|
1602
|
+
pkg: "@nightowlsdev/storage-supabase",
|
|
1603
|
+
description: "Supabase Postgres storage + the nightowls schema (migrations, Realtime, RLS).",
|
|
1604
|
+
migrations: MIGRATIONS,
|
|
1605
|
+
// { version, name, sql }[]
|
|
1606
|
+
env: [
|
|
1607
|
+
{ key: "SUPABASE_URL", example: "http://127.0.0.1:54321", comment: "Supabase project URL" },
|
|
1608
|
+
{
|
|
1609
|
+
key: "SUPABASE_SECRET_KEY",
|
|
1610
|
+
example: "",
|
|
1611
|
+
comment: "service_role/secret \u2014 server storage adapter ONLY (never in the browser)"
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
key: "DATABASE_URL",
|
|
1615
|
+
example: "postgresql://postgres:postgres@127.0.0.1:5432/postgres",
|
|
1616
|
+
comment: "Session/Direct port 5432 \u2014 NOT 6543 (nightowls migrations + the pg pool)"
|
|
1617
|
+
},
|
|
1618
|
+
{ key: "SUPABASE_ANON_KEY", example: "", comment: "publishable/anon \u2014 browser auth + Realtime ONLY" }
|
|
1619
|
+
],
|
|
1620
|
+
config: {
|
|
1621
|
+
import: "import { createSupabaseStorage } from '@nightowlsdev/storage-supabase';",
|
|
1622
|
+
snippet: "storage = createSupabaseStorage({ url: env.SUPABASE_URL, secretKey: env.SUPABASE_SECRET_KEY, dbUrl: env.DATABASE_URL });",
|
|
1623
|
+
marker: "storage"
|
|
1624
|
+
},
|
|
1625
|
+
// Print-only (idempotent) — runs on init/install + `owl init storage-supabase`. NEVER applies DDL.
|
|
1626
|
+
init: (ctx) => {
|
|
1627
|
+
ctx.log(
|
|
1628
|
+
"Night Owls's migrations were installed into supabase/migrations/. Apply them with: supabase db push"
|
|
1629
|
+
);
|
|
1630
|
+
},
|
|
1631
|
+
commands: [
|
|
1632
|
+
{
|
|
1633
|
+
name: "info",
|
|
1634
|
+
description: "Show the nightowls migrations and env this adapter contributes.",
|
|
1635
|
+
// PURE — no DB/network. Reads only the static manifest data.
|
|
1636
|
+
run: (ctx) => {
|
|
1637
|
+
ctx.log(`migrations: ${MIGRATIONS.map((m) => m.version).join(", ")}`);
|
|
1638
|
+
ctx.log("env: SUPABASE_URL, SUPABASE_SECRET_KEY, DATABASE_URL, SUPABASE_ANON_KEY");
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
]
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
// src/floor.ts
|
|
1645
|
+
function createPostgresFloor(pool, opts = {}) {
|
|
1646
|
+
const maxHoldSecs = (opts.maxHoldMs ?? 18e4) / 1e3;
|
|
1647
|
+
const pollMs = opts.pollMs ?? 250;
|
|
1648
|
+
const waiterStaleSecs = Math.max(pollMs * 20, 1e4) / 1e3;
|
|
1649
|
+
const releaseLock = (container, runId) => pool.query("delete from nightowls.floor_lock where container=$1 and holder_run_id=$2", [container, runId]);
|
|
1650
|
+
const makeRelease = (container, runId) => {
|
|
1651
|
+
let released = false;
|
|
1652
|
+
return async () => {
|
|
1653
|
+
if (released) return;
|
|
1654
|
+
released = true;
|
|
1655
|
+
await releaseLock(container, runId);
|
|
1656
|
+
};
|
|
1657
|
+
};
|
|
1658
|
+
async function claim(container, who, ahead, params) {
|
|
1659
|
+
const { rows } = await pool.query(
|
|
1660
|
+
`insert into nightowls.floor_lock as fl (container, holder_label, holder_run_id, acquired_at)
|
|
1661
|
+
select $1, $2, $3, now()
|
|
1662
|
+
where not exists (
|
|
1663
|
+
select 1 from nightowls.floor_waiter w
|
|
1664
|
+
where w.container = $1 and w.last_seen >= now() - make_interval(secs => $5) ${ahead}
|
|
1665
|
+
)
|
|
1666
|
+
on conflict (container) do update
|
|
1667
|
+
set holder_label = excluded.holder_label, holder_run_id = excluded.holder_run_id, acquired_at = now()
|
|
1668
|
+
where fl.acquired_at < now() - make_interval(secs => $4)
|
|
1669
|
+
returning holder_run_id`,
|
|
1670
|
+
[container, who.label, who.runId, maxHoldSecs, waiterStaleSecs, ...params]
|
|
1671
|
+
);
|
|
1672
|
+
return rows.length > 0;
|
|
1673
|
+
}
|
|
1674
|
+
async function tryAcquire(container, who) {
|
|
1675
|
+
const got = await claim(container, who, "", []);
|
|
1676
|
+
return got ? makeRelease(container, who.runId) : null;
|
|
1677
|
+
}
|
|
1678
|
+
function sleep(ms, signal) {
|
|
1679
|
+
return new Promise((resolve) => {
|
|
1680
|
+
if (signal?.aborted) return resolve();
|
|
1681
|
+
const onAbort = () => {
|
|
1682
|
+
clearTimeout(t);
|
|
1683
|
+
resolve();
|
|
1684
|
+
};
|
|
1685
|
+
const t = setTimeout(() => {
|
|
1686
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1687
|
+
resolve();
|
|
1688
|
+
}, ms);
|
|
1689
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
const AHEAD = "and (date_trunc('milliseconds', w.enqueued_at), w.run_id) < (date_trunc('milliseconds', $6::timestamptz), $3)";
|
|
1693
|
+
async function acquire(container, who, signal) {
|
|
1694
|
+
const noop = () => {
|
|
1695
|
+
};
|
|
1696
|
+
const deleteWaiter = () => pool.query("delete from nightowls.floor_waiter where container=$1 and run_id=$2", [container, who.runId]);
|
|
1697
|
+
if (signal?.aborted) return noop;
|
|
1698
|
+
const reg = await pool.query(
|
|
1699
|
+
`insert into nightowls.floor_waiter(container, run_id, label) values($1,$2,$3)
|
|
1700
|
+
on conflict (container, run_id) do update set last_seen = now()
|
|
1701
|
+
returning enqueued_at`,
|
|
1702
|
+
[container, who.runId, who.label]
|
|
1703
|
+
);
|
|
1704
|
+
const myEnqueuedAt = reg.rows[0].enqueued_at;
|
|
1705
|
+
try {
|
|
1706
|
+
for (; ; ) {
|
|
1707
|
+
if (signal?.aborted) {
|
|
1708
|
+
await deleteWaiter();
|
|
1709
|
+
return noop;
|
|
1710
|
+
}
|
|
1711
|
+
await pool.query("update nightowls.floor_waiter set last_seen = now() where container=$1 and run_id=$2", [container, who.runId]);
|
|
1712
|
+
if (await claim(container, who, AHEAD, [myEnqueuedAt])) {
|
|
1713
|
+
await deleteWaiter();
|
|
1714
|
+
return makeRelease(container, who.runId);
|
|
1715
|
+
}
|
|
1716
|
+
await sleep(pollMs, signal);
|
|
1717
|
+
}
|
|
1718
|
+
} catch (e) {
|
|
1719
|
+
await deleteWaiter().catch(() => {
|
|
1720
|
+
});
|
|
1721
|
+
throw e;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
async function holder(container) {
|
|
1725
|
+
const { rows } = await pool.query(
|
|
1726
|
+
`select holder_label, holder_run_id from nightowls.floor_lock
|
|
1727
|
+
where container=$1 and acquired_at >= now() - make_interval(secs => $2)`,
|
|
1728
|
+
[container, maxHoldSecs]
|
|
1729
|
+
);
|
|
1730
|
+
return rows.length ? { label: rows[0].holder_label, runId: rows[0].holder_run_id } : null;
|
|
1731
|
+
}
|
|
1732
|
+
async function queueDepth(container) {
|
|
1733
|
+
const { rows } = await pool.query(
|
|
1734
|
+
`select count(*)::int as n from nightowls.floor_waiter
|
|
1735
|
+
where container=$1 and last_seen >= now() - make_interval(secs => $2)`,
|
|
1736
|
+
[container, waiterStaleSecs]
|
|
1737
|
+
);
|
|
1738
|
+
return rows[0].n;
|
|
1739
|
+
}
|
|
1740
|
+
return { tryAcquire, acquire, holder, queueDepth };
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// src/index.ts
|
|
1744
|
+
function createSupabaseStorage(opts) {
|
|
1745
|
+
const ctx = makeCtx(opts);
|
|
1746
|
+
let invalidationSub = null;
|
|
1747
|
+
return {
|
|
1748
|
+
agents: makeAgentRepo(ctx),
|
|
1749
|
+
runs: makeRunStore(ctx),
|
|
1750
|
+
events: makeEventStore(ctx),
|
|
1751
|
+
messages: makeMessageStore(ctx),
|
|
1752
|
+
scratchpad: makeScratchpadStore(ctx),
|
|
1753
|
+
// Record the suspended run in the tenant-scoped followup index so `runs.findSuspended` (the resume
|
|
1754
|
+
// authz gate) can resolve it. RE-OPEN on conflict (reset `answered_at`): Mastra REUSES the same
|
|
1755
|
+
// `toolCallId` when an agent asks AGAIN after a resume, so the engine's `followupId = runId:toolCallId`
|
|
1756
|
+
// collides with the prior (now-answered) round. `do nothing` would leave it answered → `findSuspended`
|
|
1757
|
+
// (filters `answered_at is null`) returns null → the next answer 403s. Re-opening makes each successive
|
|
1758
|
+
// `ask` answerable. (Safe: a redundant re-record of the SAME live suspend just re-nulls an already-null
|
|
1759
|
+
// `answered_at`.)
|
|
1760
|
+
recordSuspend: async (runId, tenantId, followupId, toolCallId) => {
|
|
1761
|
+
await ctx.pool.query(
|
|
1762
|
+
"insert into followups(id, run_id, org_id, tool_call_id, created_at) values($1,$2,$3,$4, now()) on conflict (id) do update set answered_at = null, created_at = now()",
|
|
1763
|
+
[followupId, runId, tenantId, toolCallId]
|
|
1764
|
+
);
|
|
1765
|
+
},
|
|
1766
|
+
// Mark a followup answered so `findSuspended` (which filters `answered_at is null`) stops returning
|
|
1767
|
+
// it — closes the replay window once a resume begins. Tenant-scoped + idempotent.
|
|
1768
|
+
markFollowupAnswered: async (followupId, tenantId) => {
|
|
1769
|
+
await ctx.pool.query("update followups set answered_at = now() where id=$1 and org_id=$2 and answered_at is null", [
|
|
1770
|
+
followupId,
|
|
1771
|
+
tenantId
|
|
1772
|
+
]);
|
|
1773
|
+
},
|
|
1774
|
+
// R12: cross-process cache invalidation via Postgres LISTEN. The engine wires this to
|
|
1775
|
+
// `rowCache.invalidate`; a `publishAgentVersion` elsewhere NOTIFYs the key and every instance evicts
|
|
1776
|
+
// immediately. Returns a sync unsubscribe (the engine ignores it — `close()` owns teardown).
|
|
1777
|
+
subscribeInvalidations: (onInvalidate) => {
|
|
1778
|
+
if (invalidationSub) throw new Error("subscribeInvalidations: already registered for this storage");
|
|
1779
|
+
const sub = listenForInvalidations(ctx, onInvalidate);
|
|
1780
|
+
sub.catch(() => {
|
|
1781
|
+
});
|
|
1782
|
+
invalidationSub = sub;
|
|
1783
|
+
let torn = false;
|
|
1784
|
+
return () => {
|
|
1785
|
+
if (torn) return;
|
|
1786
|
+
torn = true;
|
|
1787
|
+
const pending = invalidationSub;
|
|
1788
|
+
invalidationSub = null;
|
|
1789
|
+
void pending?.then((s) => s.unsubscribe()).catch(() => {
|
|
1790
|
+
});
|
|
1791
|
+
};
|
|
1792
|
+
},
|
|
1793
|
+
ctx,
|
|
1794
|
+
// typed internal handle reused by publishAgentVersion (Task 10) — no `__ctx` any-cast.
|
|
1795
|
+
// Gracefully tear down Realtime (remove channels + close the WS) BEFORE ending the
|
|
1796
|
+
// pg pool, so a force-closed socket can't surface a late "transport failure" rejection.
|
|
1797
|
+
close: async () => {
|
|
1798
|
+
if (invalidationSub) {
|
|
1799
|
+
try {
|
|
1800
|
+
await (await invalidationSub).unsubscribe();
|
|
1801
|
+
} catch {
|
|
1802
|
+
}
|
|
1803
|
+
invalidationSub = null;
|
|
1804
|
+
}
|
|
1805
|
+
try {
|
|
1806
|
+
await ctx.sb.removeAllChannels();
|
|
1807
|
+
await ctx.sb.realtime.disconnect();
|
|
1808
|
+
} catch {
|
|
1809
|
+
}
|
|
1810
|
+
await ctx.pool.end();
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1815
|
+
0 && (module.exports = {
|
|
1816
|
+
MIGRATIONS,
|
|
1817
|
+
createMastraPgStore,
|
|
1818
|
+
createMastraVectorStore,
|
|
1819
|
+
createPostgresFloor,
|
|
1820
|
+
createSupabaseStorage,
|
|
1821
|
+
listAgentVersions,
|
|
1822
|
+
nightOwlsPlugin,
|
|
1823
|
+
publishAgentVersion,
|
|
1824
|
+
rollbackAgentVersion
|
|
1825
|
+
});
|