@agent-native/core 0.209.2 → 0.209.3-nightly-20261016212700
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/a2a-claims.js +5 -5
- package/dist/agent/context-xray/plugin.js +2 -6
- package/dist/agent/context-xray/schema-store.d.ts +1 -0
- package/dist/agent/context-xray/schema-store.js +7 -0
- package/dist/agent/harness/store.js +3 -0
- package/dist/agent/observational-memory/plugin.js +2 -6
- package/dist/agent/observational-memory/store.js +3 -0
- package/dist/agent/run-store.js +3 -0
- package/dist/agent/tool-approval-store.js +4 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/chat-threads/store.js +3 -0
- package/dist/client/action-query-cache-context.d.ts +1 -0
- package/dist/client/action-query-cache-context.js +2 -0
- package/dist/client/action-query-cache.d.ts +1 -1
- package/dist/client/action-query-cache.js +34 -15
- package/dist/client/motion-text.css +8 -0
- package/dist/client/motion-text.d.ts +6 -0
- package/dist/client/motion-text.js +154 -29
- package/dist/client/use-action.js +18 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/migrations.d.ts +1 -0
- package/dist/db/migrations.js +1 -1
- package/dist/db/runtime-facts.d.ts +7 -0
- package/dist/db/runtime-facts.js +18 -0
- package/dist/db/schema-catalog.js +4 -1
- package/dist/db/store-ledger.test-helpers.d.ts +5 -0
- package/dist/db/store-ledger.test-helpers.js +19 -0
- package/dist/db/store-registry.d.ts +34 -0
- package/dist/db/store-registry.generated.js +13 -0
- package/dist/db/store-registry.js +288 -46
- package/dist/db/versioned-store-migrations.d.ts +14 -0
- package/dist/db/versioned-store-migrations.js +37 -0
- package/dist/db/widen-columns.js +5 -2
- package/dist/extensions/html-shell.js +67 -0
- package/dist/integrations/remote-devices-store.js +3 -0
- package/dist/jobs/run-history.d.ts +3 -2
- package/dist/jobs/run-history.js +5 -5
- package/dist/jobs/scheduler-health.d.ts +3 -2
- package/dist/jobs/scheduler-health.js +5 -5
- package/dist/mcp/build-server.d.ts +3 -0
- package/dist/mcp/build-server.js +24 -7
- package/dist/mcp/server.js +40 -9
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/oauth-tokens/store.js +3 -0
- package/dist/observability/routes.d.ts +4 -4
- package/dist/observability/store.js +4 -0
- package/dist/org/auto-join-domain.js +23 -15
- package/dist/org/context.d.ts +12 -0
- package/dist/org/context.js +27 -19
- package/dist/org/handlers.js +31 -13
- package/dist/org/index.d.ts +2 -1
- package/dist/org/index.js +1 -0
- package/dist/org/plugin.js +2 -4
- package/dist/org/schema-store.d.ts +1 -0
- package/dist/org/schema-store.js +13 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
- package/dist/provider-api/actions/provider-api.d.ts +9 -9
- package/dist/resources/store.js +5 -2
- package/dist/server/agent-access.js +2 -6
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/auth-plugin.js +2 -6
- package/dist/server/auth.js +22 -2
- package/dist/server/better-auth-migrations.d.ts +4 -2
- package/dist/server/better-auth-migrations.js +18 -6
- package/dist/server/identity-sso-store.js +3 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/release-migrations.d.ts +1 -1
- package/dist/server/release-migrations.js +3 -65
- package/dist/server/release-schema.js +10 -2
- package/dist/server/short-lived-token.d.ts +3 -2
- package/dist/server/short-lived-token.js +3 -2
- package/dist/triggers/actions/manage-automation.d.ts +3 -3
- package/dist/triggers/event-queue.d.ts +4 -2
- package/dist/triggers/event-queue.js +38 -38
- package/dist/usage/alerts-store.js +3 -0
- package/dist/workspace-connections/groups.js +4 -0
- package/dist/workspace-connections/store.js +3 -0
- package/package.json +3 -3
|
@@ -16,8 +16,9 @@ import { getAsyncLocalStorageCtor } from "../shared/optional-node-builtins.js";
|
|
|
16
16
|
import { getDbExec } from "./client.js";
|
|
17
17
|
import { appMigratesAtRelease, schemaEnsureSkipRequested, } from "./migration-policy.js";
|
|
18
18
|
import { isHostedFunctionInvocationRuntime, isMigrationAuthorizedRuntime, isProductionServerlessFunctionRuntime, withMigrationExecutionRuntime, } from "./migration-runtime.js";
|
|
19
|
+
import { runMigrations } from "./migrations.js";
|
|
19
20
|
import { retainRequestDbPoolScope } from "./request-pool-context.js";
|
|
20
|
-
import { dbClientGeneration, retryOnDdlRace } from "./runtime-facts.js";
|
|
21
|
+
import { dbClientGeneration, retryOnDdlRace, runRecordingStoreMigration, } from "./runtime-facts.js";
|
|
21
22
|
export const STORE_MIGRATIONS_TABLE = "_an_store_migrations";
|
|
22
23
|
// checksum, kind, source and core_version are written now because Phase 2
|
|
23
24
|
// (snapshot installs, checksum enforcement) needs them for rows that already
|
|
@@ -65,10 +66,12 @@ export function schemaIsReleaseOwned() {
|
|
|
65
66
|
* the caller's transaction, so a ledger row written there could claim a
|
|
66
67
|
* migration that production never kept.
|
|
67
68
|
*/
|
|
69
|
+
function isReleaseStep() {
|
|
70
|
+
return isMigrationAuthorizedRuntime() && !isHostedFunctionInvocationRuntime();
|
|
71
|
+
}
|
|
68
72
|
function readiness() {
|
|
69
|
-
if (
|
|
73
|
+
if (isReleaseStep())
|
|
70
74
|
return "apply";
|
|
71
|
-
}
|
|
72
75
|
// No ledger read here: a cold start does zero schema queries, and a missed
|
|
73
76
|
// release is caught by the release step's own verification, not turned into
|
|
74
77
|
// a sitewide error by every request.
|
|
@@ -76,7 +79,13 @@ function readiness() {
|
|
|
76
79
|
return "trust";
|
|
77
80
|
if (schemaEnsureSkipRequested())
|
|
78
81
|
return "skip";
|
|
79
|
-
|
|
82
|
+
// Hosted requests never write the ledger: PR previews share production's
|
|
83
|
+
// database and a request's writes can roll back with its caller.
|
|
84
|
+
if (isHostedRuntime())
|
|
85
|
+
return "hosted-replay";
|
|
86
|
+
// Local dev, tests and self-hosted servers own their database, so they
|
|
87
|
+
// record migrations like the release step: each runs once per database.
|
|
88
|
+
return "apply";
|
|
80
89
|
}
|
|
81
90
|
function statementsOf(migration) {
|
|
82
91
|
if (migration.sql === undefined)
|
|
@@ -113,66 +122,279 @@ async function runMigration(migration, exec) {
|
|
|
113
122
|
if (migration.run)
|
|
114
123
|
await retryOnDdlRace(() => migration.run(exec));
|
|
115
124
|
}
|
|
116
|
-
async function applyWithLedger(store) {
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
async function applyWithLedger(store, exec) {
|
|
126
|
+
try {
|
|
127
|
+
await applyPending(store, exec);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
// The ledger can vanish behind a long-lived executor (a database dropped
|
|
131
|
+
// and recreated under the same client). Rebuild this executor's view once
|
|
132
|
+
// rather than failing every store until the process restarts.
|
|
133
|
+
if (!isMissingLedger(err))
|
|
134
|
+
throw err;
|
|
135
|
+
ledgerStates.delete(exec);
|
|
136
|
+
await applyPending(store, exec);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function isMissingLedger(err) {
|
|
140
|
+
for (let e = err, depth = 0; e && depth < 4; e = e.cause, depth++) {
|
|
141
|
+
if ((e.code === "42P01" ||
|
|
142
|
+
/relation .* does not exist/i.test(String(e.message ?? ""))) &&
|
|
143
|
+
String(e.message ?? "").includes(STORE_MIGRATIONS_TABLE)) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
async function applyPending(store, exec) {
|
|
150
|
+
const state = ledgerStateFor(exec);
|
|
151
|
+
if (!state.present) {
|
|
119
152
|
await retryOnDdlRace(() => exec.execute(LEDGER_DDL));
|
|
120
|
-
|
|
153
|
+
state.present = true;
|
|
121
154
|
}
|
|
122
|
-
const recorded = (await readLedgerSnapshot()).get(store.id);
|
|
155
|
+
const recorded = (await readLedgerSnapshot(exec)).get(store.id);
|
|
123
156
|
if (recorded && store.migrations.every((m) => recorded.has(m.name)))
|
|
124
157
|
return;
|
|
125
158
|
const applied = new Set(recorded);
|
|
159
|
+
const legacy = new LegacyBookkeeping(exec);
|
|
160
|
+
const isRelease = isReleaseStep();
|
|
161
|
+
let ran = 0;
|
|
126
162
|
for (const migration of store.migrations) {
|
|
127
163
|
if (applied.has(migration.name))
|
|
128
164
|
continue;
|
|
165
|
+
// An entry carried over from a `runMigrations` list ran only in the release
|
|
166
|
+
// step on main; local runtimes get those tables from the store's own
|
|
167
|
+
// migrations, and running the list there would need its release-only order.
|
|
168
|
+
if (migration.legacy && !migration.legacy.ranAtBoot && !isRelease) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (migration.legacy && (await legacy.recorded(migration.legacy))) {
|
|
172
|
+
await recordMigration(exec, store, migration, "legacy");
|
|
173
|
+
applied.add(migration.name);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
129
176
|
try {
|
|
130
|
-
await runMigration(migration, exec);
|
|
177
|
+
await runRecordingStoreMigration(() => runMigration(migration, exec));
|
|
131
178
|
}
|
|
132
179
|
catch (err) {
|
|
133
180
|
throw new Error(`Store "${store.id}" migration "${migration.name}" failed: ${err?.message ?? String(err)}`, { cause: err });
|
|
134
181
|
}
|
|
182
|
+
await recordMigration(exec, store, migration, "step");
|
|
183
|
+
// Hosted apps without release migrations still replay boot lists against
|
|
184
|
+
// their old table; without this row they would run this entry again.
|
|
185
|
+
if (migration.legacy?.ranAtBoot) {
|
|
186
|
+
await recordLegacyMigrationEntry(exec, migration.legacy.table, {
|
|
187
|
+
version: migration.legacy.version,
|
|
188
|
+
name: migration.legacy.name,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
applied.add(migration.name);
|
|
192
|
+
ran++;
|
|
193
|
+
}
|
|
194
|
+
state.snapshot = undefined;
|
|
195
|
+
if (ran > 0) {
|
|
196
|
+
console.log(`[db] Applied ${ran} migration(s) to store "${store.id}"`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* A deployed server with no hosted database refuses every database open, and
|
|
201
|
+
* its sign-in page explains the fix; rejecting here would keep the auth and org
|
|
202
|
+
* plugins from mounting that page, as `runMigrations` never did. Every later
|
|
203
|
+
* query raises the same refusal, so nothing downstream reads as migrated.
|
|
204
|
+
*/
|
|
205
|
+
// By name rather than `instanceof`: this module must not import the class from
|
|
206
|
+
// `client.js`, which nearly every spec mocks, and the runner wraps the error.
|
|
207
|
+
function databaseRefusal(err) {
|
|
208
|
+
for (let cause = err; cause; cause = cause.cause) {
|
|
209
|
+
if (cause.name === "HostedRuntimeLocalDatabaseError") {
|
|
210
|
+
return cause;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
async function tolerateRefusedDatabase(storeId, apply) {
|
|
216
|
+
try {
|
|
217
|
+
await apply();
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
const refusal = databaseRefusal(err);
|
|
221
|
+
if (!refusal || isReleaseStep())
|
|
222
|
+
throw err;
|
|
223
|
+
console.error(`[db] Skipping store "${storeId}". ${refusal.message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Records an entry in a `runMigrations` list's own bookkeeping as that runner
|
|
228
|
+
* would after applying it: by name when named, and the version only when it
|
|
229
|
+
* advances `MAX(version)`, the one value that gates unnamed entries.
|
|
230
|
+
*/
|
|
231
|
+
async function recordLegacyMigrationEntry(exec, table, entry) {
|
|
232
|
+
await retryOnDdlRace(() => exec.execute(`CREATE TABLE IF NOT EXISTS ${table} (version BIGINT PRIMARY KEY)`));
|
|
233
|
+
await retryOnDdlRace(() => exec.execute(`CREATE TABLE IF NOT EXISTS ${table}_named (name TEXT PRIMARY KEY, version BIGINT, applied_at TIMESTAMP NOT NULL DEFAULT now())`));
|
|
234
|
+
if (entry.name) {
|
|
135
235
|
await exec.execute({
|
|
136
|
-
sql: `INSERT INTO ${
|
|
137
|
-
args: [
|
|
138
|
-
store.id,
|
|
139
|
-
migration.name,
|
|
140
|
-
Date.now(),
|
|
141
|
-
migrationChecksum(migration),
|
|
142
|
-
migration.kind ?? "schema",
|
|
143
|
-
],
|
|
236
|
+
sql: `INSERT INTO ${table}_named (name, version) VALUES (?, ?) ON CONFLICT DO NOTHING`,
|
|
237
|
+
args: [entry.name, entry.version],
|
|
144
238
|
});
|
|
145
|
-
applied.add(migration.name);
|
|
146
239
|
}
|
|
147
|
-
|
|
240
|
+
const { rows } = await exec.execute(`SELECT MAX(version) AS v FROM ${table}`);
|
|
241
|
+
if (entry.version > Number(rows[0]?.v ?? 0)) {
|
|
242
|
+
await exec.execute({
|
|
243
|
+
sql: `INSERT INTO ${table} (version) VALUES (?) ON CONFLICT DO NOTHING`,
|
|
244
|
+
args: [entry.version],
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function recordMigration(exec, store, migration, source) {
|
|
249
|
+
await exec.execute({
|
|
250
|
+
sql: `INSERT INTO ${STORE_MIGRATIONS_TABLE} (store_id, name, applied_at, checksum, kind, source) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (store_id, name) DO NOTHING`,
|
|
251
|
+
args: [
|
|
252
|
+
store.id,
|
|
253
|
+
migration.name,
|
|
254
|
+
Date.now(),
|
|
255
|
+
migrationChecksum(migration),
|
|
256
|
+
migration.kind ?? "schema",
|
|
257
|
+
source,
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Reads a `runMigrations` table's bookkeeping with its own gating: an unnamed
|
|
263
|
+
* entry counts as applied when its version is at or below `MAX(version)`, a
|
|
264
|
+
* named one only when its name is in `${table}_named`. A missing table means
|
|
265
|
+
* nothing was applied. Existence is checked with `to_regclass` so a missing
|
|
266
|
+
* table never aborts a caller's open transaction.
|
|
267
|
+
*/
|
|
268
|
+
class LegacyBookkeeping {
|
|
269
|
+
exec;
|
|
270
|
+
cache = new Map();
|
|
271
|
+
constructor(exec) {
|
|
272
|
+
this.exec = exec;
|
|
273
|
+
}
|
|
274
|
+
async recorded(ref) {
|
|
275
|
+
const state = await this.read(ref.table);
|
|
276
|
+
return ref.name
|
|
277
|
+
? state.names.has(ref.name)
|
|
278
|
+
: ref.version <= state.maxVersion;
|
|
279
|
+
}
|
|
280
|
+
read(table) {
|
|
281
|
+
let pending = this.cache.get(table);
|
|
282
|
+
if (!pending)
|
|
283
|
+
this.cache.set(table, (pending = this.load(table)));
|
|
284
|
+
return pending;
|
|
285
|
+
}
|
|
286
|
+
async load(table) {
|
|
287
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) {
|
|
288
|
+
throw new Error(`Invalid legacy migration table "${table}"`);
|
|
289
|
+
}
|
|
290
|
+
const exists = async (name) => {
|
|
291
|
+
const { rows } = await this.exec.execute({
|
|
292
|
+
sql: "SELECT to_regclass(?) IS NOT NULL AS present",
|
|
293
|
+
args: [name],
|
|
294
|
+
});
|
|
295
|
+
const present = rows[0]?.present;
|
|
296
|
+
return present === true || present === "t";
|
|
297
|
+
};
|
|
298
|
+
let maxVersion = 0;
|
|
299
|
+
const names = new Set();
|
|
300
|
+
if (await exists(table)) {
|
|
301
|
+
const { rows } = await this.exec.execute(`SELECT MAX(version) AS v FROM ${table}`);
|
|
302
|
+
maxVersion = Number(rows[0]?.v ?? 0);
|
|
303
|
+
}
|
|
304
|
+
if (await exists(`${table}_named`)) {
|
|
305
|
+
const { rows } = await this.exec.execute(
|
|
306
|
+
// guard:allow-unbounded-read — one row per applied entry of one migration list
|
|
307
|
+
`SELECT name FROM ${table}_named`);
|
|
308
|
+
for (const row of rows)
|
|
309
|
+
names.add(String(row.name));
|
|
310
|
+
}
|
|
311
|
+
return { maxVersion, names };
|
|
312
|
+
}
|
|
148
313
|
}
|
|
149
314
|
async function replay(store) {
|
|
150
315
|
const exec = getDbExec();
|
|
316
|
+
const bootLists = new Map();
|
|
151
317
|
for (const migration of store.migrations) {
|
|
318
|
+
if (migration.legacy?.ranAtBoot && migration.legacy.entry) {
|
|
319
|
+
const list = bootLists.get(migration.legacy.table) ?? [];
|
|
320
|
+
list.push(migration.legacy.entry);
|
|
321
|
+
bootLists.set(migration.legacy.table, list);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
// A request never runs a backfill or seed, nor an entry carried over from
|
|
325
|
+
// a release-only `runMigrations` list: neither ever ran in these apps'
|
|
326
|
+
// requests, and versioned entries are not all safe to repeat.
|
|
327
|
+
if (migration.kind === "data" || migration.legacy)
|
|
328
|
+
continue;
|
|
152
329
|
await runMigration(migration, exec);
|
|
153
330
|
}
|
|
331
|
+
// Lists that ran at boot on main keep their own bookkeeping here, so these
|
|
332
|
+
// apps neither re-run nor skip an entry when they upgrade.
|
|
333
|
+
for (const [table, entries] of bootLists) {
|
|
334
|
+
await runMigrations(entries, { table })(undefined);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Registry order with each store's `after` dependencies first. Fails on an
|
|
339
|
+
* unknown id or a cycle rather than guessing an order.
|
|
340
|
+
*/
|
|
341
|
+
export function orderStoresForRelease(stores) {
|
|
342
|
+
const byId = new Map(stores.map((store) => [store.id, store]));
|
|
343
|
+
const ordered = [];
|
|
344
|
+
const state = new Map();
|
|
345
|
+
const visit = (store, path) => {
|
|
346
|
+
const seen = state.get(store.id);
|
|
347
|
+
if (seen === "done")
|
|
348
|
+
return;
|
|
349
|
+
if (seen === "visiting") {
|
|
350
|
+
throw new Error(`Store order cycle: ${[...path, store.id].join(" -> ")}`);
|
|
351
|
+
}
|
|
352
|
+
state.set(store.id, "visiting");
|
|
353
|
+
for (const dependency of store.after ?? []) {
|
|
354
|
+
const before = byId.get(dependency);
|
|
355
|
+
if (!before) {
|
|
356
|
+
throw new Error(`Store "${store.id}" must run after unknown store "${dependency}"`);
|
|
357
|
+
}
|
|
358
|
+
visit(before, [...path, store.id]);
|
|
359
|
+
}
|
|
360
|
+
state.set(store.id, "done");
|
|
361
|
+
ordered.push(store);
|
|
362
|
+
};
|
|
363
|
+
for (const store of stores)
|
|
364
|
+
visit(store, []);
|
|
365
|
+
return ordered;
|
|
154
366
|
}
|
|
155
|
-
|
|
156
|
-
|
|
367
|
+
/**
|
|
368
|
+
* Ledger state is per database executor, not per process. A transaction
|
|
369
|
+
* (`withDbExec(tx, ...)`) gets its own, so nothing it records can outlive a
|
|
370
|
+
* rollback; tests that hand each case a fresh database get a fresh ledger
|
|
371
|
+
* view; and `closeDbExec()` replaces the shared executor, which starts over.
|
|
372
|
+
*/
|
|
373
|
+
let ledgerStates = new WeakMap();
|
|
157
374
|
const allPending = new Set();
|
|
158
375
|
let stateGeneration;
|
|
376
|
+
function ledgerStateFor(exec) {
|
|
377
|
+
let state = ledgerStates.get(exec);
|
|
378
|
+
if (!state)
|
|
379
|
+
ledgerStates.set(exec, (state = { present: false }));
|
|
380
|
+
return state;
|
|
381
|
+
}
|
|
159
382
|
/**
|
|
160
|
-
*
|
|
161
|
-
* database clients (tests, dev reloads
|
|
162
|
-
*
|
|
383
|
+
* Process-wide memos (hosted replay, trust, skip) describe one database.
|
|
384
|
+
* Closing the database clients (tests, dev reloads) may point the next query
|
|
385
|
+
* at another, so start over.
|
|
163
386
|
*/
|
|
164
387
|
function syncWithDatabaseGeneration() {
|
|
165
388
|
const generation = dbClientGeneration();
|
|
166
389
|
if (stateGeneration === generation)
|
|
167
390
|
return;
|
|
168
391
|
stateGeneration = generation;
|
|
169
|
-
|
|
170
|
-
ledgerPresent = false;
|
|
392
|
+
ledgerStates = new WeakMap();
|
|
171
393
|
for (const pending of allPending)
|
|
172
394
|
pending.clear();
|
|
173
395
|
}
|
|
174
|
-
async function loadLedger() {
|
|
175
|
-
const { rows } = await
|
|
396
|
+
async function loadLedger(exec) {
|
|
397
|
+
const { rows } = await exec.execute(
|
|
176
398
|
// guard:allow-unbounded-read — one row per declared migration; this is the single shared read
|
|
177
399
|
`SELECT store_id, name FROM ${STORE_MIGRATIONS_TABLE}`);
|
|
178
400
|
const ledger = new Map();
|
|
@@ -185,11 +407,12 @@ async function loadLedger() {
|
|
|
185
407
|
}
|
|
186
408
|
return ledger;
|
|
187
409
|
}
|
|
188
|
-
function readLedgerSnapshot() {
|
|
189
|
-
const
|
|
410
|
+
function readLedgerSnapshot(exec) {
|
|
411
|
+
const state = ledgerStateFor(exec);
|
|
412
|
+
const snapshot = (state.snapshot ??= loadLedger(exec));
|
|
190
413
|
snapshot.catch(() => {
|
|
191
|
-
if (
|
|
192
|
-
|
|
414
|
+
if (state.snapshot === snapshot)
|
|
415
|
+
state.snapshot = undefined;
|
|
193
416
|
});
|
|
194
417
|
return snapshot;
|
|
195
418
|
}
|
|
@@ -199,8 +422,9 @@ function readLedgerSnapshot() {
|
|
|
199
422
|
* with the code it is about to publish fails there instead of in a request.
|
|
200
423
|
*/
|
|
201
424
|
export async function findUnappliedStoreMigrations(stores) {
|
|
202
|
-
|
|
203
|
-
|
|
425
|
+
const exec = getDbExec();
|
|
426
|
+
ledgerStateFor(exec).snapshot = undefined;
|
|
427
|
+
const ledger = await readLedgerSnapshot(exec);
|
|
204
428
|
return stores
|
|
205
429
|
.map((store) => ({
|
|
206
430
|
storeId: store.id,
|
|
@@ -313,25 +537,42 @@ export function defineStore(definition) {
|
|
|
313
537
|
}
|
|
314
538
|
const pending = new Map();
|
|
315
539
|
allPending.add(pending);
|
|
540
|
+
// The release step also runs list-origin migrations that local runtimes
|
|
541
|
+
// skip, so its readiness is memoized separately.
|
|
542
|
+
let applied = new WeakMap();
|
|
543
|
+
let appliedAtRelease = new WeakMap();
|
|
316
544
|
const store = {
|
|
317
545
|
id,
|
|
546
|
+
after: definition.after,
|
|
318
547
|
releaseOnly: definition.releaseOnly,
|
|
319
548
|
migrations,
|
|
320
549
|
ready() {
|
|
321
550
|
syncWithDatabaseGeneration();
|
|
322
551
|
const mode = readiness();
|
|
552
|
+
if (mode === "apply") {
|
|
553
|
+
return tolerateRefusedDatabase(id, () => {
|
|
554
|
+
const exec = getDbExec();
|
|
555
|
+
const memo = isReleaseStep() ? appliedAtRelease : applied;
|
|
556
|
+
const existing = memo.get(exec);
|
|
557
|
+
if (existing)
|
|
558
|
+
return existing;
|
|
559
|
+
const tracked = retainRequestDbPoolScope(applyWithLedger(store, exec).catch((err) => {
|
|
560
|
+
if (memo.get(exec) === tracked)
|
|
561
|
+
memo.delete(exec);
|
|
562
|
+
throw err;
|
|
563
|
+
}));
|
|
564
|
+
memo.set(exec, tracked);
|
|
565
|
+
return tracked;
|
|
566
|
+
});
|
|
567
|
+
}
|
|
323
568
|
const existing = pending.get(mode);
|
|
324
569
|
if (existing)
|
|
325
570
|
return existing;
|
|
326
|
-
const run = mode === "
|
|
327
|
-
?
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
: withMigrationExecutionRuntime(() => replay(store))
|
|
332
|
-
: mode === "replay"
|
|
333
|
-
? replay(store)
|
|
334
|
-
: Promise.resolve();
|
|
571
|
+
const run = mode === "hosted-replay"
|
|
572
|
+
? definition.releaseOnly
|
|
573
|
+
? Promise.resolve()
|
|
574
|
+
: withMigrationExecutionRuntime(() => replay(store))
|
|
575
|
+
: Promise.resolve();
|
|
335
576
|
// The memo outlives the request that started it; on Workers that
|
|
336
577
|
// request's pool must stay open until the work settles.
|
|
337
578
|
const tracked = retainRequestDbPoolScope(run.catch((err) => {
|
|
@@ -344,11 +585,12 @@ export function defineStore(definition) {
|
|
|
344
585
|
},
|
|
345
586
|
reset() {
|
|
346
587
|
pending.clear();
|
|
588
|
+
applied = new WeakMap();
|
|
589
|
+
appliedAtRelease = new WeakMap();
|
|
347
590
|
},
|
|
348
591
|
};
|
|
349
592
|
return store;
|
|
350
593
|
}
|
|
351
594
|
export function __resetStoreLedgerSnapshotForTests() {
|
|
352
|
-
|
|
353
|
-
ledgerPresent = false;
|
|
595
|
+
ledgerStates = new WeakMap();
|
|
354
596
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type MigrationEntry } from "./migrations.js";
|
|
2
|
+
import type { StoreMigration } from "./store-registry.js";
|
|
3
|
+
/**
|
|
4
|
+
* Turns `runMigrations` entries into store migrations that keep each entry's
|
|
5
|
+
* identity: a named entry keeps its name, an unnamed one becomes `v<version>`,
|
|
6
|
+
* and each carries a `legacy` reference so a database that already applied it
|
|
7
|
+
* through the old list records it instead of running it again. Multi-statement
|
|
8
|
+
* SQL is split here because the store runner sends one statement per call.
|
|
9
|
+
*/
|
|
10
|
+
export declare function fromMigrationEntries(table: string, entries: readonly MigrationEntry[], options?: {
|
|
11
|
+
data?: readonly (string | number)[];
|
|
12
|
+
/** The list also ran at plugin boot on main, not only at release. */
|
|
13
|
+
ranAtBoot?: boolean;
|
|
14
|
+
}): StoreMigration[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MIGRATION_DEFERRED, splitSqlStatements, } from "./migrations.js";
|
|
2
|
+
/**
|
|
3
|
+
* Turns `runMigrations` entries into store migrations that keep each entry's
|
|
4
|
+
* identity: a named entry keeps its name, an unnamed one becomes `v<version>`,
|
|
5
|
+
* and each carries a `legacy` reference so a database that already applied it
|
|
6
|
+
* through the old list records it instead of running it again. Multi-statement
|
|
7
|
+
* SQL is split here because the store runner sends one statement per call.
|
|
8
|
+
*/
|
|
9
|
+
export function fromMigrationEntries(table, entries, options = {}) {
|
|
10
|
+
const data = new Set(options.data ?? []);
|
|
11
|
+
return entries.map((entry) => {
|
|
12
|
+
const raw = typeof entry.sql === "string" ? entry.sql : entry.sql.postgres;
|
|
13
|
+
const name = entry.name ?? `v${entry.version}`;
|
|
14
|
+
const run = entry.run;
|
|
15
|
+
return {
|
|
16
|
+
name,
|
|
17
|
+
kind: data.has(name) || data.has(entry.version)
|
|
18
|
+
? "data"
|
|
19
|
+
: undefined,
|
|
20
|
+
sql: raw ? splitSqlStatements(raw) : undefined,
|
|
21
|
+
run: run
|
|
22
|
+
? async (exec) => {
|
|
23
|
+
if ((await run(exec)) === MIGRATION_DEFERRED) {
|
|
24
|
+
throw new Error(`Migration "${name}" deferred itself; store migrations cannot be deferred.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
: undefined,
|
|
28
|
+
legacy: {
|
|
29
|
+
table,
|
|
30
|
+
version: entry.version,
|
|
31
|
+
name: entry.name,
|
|
32
|
+
ranAtBoot: options.ranAtBoot,
|
|
33
|
+
entry: options.ranAtBoot ? entry : undefined,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
package/dist/db/widen-columns.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getDbExec } from "./client.js";
|
|
2
2
|
import { isHostedFunctionInvocationRuntime, isMigrationExecutingRuntime, isMigrationAuthorizedRuntime, isProductionServerlessFunctionRuntime, } from "./migration-runtime.js";
|
|
3
|
+
import { isRecordingStoreMigration } from "./runtime-facts.js";
|
|
3
4
|
const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4
5
|
/**
|
|
5
6
|
* Widen pre-existing 32-bit `integer` columns to 64-bit `BIGINT` in place on
|
|
@@ -48,8 +49,9 @@ export async function widenIntColumnsToBigInt(table, columns, injectedClient) {
|
|
|
48
49
|
}
|
|
49
50
|
catch (err) {
|
|
50
51
|
// At release the store ledger records success, so a skipped widen must fail.
|
|
51
|
-
if (isMigrationAuthorizedRuntime())
|
|
52
|
+
if (isMigrationAuthorizedRuntime() || isRecordingStoreMigration()) {
|
|
52
53
|
throw err;
|
|
54
|
+
}
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
55
57
|
for (const col of columns) {
|
|
@@ -59,8 +61,9 @@ export async function widenIntColumnsToBigInt(table, columns, injectedClient) {
|
|
|
59
61
|
await client.execute(`ALTER TABLE ${table} ALTER COLUMN ${col} TYPE BIGINT`);
|
|
60
62
|
}
|
|
61
63
|
catch (err) {
|
|
62
|
-
if (isMigrationAuthorizedRuntime())
|
|
64
|
+
if (isMigrationAuthorizedRuntime() || isRecordingStoreMigration()) {
|
|
63
65
|
throw err;
|
|
66
|
+
}
|
|
64
67
|
// A concurrent boot already widened it, or the role lacks ALTER — both
|
|
65
68
|
// are safe to ignore; a later boot retries if still needed.
|
|
66
69
|
}
|
|
@@ -517,6 +517,16 @@ export function buildExtensionHtml(content, themeVars, isDark, extensionId, bind
|
|
|
517
517
|
|
|
518
518
|
var _extensionId = ${extensionIdJson};
|
|
519
519
|
var _extensionBinding = ${bindingJson};
|
|
520
|
+
// Proves an open-url request comes from this shell document, not from a
|
|
521
|
+
// page the iframe later navigated to. Announced with the binding below.
|
|
522
|
+
// Without secure randomness there is no token, the host never offers to
|
|
523
|
+
// open links, and links keep their native behavior.
|
|
524
|
+
var _openUrlToken =
|
|
525
|
+
window.crypto && typeof window.crypto.getRandomValues === 'function'
|
|
526
|
+
? Array.prototype.map.call(window.crypto.getRandomValues(new Uint8Array(16)), function(b) {
|
|
527
|
+
return ('0' + b.toString(16)).slice(-2);
|
|
528
|
+
}).join('')
|
|
529
|
+
: '';
|
|
520
530
|
window.extensionBinding = _extensionBinding;
|
|
521
531
|
// Legacy alias for extension bodies authored before the rename.
|
|
522
532
|
window.toolBinding = _extensionBinding;
|
|
@@ -532,10 +542,67 @@ export function buildExtensionHtml(content, themeVars, isDark, extensionId, bind
|
|
|
532
542
|
type: 'agent-native-extension-binding',
|
|
533
543
|
extensionId: _extensionId,
|
|
534
544
|
binding: _extensionBinding,
|
|
545
|
+
openUrlToken: _openUrlToken,
|
|
535
546
|
},
|
|
536
547
|
'*',
|
|
537
548
|
);
|
|
538
549
|
} catch (_) {}
|
|
550
|
+
// A tab opened from this sandboxed iframe inherits the sandbox (an opaque
|
|
551
|
+
// origin with no cookies), so apps like HubSpot render blank. Once the host
|
|
552
|
+
// says it can, hand new-tab web links to it: it opens them unsandboxed with
|
|
553
|
+
// noopener, so the new tab gets no handle on the host page. Without that
|
|
554
|
+
// reply (no host, a standalone /render, or no token) links stay native, and
|
|
555
|
+
// window.open is never touched. Set up here, before extension content runs,
|
|
556
|
+
// so the reply can't be missed and extension globals can't interfere.
|
|
557
|
+
(function(token) {
|
|
558
|
+
if (!token) return;
|
|
559
|
+
var hostOpensLinks = false;
|
|
560
|
+
window.addEventListener('message', function(event) {
|
|
561
|
+
if (event.source !== window.parent) return;
|
|
562
|
+
var msg = event.data;
|
|
563
|
+
if (msg && msg.type === 'agent-native-extension-open-url-ready') hostOpensLinks = true;
|
|
564
|
+
});
|
|
565
|
+
function linkOf(e) {
|
|
566
|
+
var path = typeof e.composedPath === 'function' ? e.composedPath() : [e.target];
|
|
567
|
+
for (var i = 0; i < path.length; i++) {
|
|
568
|
+
var node = path[i];
|
|
569
|
+
if (node instanceof HTMLAnchorElement || node instanceof HTMLAreaElement) {
|
|
570
|
+
if (node.hasAttribute('href')) return { element: node, url: node };
|
|
571
|
+
} else if (node && node.namespaceURI === 'http://www.w3.org/2000/svg' && node.localName === 'a') {
|
|
572
|
+
// An SVG link's href isn't a URL string; resolve it the way an HTML
|
|
573
|
+
// link would, against the document's base URL.
|
|
574
|
+
var raw = node.getAttribute('href') || node.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
|
|
575
|
+
if (raw) {
|
|
576
|
+
var resolved = document.createElement('a');
|
|
577
|
+
resolved.href = raw;
|
|
578
|
+
return { element: node, url: resolved };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
function targetOf(link) {
|
|
585
|
+
if (link.hasAttribute('target')) return link.getAttribute('target');
|
|
586
|
+
var base = document.querySelector('base[target]');
|
|
587
|
+
return base ? base.getAttribute('target') : '';
|
|
588
|
+
}
|
|
589
|
+
function route(e) {
|
|
590
|
+
if (!hostOpensLinks || e.defaultPrevented) return;
|
|
591
|
+
var link = linkOf(e);
|
|
592
|
+
if (!link || link.element.hasAttribute('download')) return;
|
|
593
|
+
if (link.url.protocol !== 'http:' && link.url.protocol !== 'https:') return;
|
|
594
|
+
var newTab = e.type === 'auxclick'
|
|
595
|
+
? e.button === 1
|
|
596
|
+
: e.button === 0 && !e.altKey &&
|
|
597
|
+
(e.metaKey || e.ctrlKey || e.shiftKey || /^_blank$/i.test(targetOf(link.element)));
|
|
598
|
+
if (!newTab) return;
|
|
599
|
+
e.preventDefault();
|
|
600
|
+
window.parent.postMessage({ type: 'agent-native-extension-open-url', url: link.url.href, token: token }, '*');
|
|
601
|
+
}
|
|
602
|
+
// On window, so extension handlers delegated on document run first.
|
|
603
|
+
window.addEventListener('click', route);
|
|
604
|
+
window.addEventListener('auxclick', route);
|
|
605
|
+
})(_openUrlToken);
|
|
539
606
|
// SECURITY: when the viewer is not the author of this extension, emit a
|
|
540
607
|
// clear console warning. The bridge currently runs every helper with the
|
|
541
608
|
// viewer's session — a malicious shared extension can call any action,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getDbExec } from "../db/client.js";
|
|
2
2
|
import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
|
|
3
3
|
import { defineStore } from "../db/store-registry.js";
|
|
4
|
+
import { fromMigrationEntries } from "../db/versioned-store-migrations.js";
|
|
5
|
+
import { REMOTE_DEVICE_MIGRATIONS, REMOTE_DEVICE_MIGRATIONS_TABLE, } from "./remote-device-migrations.js";
|
|
4
6
|
export const remoteDevicesStore = defineStore({
|
|
5
7
|
id: "remote_devices",
|
|
6
8
|
migrations: [
|
|
@@ -35,6 +37,7 @@ export const remoteDevicesStore = defineStore({
|
|
|
35
37
|
await ensureIndexExists("idx_remote_devices_owner", `CREATE INDEX IF NOT EXISTS idx_remote_devices_owner ON integration_remote_devices(owner_email, org_id)`);
|
|
36
38
|
},
|
|
37
39
|
},
|
|
40
|
+
...fromMigrationEntries(REMOTE_DEVICE_MIGRATIONS_TABLE, REMOTE_DEVICE_MIGRATIONS),
|
|
38
41
|
],
|
|
39
42
|
});
|
|
40
43
|
export function ensureTable() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DbExec } from "../db/client.js";
|
|
2
|
-
import {
|
|
2
|
+
import type { MigrationEntry } from "../db/migrations.js";
|
|
3
3
|
export type AutomationRunStatus = "running" | "success" | "error" | "interrupted" | "skipped";
|
|
4
4
|
export interface AutomationRun {
|
|
5
5
|
id: string;
|
|
@@ -42,7 +42,8 @@ export declare function automationRunClaimLeaseMs(): number;
|
|
|
42
42
|
/** How many finished runs each automation retains, in addition to unfinished runs. */
|
|
43
43
|
export declare const RUNS_RETAINED_PER_AUTOMATION = 50;
|
|
44
44
|
export declare const AUTOMATION_RUN_MIGRATIONS: MigrationEntry[];
|
|
45
|
-
export
|
|
45
|
+
/** Kept for callers of the public export; the store owns these migrations. */
|
|
46
|
+
export declare function runAutomationRunMigrations(_nitroApp?: unknown): Promise<void>;
|
|
46
47
|
export declare const automationRunHistoryStore: import("../db/store-registry.js").Store;
|
|
47
48
|
export declare function ensureTable(): Promise<void>;
|
|
48
49
|
export interface StartAutomationRunOptions {
|
package/dist/jobs/run-history.js
CHANGED
|
@@ -3,8 +3,8 @@ import { z } from "zod";
|
|
|
3
3
|
import { resolveBackgroundRunHardTimeoutMs } from "../agent/run-manager.js";
|
|
4
4
|
import { getDbExec } from "../db/client.js";
|
|
5
5
|
import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
|
|
6
|
-
import { runMigrations } from "../db/migrations.js";
|
|
7
6
|
import { defineStore } from "../db/store-registry.js";
|
|
7
|
+
import { fromMigrationEntries } from "../db/versioned-store-migrations.js";
|
|
8
8
|
import { emit as emitBusEvent, registerEvent } from "../event-bus/index.js";
|
|
9
9
|
import { automationRecoveryMessagesForLocale } from "../localization/automation-recovery-messages.js";
|
|
10
10
|
import { decryptSecretValue, encryptSecretValue } from "../secrets/crypto.js";
|
|
@@ -122,10 +122,9 @@ export const AUTOMATION_RUN_MIGRATIONS = [
|
|
|
122
122
|
ADD COLUMN IF NOT EXISTS failure_alert_unsubscribe_token TEXT`,
|
|
123
123
|
},
|
|
124
124
|
];
|
|
125
|
-
export
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
})(nitroApp);
|
|
125
|
+
/** Kept for callers of the public export; the store owns these migrations. */
|
|
126
|
+
export async function runAutomationRunMigrations(_nitroApp) {
|
|
127
|
+
await automationRunHistoryStore.ready();
|
|
129
128
|
}
|
|
130
129
|
export const automationRunHistoryStore = defineStore({
|
|
131
130
|
id: "automation_run_history",
|
|
@@ -179,6 +178,7 @@ export const automationRunHistoryStore = defineStore({
|
|
|
179
178
|
await ensureIndexExists(`idx_${TABLE}_owner_automation`, indexSql);
|
|
180
179
|
},
|
|
181
180
|
},
|
|
181
|
+
...fromMigrationEntries("_automation_run_migrations", AUTOMATION_RUN_MIGRATIONS),
|
|
182
182
|
],
|
|
183
183
|
});
|
|
184
184
|
export function ensureTable() {
|