@agent-native/core 0.157.8 → 0.157.10

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 (154) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/purge-first-party-analytics-postgres.ts +26 -8
  3. package/corpus/templates/analytics/scripts/first-party-analytics-postgres-retention-dry-run.sql +261 -0
  4. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +3 -1
  5. package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +75 -31
  6. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +4 -1
  7. package/corpus/templates/analytics/server/plugins/db.ts +139 -0
  8. package/corpus/templates/slides/AGENTS.md +14 -10
  9. package/corpus/templates/slides/actions/export-pptx.ts +1311 -121
  10. package/corpus/templates/slides/actions/import-file.ts +28 -1
  11. package/corpus/templates/slides/actions/import-pptx.ts +69 -42
  12. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +222 -29
  13. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +71 -0
  14. package/corpus/templates/slides/app/global.css +19 -1
  15. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +44 -5
  16. package/corpus/templates/slides/app/lib/export-pdf-client.ts +13 -6
  17. package/corpus/templates/slides/app/lib/export-pptx-client.ts +601 -30
  18. package/corpus/templates/slides/app/pages/DeckEditor.tsx +18 -1
  19. package/corpus/templates/slides/server/handlers/import/html-converter.ts +1240 -22
  20. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +329 -64
  21. package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +24 -0
  22. package/corpus/templates/slides/server/lib/source-import.ts +10 -1
  23. package/corpus/templates/slides/shared/aspect-ratios.ts +13 -1
  24. package/dist/agent/harness/migrations.d.ts +4 -0
  25. package/dist/agent/harness/migrations.js +60 -0
  26. package/dist/agent/production-agent.d.ts +2 -0
  27. package/dist/agent/production-agent.js +15 -0
  28. package/dist/agent/run-loop-with-resume.d.ts +1 -0
  29. package/dist/agent/run-loop-with-resume.js +44 -12
  30. package/dist/agent/run-migrations.d.ts +8 -0
  31. package/dist/agent/run-migrations.js +61 -0
  32. package/dist/automations/service.d.ts +6 -0
  33. package/dist/automations/service.js +35 -0
  34. package/dist/chat-threads/migrations.d.ts +1 -0
  35. package/dist/chat-threads/migrations.js +1 -0
  36. package/dist/chat-threads/schema-migrations.d.ts +8 -0
  37. package/dist/chat-threads/schema-migrations.js +60 -0
  38. package/dist/cli/code-agent-connector.d.ts +4 -1
  39. package/dist/cli/code-agent-connector.js +154 -23
  40. package/dist/cli/code-agent-executor.js +255 -13
  41. package/dist/cli/code-agent-runs.d.ts +1 -0
  42. package/dist/cli/code-agent-runs.js +2 -1
  43. package/dist/cli/portal-workspace.d.ts +49 -0
  44. package/dist/cli/portal-workspace.js +320 -0
  45. package/dist/client/AssistantChat.d.ts +1 -1
  46. package/dist/client/AssistantChat.js +43 -61
  47. package/dist/client/ConnectBuilderCard.js +33 -5
  48. package/dist/client/agent-chat-adapter.js +4 -0
  49. package/dist/client/analytics-platform.d.ts +2 -0
  50. package/dist/client/analytics-platform.js +12 -0
  51. package/dist/client/analytics.d.ts +3 -0
  52. package/dist/client/analytics.js +10 -1
  53. package/dist/client/chat/index.d.ts +1 -0
  54. package/dist/client/chat/index.js +1 -0
  55. package/dist/client/chat/message-components.d.ts +2 -0
  56. package/dist/client/chat/message-components.js +11 -1
  57. package/dist/client/chat/run-recovery.d.ts +2 -1
  58. package/dist/client/chat/run-recovery.js +2 -2
  59. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  60. package/dist/client/chat-first/apps-rail.js +11 -11
  61. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  62. package/dist/client/chat-first/surface-tabs.js +38 -36
  63. package/dist/client/chat-first/types.d.ts +5 -0
  64. package/dist/client/chat-first.d.ts +1 -1
  65. package/dist/client/chat-first.js +4 -4
  66. package/dist/client/desktop-local-code-change.d.ts +10 -0
  67. package/dist/client/desktop-local-code-change.js +16 -0
  68. package/dist/client/index.d.ts +1 -0
  69. package/dist/client/index.js +1 -0
  70. package/dist/client/observability/ThumbsFeedback.js +53 -26
  71. package/dist/client/observability/useObservability.d.ts +1 -1
  72. package/dist/client/observability/useObservability.js +6 -3
  73. package/dist/client/terminal/AgentTerminal.d.ts +9 -1
  74. package/dist/client/terminal/AgentTerminal.js +38 -3
  75. package/dist/client/terminal/index.d.ts +1 -1
  76. package/dist/client/terminal/index.js +1 -1
  77. package/dist/client/track.js +9 -1
  78. package/dist/client/use-action.js +3 -0
  79. package/dist/collab/awareness.d.ts +2 -2
  80. package/dist/deploy/build.js +1 -1
  81. package/dist/feature-flags/a2a-action-route.js +4 -2
  82. package/dist/feature-flags/permissions.d.ts +2 -0
  83. package/dist/feature-flags/permissions.js +6 -1
  84. package/dist/ingestion/pptx.d.ts +92 -1
  85. package/dist/ingestion/pptx.js +1180 -115
  86. package/dist/integrations/index.d.ts +3 -3
  87. package/dist/integrations/index.js +2 -2
  88. package/dist/integrations/plugin.js +95 -6
  89. package/dist/integrations/remote-commands-store.d.ts +5 -0
  90. package/dist/integrations/remote-commands-store.js +63 -22
  91. package/dist/integrations/remote-devices-store.d.ts +2 -1
  92. package/dist/integrations/remote-devices-store.js +46 -0
  93. package/dist/integrations/remote-types.d.ts +20 -0
  94. package/dist/jobs/actions/list-recurring-jobs.d.ts +3 -0
  95. package/dist/jobs/actions/list-recurring-jobs.js +5 -1
  96. package/dist/jobs/frontmatter.d.ts +16 -0
  97. package/dist/jobs/frontmatter.js +53 -0
  98. package/dist/jobs/remote-execution.d.ts +42 -0
  99. package/dist/jobs/remote-execution.js +287 -0
  100. package/dist/jobs/scheduler.js +102 -3
  101. package/dist/jobs/tools.js +50 -3
  102. package/dist/localization/core-messages/ar-SA.js +4 -0
  103. package/dist/localization/core-messages/de-DE.js +4 -0
  104. package/dist/localization/core-messages/en-US.d.ts +4 -0
  105. package/dist/localization/core-messages/en-US.js +4 -0
  106. package/dist/localization/core-messages/es-ES.js +4 -0
  107. package/dist/localization/core-messages/fr-FR.js +4 -0
  108. package/dist/localization/core-messages/hi-IN.js +4 -0
  109. package/dist/localization/core-messages/ja-JP.js +4 -0
  110. package/dist/localization/core-messages/ko-KR.js +4 -0
  111. package/dist/localization/core-messages/pt-BR.js +4 -0
  112. package/dist/localization/core-messages/zh-CN.js +4 -0
  113. package/dist/localization/core-messages/zh-TW.js +4 -0
  114. package/dist/localization/core-messages.d.ts +4 -0
  115. package/dist/notifications/routes.d.ts +3 -3
  116. package/dist/observability/feedback.js +40 -16
  117. package/dist/observability/routes.d.ts +6 -6
  118. package/dist/observability/routes.js +15 -3
  119. package/dist/observability/store.js +2 -0
  120. package/dist/observability/traces.js +5 -1
  121. package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
  122. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  123. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  124. package/dist/resources/handlers.d.ts +1 -1
  125. package/dist/secrets/routes.d.ts +9 -9
  126. package/dist/server/action-routes.d.ts +7 -5
  127. package/dist/server/action-routes.js +13 -6
  128. package/dist/server/agent-chat-plugin.js +6 -0
  129. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  130. package/dist/server/agent-run-context.d.ts +1 -0
  131. package/dist/server/agent-run-context.js +9 -0
  132. package/dist/server/better-auth-instance.js +4 -0
  133. package/dist/server/core-routes-plugin.js +8 -1
  134. package/dist/server/realtime-voice.js +2 -1
  135. package/dist/server/release-migrations.js +16 -0
  136. package/dist/server/request-context.d.ts +2 -0
  137. package/dist/server/transcribe-voice.d.ts +1 -1
  138. package/dist/shared/analytics-platform.d.ts +6 -0
  139. package/dist/shared/analytics-platform.js +14 -0
  140. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  141. package/dist/shared/mcp-embed-headers.js +1 -1
  142. package/dist/styles/agent-native.css +83 -103
  143. package/dist/terminal/cli-registry.js +5 -0
  144. package/dist/tracking/registry.js +9 -1
  145. package/dist/triggers/actions/list-automations.d.ts +3 -0
  146. package/dist/triggers/actions/list-automations.js +3 -0
  147. package/dist/triggers/actions/manage-automation.d.ts +9 -0
  148. package/dist/triggers/actions/manage-automation.js +14 -2
  149. package/dist/triggers/actions.js +82 -7
  150. package/dist/usage/migrations.d.ts +4 -0
  151. package/dist/usage/migrations.js +45 -0
  152. package/docs/content/template-content-developers.mdx +48 -0
  153. package/docs/content/toolkit-ui.mdx +57 -0
  154. package/package.json +3 -3
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8256
34
+ - template files: 8257
@@ -88,11 +88,20 @@ export default defineAction({
88
88
  startReceivedAt,
89
89
  startEventDate: startReceivedAt.slice(0, 10),
90
90
  };
91
- const postgres = await countFirstPartyAnalyticsPostgresRows(
92
- scope,
93
- includeLegacyOwnerRows,
94
- window,
95
- );
91
+ let postgres: Awaited<
92
+ ReturnType<typeof countFirstPartyAnalyticsPostgresRows>
93
+ > | null = null;
94
+ let postgresError: string | null = null;
95
+ try {
96
+ postgres = await countFirstPartyAnalyticsPostgresRows(
97
+ scope,
98
+ includeLegacyOwnerRows,
99
+ window,
100
+ );
101
+ } catch (error) {
102
+ if (!dryRun) throw error;
103
+ postgresError = error instanceof Error ? error.message : String(error);
104
+ }
96
105
  let bigQuery: {
97
106
  eventCount: number;
98
107
  dailyRollupRows: number;
@@ -115,12 +124,15 @@ export default defineAction({
115
124
  }
116
125
  }
117
126
 
118
- const uncopiedEventRows = bigQuery
119
- ? Math.max(0, postgres.eventRows - bigQuery.eventCount)
120
- : null;
127
+ const uncopiedEventRows =
128
+ bigQuery && postgres
129
+ ? Math.max(0, postgres.eventRows - bigQuery.eventCount)
130
+ : null;
121
131
  const safeToDelete =
122
132
  backend.sink === "bigquery" &&
123
133
  backend.backfillCompleted === true &&
134
+ postgresError === null &&
135
+ postgres !== null &&
124
136
  bigQueryError === null &&
125
137
  bigQuery !== null &&
126
138
  uncopiedEventRows === 0;
@@ -136,6 +148,11 @@ export default defineAction({
136
148
  "Postgres purge is blocked until the durable BigQuery backfill reports completed.",
137
149
  );
138
150
  }
151
+ if (postgresError || !postgres) {
152
+ throw new Error(
153
+ `Postgres purge is blocked because the scoped Postgres inventory could not be verified: ${postgresError ?? "unknown Postgres state"}`,
154
+ );
155
+ }
139
156
  if (bigQueryError || !bigQuery) {
140
157
  throw new Error(
141
158
  `Postgres purge is blocked because BigQuery readiness could not be verified: ${bigQueryError ?? "unknown warehouse state"}`,
@@ -179,6 +196,7 @@ export default defineAction({
179
196
  sink: backend.sink,
180
197
  backfillCompleted: backend.backfillCompleted === true,
181
198
  postgres,
199
+ postgresError,
182
200
  bigQuery,
183
201
  bigQueryError,
184
202
  uncopiedEventRows,
@@ -0,0 +1,261 @@
1
+ -- FIRST-PARTY ANALYTICS POSTGRES RETENTION PREVIEW - READ ONLY
2
+ --
3
+ -- This file intentionally contains no INSERT, UPDATE, DELETE, TRUNCATE, or DDL.
4
+ -- It reports only rows in the explicitly resolved Builder.io organization and
5
+ -- the exact 60-day window that was copied to BigQuery by the migration.
6
+ --
7
+ -- The candidate set excludes http.response because that event class was
8
+ -- intentionally omitted from the BigQuery backfill. Rows older than the
9
+ -- lookback window and legacy owner-only rows are reported separately and are
10
+ -- never included in the candidate count. Session replay, exception issues,
11
+ -- public-key metadata, rollups, alerts, and pressure data are reported as
12
+ -- preserved SQL rows; they are not deletion candidates.
13
+ -- The organization and owner values below are intentionally fake safe
14
+ -- defaults. Supply scoped values in a local copy or SQL-client parameters;
15
+ -- never commit real organization IDs or employee addresses here.
16
+
17
+ WITH params AS (
18
+ SELECT
19
+ 'org-example'::text AS org_id,
20
+ 'owner@example.com'::text AS owner_email,
21
+ '2026-06-10T18:50:43.370Z'::text AS lookback_start,
22
+ '2026-08-09T19:11:44.834Z'::text AS cutoff,
23
+ 'http.response'::text AS excluded_event_name
24
+ ),
25
+ org_window AS (
26
+ SELECT e.id, e.event_name, e.received_at
27
+ FROM analytics_events AS e
28
+ CROSS JOIN params AS p
29
+ WHERE e.org_id = p.org_id
30
+ AND e.received_at >= p.lookback_start
31
+ AND e.received_at < p.cutoff
32
+ ),
33
+ org_older AS (
34
+ SELECT e.received_at
35
+ FROM analytics_events AS e
36
+ CROSS JOIN params AS p
37
+ WHERE e.org_id = p.org_id
38
+ AND e.received_at < p.lookback_start
39
+ ),
40
+ legacy_window AS (
41
+ SELECT e.received_at
42
+ FROM analytics_events AS e
43
+ CROSS JOIN params AS p
44
+ WHERE e.org_id IS NULL
45
+ AND e.owner_email = p.owner_email
46
+ AND e.received_at >= p.lookback_start
47
+ AND e.received_at < p.cutoff
48
+ ),
49
+ candidate AS (
50
+ SELECT w.id, w.event_name, w.received_at
51
+ FROM org_window AS w
52
+ CROSS JOIN params AS p
53
+ WHERE w.event_name IS DISTINCT FROM p.excluded_event_name
54
+ ),
55
+ metrics AS (
56
+ SELECT
57
+ 'events'::text AS section,
58
+ 'candidate_non_http_response'::text AS metric,
59
+ COUNT(*) AS row_count,
60
+ MIN(received_at) AS first_seen,
61
+ MAX(received_at) AS last_seen,
62
+ 'Only rows in the copied 60-day org window; not a delete.'::text AS notes
63
+ FROM candidate
64
+
65
+ UNION ALL
66
+ SELECT
67
+ 'events',
68
+ 'excluded_http_response',
69
+ COUNT(*),
70
+ MIN(received_at),
71
+ MAX(received_at),
72
+ 'Excluded from BigQuery backfill; do not delete without a separate decision.'
73
+ FROM org_window AS w
74
+ CROSS JOIN params AS p
75
+ WHERE w.event_name = p.excluded_event_name
76
+
77
+ UNION ALL
78
+ SELECT
79
+ 'events',
80
+ 'older_than_lookback',
81
+ COUNT(*),
82
+ MIN(received_at),
83
+ MAX(received_at),
84
+ 'Not copied by the 60-day migration; not a delete candidate.'
85
+ FROM org_older
86
+
87
+ UNION ALL
88
+ SELECT
89
+ 'events',
90
+ 'legacy_owner_only_in_window',
91
+ COUNT(*),
92
+ MIN(received_at),
93
+ MAX(received_at),
94
+ 'org_id IS NULL branch; migration checkpoint copied zero legacy rows.'
95
+ FROM legacy_window
96
+
97
+ UNION ALL
98
+ SELECT
99
+ 'event_names',
100
+ event_name,
101
+ COUNT(*),
102
+ MIN(received_at),
103
+ MAX(received_at),
104
+ 'Candidate rows grouped by event name.'
105
+ FROM candidate
106
+ GROUP BY event_name
107
+
108
+ UNION ALL
109
+ SELECT
110
+ 'preserved_sql',
111
+ 'analytics_event_daily_rollups',
112
+ COUNT(*),
113
+ NULL,
114
+ NULL,
115
+ 'Preserved derived event counts.'
116
+ FROM analytics_event_daily_rollups AS r
117
+ CROSS JOIN params AS p
118
+ WHERE r.org_id = p.org_id
119
+
120
+ UNION ALL
121
+ SELECT
122
+ 'preserved_sql',
123
+ 'analytics_user_days',
124
+ COUNT(*),
125
+ NULL,
126
+ NULL,
127
+ 'Preserved active-user/retention rows.'
128
+ FROM analytics_user_days AS u
129
+ CROSS JOIN params AS p
130
+ WHERE u.org_id = p.org_id
131
+
132
+ UNION ALL
133
+ SELECT
134
+ 'preserved_sql',
135
+ 'analytics_event_volume_usage',
136
+ COUNT(*),
137
+ NULL,
138
+ NULL,
139
+ 'Preserved volume-cap state.'
140
+ FROM analytics_event_volume_usage AS v
141
+ CROSS JOIN params AS p
142
+ WHERE v.org_id = p.org_id
143
+
144
+ UNION ALL
145
+ SELECT
146
+ 'preserved_sql',
147
+ 'analytics_query_pressure_daily',
148
+ COUNT(*),
149
+ NULL,
150
+ NULL,
151
+ 'Preserved query-pressure diagnostics.'
152
+ FROM analytics_query_pressure_daily AS q
153
+ CROSS JOIN params AS p
154
+ WHERE q.org_id = p.org_id
155
+
156
+ UNION ALL
157
+ SELECT
158
+ 'preserved_sql',
159
+ 'analytics_alert_rules',
160
+ COUNT(*),
161
+ NULL,
162
+ NULL,
163
+ 'Preserved alert definitions.'
164
+ FROM analytics_alert_rules AS a
165
+ CROSS JOIN params AS p
166
+ WHERE a.org_id = p.org_id
167
+
168
+ UNION ALL
169
+ SELECT
170
+ 'preserved_sql',
171
+ 'analytics_alert_incidents',
172
+ COUNT(*),
173
+ NULL,
174
+ NULL,
175
+ 'Preserved alert history.'
176
+ FROM analytics_alert_incidents AS i
177
+ CROSS JOIN params AS p
178
+ WHERE i.org_id = p.org_id
179
+
180
+ UNION ALL
181
+ SELECT
182
+ 'preserved_sql',
183
+ 'session_recordings',
184
+ COUNT(*),
185
+ MIN(started_at),
186
+ MAX(COALESCE(ended_at, started_at)),
187
+ 'Session replay summaries; never delete with event rows.'
188
+ FROM session_recordings AS s
189
+ CROSS JOIN params AS p
190
+ WHERE s.org_id = p.org_id
191
+
192
+ UNION ALL
193
+ SELECT
194
+ 'preserved_sql',
195
+ 'session_replay_chunks',
196
+ COUNT(*),
197
+ MIN(created_at),
198
+ MAX(created_at),
199
+ 'Session replay payload references; never delete with event rows.'
200
+ FROM session_replay_chunks AS c
201
+ CROSS JOIN params AS p
202
+ WHERE c.org_id = p.org_id
203
+
204
+ UNION ALL
205
+ SELECT
206
+ 'preserved_sql',
207
+ 'session_replay_ingests',
208
+ COUNT(*),
209
+ MIN(created_at),
210
+ MAX(created_at),
211
+ 'Session replay ingest audit rows; never delete with event rows.'
212
+ FROM session_replay_ingests AS g
213
+ CROSS JOIN params AS p
214
+ WHERE g.org_id = p.org_id
215
+
216
+ UNION ALL
217
+ SELECT
218
+ 'preserved_sql',
219
+ 'error_issues',
220
+ COUNT(*),
221
+ MIN(first_seen_at),
222
+ MAX(last_seen_at),
223
+ 'Grouped exception issues; preserved separately from analytics_events.'
224
+ FROM error_issues AS x
225
+ CROSS JOIN params AS p
226
+ WHERE x.org_id = p.org_id
227
+
228
+ UNION ALL
229
+ SELECT
230
+ 'preserved_sql',
231
+ 'error_events',
232
+ COUNT(*),
233
+ MIN(occurred_at),
234
+ MAX(occurred_at),
235
+ 'Exception occurrences; preserved separately from analytics_events.'
236
+ FROM error_events AS x
237
+ CROSS JOIN params AS p
238
+ WHERE x.org_id = p.org_id
239
+
240
+ UNION ALL
241
+ SELECT
242
+ 'preserved_sql',
243
+ 'analytics_public_keys',
244
+ COUNT(*),
245
+ MIN(created_at),
246
+ MAX(created_at),
247
+ 'Public-key metadata; preserved separately from analytics_events.'
248
+ FROM analytics_public_keys AS k
249
+ CROSS JOIN params AS p
250
+ WHERE k.org_id = p.org_id
251
+ )
252
+ SELECT section, metric, row_count, first_seen, last_seen, notes
253
+ FROM metrics
254
+ ORDER BY
255
+ CASE section
256
+ WHEN 'events' THEN 1
257
+ WHEN 'event_names' THEN 2
258
+ ELSE 3
259
+ END,
260
+ row_count DESC,
261
+ metric;
@@ -1130,7 +1130,9 @@ export async function getFirstPartyAnalyticsBigQueryMetrics(
1130
1130
  WITH scoped_events AS (
1131
1131
  SELECT *
1132
1132
  FROM \`${physical.events}\`
1133
- WHERE ${tenantFilter}${startDateFilter} AND event_date <= ${today}
1133
+ WHERE ${tenantFilter}${startDateFilter}
1134
+ AND event_name IS DISTINCT FROM 'http.response'
1135
+ AND event_date <= ${today}
1134
1136
  )
1135
1137
  SELECT
1136
1138
  COUNT(*) AS event_count,
@@ -1,4 +1,5 @@
1
- import { and, count, eq, gte, isNull, or } from "drizzle-orm";
1
+ import { getDbExec } from "@agent-native/core/db";
2
+ import { and, eq, gte, isNull, ne, or } from "drizzle-orm";
2
3
 
3
4
  import { getDb, schema } from "../db/index.js";
4
5
 
@@ -46,13 +47,52 @@ function windowPredicate(
46
47
  : and(predicate, gte(table.eventDate, window.startEventDate));
47
48
  }
48
49
 
49
- async function countScopedRows(table: any, predicate: any): Promise<number> {
50
- const [row] = await (getDb() as any)
51
- .select({ count: count() })
52
- .from(table)
53
- .where(predicate);
54
- const value = Number(row?.count ?? 0);
55
- return Number.isFinite(value) ? value : 0;
50
+ type CountTable =
51
+ | "analytics_events"
52
+ | "analytics_event_daily_rollups"
53
+ | "analytics_user_days";
54
+
55
+ type CountTimeColumn = "received_at" | "event_date";
56
+
57
+ async function countScopedRows(
58
+ table: CountTable,
59
+ timeColumn: CountTimeColumn,
60
+ scope: FirstPartyAnalyticsPurgeScope,
61
+ includeLegacyOwnerRows: boolean,
62
+ window: FirstPartyAnalyticsPurgeWindow,
63
+ ): Promise<number> {
64
+ const scopeSql = includeLegacyOwnerRows
65
+ ? "(org_id = ? OR (org_id IS NULL AND owner_email = ?))"
66
+ : "org_id = ?";
67
+ const args: unknown[] = includeLegacyOwnerRows
68
+ ? [scope.orgId, scope.userEmail]
69
+ : [scope.orgId];
70
+ const eventFilter =
71
+ table === "analytics_events"
72
+ ? "\n AND event_name IS DISTINCT FROM 'http.response'"
73
+ : "";
74
+ args.push(
75
+ timeColumn === "received_at"
76
+ ? window.startReceivedAt
77
+ : window.startEventDate,
78
+ );
79
+
80
+ const { rows } = await getDbExec().execute({
81
+ sql: `SELECT COUNT(*) AS row_count
82
+ FROM ${table}
83
+ WHERE ${scopeSql}
84
+ ${eventFilter}
85
+ AND ${timeColumn} >= ?`,
86
+ args,
87
+ timeoutMs: 5_000,
88
+ maxAttempts: 1,
89
+ });
90
+ const rawCount = (rows[0] as { row_count?: unknown } | undefined)?.row_count;
91
+ const value = Number(rawCount);
92
+ if (!Number.isFinite(value)) {
93
+ throw new Error(`Postgres count returned an invalid value for ${table}`);
94
+ }
95
+ return value;
56
96
  }
57
97
 
58
98
  export async function countFirstPartyAnalyticsPostgresRows(
@@ -60,31 +100,28 @@ export async function countFirstPartyAnalyticsPostgresRows(
60
100
  includeLegacyOwnerRows: boolean,
61
101
  window: FirstPartyAnalyticsPurgeWindow,
62
102
  ): Promise<FirstPartyAnalyticsPostgresPurgeCounts> {
63
- const predicates = [
64
- windowPredicate(
65
- schema.analyticsEvents,
66
- scopePredicate(schema.analyticsEvents, scope, includeLegacyOwnerRows),
103
+ const [eventRows, dailyRollupRows, userDayRows] = await Promise.all([
104
+ countScopedRows(
105
+ "analytics_events",
106
+ "received_at",
107
+ scope,
108
+ includeLegacyOwnerRows,
67
109
  window,
68
110
  ),
69
- windowPredicate(
70
- schema.analyticsEventDailyRollups,
71
- scopePredicate(
72
- schema.analyticsEventDailyRollups,
73
- scope,
74
- includeLegacyOwnerRows,
75
- ),
111
+ countScopedRows(
112
+ "analytics_event_daily_rollups",
113
+ "event_date",
114
+ scope,
115
+ includeLegacyOwnerRows,
76
116
  window,
77
117
  ),
78
- windowPredicate(
79
- schema.analyticsUserDays,
80
- scopePredicate(schema.analyticsUserDays, scope, includeLegacyOwnerRows),
118
+ countScopedRows(
119
+ "analytics_user_days",
120
+ "event_date",
121
+ scope,
122
+ includeLegacyOwnerRows,
81
123
  window,
82
124
  ),
83
- ];
84
- const [eventRows, dailyRollupRows, userDayRows] = await Promise.all([
85
- countScopedRows(schema.analyticsEvents, predicates[0]),
86
- countScopedRows(schema.analyticsEventDailyRollups, predicates[1]),
87
- countScopedRows(schema.analyticsUserDays, predicates[2]),
88
125
  ]);
89
126
  return { eventRows, dailyRollupRows, userDayRows };
90
127
  }
@@ -103,10 +140,17 @@ export async function purgeFirstPartyAnalyticsPostgresRows(
103
140
  await tx
104
141
  .delete(schema.analyticsEvents)
105
142
  .where(
106
- windowPredicate(
107
- schema.analyticsEvents,
108
- scopePredicate(schema.analyticsEvents, scope, includeLegacyOwnerRows),
109
- window,
143
+ and(
144
+ windowPredicate(
145
+ schema.analyticsEvents,
146
+ scopePredicate(
147
+ schema.analyticsEvents,
148
+ scope,
149
+ includeLegacyOwnerRows,
150
+ ),
151
+ window,
152
+ ),
153
+ ne(schema.analyticsEvents.eventName, "http.response"),
110
154
  ),
111
155
  );
112
156
  await tx
@@ -68,6 +68,7 @@ export function validateWorkspaceFeatureFlagMutation(
68
68
  expected: {
69
69
  key: string;
70
70
  orgDomain: string;
71
+ allowExplicitNoOrgTarget?: boolean;
71
72
  rules?: Record<string, unknown>;
72
73
  enabledForEmail?: string;
73
74
  },
@@ -82,7 +83,8 @@ export function validateWorkspaceFeatureFlagMutation(
82
83
  !Array.isArray(payload.rules) &&
83
84
  !!payload.scope &&
84
85
  typeof payload.scope === "object" &&
85
- payload.scope.orgDomain === expected.orgDomain;
86
+ (payload.scope.orgDomain === expected.orgDomain ||
87
+ (expected.allowExplicitNoOrgTarget && payload.scope.orgDomain === null));
86
88
  if (!valid)
87
89
  throw new Error(
88
90
  "The target app returned an unsupported or unverified feature flag mutation response.",
@@ -360,6 +362,7 @@ export async function setWorkspaceFeatureFlag(
360
362
  return validateWorkspaceFeatureFlagMutation(result.body, {
361
363
  key: input.key,
362
364
  orgDomain,
365
+ allowExplicitNoOrgTarget: true,
363
366
  ...(input.operation === "replace-rules" && input.rules
364
367
  ? {
365
368
  rules: {
@@ -1,6 +1,11 @@
1
1
  import {
2
+ deferMigration,
2
3
  ensureAdditiveColumns,
4
+ createDbExec,
3
5
  getDbExec,
6
+ getDatabaseUrl,
7
+ isPostgres,
8
+ MIGRATION_DEFERRED,
4
9
  runMigrations,
5
10
  withMigrationRuntime,
6
11
  } from "@agent-native/core/db";
@@ -34,6 +39,102 @@ const schemaTables = Object.values(schema).filter(isDrizzleTable);
34
39
  // packages/core/src/db/migrations.ts for the full rationale). Version numbers
35
40
  // alone are not a safe identity across parallel branches that each extend
36
41
  // this list independently — see the v75-v83 incident documented on v75 below.
42
+ const ANALYTICS_EVENT_CURSOR_INDEX_REPAIR_TIMEOUT_MS = 15 * 60 * 1000;
43
+
44
+ function getAnalyticsMigrationDatabaseUrl(): string {
45
+ const appName = process.env.APP_NAME?.toUpperCase().replace(/-/g, "_");
46
+ const directUrl = appName
47
+ ? process.env[`${appName}_DATABASE_URL_UNPOOLED`]
48
+ : undefined;
49
+ const url =
50
+ directUrl ||
51
+ process.env.NETLIFY_DATABASE_URL_UNPOOLED ||
52
+ process.env.DATABASE_URL_UNPOOLED ||
53
+ getDatabaseUrl();
54
+ return url.replace(/-pooler(\.[a-z0-9.-]+\.neon\.tech)/, "$1");
55
+ }
56
+
57
+ const ANALYTICS_EVENT_CURSOR_INDEX_REPAIR_LOCK =
58
+ "hashtext('agent-native:analytics-event-cursor-index-repair')";
59
+
60
+ async function repairAnalyticsEventCursorIndexes(): Promise<
61
+ void | typeof MIGRATION_DEFERRED
62
+ > {
63
+ if (!isPostgres()) return;
64
+
65
+ const repairIndexes = [
66
+ {
67
+ name: "analytics_events_org_received_id_non_http_idx",
68
+ createSql: `CREATE INDEX CONCURRENTLY IF NOT EXISTS analytics_events_org_received_id_non_http_idx
69
+ ON analytics_events (org_id, received_at, id)
70
+ WHERE event_name IS DISTINCT FROM 'http.response'`,
71
+ },
72
+ {
73
+ name: "analytics_events_owner_received_id_non_http_idx",
74
+ createSql: `CREATE INDEX CONCURRENTLY IF NOT EXISTS analytics_events_owner_received_id_non_http_idx
75
+ ON analytics_events (owner_email, received_at, id)
76
+ WHERE org_id IS NULL AND event_name IS DISTINCT FROM 'http.response'`,
77
+ },
78
+ {
79
+ name: "analytics_event_daily_rollups_org_event_date_idx",
80
+ createSql: `CREATE INDEX CONCURRENTLY IF NOT EXISTS analytics_event_daily_rollups_org_event_date_idx
81
+ ON analytics_event_daily_rollups (org_id, event_date)`,
82
+ },
83
+ {
84
+ name: "analytics_user_days_org_event_date_idx",
85
+ createSql: `CREATE INDEX CONCURRENTLY IF NOT EXISTS analytics_user_days_org_event_date_idx
86
+ ON analytics_user_days (org_id, event_date)`,
87
+ },
88
+ ];
89
+
90
+ const exec = await createDbExec({ url: getAnalyticsMigrationDatabaseUrl() });
91
+ const query = (sql: string) =>
92
+ exec.execute({
93
+ sql,
94
+ timeoutMs: ANALYTICS_EVENT_CURSOR_INDEX_REPAIR_TIMEOUT_MS,
95
+ maxAttempts: 1,
96
+ });
97
+
98
+ try {
99
+ const lockResult = await query(
100
+ `SELECT pg_try_advisory_lock(${ANALYTICS_EVENT_CURSOR_INDEX_REPAIR_LOCK}) AS acquired`,
101
+ );
102
+ if (lockResult.rows[0]?.acquired !== true) return deferMigration();
103
+
104
+ let lockHeld = true;
105
+ try {
106
+ const { rows } = await query(`
107
+ SELECT c.relname, i.indisvalid, i.indisready
108
+ FROM pg_class c
109
+ JOIN pg_index i ON i.indexrelid = c.oid
110
+ WHERE c.relname IN (${repairIndexes.map(({ name }) => `'${name}'`).join(", ")})
111
+ `);
112
+ const readyIndexes = new Set(
113
+ rows
114
+ .filter((row) => row.indisvalid === true && row.indisready === true)
115
+ .map((row) => String(row.relname)),
116
+ );
117
+ const expectedIndexes = repairIndexes.map(({ name }) => name);
118
+ if (expectedIndexes.every((name) => readyIndexes.has(name))) return;
119
+
120
+ for (const { name, createSql } of repairIndexes) {
121
+ if (readyIndexes.has(name)) continue;
122
+ await query(`DROP INDEX CONCURRENTLY IF EXISTS ${name}`);
123
+ await query(createSql);
124
+ }
125
+ } finally {
126
+ if (lockHeld) {
127
+ await query(
128
+ `SELECT pg_advisory_unlock(${ANALYTICS_EVENT_CURSOR_INDEX_REPAIR_LOCK})`,
129
+ );
130
+ lockHeld = false;
131
+ }
132
+ }
133
+ } finally {
134
+ await exec.close?.();
135
+ }
136
+ }
137
+
37
138
  export const runAnalyticsMigrations = runMigrations(
38
139
  [
39
140
  {
@@ -1660,6 +1761,44 @@ export const runAnalyticsMigrations = runMigrations(
1660
1761
  ON analytics_bigquery_backfill_shards (job_id, status, next_run_at, lease_expires_at, start_at);`,
1661
1762
  },
1662
1763
  },
1764
+ {
1765
+ version: 144,
1766
+ name: "analytics-events-backfill-filtered-cursor-index-repair",
1767
+ sql: {
1768
+ postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_org_received_id_non_http_idx;
1769
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS analytics_events_org_received_id_non_http_idx
1770
+ ON analytics_events (org_id, received_at, id)
1771
+ WHERE event_name IS DISTINCT FROM 'http.response';
1772
+ DROP INDEX CONCURRENTLY IF EXISTS analytics_events_owner_received_id_non_http_idx;
1773
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS analytics_events_owner_received_id_non_http_idx
1774
+ ON analytics_events (owner_email, received_at, id)
1775
+ WHERE org_id IS NULL AND event_name IS DISTINCT FROM 'http.response';`,
1776
+ sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_org_received_id_non_http_idx
1777
+ ON analytics_events (org_id, received_at, id)
1778
+ WHERE event_name IS NOT 'http.response';
1779
+ CREATE INDEX IF NOT EXISTS analytics_events_owner_received_id_non_http_idx
1780
+ ON analytics_events (owner_email, received_at, id)
1781
+ WHERE org_id IS NULL AND event_name IS NOT 'http.response';`,
1782
+ },
1783
+ },
1784
+ {
1785
+ version: 145,
1786
+ name: "analytics-events-backfill-filtered-cursor-index-direct-repair",
1787
+ run: repairAnalyticsEventCursorIndexes,
1788
+ sql: {
1789
+ postgres: "SELECT 1",
1790
+ sqlite: "SELECT 1",
1791
+ },
1792
+ },
1793
+ {
1794
+ version: 146,
1795
+ name: "analytics-events-purge-inventory-index-direct-repair",
1796
+ run: repairAnalyticsEventCursorIndexes,
1797
+ sql: {
1798
+ postgres: "SELECT 1",
1799
+ sqlite: "SELECT 1",
1800
+ },
1801
+ },
1663
1802
  ],
1664
1803
  { table: "analytics_migrations" },
1665
1804
  );