@agent-native/core 0.226.0 → 0.226.1-nightly-20261107004051
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/agent/execution-fence.d.ts +14 -0
- package/dist/agent/execution-fence.js +49 -1
- package/dist/agent/run-manager.js +18 -2
- package/dist/agent/run-store.js +81 -10
- package/dist/chat-threads/store.js +40 -0
- package/dist/client/use-action.js +5 -4
- package/dist/db/client.js +79 -30
- package/dist/db/schema-snapshot.generated.js +1 -0
- package/dist/deploy/build.d.ts +21 -0
- package/dist/deploy/build.js +67 -27
- package/dist/framework-package-versions.json +6 -6
- package/dist/server/builder-signup-experiment.js +72 -53
- package/dist/server/onboarding-html.js +7 -3
- package/dist/shared/auth-copy.js +11 -11
- package/package.json +3 -3
|
@@ -31,6 +31,20 @@ export declare function withExecutionLeaseRequested<T>(work: () => T): T;
|
|
|
31
31
|
export declare function withExecutionFence<T>(fence: ExecutionFence, work: () => T): T;
|
|
32
32
|
export declare function runWithoutExecutionFence<T>(work: () => T): T;
|
|
33
33
|
export declare function lockAndAssertCurrentExecutionFences(tx: DbExec, ignoredFence?: ExecutionFence): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* A turn lease that lapsed on its own still belongs to its worker: a stalled
|
|
36
|
+
* DB call or a blocked event loop outlived the 15s lease, and nobody acted on
|
|
37
|
+
* it. Ending the run here ends it with no terminal event, and the reaper later
|
|
38
|
+
* reports it as "stopped before finishing".
|
|
39
|
+
*
|
|
40
|
+
* Every revoke (Stop, the reaper, Agent Teams, A2A) sets `lease_expires_at`
|
|
41
|
+
* and `updated_at` to now together, and every new claim bumps the generation,
|
|
42
|
+
* so only a lease that expired well after its last renewal counts. The run row
|
|
43
|
+
* must also still be running at that generation; task leases have no such row
|
|
44
|
+
* and stay strict. Locking the lease row first orders this read after any
|
|
45
|
+
* revoke that committed and before any that has not.
|
|
46
|
+
*/
|
|
47
|
+
export declare function ownsLapsedTurnLease(tx: DbExec, fence: ExecutionFence): Promise<boolean>;
|
|
34
48
|
export declare function withFencedExecutionWrite<T>(write: (tx: DbExec) => Promise<T>, scope?: {
|
|
35
49
|
runId?: string;
|
|
36
50
|
threadId?: string;
|
|
@@ -92,10 +92,58 @@ export async function lockAndAssertCurrentExecutionFences(tx, ignoredFence) {
|
|
|
92
92
|
activeFence.generation,
|
|
93
93
|
],
|
|
94
94
|
});
|
|
95
|
-
if (rows.length
|
|
95
|
+
if (rows.length === 1)
|
|
96
|
+
continue;
|
|
97
|
+
if (activeFence.outlivesStop ||
|
|
98
|
+
!(await ownsLapsedTurnLease(tx, activeFence))) {
|
|
96
99
|
throw new StaleExecutionFenceError(activeFence);
|
|
100
|
+
}
|
|
97
101
|
}
|
|
98
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* A turn lease that lapsed on its own still belongs to its worker: a stalled
|
|
105
|
+
* DB call or a blocked event loop outlived the 15s lease, and nobody acted on
|
|
106
|
+
* it. Ending the run here ends it with no terminal event, and the reaper later
|
|
107
|
+
* reports it as "stopped before finishing".
|
|
108
|
+
*
|
|
109
|
+
* Every revoke (Stop, the reaper, Agent Teams, A2A) sets `lease_expires_at`
|
|
110
|
+
* and `updated_at` to now together, and every new claim bumps the generation,
|
|
111
|
+
* so only a lease that expired well after its last renewal counts. The run row
|
|
112
|
+
* must also still be running at that generation; task leases have no such row
|
|
113
|
+
* and stay strict. Locking the lease row first orders this read after any
|
|
114
|
+
* revoke that committed and before any that has not.
|
|
115
|
+
*/
|
|
116
|
+
export async function ownsLapsedTurnLease(tx, fence) {
|
|
117
|
+
const lease = await tx.execute({
|
|
118
|
+
sql: `SELECT generation
|
|
119
|
+
FROM agent_execution_leases
|
|
120
|
+
WHERE thread_id = ?
|
|
121
|
+
AND turn_id = ?
|
|
122
|
+
AND owner_run_id = ?
|
|
123
|
+
AND generation = CAST(? AS BIGINT)
|
|
124
|
+
AND lease_expires_at >= updated_at + INTERVAL '1 second'
|
|
125
|
+
FOR UPDATE`,
|
|
126
|
+
args: [fence.threadId, fence.turnId, fence.runId, fence.generation],
|
|
127
|
+
timeoutMs: LAPSED_LEASE_STATEMENT_TIMEOUT_MS,
|
|
128
|
+
maxAttempts: 1,
|
|
129
|
+
});
|
|
130
|
+
if (lease.rows.length !== 1)
|
|
131
|
+
return false;
|
|
132
|
+
const run = await tx.execute({
|
|
133
|
+
sql: `SELECT 1
|
|
134
|
+
FROM agent_runs
|
|
135
|
+
WHERE id = ?
|
|
136
|
+
AND status = 'running'
|
|
137
|
+
AND worker_generation = CAST(? AS BIGINT)`,
|
|
138
|
+
args: [fence.runId, fence.generation],
|
|
139
|
+
timeoutMs: LAPSED_LEASE_STATEMENT_TIMEOUT_MS,
|
|
140
|
+
maxAttempts: 1,
|
|
141
|
+
});
|
|
142
|
+
return run.rows.length === 1;
|
|
143
|
+
}
|
|
144
|
+
// Bounded like the lease renewal that calls it, so a lapsed lease never holds
|
|
145
|
+
// a pooled connection open past the next heartbeat.
|
|
146
|
+
const LAPSED_LEASE_STATEMENT_TIMEOUT_MS = 4_000;
|
|
99
147
|
export async function withFencedExecutionWrite(write, scope) {
|
|
100
148
|
const context = executionFenceStorage.getStore();
|
|
101
149
|
const fences = context?.fences ?? [];
|
|
@@ -54,6 +54,8 @@ async function flushRunEventsBeforeAbort(run) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
const CLEANUP_DELAY_MS = 5 * 60 * 1000;
|
|
57
|
+
const HEARTBEAT_ATTEMPT_DEADLINE_MS = 5_000;
|
|
58
|
+
const MAX_PENDING_HEARTBEATS = 2;
|
|
57
59
|
export const DEFAULT_HOSTED_RUN_SOFT_TIMEOUT_MS = 40_000;
|
|
58
60
|
export const HOSTED_SOFT_TIMEOUT_CEILING_MS = 40_000;
|
|
59
61
|
export const DEFAULT_BACKGROUND_NO_PROGRESS_TIMEOUT_MS = RUN_NO_PROGRESS_HARD_TIMEOUT_MS;
|
|
@@ -1313,9 +1315,20 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1313
1315
|
let consecutiveHeartbeatFailures = 0;
|
|
1314
1316
|
let heartbeatInFlight = false;
|
|
1315
1317
|
let livenessResolved = false;
|
|
1318
|
+
let heartbeatAttempt = 0;
|
|
1319
|
+
let pendingHeartbeats = 0;
|
|
1316
1320
|
const heartbeatTimer = setInterval(() => {
|
|
1317
|
-
if (!heartbeatInFlight) {
|
|
1321
|
+
if (!heartbeatInFlight && pendingHeartbeats < MAX_PENDING_HEARTBEATS) {
|
|
1318
1322
|
heartbeatInFlight = true;
|
|
1323
|
+
pendingHeartbeats += 1;
|
|
1324
|
+
const attempt = ++heartbeatAttempt;
|
|
1325
|
+
// One renewal stuck acquiring a connection must not hold the slot until
|
|
1326
|
+
// the lease lapses, so a later tick retries while it settles. Only one
|
|
1327
|
+
// retry may overlap it, or a stalled pool would fill with renewals.
|
|
1328
|
+
const releaseStuckHeartbeat = setTimeout(() => {
|
|
1329
|
+
if (heartbeatAttempt === attempt)
|
|
1330
|
+
heartbeatInFlight = false;
|
|
1331
|
+
}, HEARTBEAT_ATTEMPT_DEADLINE_MS);
|
|
1319
1332
|
(options?.executionLease
|
|
1320
1333
|
? executionFencePromise.then(async (fence) => {
|
|
1321
1334
|
if (!fence)
|
|
@@ -1353,7 +1366,10 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1353
1366
|
}
|
|
1354
1367
|
})
|
|
1355
1368
|
.finally(() => {
|
|
1356
|
-
|
|
1369
|
+
clearTimeout(releaseStuckHeartbeat);
|
|
1370
|
+
pendingHeartbeats -= 1;
|
|
1371
|
+
if (heartbeatAttempt === attempt)
|
|
1372
|
+
heartbeatInFlight = false;
|
|
1357
1373
|
});
|
|
1358
1374
|
}
|
|
1359
1375
|
void checkSqlAbort();
|
package/dist/agent/run-store.js
CHANGED
|
@@ -18,7 +18,7 @@ import { approvalKeyForHistory, approvalKeyForPrivateHistory, PRIVATE_APPROVAL_U
|
|
|
18
18
|
import { admitAutoContinue, admitManualContinue, } from "./auto-continue.js";
|
|
19
19
|
import { AGENT_CHAT_RECOVERY_OF_RUN_FIELD } from "./durable-background.js";
|
|
20
20
|
import { LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./engine/credential-errors.js";
|
|
21
|
-
import { currentExecutionFence, currentExecutionFenceForScope, ExecutionFenceError, MissingExecutionFenceError, lockAndAssertCurrentExecutionFences, StaleExecutionFenceError, withFencedExecutionWrite, } from "./execution-fence.js";
|
|
21
|
+
import { currentExecutionFence, currentExecutionFenceForScope, ExecutionFenceError, MissingExecutionFenceError, lockAndAssertCurrentExecutionFences, ownsLapsedTurnLease, StaleExecutionFenceError, withFencedExecutionWrite, } from "./execution-fence.js";
|
|
22
22
|
import { AGENT_RUN_MIGRATIONS, AGENT_RUN_MIGRATIONS_TABLE, } from "./run-migrations.js";
|
|
23
23
|
import { lockThreadRunSlot } from "./run-slot-lock.js";
|
|
24
24
|
import { trackRunTerminal, } from "./run-terminal-tracking.js";
|
|
@@ -295,7 +295,16 @@ export const agentRunsStore = defineStore({
|
|
|
295
295
|
...fromMigrationEntries(AGENT_RUN_MIGRATIONS_TABLE, AGENT_RUN_MIGRATIONS),
|
|
296
296
|
{
|
|
297
297
|
name: "thread-projection-retry-quarantine-runtime-schema",
|
|
298
|
+
// Stands in for legacy v9–v11, which a local boot skips: a database
|
|
299
|
+
// whose baseline was already recorded never re-runs the baseline's
|
|
300
|
+
// column ensures either, so this entry must create every column it
|
|
301
|
+
// touches before the index that reads them. The retry_order backfill
|
|
302
|
+
// does not belong here: a database that already recorded this entry
|
|
303
|
+
// never runs an edited body again, so the backfill lives in the
|
|
304
|
+
// append-only repair entry at the end of this list.
|
|
298
305
|
run: async (exec) => {
|
|
306
|
+
await ensureColumnExists("agent_runs", "thread_projection_pending", "ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS thread_projection_pending BOOLEAN NOT NULL DEFAULT FALSE");
|
|
307
|
+
await ensureColumnExists("agent_runs", "thread_projection_retry_order", "ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS thread_projection_retry_order BIGINT");
|
|
299
308
|
await exec.execute({
|
|
300
309
|
sql: "ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS thread_projection_error_code TEXT",
|
|
301
310
|
});
|
|
@@ -406,12 +415,64 @@ export const agentRunsStore = defineStore({
|
|
|
406
415
|
)`,
|
|
407
416
|
],
|
|
408
417
|
},
|
|
418
|
+
{
|
|
419
|
+
name: "thread-projection-retry-order-backfill-repair",
|
|
420
|
+
kind: "data",
|
|
421
|
+
// Append-only repair for databases that recorded
|
|
422
|
+
// thread-projection-retry-quarantine-runtime-schema before its
|
|
423
|
+
// column ensures existed: the ledger skips that entry forever, so
|
|
424
|
+
// the retry_order backfill legacy v10 performed for pending rows
|
|
425
|
+
// has to land here instead of in an edited body. Marked data so
|
|
426
|
+
// hosted replays skip it entirely. The self-check makes the body
|
|
427
|
+
// once-only even for a runtime whose ledger view is unavailable:
|
|
428
|
+
// re-running the backfill against live rows would re-impose queue
|
|
429
|
+
// order on projections that are still being written.
|
|
430
|
+
run: async (exec) => {
|
|
431
|
+
const alreadyApplied = await exec.execute({
|
|
432
|
+
// Literal name of STORE_MIGRATIONS_TABLE, kept interpolation-free.
|
|
433
|
+
sql: "SELECT 1 FROM _an_store_migrations WHERE store_id = 'agent_runs' AND name = ? LIMIT 1",
|
|
434
|
+
args: ["thread-projection-retry-order-backfill-repair"],
|
|
435
|
+
});
|
|
436
|
+
if (alreadyApplied.rows.length > 0)
|
|
437
|
+
return;
|
|
438
|
+
await ensureColumnExists("agent_runs", "thread_projection_pending", "ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS thread_projection_pending BOOLEAN NOT NULL DEFAULT FALSE");
|
|
439
|
+
await ensureColumnExists("agent_runs", "thread_projection_retry_order", "ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS thread_projection_retry_order BIGINT");
|
|
440
|
+
await exec.execute({
|
|
441
|
+
sql: "CREATE SEQUENCE IF NOT EXISTS agent_run_projection_retry_order_seq",
|
|
442
|
+
});
|
|
443
|
+
await exec.execute({
|
|
444
|
+
sql: `CREATE INDEX IF NOT EXISTS agent_runs_thread_projection_retry_order_idx
|
|
445
|
+
ON agent_runs (thread_projection_retry_order ASC NULLS FIRST, completed_at ASC, id ASC)
|
|
446
|
+
WHERE thread_projection_pending IS TRUE`,
|
|
447
|
+
});
|
|
448
|
+
const unassigned = await exec.execute({
|
|
449
|
+
sql: `SELECT 1 FROM agent_runs
|
|
450
|
+
WHERE thread_projection_pending IS TRUE
|
|
451
|
+
AND thread_projection_retry_order IS NULL
|
|
452
|
+
LIMIT 1`,
|
|
453
|
+
args: [],
|
|
454
|
+
});
|
|
455
|
+
if (unassigned.rows.length === 0)
|
|
456
|
+
return;
|
|
457
|
+
await exec.execute({
|
|
458
|
+
sql: `UPDATE agent_runs
|
|
459
|
+
SET thread_projection_retry_order = nextval('agent_run_projection_retry_order_seq')
|
|
460
|
+
WHERE thread_projection_pending IS TRUE
|
|
461
|
+
AND thread_projection_retry_order IS NULL`,
|
|
462
|
+
});
|
|
463
|
+
},
|
|
464
|
+
},
|
|
409
465
|
],
|
|
410
466
|
});
|
|
411
467
|
export function ensureRunTables() {
|
|
412
468
|
return agentRunsStore.ready();
|
|
413
469
|
}
|
|
414
470
|
export const TURN_EXECUTION_LEASE_MS = 15_000;
|
|
471
|
+
// A hung renewal holds the worker's only heartbeat slot, so the database gives
|
|
472
|
+
// up on each of its statements well inside the lease and the next tick tries
|
|
473
|
+
// again. Set once for the renewal's own short transaction: it runs every
|
|
474
|
+
// 1.5 s per active run, so a per-statement setting would triple its queries.
|
|
475
|
+
const LEASE_RENEWAL_STATEMENT_TIMEOUT_MS = 4_000;
|
|
415
476
|
function truthySqlValue(value) {
|
|
416
477
|
return value === true || value === "t" || value === 1 || value === "1";
|
|
417
478
|
}
|
|
@@ -525,24 +586,34 @@ async function renewExecutionLease(fence, renewRunLiveness) {
|
|
|
525
586
|
throw new Error("Turn execution leases require transaction support");
|
|
526
587
|
}
|
|
527
588
|
await client.transaction(async (tx) => {
|
|
528
|
-
|
|
589
|
+
await tx.execute({
|
|
590
|
+
sql: "SELECT set_config('statement_timeout', ?, true)",
|
|
591
|
+
args: [`${LEASE_RENEWAL_STATEMENT_TIMEOUT_MS}ms`],
|
|
592
|
+
});
|
|
593
|
+
const renew = (requireLive) => tx.execute({
|
|
529
594
|
sql: `UPDATE agent_execution_leases
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
595
|
+
SET lease_expires_at = clock_timestamp() +
|
|
596
|
+
(CAST(? AS BIGINT) * INTERVAL '1 millisecond'),
|
|
597
|
+
updated_at = clock_timestamp()
|
|
598
|
+
WHERE thread_id = ? AND turn_id = ? AND owner_run_id = ?
|
|
599
|
+
AND generation = CAST(? AS BIGINT)
|
|
600
|
+
AND (lease_expires_at > clock_timestamp() OR NOT CAST(? AS BOOLEAN))`,
|
|
536
601
|
args: [
|
|
537
602
|
TURN_EXECUTION_LEASE_MS,
|
|
538
603
|
fence.threadId,
|
|
539
604
|
fence.turnId,
|
|
540
605
|
fence.runId,
|
|
541
606
|
fence.generation,
|
|
607
|
+
requireLive,
|
|
542
608
|
],
|
|
543
609
|
});
|
|
544
|
-
if (
|
|
545
|
-
|
|
610
|
+
if ((await renew(true)).rowsAffected !== 1) {
|
|
611
|
+
if (!renewRunLiveness ||
|
|
612
|
+
!(await ownsLapsedTurnLease(tx, fence)) ||
|
|
613
|
+
(await renew(false)).rowsAffected !== 1) {
|
|
614
|
+
throw new StaleExecutionFenceError(fence);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
546
617
|
if (renewRunLiveness) {
|
|
547
618
|
await tx.execute({
|
|
548
619
|
sql: `UPDATE agent_runs
|
|
@@ -172,6 +172,46 @@ export const chatThreadsStore = defineStore({
|
|
|
172
172
|
PRIMARY KEY (thread_id, user_email)
|
|
173
173
|
)`,
|
|
174
174
|
},
|
|
175
|
+
{
|
|
176
|
+
// Stands in for the inbox entries of the folded schema list (v5–v8),
|
|
177
|
+
// which a local boot skips: a database whose snapshot or baseline was
|
|
178
|
+
// already recorded otherwise keeps the v5 inbox shape while the
|
|
179
|
+
// queued-submission queries select the v6/v8 columns. The ensure*
|
|
180
|
+
// wrappers probe the catalog first, so a hosted replay of this entry
|
|
181
|
+
// skips the lock-taking DDL once the shape exists.
|
|
182
|
+
name: "chat-thread-inbox-runtime-schema",
|
|
183
|
+
run: async () => {
|
|
184
|
+
await ensureTableExists("agent_chat_submission_inbox", `CREATE TABLE IF NOT EXISTS agent_chat_submission_inbox (
|
|
185
|
+
thread_id TEXT NOT NULL,
|
|
186
|
+
message_id TEXT NOT NULL,
|
|
187
|
+
state TEXT NOT NULL,
|
|
188
|
+
actor_email TEXT NOT NULL,
|
|
189
|
+
actor_auth_user_id TEXT,
|
|
190
|
+
actor_identity_authenticated_at_ms BIGINT,
|
|
191
|
+
actor_name TEXT,
|
|
192
|
+
actor_org_id TEXT,
|
|
193
|
+
actor_org_scope TEXT,
|
|
194
|
+
actor_anonymous BOOLEAN NOT NULL DEFAULT FALSE,
|
|
195
|
+
payload_hash TEXT NOT NULL,
|
|
196
|
+
payload_json TEXT NOT NULL,
|
|
197
|
+
interrupt_active_run BOOLEAN NOT NULL DEFAULT FALSE,
|
|
198
|
+
submitted_run_id TEXT,
|
|
199
|
+
queue_order BIGSERIAL NOT NULL,
|
|
200
|
+
move_to_top_at BIGINT,
|
|
201
|
+
retry_after BIGINT,
|
|
202
|
+
created_at BIGINT NOT NULL,
|
|
203
|
+
updated_at BIGINT NOT NULL,
|
|
204
|
+
PRIMARY KEY (thread_id, message_id)
|
|
205
|
+
)`);
|
|
206
|
+
await ensureIndexExists("agent_chat_submission_inbox_pending_idx", `CREATE INDEX IF NOT EXISTS agent_chat_submission_inbox_pending_idx
|
|
207
|
+
ON agent_chat_submission_inbox (state, retry_after, interrupt_active_run DESC, move_to_top_at DESC, queue_order ASC)`);
|
|
208
|
+
await ensureColumnExists("agent_chat_submission_inbox", "interrupt_run_id", "ALTER TABLE agent_chat_submission_inbox ADD COLUMN IF NOT EXISTS interrupt_run_id TEXT");
|
|
209
|
+
await ensureColumnExists("agent_chat_submission_inbox", "retry_count", "ALTER TABLE agent_chat_submission_inbox ADD COLUMN IF NOT EXISTS retry_count INTEGER NOT NULL DEFAULT 0");
|
|
210
|
+
await ensureIndexExists("agent_chat_submission_inbox_thread_pending_idx", `CREATE INDEX IF NOT EXISTS agent_chat_submission_inbox_thread_pending_idx
|
|
211
|
+
ON agent_chat_submission_inbox (thread_id, interrupt_active_run DESC, move_to_top_at DESC, queue_order ASC)
|
|
212
|
+
WHERE state = 'queued'`);
|
|
213
|
+
},
|
|
214
|
+
},
|
|
175
215
|
],
|
|
176
216
|
});
|
|
177
217
|
function ensureTable() {
|
|
@@ -235,10 +235,11 @@ function ensureUiActionCapability(signal) {
|
|
|
235
235
|
credentials: "same-origin",
|
|
236
236
|
cache: "no-store",
|
|
237
237
|
signal: controller?.signal,
|
|
238
|
-
}).then((res) => {
|
|
239
|
-
// The capability is the Set-Cookie header
|
|
240
|
-
// request open
|
|
241
|
-
|
|
238
|
+
}).then(async (res) => {
|
|
239
|
+
// The capability is the Set-Cookie header, but the body must still be
|
|
240
|
+
// read: an unread body keeps the request open so the page never reaches
|
|
241
|
+
// network idle, and a cancelled one is reported as an aborted request.
|
|
242
|
+
await res.arrayBuffer();
|
|
242
243
|
if (!res.ok) {
|
|
243
244
|
throw new Error("Could not establish the browser UI capability.");
|
|
244
245
|
}
|
package/dist/db/client.js
CHANGED
|
@@ -777,6 +777,82 @@ export function postgresStatementTimeoutMs(clientTimeoutMs) {
|
|
|
777
777
|
const headroomMs = Math.min(POSTGRES_STATEMENT_TIMEOUT_HEADROOM_MS, Math.max(1, Math.floor(safeTimeoutMs * 0.1)));
|
|
778
778
|
return Math.max(1, safeTimeoutMs - headroomMs);
|
|
779
779
|
}
|
|
780
|
+
// The CTE is materialized so the previous value is read before set_config
|
|
781
|
+
// replaces it; inlined, both calls share one projection with no order. A
|
|
782
|
+
// stricter cap the transaction already holds is kept; 0 means none.
|
|
783
|
+
const BOUND_STATEMENT_TIMEOUT_SQL = `WITH previous AS MATERIALIZED (
|
|
784
|
+
SELECT current_setting('statement_timeout') AS value,
|
|
785
|
+
setting::bigint AS ms
|
|
786
|
+
FROM pg_settings
|
|
787
|
+
WHERE name = 'statement_timeout'
|
|
788
|
+
)
|
|
789
|
+
SELECT value AS previous, ms AS previous_ms,
|
|
790
|
+
set_config(
|
|
791
|
+
'statement_timeout',
|
|
792
|
+
CASE WHEN ms > 0 AND ms <= CAST(? AS BIGINT) THEN value ELSE ? END,
|
|
793
|
+
true
|
|
794
|
+
)
|
|
795
|
+
FROM previous
|
|
796
|
+
LIMIT 1`;
|
|
797
|
+
const SET_STATEMENT_TIMEOUT_SQL = "SELECT set_config('statement_timeout', ?, true)";
|
|
798
|
+
/**
|
|
799
|
+
* Runs one budgeted statement inside a transaction under a server-side
|
|
800
|
+
* timeout. A client-side timeout only abandons the promise, so without it the
|
|
801
|
+
* statement keeps its row locks and its connection. The transaction's previous
|
|
802
|
+
* setting is put back once the statement succeeds, or the cap would apply to
|
|
803
|
+
* every later statement and override a stricter one set earlier; a statement
|
|
804
|
+
* that fails aborts the transaction, which leaves nothing to put back.
|
|
805
|
+
*/
|
|
806
|
+
async function withTransactionStatementTimeout(runSetting, timeoutMs, statement) {
|
|
807
|
+
const startedAt = Date.now();
|
|
808
|
+
const remainingMs = () => Math.max(1, timeoutMs - (Date.now() - startedAt));
|
|
809
|
+
const serverTimeout = () => postgresStatementTimeoutMs(remainingMs());
|
|
810
|
+
const applied = serverTimeout();
|
|
811
|
+
const bound = await runSetting({ sql: BOUND_STATEMENT_TIMEOUT_SQL, args: [applied, `${applied}ms`] }, remainingMs());
|
|
812
|
+
const row = bound[0];
|
|
813
|
+
const previous = row?.previous;
|
|
814
|
+
const previousMs = Number(row?.previous_ms);
|
|
815
|
+
if (typeof previous !== "string" || !Number.isFinite(previousMs)) {
|
|
816
|
+
throw new Error("Could not read the transaction's statement_timeout before bounding a statement");
|
|
817
|
+
}
|
|
818
|
+
let installed = previousMs > 0 && previousMs <= applied ? previousMs : applied;
|
|
819
|
+
// A slow round trip above leaves less of the budget than the server was
|
|
820
|
+
// given, so the statement could outlive its caller holding its locks.
|
|
821
|
+
// Tightening is itself a round trip, so a budget it also outruns is spent:
|
|
822
|
+
// the statement never starts.
|
|
823
|
+
if (installed >= remainingMs()) {
|
|
824
|
+
installed = serverTimeout();
|
|
825
|
+
await runSetting({ sql: SET_STATEMENT_TIMEOUT_SQL, args: [`${installed}ms`] }, remainingMs());
|
|
826
|
+
if (installed >= remainingMs()) {
|
|
827
|
+
// A caller may catch this and keep using the transaction.
|
|
828
|
+
await runSetting({ sql: SET_STATEMENT_TIMEOUT_SQL, args: [previous] }, POSTGRES_STATEMENT_TIMEOUT_RESET_MS);
|
|
829
|
+
throw Object.assign(new Error(`The ${timeoutMs}ms statement budget ran out while setting its server timeout`), { code: "STATEMENT_BUDGET_EXHAUSTED" });
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const result = await statement(remainingMs());
|
|
833
|
+
await runSetting({ sql: SET_STATEMENT_TIMEOUT_SQL, args: [previous] }, POSTGRES_STATEMENT_TIMEOUT_RESET_MS);
|
|
834
|
+
return result;
|
|
835
|
+
}
|
|
836
|
+
function postgresJsTransactionExec(txSql) {
|
|
837
|
+
const query = (sql, timeoutMs) => {
|
|
838
|
+
// raw-sql-ok: forwards a statement its caller already built and bound.
|
|
839
|
+
const { rawSql, args } = sqlAndArgs(sql);
|
|
840
|
+
const pgSql = postgresSqlForStatement(sql, rawSql);
|
|
841
|
+
return withDbTimeout("query", () => txSql.unsafe(pgSql, args), timeoutMs, undefined, { sql: rawSql });
|
|
842
|
+
};
|
|
843
|
+
return {
|
|
844
|
+
async execute(sql) {
|
|
845
|
+
const { timeoutMs } = dbExecQueryBudget(sql);
|
|
846
|
+
const result = hasExplicitDbTimeout(sql)
|
|
847
|
+
? await withTransactionStatementTimeout(async (setting, ms) => Array.from(await query(setting, ms)), timeoutMs, (remainingMs) => query(sql, remainingMs))
|
|
848
|
+
: await query(sql, timeoutMs);
|
|
849
|
+
return {
|
|
850
|
+
rows: Array.from(result),
|
|
851
|
+
rowsAffected: result.count ?? 0,
|
|
852
|
+
};
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
}
|
|
780
856
|
export function dbExecQueryBudget(statement) {
|
|
781
857
|
if (typeof statement === "string") {
|
|
782
858
|
return { timeoutMs: dbOpTimeoutMs(), maxAttempts: 3 };
|
|
@@ -1568,12 +1644,7 @@ async function createDbExecInternal(config = {}, trackSingletonResources = false
|
|
|
1568
1644
|
if (!hasExplicitDbTimeout(sql)) {
|
|
1569
1645
|
return queryNeonClient(client, sql);
|
|
1570
1646
|
}
|
|
1571
|
-
|
|
1572
|
-
const startedAt = Date.now();
|
|
1573
|
-
const remainingMs = () => Math.max(1, timeoutMs - (Date.now() - startedAt));
|
|
1574
|
-
const statementTimeoutMs = postgresStatementTimeoutMs(remainingMs());
|
|
1575
|
-
await queryNeonClient(client, `SET LOCAL statement_timeout = ${statementTimeoutMs}`, remainingMs());
|
|
1576
|
-
return queryNeonClient(client, sql, remainingMs());
|
|
1647
|
+
return withTransactionStatementTimeout(async (setting, ms) => (await queryNeonClient(client, setting, ms)).rows, dbExecQueryBudget(sql).timeoutMs, (remainingMs) => queryNeonClient(client, sql, remainingMs));
|
|
1577
1648
|
},
|
|
1578
1649
|
};
|
|
1579
1650
|
try {
|
|
@@ -1646,18 +1717,7 @@ async function createDbExecInternal(config = {}, trackSingletonResources = false
|
|
|
1646
1717
|
});
|
|
1647
1718
|
try {
|
|
1648
1719
|
const result = await conn.begin(async (txSql) => {
|
|
1649
|
-
const tx =
|
|
1650
|
-
async execute(sql) {
|
|
1651
|
-
const { rawSql, args } = sqlAndArgs(sql);
|
|
1652
|
-
const { timeoutMs } = dbExecQueryBudget(sql);
|
|
1653
|
-
const pgSql = postgresSqlForStatement(sql, rawSql);
|
|
1654
|
-
const result = await withDbTimeout("query", () => txSql.unsafe(pgSql, args), timeoutMs, undefined, { sql: rawSql });
|
|
1655
|
-
return {
|
|
1656
|
-
rows: Array.from(result),
|
|
1657
|
-
rowsAffected: result.count ?? 0,
|
|
1658
|
-
};
|
|
1659
|
-
},
|
|
1660
|
-
};
|
|
1720
|
+
const tx = postgresJsTransactionExec(txSql);
|
|
1661
1721
|
return fn(tx);
|
|
1662
1722
|
});
|
|
1663
1723
|
return result;
|
|
@@ -1710,18 +1770,7 @@ async function createDbExecInternal(config = {}, trackSingletonResources = false
|
|
|
1710
1770
|
async transaction(fn) {
|
|
1711
1771
|
const activePool = getPool();
|
|
1712
1772
|
const result = await activePool.begin(async (txSql) => {
|
|
1713
|
-
const tx =
|
|
1714
|
-
async execute(sql) {
|
|
1715
|
-
const { rawSql, args } = sqlAndArgs(sql);
|
|
1716
|
-
const { timeoutMs } = dbExecQueryBudget(sql);
|
|
1717
|
-
const pgSql = postgresSqlForStatement(sql, rawSql);
|
|
1718
|
-
const result = await withDbTimeout("query", () => txSql.unsafe(pgSql, args), timeoutMs, undefined, { sql: rawSql });
|
|
1719
|
-
return {
|
|
1720
|
-
rows: Array.from(result),
|
|
1721
|
-
rowsAffected: result.count ?? 0,
|
|
1722
|
-
};
|
|
1723
|
-
},
|
|
1724
|
-
};
|
|
1773
|
+
const tx = postgresJsTransactionExec(txSql);
|
|
1725
1774
|
return runHoldingPoolConnection(activePool, () => fn(tx));
|
|
1726
1775
|
});
|
|
1727
1776
|
return result;
|
|
@@ -1037,6 +1037,7 @@ export const SCHEMA_SNAPSHOT = {
|
|
|
1037
1037
|
],
|
|
1038
1038
|
["chat_threads", "chat-thread-inbox-interrupt-replay", "904890f43aebd9fe"],
|
|
1039
1039
|
["chat_threads", "chat-thread-inbox-retry-count", "b25ff25b6a61ccc9"],
|
|
1040
|
+
["chat_threads", "chat-thread-inbox-runtime-schema", null],
|
|
1040
1041
|
[
|
|
1041
1042
|
"chat_threads",
|
|
1042
1043
|
"chat-thread-inbox-thread-order-index",
|
package/dist/deploy/build.d.ts
CHANGED
|
@@ -156,6 +156,27 @@ export declare function emitSingleTemplateNetlifyRecurringJobsFunction(projectCw
|
|
|
156
156
|
export declare function emitSingleTemplateNetlifyBackgroundFunction(projectCwd: string): void;
|
|
157
157
|
export declare function assertEmittedBackgroundFunctionOnDisk(destDir: string, functionName: string): void;
|
|
158
158
|
export declare function emitSingleTemplateNetlifyIntegrationRecoveryFunction(projectCwd: string): void;
|
|
159
|
+
/**
|
|
160
|
+
* Names imported from `mod` with `import { a, b as c } from "mod"`, including
|
|
161
|
+
* clauses that also bind the default export (`import d, { a } from "mod"`).
|
|
162
|
+
* Matches both minified output and the spaced form the generated Worker entry
|
|
163
|
+
* uses.
|
|
164
|
+
*/
|
|
165
|
+
export declare function collectNamedImportsFrom(code: string, mod: string): string[];
|
|
166
|
+
/** Points every `from "mod"` in `code` at `importPath`. */
|
|
167
|
+
export declare function rewriteModuleImportSpecifier(code: string, mod: string, importPath: string): string;
|
|
168
|
+
/**
|
|
169
|
+
* esbuild entry that re-bundles a bare external for a Cloudflare Worker. A
|
|
170
|
+
* namespace import works for CommonJS and for ES modules with or without a
|
|
171
|
+
* default export; `import x from` and `export { default } from` fail to
|
|
172
|
+
* bundle an ES module that has no default export. A default export is kept
|
|
173
|
+
* as is, even when it is `null` or `undefined`, and so is a named export the
|
|
174
|
+
* module defines; the default export's property is only a fallback for names
|
|
175
|
+
* the module lacks. Each name is exported from a local binding, so a reserved
|
|
176
|
+
* word such as `class` is still a legal export and no export can collide with
|
|
177
|
+
* the entry's own bindings.
|
|
178
|
+
*/
|
|
179
|
+
export declare function cloudflareExternalBundleEntry(resolvedMod: string, neededExports: Iterable<string>): string;
|
|
159
180
|
/**
|
|
160
181
|
* Post-build patches for `cloudflare_module` server output. Recurses (via
|
|
161
182
|
* `walkServerJavaScriptFiles`) because esbuild/Nitro can emit a dependency at
|
package/dist/deploy/build.js
CHANGED
|
@@ -3693,6 +3693,65 @@ const CF_MODULE_NODE_BUILTINS = [
|
|
|
3693
3693
|
"inspector",
|
|
3694
3694
|
"vm",
|
|
3695
3695
|
];
|
|
3696
|
+
function escapeModuleSpecifier(mod) {
|
|
3697
|
+
return mod.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3698
|
+
}
|
|
3699
|
+
/**
|
|
3700
|
+
* Names imported from `mod` with `import { a, b as c } from "mod"`, including
|
|
3701
|
+
* clauses that also bind the default export (`import d, { a } from "mod"`).
|
|
3702
|
+
* Matches both minified output and the spaced form the generated Worker entry
|
|
3703
|
+
* uses.
|
|
3704
|
+
*/
|
|
3705
|
+
export function collectNamedImportsFrom(code, mod) {
|
|
3706
|
+
const re = new RegExp(`import(?:\\s+[\\w$]+\\s*,)?\\s*\\{([^}]+)\\}\\s*from\\s*["']${escapeModuleSpecifier(mod)}["']`, "g");
|
|
3707
|
+
const names = [];
|
|
3708
|
+
for (const m of code.matchAll(re)) {
|
|
3709
|
+
const clause = m[1];
|
|
3710
|
+
if (!clause)
|
|
3711
|
+
continue;
|
|
3712
|
+
for (const part of clause.split(",")) {
|
|
3713
|
+
const name = part
|
|
3714
|
+
.trim()
|
|
3715
|
+
.split(/\s+as\s+/)[0]
|
|
3716
|
+
?.trim();
|
|
3717
|
+
// `default` is re-exported on its own and can't be an `export const`.
|
|
3718
|
+
if (name && name !== "default" && /^[a-zA-Z_$]/.test(name)) {
|
|
3719
|
+
names.push(name);
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
return names;
|
|
3724
|
+
}
|
|
3725
|
+
/** Points every `from "mod"` in `code` at `importPath`. */
|
|
3726
|
+
export function rewriteModuleImportSpecifier(code, mod, importPath) {
|
|
3727
|
+
const re = new RegExp(`from\\s*["']${escapeModuleSpecifier(mod)}["']`, "g");
|
|
3728
|
+
return code.replace(re, `from"${importPath}"`);
|
|
3729
|
+
}
|
|
3730
|
+
/**
|
|
3731
|
+
* esbuild entry that re-bundles a bare external for a Cloudflare Worker. A
|
|
3732
|
+
* namespace import works for CommonJS and for ES modules with or without a
|
|
3733
|
+
* default export; `import x from` and `export { default } from` fail to
|
|
3734
|
+
* bundle an ES module that has no default export. A default export is kept
|
|
3735
|
+
* as is, even when it is `null` or `undefined`, and so is a named export the
|
|
3736
|
+
* module defines; the default export's property is only a fallback for names
|
|
3737
|
+
* the module lacks. Each name is exported from a local binding, so a reserved
|
|
3738
|
+
* word such as `class` is still a legal export and no export can collide with
|
|
3739
|
+
* the entry's own bindings.
|
|
3740
|
+
*/
|
|
3741
|
+
export function cloudflareExternalBundleEntry(resolvedMod, neededExports) {
|
|
3742
|
+
const names = Array.from(neededExports);
|
|
3743
|
+
return [
|
|
3744
|
+
`import * as _ns from "${resolvedMod}";`,
|
|
3745
|
+
`const _mod = "default" in _ns ? _ns.default : _ns;`,
|
|
3746
|
+
`export default _mod;`,
|
|
3747
|
+
...(names.length > 0
|
|
3748
|
+
? names.flatMap((n, i) => [
|
|
3749
|
+
`const _e${i} = ${JSON.stringify(n)} in _ns ? _ns.${n} : _mod?.${n};`,
|
|
3750
|
+
`export { _e${i} as ${n} };`,
|
|
3751
|
+
])
|
|
3752
|
+
: [`export * from "${resolvedMod}";`]),
|
|
3753
|
+
].join("\n");
|
|
3754
|
+
}
|
|
3696
3755
|
/**
|
|
3697
3756
|
* Post-build patches for `cloudflare_module` server output. Recurses (via
|
|
3698
3757
|
* `walkServerJavaScriptFiles`) because esbuild/Nitro can emit a dependency at
|
|
@@ -5099,9 +5158,7 @@ export default bundle;
|
|
|
5099
5158
|
const importPath = relPath.startsWith(".")
|
|
5100
5159
|
? relPath
|
|
5101
5160
|
: "./" + relPath;
|
|
5102
|
-
const
|
|
5103
|
-
const re = new RegExp(`from["']${escaped}["']`, "g");
|
|
5104
|
-
const rewritten = code.replace(re, `from"${importPath}"`);
|
|
5161
|
+
const rewritten = rewriteModuleImportSpecifier(code, mod, importPath);
|
|
5105
5162
|
if (rewritten !== code)
|
|
5106
5163
|
fs.writeFileSync(p, rewritten);
|
|
5107
5164
|
}
|
|
@@ -5144,28 +5201,13 @@ export default bundle;
|
|
|
5144
5201
|
if (!entry.name.endsWith(".mjs") && !entry.name.endsWith(".js"))
|
|
5145
5202
|
continue;
|
|
5146
5203
|
const code = fs.readFileSync(p, "utf-8");
|
|
5147
|
-
const
|
|
5148
|
-
|
|
5149
|
-
for (const m2 of code.matchAll(re)) {
|
|
5150
|
-
for (const part of m2[1].split(",")) {
|
|
5151
|
-
const name = part
|
|
5152
|
-
.trim()
|
|
5153
|
-
.split(/\s+as\s+/)[0]
|
|
5154
|
-
.trim();
|
|
5155
|
-
if (name && /^[a-zA-Z_$]/.test(name))
|
|
5156
|
-
neededExports.add(name);
|
|
5157
|
-
}
|
|
5204
|
+
for (const name of collectNamedImportsFrom(code, mod)) {
|
|
5205
|
+
neededExports.add(name);
|
|
5158
5206
|
}
|
|
5159
5207
|
}
|
|
5160
5208
|
}
|
|
5161
5209
|
findNeededExports(outputDir);
|
|
5162
|
-
const entryCode = neededExports
|
|
5163
|
-
? [
|
|
5164
|
-
`import _mod from "${resolvedMod}";`,
|
|
5165
|
-
`export default _mod;`,
|
|
5166
|
-
...Array.from(neededExports).map((n) => `export const ${n} = _mod.${n} ?? _mod?.default?.${n};`),
|
|
5167
|
-
].join("\n")
|
|
5168
|
-
: `export * from "${resolvedMod}"; export { default } from "${resolvedMod}";`;
|
|
5210
|
+
const entryCode = cloudflareExternalBundleEntry(resolvedMod, neededExports);
|
|
5169
5211
|
execFileSync(esbuild.executable, [
|
|
5170
5212
|
...esbuild.args,
|
|
5171
5213
|
"--bundle",
|
|
@@ -5190,18 +5232,16 @@ export default bundle;
|
|
|
5190
5232
|
}
|
|
5191
5233
|
if (!entry.name.endsWith(".mjs") && !entry.name.endsWith(".js"))
|
|
5192
5234
|
continue;
|
|
5193
|
-
|
|
5235
|
+
const code = fs.readFileSync(p, "utf-8");
|
|
5194
5236
|
const relPath = path
|
|
5195
5237
|
.relative(path.dirname(p), outFile)
|
|
5196
5238
|
.replace(/\\/g, "/");
|
|
5197
5239
|
const importPath = relPath.startsWith(".")
|
|
5198
5240
|
? relPath
|
|
5199
5241
|
: "./" + relPath;
|
|
5200
|
-
const
|
|
5201
|
-
if (
|
|
5202
|
-
|
|
5203
|
-
fs.writeFileSync(p, code);
|
|
5204
|
-
}
|
|
5242
|
+
const rewritten = rewriteModuleImportSpecifier(code, mod, importPath);
|
|
5243
|
+
if (rewritten !== code)
|
|
5244
|
+
fs.writeFileSync(p, rewritten);
|
|
5205
5245
|
}
|
|
5206
5246
|
}
|
|
5207
5247
|
rewriteImports(outputDir);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@agent-native/agentkit": "0.226.
|
|
2
|
+
"@agent-native/agentkit": "0.226.1-nightly-20261107004051",
|
|
3
3
|
"@agent-native/code-agents-ui": "0.1.75",
|
|
4
|
-
"@agent-native/core": "0.226.
|
|
5
|
-
"@agent-native/core-corpus": "0.226.
|
|
4
|
+
"@agent-native/core": "0.226.1-nightly-20261107004051",
|
|
5
|
+
"@agent-native/core-corpus": "0.226.1-nightly-20261107004051",
|
|
6
6
|
"@agent-native/creative-context": "0.8.68",
|
|
7
|
-
"@agent-native/dispatch": "0.42.
|
|
7
|
+
"@agent-native/dispatch": "0.42.4-nightly-20261107004051",
|
|
8
8
|
"@agent-native/embedding": "0.2.0",
|
|
9
9
|
"@agent-native/migrate": "0.0.7",
|
|
10
10
|
"@agent-native/otel": "0.1.35",
|
|
@@ -12,6 +12,6 @@
|
|
|
12
12
|
"@agent-native/recap-cli": "0.5.99",
|
|
13
13
|
"@agent-native/scheduling": "0.2.55",
|
|
14
14
|
"@agent-native/shared-app-config": "0.1.2",
|
|
15
|
-
"@agent-native/skills": "0.4.
|
|
16
|
-
"@agent-native/toolkit": "0.226.
|
|
15
|
+
"@agent-native/skills": "0.4.4-nightly-20261107004051",
|
|
16
|
+
"@agent-native/toolkit": "0.226.1-nightly-20261107004051"
|
|
17
17
|
}
|
|
@@ -20,6 +20,16 @@ const assignmentSchema = z.object({
|
|
|
20
20
|
email: z.string().email().max(320).optional(),
|
|
21
21
|
policy: z.literal(BUILDER_SIGNUP_POLICY),
|
|
22
22
|
});
|
|
23
|
+
function experimentProperties(assignment) {
|
|
24
|
+
return {
|
|
25
|
+
app_name: "design",
|
|
26
|
+
template_name: "design",
|
|
27
|
+
experiment_id: BUILDER_SIGNUP_EXPERIMENT,
|
|
28
|
+
...(assignment
|
|
29
|
+
? { experiment_variant: assignment.variant, assignment_id: assignment.id }
|
|
30
|
+
: {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
23
33
|
function mac(body) {
|
|
24
34
|
return createHmac("sha256", getAuthSecret())
|
|
25
35
|
.update(`${BUILDER_SIGNUP_EXPERIMENT}:${body}`)
|
|
@@ -83,35 +93,35 @@ export async function bindBuilderSignupAttempt(event, attemptId, method, email)
|
|
|
83
93
|
const assignment = decodeBuilderSignupAssignment(getCookie(event, BUILDER_SIGNUP_COOKIE));
|
|
84
94
|
if (!assignment)
|
|
85
95
|
return false;
|
|
96
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
97
|
+
const adapter = await getBetterAuthInternalAdapter();
|
|
98
|
+
if (!adapter)
|
|
99
|
+
throw new Error("Signup identity store is unavailable");
|
|
100
|
+
const identity = await adapter.findUserByEmail(normalizedEmail, {
|
|
101
|
+
includeAccounts: false,
|
|
102
|
+
});
|
|
103
|
+
const createdAt = identity?.user.createdAt;
|
|
104
|
+
const createdAtMs = createdAt ? new Date(createdAt).getTime() : Number.NaN;
|
|
105
|
+
if (!identity ||
|
|
106
|
+
!Number.isFinite(createdAtMs) ||
|
|
107
|
+
createdAtMs < assignment.issuedAt)
|
|
108
|
+
return false;
|
|
109
|
+
track("builder_signup_experiment_enrolled", { ...experimentProperties(assignment), method }, { authUserId: identity.user.id, userId: normalizedEmail });
|
|
86
110
|
if (assignment.variant === "control") {
|
|
87
|
-
const adapter = await getBetterAuthInternalAdapter();
|
|
88
|
-
if (!adapter)
|
|
89
|
-
throw new Error("Signup identity store is unavailable");
|
|
90
|
-
const identity = await adapter.findUserByEmail(email, {
|
|
91
|
-
includeAccounts: false,
|
|
92
|
-
});
|
|
93
|
-
const createdAt = identity?.user.createdAt;
|
|
94
|
-
const createdAtMs = createdAt ? new Date(createdAt).getTime() : Number.NaN;
|
|
95
|
-
if (!identity ||
|
|
96
|
-
!Number.isFinite(createdAtMs) ||
|
|
97
|
-
createdAtMs < assignment.issuedAt)
|
|
98
|
-
return false;
|
|
99
111
|
const key = `${BUILDER_SIGNUP_EXPERIMENT}:${identity.user.id}`;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
const record = await mutateSetting(key, (current) => current ?? {
|
|
113
|
+
id: assignment.id,
|
|
114
|
+
variant: "control",
|
|
115
|
+
issuedAt: assignment.issuedAt,
|
|
116
|
+
policy: assignment.policy,
|
|
117
|
+
userId: identity.user.id,
|
|
118
|
+
status: "control",
|
|
119
|
+
});
|
|
120
|
+
if (record.id === assignment.id && record.status === "control") {
|
|
109
121
|
track("builder_signup_experiment_outcome", {
|
|
110
|
-
|
|
111
|
-
experiment_variant: "control",
|
|
112
|
-
assignment_id: assignment.id,
|
|
122
|
+
...experimentProperties(assignment),
|
|
113
123
|
status: "control",
|
|
114
|
-
}, { authUserId: identity.user.id, userId:
|
|
124
|
+
}, { authUserId: identity.user.id, userId: normalizedEmail });
|
|
115
125
|
}
|
|
116
126
|
return false;
|
|
117
127
|
}
|
|
@@ -121,21 +131,10 @@ export async function bindBuilderSignupAttempt(event, attemptId, method, email)
|
|
|
121
131
|
assignment.attemptId !== attemptId)
|
|
122
132
|
return false;
|
|
123
133
|
const key = `${BUILDER_SIGNUP_EXPERIMENT}:attempt:${attemptId}`;
|
|
124
|
-
const normalizedEmail = email.trim().toLowerCase();
|
|
125
134
|
if (assignment.method !== method ||
|
|
126
135
|
(assignment.email && assignment.email !== normalizedEmail))
|
|
127
136
|
return false;
|
|
128
|
-
|
|
129
|
-
if (!adapter)
|
|
130
|
-
throw new Error("Signup identity store is unavailable");
|
|
131
|
-
const identity = await adapter.findUserByEmail(normalizedEmail, {
|
|
132
|
-
includeAccounts: false,
|
|
133
|
-
});
|
|
134
|
-
const createdAt = identity?.user.createdAt;
|
|
135
|
-
const createdAtMs = createdAt ? new Date(createdAt).getTime() : Number.NaN;
|
|
136
|
-
if (!identity ||
|
|
137
|
-
!Number.isFinite(createdAtMs) ||
|
|
138
|
-
createdAtMs < assignment.acceptedAt)
|
|
137
|
+
if (createdAtMs < assignment.acceptedAt)
|
|
139
138
|
return false;
|
|
140
139
|
const bound = z
|
|
141
140
|
.object({ id: z.string(), boundUserId: z.string() })
|
|
@@ -239,9 +238,15 @@ export function createBuilderSignupExperimentHandler(dependencies) {
|
|
|
239
238
|
return refuse(400, "invalid_request");
|
|
240
239
|
if (!assignment) {
|
|
241
240
|
deleteCookie(event, "an_builder_signup_pending", { path: "/" });
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
241
|
+
if (body.data.operation === "activate") {
|
|
242
|
+
if (session?.emailVerified && session.userId)
|
|
243
|
+
track("builder_signup_experiment_unattributed", {
|
|
244
|
+
...experimentProperties(),
|
|
245
|
+
code: "assignment_missing",
|
|
246
|
+
}, { userId: session.email, authUserId: session.userId });
|
|
247
|
+
return { status: "fallback", code: "assignment_missing" };
|
|
248
|
+
}
|
|
249
|
+
return refuse(409, "assignment_missing");
|
|
245
250
|
}
|
|
246
251
|
if (body.data.operation === "accept") {
|
|
247
252
|
if (session || body.data.assignmentId !== assignment.id)
|
|
@@ -272,14 +277,27 @@ export function createBuilderSignupExperimentHandler(dependencies) {
|
|
|
272
277
|
}
|
|
273
278
|
if (!session?.emailVerified || !session.userId)
|
|
274
279
|
return refuse(401, "verified_session_required");
|
|
275
|
-
const
|
|
280
|
+
const trackingSource = {
|
|
281
|
+
userId: session.email,
|
|
282
|
+
authUserId: session.userId,
|
|
283
|
+
};
|
|
284
|
+
const terminal = (status, code, replayed = false, durationMs) => {
|
|
285
|
+
track("builder_signup_experiment_outcome", {
|
|
286
|
+
...experimentProperties(assignment),
|
|
287
|
+
status,
|
|
288
|
+
code: code ?? status,
|
|
289
|
+
replayed,
|
|
290
|
+
...(durationMs === undefined
|
|
291
|
+
? {}
|
|
292
|
+
: { activation_duration_ms: durationMs }),
|
|
293
|
+
}, trackingSource);
|
|
276
294
|
deleteCookie(event, "an_builder_signup_pending", { path: "/" });
|
|
277
295
|
return { status, ...(code ? { code } : {}) };
|
|
278
296
|
};
|
|
279
297
|
if (assignment.variant !== "automatic")
|
|
280
298
|
return terminal("fallback", "control");
|
|
281
299
|
if (!assignment.acceptedAt || !assignment.attemptId)
|
|
282
|
-
return terminal("fallback");
|
|
300
|
+
return terminal("fallback", "acceptance_missing");
|
|
283
301
|
const consent = await getSetting(`${BUILDER_SIGNUP_EXPERIMENT}:consent:${session.userId}`);
|
|
284
302
|
if (!consent ||
|
|
285
303
|
consent.attemptId !== assignment.attemptId ||
|
|
@@ -288,8 +306,13 @@ export function createBuilderSignupExperimentHandler(dependencies) {
|
|
|
288
306
|
consent.email !== session.email.trim().toLowerCase())
|
|
289
307
|
return terminal("fallback", "signup_attempt_unbound");
|
|
290
308
|
const adapter = await getBetterAuthInternalAdapter();
|
|
291
|
-
if (!adapter)
|
|
309
|
+
if (!adapter) {
|
|
310
|
+
track("builder_signup_experiment_activation_unavailable", {
|
|
311
|
+
...experimentProperties(assignment),
|
|
312
|
+
code: "identity_unavailable",
|
|
313
|
+
}, { userId: session.email, authUserId: session.userId });
|
|
292
314
|
return refuse(503, "identity_unavailable");
|
|
315
|
+
}
|
|
293
316
|
const identity = await adapter.findUserByEmail(session.email, {
|
|
294
317
|
includeAccounts: false,
|
|
295
318
|
});
|
|
@@ -299,7 +322,7 @@ export function createBuilderSignupExperimentHandler(dependencies) {
|
|
|
299
322
|
identity.user.id !== session.userId ||
|
|
300
323
|
!Number.isFinite(createdAtMs) ||
|
|
301
324
|
createdAtMs < assignment.acceptedAt)
|
|
302
|
-
return terminal("fallback");
|
|
325
|
+
return terminal("fallback", "identity_ineligible");
|
|
303
326
|
const key = `${BUILDER_SIGNUP_EXPERIMENT}:${session.userId}`;
|
|
304
327
|
const existing = await getSetting(key);
|
|
305
328
|
if (existing) {
|
|
@@ -307,7 +330,7 @@ export function createBuilderSignupExperimentHandler(dependencies) {
|
|
|
307
330
|
.enum(["started", "connected", "fallback"])
|
|
308
331
|
.parse(existing.status);
|
|
309
332
|
if (status !== "started")
|
|
310
|
-
return terminal(status);
|
|
333
|
+
return terminal(status, z.string().nullable().optional().parse(existing.code) ?? undefined, true);
|
|
311
334
|
return { status: "pending" };
|
|
312
335
|
}
|
|
313
336
|
const attempt = randomBytes(16).toString("hex");
|
|
@@ -320,6 +343,9 @@ export function createBuilderSignupExperimentHandler(dependencies) {
|
|
|
320
343
|
});
|
|
321
344
|
if (record.attempt !== attempt)
|
|
322
345
|
return { status: "pending" };
|
|
346
|
+
track("builder_signup_experiment_activation_started", {
|
|
347
|
+
...experimentProperties(assignment),
|
|
348
|
+
}, { userId: session.email, authUserId: session.userId });
|
|
323
349
|
let result;
|
|
324
350
|
try {
|
|
325
351
|
result = await dependencies.activate(event, session);
|
|
@@ -333,13 +359,6 @@ export function createBuilderSignupExperimentHandler(dependencies) {
|
|
|
333
359
|
...(result.message ? { message: result.message } : {}),
|
|
334
360
|
});
|
|
335
361
|
const status = committed.status;
|
|
336
|
-
|
|
337
|
-
experiment_id: BUILDER_SIGNUP_EXPERIMENT,
|
|
338
|
-
experiment_variant: assignment.variant,
|
|
339
|
-
assignment_id: assignment.id,
|
|
340
|
-
status,
|
|
341
|
-
code: committed.code ?? null,
|
|
342
|
-
}, { userId: session.email, authUserId: session.userId });
|
|
343
|
-
return terminal(status, committed.code ?? undefined);
|
|
362
|
+
return terminal(status, committed.code ?? undefined, false, Date.now() - z.number().parse(record.startedAt));
|
|
344
363
|
});
|
|
345
364
|
}
|
|
@@ -984,9 +984,13 @@ export function getOnboardingHtml(opts = {}) {
|
|
|
984
984
|
homePath: appHomePath,
|
|
985
985
|
}).resumeHref;
|
|
986
986
|
const workspaceRuntime = isWorkspaceRuntime();
|
|
987
|
-
const trackingApp = getAppConfig().app.slug
|
|
988
|
-
getAppConfig().app.template
|
|
989
|
-
getAppConfig().app.id
|
|
987
|
+
const trackingApp = getAppConfig().app.slug?.trim() ||
|
|
988
|
+
getAppConfig().app.template?.trim() ||
|
|
989
|
+
getAppConfig().app.id?.trim() ||
|
|
990
|
+
resolveBuiltInAuthMarketingSlug({
|
|
991
|
+
...(opts.requestHost ? { requestHost: opts.requestHost } : {}),
|
|
992
|
+
...(opts.requestPath ? { requestPath: opts.requestPath } : {}),
|
|
993
|
+
}) ||
|
|
990
994
|
"";
|
|
991
995
|
const publicOAuthOrigin = getPublicOAuthOrigin();
|
|
992
996
|
const workspaceGatewayReturnOrigin = getWorkspaceGatewayReturnOrigin();
|
package/dist/shared/auth-copy.js
CHANGED
|
@@ -26,7 +26,7 @@ export const NATIVE_AUTH_COPY = {
|
|
|
26
26
|
emailLinkContinueAction: "Continue",
|
|
27
27
|
email: "Email",
|
|
28
28
|
emailPlaceholder: "you@example.com",
|
|
29
|
-
builderSignupDisclosure: "
|
|
29
|
+
builderSignupDisclosure: "You also create a Builder.io account and agree to Builder’s",
|
|
30
30
|
legalPrefix: "By signing up, you accept our",
|
|
31
31
|
legalTerms: "Terms",
|
|
32
32
|
legalConnector: "and",
|
|
@@ -69,7 +69,7 @@ export const NATIVE_AUTH_COPY = {
|
|
|
69
69
|
emailLinkContinueAction: "继续",
|
|
70
70
|
email: "电子邮箱",
|
|
71
71
|
emailPlaceholder: "you@example.com",
|
|
72
|
-
builderSignupDisclosure: "
|
|
72
|
+
builderSignupDisclosure: "你还将创建 Builder.io 帐户,并同意 Builder 的",
|
|
73
73
|
legalPrefix: "注册即表示你接受我们的",
|
|
74
74
|
legalTerms: "条款",
|
|
75
75
|
legalConnector: "和",
|
|
@@ -112,7 +112,7 @@ export const NATIVE_AUTH_COPY = {
|
|
|
112
112
|
emailLinkContinueAction: "繼續",
|
|
113
113
|
email: "電子郵件",
|
|
114
114
|
emailPlaceholder: "you@example.com",
|
|
115
|
-
builderSignupDisclosure: "
|
|
115
|
+
builderSignupDisclosure: "你也將建立 Builder.io 帳戶,並同意 Builder 的",
|
|
116
116
|
legalPrefix: "註冊即表示你接受我們的",
|
|
117
117
|
legalTerms: "條款",
|
|
118
118
|
legalConnector: "和",
|
|
@@ -155,7 +155,7 @@ export const NATIVE_AUTH_COPY = {
|
|
|
155
155
|
emailLinkContinueAction: "Continuar",
|
|
156
156
|
email: "Email",
|
|
157
157
|
emailPlaceholder: "you@example.com",
|
|
158
|
-
builderSignupDisclosure: "
|
|
158
|
+
builderSignupDisclosure: "También creas una cuenta de Builder.io y aceptas sus",
|
|
159
159
|
legalPrefix: "Al registrarte, aceptas nuestros",
|
|
160
160
|
legalTerms: "Términos",
|
|
161
161
|
legalConnector: "y",
|
|
@@ -198,7 +198,7 @@ export const NATIVE_AUTH_COPY = {
|
|
|
198
198
|
emailLinkContinueAction: "Continuer",
|
|
199
199
|
email: "E-mail",
|
|
200
200
|
emailPlaceholder: "you@example.com",
|
|
201
|
-
builderSignupDisclosure: "
|
|
201
|
+
builderSignupDisclosure: "Vous créez aussi un compte Builder.io et acceptez ses",
|
|
202
202
|
legalPrefix: "En vous inscrivant, vous acceptez nos",
|
|
203
203
|
legalTerms: "Conditions",
|
|
204
204
|
legalConnector: "et",
|
|
@@ -241,7 +241,7 @@ export const NATIVE_AUTH_COPY = {
|
|
|
241
241
|
emailLinkContinueAction: "Weiter",
|
|
242
242
|
email: "E-Mail",
|
|
243
243
|
emailPlaceholder: "you@example.com",
|
|
244
|
-
builderSignupDisclosure: "
|
|
244
|
+
builderSignupDisclosure: "Du erstellst auch ein Builder.io-Konto und akzeptierst Builders",
|
|
245
245
|
legalPrefix: "Mit der Registrierung akzeptierst du unsere",
|
|
246
246
|
legalTerms: "Bedingungen",
|
|
247
247
|
legalConnector: "und",
|
|
@@ -284,7 +284,7 @@ export const NATIVE_AUTH_COPY = {
|
|
|
284
284
|
emailLinkContinueAction: "続行",
|
|
285
285
|
email: "メール",
|
|
286
286
|
emailPlaceholder: "you@example.com",
|
|
287
|
-
builderSignupDisclosure: "
|
|
287
|
+
builderSignupDisclosure: "Builder.io アカウントも作成し、Builder の以下に同意します:",
|
|
288
288
|
legalPrefix: "登録すると、以下に同意したものとみなされます:",
|
|
289
289
|
legalTerms: "利用規約",
|
|
290
290
|
legalConnector: "および",
|
|
@@ -327,7 +327,7 @@ export const NATIVE_AUTH_COPY = {
|
|
|
327
327
|
emailLinkContinueAction: "계속",
|
|
328
328
|
email: "이메일",
|
|
329
329
|
emailPlaceholder: "you@example.com",
|
|
330
|
-
builderSignupDisclosure: "
|
|
330
|
+
builderSignupDisclosure: "Builder.io 계정도 생성하고 Builder의 다음 사항에 동의합니다:",
|
|
331
331
|
legalPrefix: "가입하면 다음에 동의하게 됩니다:",
|
|
332
332
|
legalTerms: "약관",
|
|
333
333
|
legalConnector: "및",
|
|
@@ -370,7 +370,7 @@ export const NATIVE_AUTH_COPY = {
|
|
|
370
370
|
emailLinkContinueAction: "Continuar",
|
|
371
371
|
email: "Email",
|
|
372
372
|
emailPlaceholder: "you@example.com",
|
|
373
|
-
builderSignupDisclosure: "
|
|
373
|
+
builderSignupDisclosure: "Você também cria uma conta Builder.io e aceita seus",
|
|
374
374
|
legalPrefix: "Ao se cadastrar, você aceita nossos",
|
|
375
375
|
legalTerms: "Termos",
|
|
376
376
|
legalConnector: "e",
|
|
@@ -413,7 +413,7 @@ export const NATIVE_AUTH_COPY = {
|
|
|
413
413
|
emailLinkContinueAction: "जारी रखें",
|
|
414
414
|
email: "ईमेल",
|
|
415
415
|
emailPlaceholder: "you@example.com",
|
|
416
|
-
builderSignupDisclosure: "
|
|
416
|
+
builderSignupDisclosure: "आप Builder.io खाता भी बनाते हैं और Builder की निम्न नीतियों से सहमत हैं:",
|
|
417
417
|
legalPrefix: "साइन अप करके, आप हमारी",
|
|
418
418
|
legalTerms: "शर्तें",
|
|
419
419
|
legalConnector: "और",
|
|
@@ -456,7 +456,7 @@ export const NATIVE_AUTH_COPY = {
|
|
|
456
456
|
emailLinkContinueAction: "متابعة",
|
|
457
457
|
email: "البريد الإلكتروني",
|
|
458
458
|
emailPlaceholder: "you@example.com",
|
|
459
|
-
builderSignupDisclosure: "
|
|
459
|
+
builderSignupDisclosure: "تنشئ أيضًا حساب Builder.io وتوافق على",
|
|
460
460
|
legalPrefix: "بالتسجيل، فإنك توافق على",
|
|
461
461
|
legalTerms: "الشروط",
|
|
462
462
|
legalConnector: "و",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.226.
|
|
3
|
+
"version": "0.226.1-nightly-20261107004051",
|
|
4
4
|
"description": "The agentic application framework for building autonomous agents with intuitive UIs",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -765,7 +765,7 @@
|
|
|
765
765
|
"yaml": "^2.9.0",
|
|
766
766
|
"yjs": "^13.6.33",
|
|
767
767
|
"zod": "^4.3.6",
|
|
768
|
-
"@agent-native/agentkit": "^0.226.
|
|
768
|
+
"@agent-native/agentkit": "^0.226.1-nightly-20261107004051",
|
|
769
769
|
"@agent-native/recap-cli": "^0.5.99"
|
|
770
770
|
},
|
|
771
771
|
"devDependencies": {
|
|
@@ -838,7 +838,7 @@
|
|
|
838
838
|
"xlsx": "^0.18.5"
|
|
839
839
|
},
|
|
840
840
|
"peerDependencies": {
|
|
841
|
-
"@agent-native/core-corpus": "
|
|
841
|
+
"@agent-native/core-corpus": "0.226.1-nightly-20261107004051",
|
|
842
842
|
"@ai-sdk/anthropic": ">=3",
|
|
843
843
|
"@ai-sdk/cohere": ">=3",
|
|
844
844
|
"@ai-sdk/google": ">=3",
|