@agent-native/core 0.145.6 → 0.145.8

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.
Files changed (95) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +5 -1
  3. package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +3 -2
  4. package/corpus/templates/analytics/changelog/2026-08-07-analytics-no-longer-stalls-under-bursts-of-event-ingest-key-.md +6 -0
  5. package/corpus/templates/analytics/changelog/2026-08-07-settings-skeleton-placeholders.md +6 -0
  6. package/corpus/templates/analytics/netlify.toml +1 -1
  7. package/corpus/templates/analytics/package.json +1 -0
  8. package/corpus/templates/analytics/scripts/migrate-production.ts +24 -0
  9. package/corpus/templates/analytics/server/jobs/analytics-rollup-backfill.ts +9 -0
  10. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +65 -10
  11. package/corpus/templates/analytics/server/lib/session-replay.ts +5 -5
  12. package/corpus/templates/analytics/server/plugins/db.ts +14 -3
  13. package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +5 -1
  14. package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +5 -1
  15. package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +5 -1
  16. package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -1
  17. package/corpus/templates/chat/AGENTS.md +3 -0
  18. package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +5 -1
  19. package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +5 -1
  20. package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +21 -3
  21. package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +23 -5
  22. package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +24 -5
  23. package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +23 -3
  24. package/corpus/templates/crm/changelog/2026-08-07-settings-loading-skeletons.md +6 -0
  25. package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +5 -1
  26. package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +5 -1
  27. package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +5 -1
  28. package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +5 -1
  29. package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +5 -1
  30. package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +5 -1
  31. package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +5 -1
  32. package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +5 -1
  33. package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +5 -1
  34. package/dist/client/components/ApiKeySettings.js +2 -5
  35. package/dist/client/org/TeamPage.js +4 -2
  36. package/dist/client/settings/AccountSettingsCard.js +2 -1
  37. package/dist/client/settings/AgentsSection.js +2 -1
  38. package/dist/client/settings/SecretsSection.js +3 -2
  39. package/dist/client/settings/SettingsPanel.js +6 -12
  40. package/dist/client/settings/SettingsSkeleton.d.ts +12 -0
  41. package/dist/client/settings/SettingsSkeleton.js +12 -0
  42. package/dist/client/settings/VoiceTranscriptionSection.js +5 -4
  43. package/dist/client/settings/index.d.ts +1 -0
  44. package/dist/client/settings/index.js +1 -0
  45. package/dist/client/use-db-sync.js +4 -0
  46. package/dist/collab/routes.d.ts +1 -1
  47. package/dist/db/client.d.ts +13 -7
  48. package/dist/db/client.js +124 -7
  49. package/dist/db/ddl-guard.js +3 -1
  50. package/dist/db/ensure-additive-columns.d.ts +2 -0
  51. package/dist/db/ensure-additive-columns.js +5 -2
  52. package/dist/db/index.d.ts +2 -2
  53. package/dist/db/index.js +2 -2
  54. package/dist/db/migrations.d.ts +11 -54
  55. package/dist/db/migrations.js +70 -0
  56. package/dist/jobs/index.d.ts +1 -0
  57. package/dist/jobs/index.js +1 -0
  58. package/dist/jobs/run-history.d.ts +4 -0
  59. package/dist/jobs/run-history.js +44 -0
  60. package/dist/notifications/routes.d.ts +3 -3
  61. package/dist/observability/routes.d.ts +3 -3
  62. package/dist/progress/routes.d.ts +1 -1
  63. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  64. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  65. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  66. package/dist/secrets/routes.d.ts +6 -6
  67. package/dist/server/agent-chat-plugin.js +6 -4
  68. package/dist/server/auth-plugin.js +5 -0
  69. package/dist/server/better-auth-instance.js +0 -61
  70. package/dist/server/better-auth-migrations.d.ts +8 -0
  71. package/dist/server/better-auth-migrations.js +179 -0
  72. package/dist/server/edge.d.ts +2 -0
  73. package/dist/server/edge.js +2 -0
  74. package/dist/server/index.d.ts +2 -0
  75. package/dist/server/index.js +2 -0
  76. package/dist/server/release-migrations.d.ts +8 -0
  77. package/dist/server/release-migrations.js +24 -0
  78. package/dist/server/sweep-runtime.d.ts +1 -1
  79. package/dist/server/sweep-runtime.js +15 -0
  80. package/dist/server/transcribe-voice.d.ts +1 -1
  81. package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -1
  82. package/dist/templates/chat/AGENTS.md +3 -0
  83. package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +5 -1
  84. package/dist/templates/default/AGENTS.md +3 -0
  85. package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -1
  86. package/dist/templates/workspace-core/AGENTS.md +3 -0
  87. package/dist/templates/workspace-root/AGENTS.md +3 -0
  88. package/package.json +1 -1
  89. package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -1
  90. package/src/templates/chat/AGENTS.md +3 -0
  91. package/src/templates/default/.agents/skills/frontend-design/SKILL.md +5 -1
  92. package/src/templates/default/AGENTS.md +3 -0
  93. package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -1
  94. package/src/templates/workspace-core/AGENTS.md +3 -0
  95. package/src/templates/workspace-root/AGENTS.md +3 -0
package/dist/db/client.js CHANGED
@@ -765,6 +765,47 @@ export function isServerlessRuntime() {
765
765
  !!process.env.LAMBDA_TASK_ROOT ||
766
766
  !!process.env.CF_PAGES);
767
767
  }
768
+ /**
769
+ * True for production serverless execution contexts. Netlify also exposes
770
+ * `NETLIFY=true` in its build environment, so build-time code that owns
771
+ * migrations must use `withMigrationRuntime()` explicitly.
772
+ */
773
+ export function isProductionServerlessFunctionRuntime(env = process.env) {
774
+ if (env.NODE_ENV !== "production" || env.NETLIFY_LOCAL === "true") {
775
+ return false;
776
+ }
777
+ return Boolean(env.NETLIFY === "true" ||
778
+ env.NETLIFY_FUNCTION_NAME ||
779
+ env.AWS_LAMBDA_FUNCTION_NAME ||
780
+ env.AWS_LAMBDA_FUNCTION_VERSION ||
781
+ env.LAMBDA_TASK_ROOT ||
782
+ env.AWS_EXECUTION_ENV?.startsWith("AWS_Lambda") === true ||
783
+ env.VERCEL_FUNCTION_ID ||
784
+ env.VERCEL_REGION ||
785
+ env.VERCEL === "1");
786
+ }
787
+ const SCHEMA_MUTATION_STATEMENT = /^\s*(?:CREATE|ALTER|DROP|TRUNCATE|COMMENT|REINDEX|GRANT|REVOKE)\b/i;
788
+ function rawSql(statement) {
789
+ return typeof statement === "string" ? statement : statement.sql;
790
+ }
791
+ /** True for statements that change database schema or database privileges. */
792
+ export function isSchemaMutationStatement(statement) {
793
+ return SCHEMA_MUTATION_STATEMENT.test(rawSql(statement));
794
+ }
795
+ /**
796
+ * Request functions must never be able to prepare or mutate schema. Throwing
797
+ * here makes an unconverted ensureTable path fail loudly instead of silently
798
+ * reporting success against a missing table. The release migration wrapper
799
+ * is the only supported production opt-in.
800
+ */
801
+ export function assertSchemaMutationAllowed(statement) {
802
+ const migrationRuntime = globalThis.__AGENT_NATIVE_MIGRATION_RUNTIME__ === true;
803
+ if (isProductionServerlessFunctionRuntime() &&
804
+ !migrationRuntime &&
805
+ isSchemaMutationStatement(statement)) {
806
+ throw new Error("Schema mutation attempted in a production serverless request. Run migrations in the release job instead.");
807
+ }
808
+ }
768
809
  /**
769
810
  * postgres.js pool options tuned per runtime. idle_timeout is shortened on
770
811
  * serverless so a thawed-but-idle instance releases its connections quickly.
@@ -774,10 +815,27 @@ export function isServerlessRuntime() {
774
815
  * process, not one per consumer. See {@link neonPoolMax} for why the cap must
775
816
  * leave room for concurrency.
776
817
  */
818
+ /**
819
+ * Identifies the connection in `pg_stat_activity.application_name`.
820
+ *
821
+ * Without it every backend reports `pgbouncer` and a runaway query is
822
+ * anonymous. A 58 MB `SELECT id, config FROM dashboards` ran 20-wide against
823
+ * production and could not be traced to a caller: it appears nowhere in the
824
+ * repo or any built bundle, and `pg_stat_statements` is not installed. This is
825
+ * the cheapest thing that would have named it.
826
+ */
827
+ function poolApplicationName() {
828
+ const site = process.env.SITE_NAME ??
829
+ process.env.NETLIFY_SITE_NAME ??
830
+ process.env.AGENT_NATIVE_APP_NAME ??
831
+ "app";
832
+ return `agent-native:${site}`.slice(0, 63);
833
+ }
777
834
  export function pgPoolOptions(url) {
778
835
  const serverless = isServerlessRuntime();
779
836
  return {
780
837
  onnotice: () => { },
838
+ connection: { application_name: poolApplicationName() },
781
839
  max: serverless ? serverlessPoolMax() : 20,
782
840
  idle_timeout: serverless ? 20 : 240,
783
841
  max_lifetime: 60 * 30,
@@ -785,6 +843,7 @@ export function pgPoolOptions(url) {
785
843
  ...(serverless
786
844
  ? {
787
845
  connection: {
846
+ application_name: poolApplicationName(),
788
847
  idle_in_transaction_session_timeout: 30_000,
789
848
  },
790
849
  }
@@ -1504,7 +1563,36 @@ async function createDbExecInternal(config = {}, trackSingletonResources = false
1504
1563
  };
1505
1564
  }
1506
1565
  export async function createDbExec(config = {}) {
1507
- return createDbExecInternal(config, false);
1566
+ const exec = await createDbExecInternal(config, false);
1567
+ return guardSchemaMutations(exec);
1568
+ }
1569
+ function guardSchemaMutations(exec) {
1570
+ const guarded = {
1571
+ async execute(statement) {
1572
+ assertSchemaMutationAllowed(statement);
1573
+ return exec.execute(statement);
1574
+ },
1575
+ };
1576
+ if (exec.atomicBatch) {
1577
+ guarded.atomicBatch = async (statements) => {
1578
+ for (const statement of statements) {
1579
+ assertSchemaMutationAllowed(statement);
1580
+ }
1581
+ return exec.atomicBatch(statements);
1582
+ };
1583
+ }
1584
+ if (exec.transaction) {
1585
+ guarded.transaction = (fn) => exec.transaction((tx) => fn({
1586
+ ...tx,
1587
+ execute: async (statement) => {
1588
+ assertSchemaMutationAllowed(statement);
1589
+ return tx.execute(statement);
1590
+ },
1591
+ }));
1592
+ }
1593
+ if (exec.close)
1594
+ guarded.close = () => exec.close();
1595
+ return guarded;
1508
1596
  }
1509
1597
  async function initClient() {
1510
1598
  if (_exec)
@@ -1561,6 +1649,7 @@ export function getDbExec() {
1561
1649
  return sql;
1562
1650
  }
1563
1651
  async function execAnnotated(s) {
1652
+ assertSchemaMutationAllowed(s);
1564
1653
  try {
1565
1654
  return await _exec.execute(sanitize(s));
1566
1655
  }
@@ -1571,6 +1660,7 @@ export function getDbExec() {
1571
1660
  // Return a proxy that lazy-inits on first call
1572
1661
  const proxy = {
1573
1662
  async execute(sql) {
1663
+ assertSchemaMutationAllowed(sql);
1574
1664
  if (!_initPromise)
1575
1665
  _initPromise = initClient();
1576
1666
  try {
@@ -1588,11 +1678,19 @@ export function getDbExec() {
1588
1678
  const wrapper = {
1589
1679
  execute: (s) => execAnnotated(s),
1590
1680
  atomicBatch: _exec.atomicBatch
1591
- ? (statements) => _exec.atomicBatch(statements.map((s) => sanitize(s)))
1681
+ ? async (statements) => {
1682
+ for (const statement of statements) {
1683
+ assertSchemaMutationAllowed(statement);
1684
+ }
1685
+ return _exec.atomicBatch(statements.map((s) => sanitize(s)));
1686
+ }
1592
1687
  : undefined,
1593
1688
  transaction: _exec.transaction
1594
1689
  ? (fn) => _exec.transaction((tx) => fn({
1595
- execute: (s) => tx.execute(sanitize(s)),
1690
+ execute: (s) => {
1691
+ assertSchemaMutationAllowed(s);
1692
+ return tx.execute(sanitize(s));
1693
+ },
1596
1694
  transaction: tx.transaction,
1597
1695
  }))
1598
1696
  : undefined,
@@ -1614,11 +1712,19 @@ export function getDbExec() {
1614
1712
  const wrapper = {
1615
1713
  execute: (s) => execAnnotated(s),
1616
1714
  atomicBatch: _exec.atomicBatch
1617
- ? (statements) => _exec.atomicBatch(statements.map((s) => sanitize(s)))
1715
+ ? async (statements) => {
1716
+ for (const statement of statements) {
1717
+ assertSchemaMutationAllowed(statement);
1718
+ }
1719
+ return _exec.atomicBatch(statements.map((s) => sanitize(s)));
1720
+ }
1618
1721
  : undefined,
1619
1722
  transaction: _exec.transaction
1620
1723
  ? (innerFn) => _exec.transaction((tx) => innerFn({
1621
- execute: (s) => tx.execute(sanitize(s)),
1724
+ execute: (s) => {
1725
+ assertSchemaMutationAllowed(s);
1726
+ return tx.execute(sanitize(s));
1727
+ },
1622
1728
  transaction: tx.transaction,
1623
1729
  }))
1624
1730
  : undefined,
@@ -1626,7 +1732,10 @@ export function getDbExec() {
1626
1732
  Object.assign(proxy, wrapper);
1627
1733
  if (_exec.transaction) {
1628
1734
  return _exec.transaction((tx) => fn({
1629
- execute: (s) => tx.execute(sanitize(s)),
1735
+ execute: (s) => {
1736
+ assertSchemaMutationAllowed(s);
1737
+ return tx.execute(sanitize(s));
1738
+ },
1630
1739
  transaction: tx.transaction,
1631
1740
  }));
1632
1741
  }
@@ -1636,6 +1745,9 @@ export function getDbExec() {
1636
1745
  return explicitTransaction(wrapper.execute)(fn);
1637
1746
  },
1638
1747
  async atomicBatch(statements) {
1748
+ for (const statement of statements) {
1749
+ assertSchemaMutationAllowed(statement);
1750
+ }
1639
1751
  if (!_initPromise)
1640
1752
  _initPromise = initClient();
1641
1753
  try {
@@ -1649,7 +1761,12 @@ export function getDbExec() {
1649
1761
  if (!_exec.atomicBatch) {
1650
1762
  throw new Error("This database does not support atomic batches.");
1651
1763
  }
1652
- const batch = (items) => _exec.atomicBatch(items.map((item) => sanitize(item)));
1764
+ const batch = async (items) => {
1765
+ for (const item of items) {
1766
+ assertSchemaMutationAllowed(item);
1767
+ }
1768
+ return _exec.atomicBatch(items.map((item) => sanitize(item)));
1769
+ };
1653
1770
  Object.assign(proxy, { atomicBatch: batch });
1654
1771
  return batch(statements);
1655
1772
  },
@@ -28,7 +28,7 @@
28
28
  * (local dev) there is no such lock problem, so callers keep their existing
29
29
  * behaviour there.
30
30
  */
31
- import { isPostgres, getDbExec } from "./client.js";
31
+ import { getDbExec, isPostgres, isProductionServerlessFunctionRuntime, } from "./client.js";
32
32
  const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
33
33
  /**
34
34
  * Skip on-demand schema DDL entirely. For deployments that run real migrations,
@@ -45,6 +45,8 @@ const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
45
45
  * down the DDL path this flag exists to avoid.
46
46
  */
47
47
  function schemaEnsureDisabled() {
48
+ if (isProductionServerlessFunctionRuntime())
49
+ return true;
48
50
  const raw = process.env.AGENT_NATIVE_SKIP_ENSURE_TABLES?.trim();
49
51
  return !!raw && ["1", "true", "yes", "on"].includes(raw.toLowerCase());
50
52
  }
@@ -67,6 +67,8 @@ export interface EnsureAdditiveColumnsOptions {
67
67
  logger?: EnsureAdditiveColumnsLogger;
68
68
  }
69
69
  export interface EnsureAdditiveColumnsResult {
70
+ /** Whether this invocation actually inspected the live schema. */
71
+ mode: "checked" | "skipped-serverless";
70
72
  /** `"table.column"` entries that were successfully added. */
71
73
  applied: string[];
72
74
  /** Declared columns that were intentionally left unpatched, with why. */
@@ -54,7 +54,7 @@
54
54
  * unreadable) must never crash boot — the caller decides whether/how to
55
55
  * surface `errors` in the returned summary.
56
56
  */
57
- import { isPostgres } from "./client.js";
57
+ import { isPostgres, isProductionServerlessFunctionRuntime, } from "./client.js";
58
58
  const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
59
59
  const defaultLogger = {
60
60
  info: (message) => console.log(message),
@@ -62,7 +62,7 @@ const defaultLogger = {
62
62
  error: (message) => console.error(message),
63
63
  };
64
64
  function emptyResult() {
65
- return { applied: [], skipped: [], errors: [] };
65
+ return { mode: "checked", applied: [], skipped: [], errors: [] };
66
66
  }
67
67
  /**
68
68
  * Resolve `getTableConfig` for the dialect currently in effect. Both
@@ -246,6 +246,9 @@ function describeSchemaDriftError(err) {
246
246
  */
247
247
  export async function ensureAdditiveColumns(options) {
248
248
  const { db, tables, logger = defaultLogger } = options;
249
+ if (isProductionServerlessFunctionRuntime()) {
250
+ return { ...emptyResult(), mode: "skipped-serverless" };
251
+ }
249
252
  const result = emptyResult();
250
253
  let getTableConfig;
251
254
  try {
@@ -21,7 +21,7 @@ export declare function createDb(config: DbConfig): Promise<(import("drizzle-orm
21
21
  })>;
22
22
  export type DrizzleDb = Awaited<ReturnType<typeof createDb>>;
23
23
  export { createGetDb } from "./create-get-db.js";
24
- export { deferMigration, MIGRATION_DEFERRED, runMigrations, type MigrationEntry, type MigrationRunResult, type MigrationSql, } from "./migrations.js";
25
- export { getDbExec, createDbExec, getDatabaseUrl, getDialect, isLocalDatabase, isPostgres, intType, closeDbExec, type DbExec, type DbExecConfig, type DbExecQuery, type DbExecStatement, type Dialect, } from "./client.js";
24
+ export { deferMigration, MIGRATION_DEFERRED, runMigrations, withMigrationRuntime, type MigrationEntry, type MigrationRunResult, type MigrationSql, } from "./migrations.js";
25
+ export { getDbExec, createDbExec, getDatabaseUrl, getDialect, isLocalDatabase, isPostgres, assertSchemaMutationAllowed, isSchemaMutationStatement, isProductionServerlessFunctionRuntime, intType, closeDbExec, type DbExec, type DbExecConfig, type DbExecQuery, type DbExecStatement, type Dialect, } from "./client.js";
26
26
  export { table, text, integer, now } from "./schema.js";
27
27
  export { ensureAdditiveColumns, type EnsureAdditiveColumnsOptions, type EnsureAdditiveColumnsResult, type EnsureAdditiveColumnsLogger, } from "./ensure-additive-columns.js";
package/dist/db/index.js CHANGED
@@ -20,7 +20,7 @@ export async function createDb(config) {
20
20
  throw new Error(`Unsupported driver: ${config.driver}`);
21
21
  }
22
22
  export { createGetDb } from "./create-get-db.js";
23
- export { deferMigration, MIGRATION_DEFERRED, runMigrations, } from "./migrations.js";
24
- export { getDbExec, createDbExec, getDatabaseUrl, getDialect, isLocalDatabase, isPostgres, intType, closeDbExec, } from "./client.js";
23
+ export { deferMigration, MIGRATION_DEFERRED, runMigrations, withMigrationRuntime, } from "./migrations.js";
24
+ export { getDbExec, createDbExec, getDatabaseUrl, getDialect, isLocalDatabase, isPostgres, assertSchemaMutationAllowed, isSchemaMutationStatement, isProductionServerlessFunctionRuntime, intType, closeDbExec, } from "./client.js";
25
25
  export { table, text, integer, now } from "./schema.js";
26
26
  export { ensureAdditiveColumns, } from "./ensure-additive-columns.js";
@@ -19,6 +19,13 @@ export declare function isDuplicateColumnError(err: unknown): boolean;
19
19
  */
20
20
  export declare function isPermissionError(err: unknown): boolean;
21
21
  export interface RunMigrationsOptions {
22
+ /**
23
+ * Run migrations even inside a serverless request runtime. Off by default:
24
+ * see the guard in {@link runMigrations} for why request-path DDL is what
25
+ * took analytics down. Only set this when the caller has no scheduled or
26
+ * background runtime that could migrate instead.
27
+ */
28
+ runInServerlessRequest?: boolean;
22
29
  /**
23
30
  * Name of the migrations bookkeeping table. REQUIRED — there is intentionally
24
31
  * no default. Two templates that share a database (e.g. via the same Neon URL)
@@ -85,61 +92,11 @@ export interface MigrationEntry {
85
92
  run?: () => Promise<MigrationRunResult>;
86
93
  }
87
94
  /**
88
- * Runs a list of migrations against the configured database, gated by a
89
- * per-table bookkeeping row (`options.table`) so repeated boots only apply
90
- * new migrations.
91
- *
92
- * ## Name-based tracking (why it exists)
93
- *
94
- * The legacy gate is purely numeric: `SELECT MAX(version)` from the
95
- * bookkeeping table, then apply every entry whose `version` is greater. That
96
- * scheme silently breaks down when **two independent branches each extend the
97
- * same migration list with different DDL under the same version numbers** —
98
- * exactly what happened to the analytics template's v75-v83 range. Branch A
99
- * shipped alert-rule tables as v75-v78; branch B shipped unrelated DDL as its
100
- * own v75-v83. Whichever branch deployed first recorded "v75..v83 applied"
101
- * in the bookkeeping table. When the other branch merged and deployed, its
102
- * migrations at those same version numbers were never applied — `MAX(version)`
103
- * was already ≥ their version, so the gate treated them as done even though
104
- * their actual DDL had never run. The result: `analytics_migrations` showed
105
- * rows for 1..83 with no gaps, yet `analytics_alert_rules`,
106
- * `analytics_alert_incidents`, and `session_recordings.network_error_count`
107
- * did not exist in production. Version numbers are not a stable identity —
108
- * they're just sequence position, and sequence position collides across
109
- * branches.
110
- *
111
- * Name-based tracking fixes this by keying application on a stable, unique
112
- * **string slug** instead of a position in a shared integer sequence:
113
- *
114
- * - Entries with a `name` are recorded in a companion table,
115
- * `${table}_named` (`name TEXT PRIMARY KEY`), and APPLY IFF their name is
116
- * absent from that table — completely independent of version numbers or
117
- * the legacy `MAX(version)` gate. Two branches can both ship a migration
118
- * named `"analytics-alert-rules-table"` at version 75, or one at version 75
119
- * and the other at version 90 after a rebase — either way it applies
120
- * exactly once per database, keyed on the name.
121
- * - Entries WITHOUT a `name` keep the exact legacy behavior
122
- * (`version > MAX(recorded version)`), so nothing changes for existing
123
- * unnamed migrations.
124
- * - Named migrations still execute in list order, interleaved with unnamed
125
- * ones exactly as written.
126
- * - When a named migration's DDL runs, we record BOTH the named row (always)
127
- * and — if its `version` is greater than the current legacy max — the
128
- * legacy version row too, in the SAME atomic batch/transaction as the DDL.
129
- * This keeps the legacy `MAX(version)` gate monotonically advancing for
130
- * any unnamed migrations that come after it in the list, while ensuring a
131
- * named migration is never "double recorded" in a way that would let it
132
- * re-apply.
133
- * - A duplicate `name` across the migration list throws at startup — that's
134
- * a programmer error (copy-paste or merge mistake), not a runtime data
135
- * problem, and failing loud beats silently tracking the wrong row.
95
+ * Run an explicit release-time migration job with migration duty enabled.
136
96
  *
137
- * New migrations should always set a `name`. Legacy unnamed migrations don't
138
- * need to be renamed retroactively — the two gating strategies coexist in the
139
- * same list — but if a table is EVER at risk of being shared across parallel
140
- * branches (any template's own migrations qualify, since branches routinely
141
- * extend the same list concurrently), giving new entries a name is what makes
142
- * them immune to the collision class described above.
97
+ * The flag is process-local and restored even when the job fails, so a build
98
+ * step can opt in without creating a permanent escape hatch for request code.
143
99
  */
100
+ export declare function withMigrationRuntime<T>(run: () => Promise<T>): Promise<T>;
144
101
  export declare function runMigrations(migrations: Array<MigrationEntry>, options: RunMigrationsOptions): NitroPluginDef;
145
102
  export {};
@@ -226,6 +226,56 @@ function resolveMigrationSql(sql, pg) {
226
226
  * extend the same list concurrently), giving new entries a name is what makes
227
227
  * them immune to the collision class described above.
228
228
  */
229
+ /**
230
+ * True in a production serverless runtime that serves user REQUESTS.
231
+ *
232
+ * Deliberately mirrors the analytics template's own predicate rather than
233
+ * importing it: `packages/core/src/db` must not depend on a template, and the
234
+ * agent/background modules that carry the sibling predicates would introduce a
235
+ * cycle here.
236
+ */
237
+ function isServerlessRequestRuntime() {
238
+ if (process.env.NODE_ENV !== "production")
239
+ return false;
240
+ return (process.env.NETLIFY === "true" ||
241
+ Boolean(process.env.NETLIFY_FUNCTION_NAME) ||
242
+ Boolean(process.env.AWS_LAMBDA_FUNCTION_NAME) ||
243
+ Boolean(process.env.LAMBDA_TASK_ROOT) ||
244
+ process.env.AWS_EXECUTION_ENV?.startsWith("AWS_Lambda") === true ||
245
+ process.env.VERCEL === "1");
246
+ }
247
+ /**
248
+ * A runtime that is ALLOWED to migrate: scheduled jobs and durable background
249
+ * workers, which are off the request path and may take as long as they need.
250
+ *
251
+ * Set by whichever runtime claims migration duty. Analytics sets its own
252
+ * equivalent flag before running the scheduled rollup.
253
+ */
254
+ function isMigrationAuthorizedRuntime() {
255
+ return (globalThis.__AGENT_NATIVE_MIGRATION_RUNTIME__ === true);
256
+ }
257
+ /**
258
+ * Run an explicit release-time migration job with migration duty enabled.
259
+ *
260
+ * The flag is process-local and restored even when the job fails, so a build
261
+ * step can opt in without creating a permanent escape hatch for request code.
262
+ */
263
+ export async function withMigrationRuntime(run) {
264
+ const runtime = globalThis;
265
+ const previous = runtime.__AGENT_NATIVE_MIGRATION_RUNTIME__;
266
+ runtime.__AGENT_NATIVE_MIGRATION_RUNTIME__ = true;
267
+ try {
268
+ return await run();
269
+ }
270
+ finally {
271
+ if (previous === undefined) {
272
+ delete runtime.__AGENT_NATIVE_MIGRATION_RUNTIME__;
273
+ }
274
+ else {
275
+ runtime.__AGENT_NATIVE_MIGRATION_RUNTIME__ = previous;
276
+ }
277
+ }
278
+ }
229
279
  export function runMigrations(migrations, options) {
230
280
  const table = options?.table;
231
281
  if (!table ||
@@ -256,6 +306,26 @@ export function runMigrations(migrations, options) {
256
306
  return async () => { };
257
307
  const namedTable = `${table}_named`;
258
308
  return async () => {
309
+ // Migrations are schema DDL plus an introspection pass. On serverless
310
+ // "first touch" is EVERY cold start, so this lands on the critical path of
311
+ // a user request — measured at ~5.5-8.6s for the version check alone on a
312
+ // 180-table database, with 4-6 copies running concurrently under load.
313
+ //
314
+ // Guarded HERE rather than at each call site on purpose. The analytics
315
+ // template guarded its own runner in #2708; three core plugins
316
+ // (org, context-xray, observational-memory) kept calling this unguarded,
317
+ // so the probe storm survived the fix that was supposed to end it. A
318
+ // fourth special case would have shipped the same bug a fourth time.
319
+ //
320
+ // Schema still has to exist: a scheduled/background runtime sets
321
+ // `__AGENT_NATIVE_MIGRATION_RUNTIME__`, and `runInServerlessRequest` is
322
+ // the explicit opt-in for a caller that genuinely cannot defer.
323
+ if (options?.runInServerlessRequest !== true &&
324
+ isServerlessRequestRuntime() &&
325
+ !isMigrationAuthorizedRuntime()) {
326
+ console.info(`[migrations] Skipping "${table}" migrations in a serverless request runtime`);
327
+ return;
328
+ }
259
329
  try {
260
330
  // Check for Cloudflare D1 binding (only if DATABASE_URL not set)
261
331
  const d1 = getDialect() === "d1"
@@ -3,3 +3,4 @@ export { processRecurringJobs, parseJobFrontmatter, buildJobContent, type JobFro
3
3
  export { getAutomationSchedulerHealth, recordAutomationSchedulerHealth, type AutomationSchedulerHealth, } from "./scheduler-health.js";
4
4
  export { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "./scheduler-dispatch.js";
5
5
  export { createJobTools } from "./tools.js";
6
+ export { AUTOMATION_RUN_MIGRATIONS, runAutomationRunMigrations, } from "./run-history.js";
@@ -3,3 +3,4 @@ export { processRecurringJobs, parseJobFrontmatter, buildJobContent, } from "./s
3
3
  export { getAutomationSchedulerHealth, recordAutomationSchedulerHealth, } from "./scheduler-health.js";
4
4
  export { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "./scheduler-dispatch.js";
5
5
  export { createJobTools } from "./tools.js";
6
+ export { AUTOMATION_RUN_MIGRATIONS, runAutomationRunMigrations, } from "./run-history.js";
@@ -1,3 +1,4 @@
1
+ import { type MigrationEntry } from "../db/migrations.js";
1
2
  /**
2
3
  * "interrupted" is derived at read time, never stored: a process killed
3
4
  * mid-run cannot write its own outcome, so a row left running long past the
@@ -30,6 +31,9 @@ export interface StartAutomationRunInput {
30
31
  /** A pre-created row still waiting for its background worker handoff. */
31
32
  dispatchPending?: boolean;
32
33
  }
34
+ /** Authoritative release-time schema for durable automation history. */
35
+ export declare const AUTOMATION_RUN_MIGRATIONS: MigrationEntry[];
36
+ export declare function runAutomationRunMigrations(nitroApp: unknown): Promise<void>;
33
37
  /**
34
38
  * Record an automation execution. Manual runs create the row before dispatch,
35
39
  * so `dispatchPending` distinguishes that durable handoff from a run that has
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { z } from "zod";
3
3
  import { getDbExec, intType, isPostgres } from "../db/client.js";
4
4
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
5
+ import { runMigrations } from "../db/migrations.js";
5
6
  import { emit as emitBusEvent, registerEvent } from "../event-bus/index.js";
6
7
  registerEvent({
7
8
  name: "automation.run.finished",
@@ -32,6 +33,49 @@ const INTERRUPTED_RUN_MESSAGE = "Worker stopped before a terminal result was rec
32
33
  const CLAIM_LEASE_MS = RUN_LIVENESS_CEILING_MS;
33
34
  /** Rows kept per automation, so a per-minute schedule cannot grow forever. */
34
35
  const RUNS_RETAINED_PER_AUTOMATION = 50;
36
+ /** Authoritative release-time schema for durable automation history. */
37
+ export const AUTOMATION_RUN_MIGRATIONS = [
38
+ {
39
+ version: 1,
40
+ name: "automation-runs-table",
41
+ sql: `
42
+ CREATE TABLE IF NOT EXISTS ${TABLE} (
43
+ id TEXT PRIMARY KEY,
44
+ owner TEXT NOT NULL,
45
+ automation TEXT NOT NULL,
46
+ path TEXT NOT NULL,
47
+ scope TEXT,
48
+ org_id TEXT,
49
+ run_id TEXT,
50
+ thread_id TEXT,
51
+ status TEXT NOT NULL DEFAULT 'running',
52
+ started_at INTEGER NOT NULL,
53
+ finished_at INTEGER,
54
+ error TEXT,
55
+ claimed_at INTEGER,
56
+ dispatch_pending INTEGER NOT NULL DEFAULT 0
57
+ );
58
+ CREATE INDEX IF NOT EXISTS idx_${TABLE}_owner_automation
59
+ ON ${TABLE} (owner, automation, started_at)
60
+ `,
61
+ },
62
+ {
63
+ version: 2,
64
+ name: "automation-runs-claimed-at",
65
+ sql: `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS claimed_at INTEGER`,
66
+ },
67
+ {
68
+ version: 3,
69
+ name: "automation-runs-dispatch-pending",
70
+ sql: `ALTER TABLE ${TABLE}
71
+ ADD COLUMN IF NOT EXISTS dispatch_pending INTEGER NOT NULL DEFAULT 0`,
72
+ },
73
+ ];
74
+ export async function runAutomationRunMigrations(nitroApp) {
75
+ await runMigrations(AUTOMATION_RUN_MIGRATIONS, {
76
+ table: "_automation_run_migrations",
77
+ })(nitroApp);
78
+ }
35
79
  let _initPromise;
36
80
  async function ensureTable() {
37
81
  if (!_initPromise) {
@@ -13,13 +13,13 @@
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
14
  count: number;
15
15
  updated?: undefined;
16
- error?: undefined;
17
16
  ok?: undefined;
17
+ error?: undefined;
18
18
  } | {
19
19
  count?: undefined;
20
20
  updated: number;
21
- error?: undefined;
22
21
  ok?: undefined;
22
+ error?: undefined;
23
23
  } | {
24
24
  count?: undefined;
25
25
  updated?: undefined;
@@ -28,6 +28,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- error?: undefined;
32
31
  ok: boolean;
32
+ error?: undefined;
33
33
  }>>;
@@ -41,16 +41,16 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
- error?: undefined;
45
44
  summary: import("./types.js").TraceSummary;
46
45
  spans: import("./types.js").TraceSpan[];
47
46
  id?: undefined;
47
+ error?: undefined;
48
48
  ok?: undefined;
49
49
  } | {
50
- error?: undefined;
51
50
  summary?: undefined;
52
51
  spans?: undefined;
53
52
  id: string;
53
+ error?: undefined;
54
54
  ok?: undefined;
55
55
  } | {
56
56
  summary?: undefined;
@@ -59,9 +59,9 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
59
59
  error: any;
60
60
  ok?: undefined;
61
61
  } | {
62
- error?: undefined;
63
62
  summary?: undefined;
64
63
  spans?: undefined;
65
64
  id?: undefined;
65
+ error?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- error?: undefined;
19
18
  ok: boolean;
19
+ error?: undefined;
20
20
  }>>;