@agent-native/core 0.157.7 → 0.157.9

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 (69) 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 +258 -0
  4. package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +59 -27
  5. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +4 -1
  6. package/corpus/templates/slides/actions/export-pptx.ts +261 -71
  7. package/corpus/templates/slides/actions/import-file.ts +12 -0
  8. package/corpus/templates/slides/actions/import-pptx.ts +9 -0
  9. package/corpus/templates/slides/app/lib/export-pptx-client.ts +45 -5
  10. package/corpus/templates/slides/server/handlers/import/html-converter.ts +69 -1
  11. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +316 -64
  12. package/corpus/templates/slides/server/lib/source-import.ts +10 -1
  13. package/dist/agent/harness/migrations.d.ts +4 -0
  14. package/dist/agent/harness/migrations.js +39 -0
  15. package/dist/agent/production-agent.d.ts +2 -0
  16. package/dist/agent/production-agent.js +4 -0
  17. package/dist/agent/run-loop-with-resume.d.ts +1 -0
  18. package/dist/agent/run-loop-with-resume.js +45 -30
  19. package/dist/agent/run-migrations.d.ts +8 -0
  20. package/dist/agent/run-migrations.js +61 -0
  21. package/dist/chat-threads/migrations.d.ts +1 -0
  22. package/dist/chat-threads/migrations.js +1 -0
  23. package/dist/chat-threads/schema-migrations.d.ts +8 -0
  24. package/dist/chat-threads/schema-migrations.js +60 -0
  25. package/dist/client/AssistantChat.js +4 -0
  26. package/dist/client/chat/message-components.d.ts +2 -0
  27. package/dist/client/chat/message-components.js +11 -1
  28. package/dist/client/observability/ThumbsFeedback.js +35 -22
  29. package/dist/client/observability/useObservability.d.ts +1 -1
  30. package/dist/client/observability/useObservability.js +6 -3
  31. package/dist/collab/awareness.d.ts +2 -2
  32. package/dist/collab/struct-routes.d.ts +1 -1
  33. package/dist/feature-flags/a2a-action-route.js +4 -2
  34. package/dist/feature-flags/permissions.d.ts +2 -0
  35. package/dist/feature-flags/permissions.js +6 -1
  36. package/dist/ingestion/pptx.d.ts +14 -1
  37. package/dist/ingestion/pptx.js +222 -41
  38. package/dist/localization/core-messages/ar-SA.js +4 -0
  39. package/dist/localization/core-messages/de-DE.js +4 -0
  40. package/dist/localization/core-messages/en-US.d.ts +4 -0
  41. package/dist/localization/core-messages/en-US.js +4 -0
  42. package/dist/localization/core-messages/es-ES.js +4 -0
  43. package/dist/localization/core-messages/fr-FR.js +4 -0
  44. package/dist/localization/core-messages/hi-IN.js +4 -0
  45. package/dist/localization/core-messages/ja-JP.js +4 -0
  46. package/dist/localization/core-messages/ko-KR.js +4 -0
  47. package/dist/localization/core-messages/pt-BR.js +4 -0
  48. package/dist/localization/core-messages/zh-CN.js +4 -0
  49. package/dist/localization/core-messages/zh-TW.js +4 -0
  50. package/dist/localization/core-messages.d.ts +4 -0
  51. package/dist/notifications/routes.d.ts +1 -1
  52. package/dist/observability/feedback.js +40 -16
  53. package/dist/observability/routes.d.ts +1 -1
  54. package/dist/observability/routes.js +15 -3
  55. package/dist/observability/store.js +2 -0
  56. package/dist/observability/traces.js +5 -1
  57. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  58. package/dist/provider-api/actions/provider-api.d.ts +10 -10
  59. package/dist/resources/handlers.d.ts +1 -1
  60. package/dist/server/action-routes.d.ts +7 -5
  61. package/dist/server/action-routes.js +9 -4
  62. package/dist/server/better-auth-instance.js +4 -0
  63. package/dist/server/poll.d.ts +4 -4
  64. package/dist/server/poll.js +45 -13
  65. package/dist/server/realtime-voice.js +2 -1
  66. package/dist/server/release-migrations.js +16 -0
  67. package/dist/usage/migrations.d.ts +4 -0
  68. package/dist/usage/migrations.js +45 -0
  69. package/package.json +3 -3
@@ -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
  }>>;
@@ -30,6 +30,16 @@ export async function submitFeedback(opts) {
30
30
  await insertFeedback(entry);
31
31
  return entry;
32
32
  }
33
+ function unwrapPersistedThreadMessage(value) {
34
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
35
+ return null;
36
+ }
37
+ const record = value;
38
+ const nested = record.message;
39
+ return nested && typeof nested === "object" && !Array.isArray(nested)
40
+ ? nested
41
+ : record;
42
+ }
33
43
  async function getThreadMessages(threadId) {
34
44
  await ensureObservabilityTables();
35
45
  const client = getDbExec();
@@ -47,22 +57,36 @@ async function getThreadMessages(threadId) {
47
57
  const messages = data.messages ?? data;
48
58
  if (!Array.isArray(messages))
49
59
  return [];
50
- return messages
51
- .filter((m) => m &&
52
- typeof m.role === "string" &&
53
- (typeof m.content === "string" ||
54
- (Array.isArray(m.content) &&
55
- m.content.some((p) => p.type === "text"))))
56
- .map((m) => ({
57
- role: m.role,
58
- content: typeof m.content === "string"
59
- ? m.content
60
- : m.content
61
- .filter((p) => p.type === "text")
62
- .map((p) => p.text ?? "")
63
- .join(""),
64
- createdAt: m.createdAt ? Number(m.createdAt) : undefined,
65
- }));
60
+ return messages.flatMap((value) => {
61
+ const message = unwrapPersistedThreadMessage(value);
62
+ if (!message)
63
+ return [];
64
+ const content = message.content;
65
+ if ((message.role !== "user" && message.role !== "assistant") ||
66
+ (typeof content !== "string" && !Array.isArray(content))) {
67
+ return [];
68
+ }
69
+ const text = typeof content === "string"
70
+ ? content
71
+ : content
72
+ .filter((part) => Boolean(part) &&
73
+ typeof part === "object" &&
74
+ !Array.isArray(part) &&
75
+ part.type === "text")
76
+ .map((part) => (typeof part.text === "string" ? part.text : ""))
77
+ .join("");
78
+ if (!text)
79
+ return [];
80
+ return [
81
+ {
82
+ role: message.role,
83
+ content: text,
84
+ createdAt: typeof message.createdAt === "number"
85
+ ? message.createdAt
86
+ : undefined,
87
+ },
88
+ ];
89
+ });
66
90
  }
67
91
  catch {
68
92
  return [];
@@ -8,7 +8,7 @@
8
8
  * GET /traces/:runId — get trace detail (spans + summary)
9
9
  * GET /traces/:runId/evals — get evals for a run
10
10
  * POST /feedback — submit feedback
11
- * GET /feedback?since=N&limit=N — list feedback entries
11
+ * GET /feedback?since=N&limit=N&feedbackType=text — list feedback entries
12
12
  * GET /feedback/stats?since=N — feedback aggregation stats
13
13
  * GET /satisfaction?since=N — satisfaction scores
14
14
  * GET /evals/stats?since=N — eval stats
@@ -8,7 +8,7 @@
8
8
  * GET /traces/:runId — get trace detail (spans + summary)
9
9
  * GET /traces/:runId/evals — get evals for a run
10
10
  * POST /feedback — submit feedback
11
- * GET /feedback?since=N&limit=N — list feedback entries
11
+ * GET /feedback?since=N&limit=N&feedbackType=text — list feedback entries
12
12
  * GET /feedback/stats?since=N — feedback aggregation stats
13
13
  * GET /satisfaction?since=N — satisfaction scores
14
14
  * GET /evals/stats?since=N — eval stats
@@ -27,6 +27,16 @@ import { track } from "../tracking/registry.js";
27
27
  import { emitAiFeedbackSurveyEvent } from "./posthog-ai.js";
28
28
  import { getObservabilityOverview, getTraceSummaries, getTraceSummary, getTraceSpansForRun, getEvalsForRun, insertFeedback, getFeedback, getFeedbackStats, getSatisfactionScores, getEvalStats, listExperiments, insertExperiment, getExperiment, updateExperiment, getExperimentResults, } from "./store.js";
29
29
  import { trackingIdentityProperties } from "./tracking-identity.js";
30
+ const FEEDBACK_TYPES = [
31
+ "thumbs_up",
32
+ "thumbs_down",
33
+ "category",
34
+ "text",
35
+ ];
36
+ function isFeedbackType(value) {
37
+ return (typeof value === "string" &&
38
+ FEEDBACK_TYPES.includes(value));
39
+ }
30
40
  function nanoid(size = 21) {
31
41
  const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
32
42
  let id = "";
@@ -143,8 +153,7 @@ export function createObservabilityHandler() {
143
153
  return { error: "Invalid JSON body" };
144
154
  }
145
155
  const feedbackType = body?.feedbackType;
146
- if (!feedbackType ||
147
- !["thumbs_up", "thumbs_down", "category", "text"].includes(feedbackType)) {
156
+ if (!isFeedbackType(feedbackType)) {
148
157
  setResponseStatus(event, 400);
149
158
  return { error: "feedbackType is required" };
150
159
  }
@@ -229,6 +238,9 @@ export function createObservabilityHandler() {
229
238
  return getFeedback({
230
239
  sinceMs: parseSince(q),
231
240
  limit: parseLimit(q),
241
+ feedbackType: isFeedbackType(q.feedbackType)
242
+ ? q.feedbackType
243
+ : undefined,
232
244
  userId: owner,
233
245
  });
234
246
  }
@@ -197,6 +197,7 @@ export async function ensureObservabilityTables() {
197
197
  await ensureIndexExists("idx_feedback_thread", `CREATE INDEX IF NOT EXISTS idx_feedback_thread ON agent_feedback (thread_id)`);
198
198
  await ensureIndexExists("idx_feedback_created", `CREATE INDEX IF NOT EXISTS idx_feedback_created ON agent_feedback (created_at)`);
199
199
  await ensureIndexExists("idx_feedback_user", `CREATE INDEX IF NOT EXISTS idx_feedback_user ON agent_feedback (user_id, created_at)`);
200
+ await ensureIndexExists("idx_feedback_type_created", `CREATE INDEX IF NOT EXISTS idx_feedback_type_created ON agent_feedback (feedback_type, created_at)`);
200
201
  await ensureIndexExists("idx_satisfaction_thread", `CREATE INDEX IF NOT EXISTS idx_satisfaction_thread ON agent_satisfaction_scores (thread_id)`);
201
202
  await ensureIndexExists("idx_satisfaction_user", `CREATE INDEX IF NOT EXISTS idx_satisfaction_user ON agent_satisfaction_scores (user_id, computed_at)`);
202
203
  await ensureIndexExists("idx_evals_run", `CREATE INDEX IF NOT EXISTS idx_evals_run ON agent_evals (run_id)`);
@@ -252,6 +253,7 @@ export async function ensureObservabilityTables() {
252
253
  `CREATE INDEX IF NOT EXISTS idx_feedback_thread ON agent_feedback (thread_id)`,
253
254
  `CREATE INDEX IF NOT EXISTS idx_feedback_created ON agent_feedback (created_at)`,
254
255
  `CREATE INDEX IF NOT EXISTS idx_feedback_user ON agent_feedback (user_id, created_at)`,
256
+ `CREATE INDEX IF NOT EXISTS idx_feedback_type_created ON agent_feedback (feedback_type, created_at)`,
255
257
  `CREATE INDEX IF NOT EXISTS idx_satisfaction_thread ON agent_satisfaction_scores (thread_id)`,
256
258
  `CREATE INDEX IF NOT EXISTS idx_satisfaction_user ON agent_satisfaction_scores (user_id, computed_at)`,
257
259
  `CREATE INDEX IF NOT EXISTS idx_evals_run ON agent_evals (run_id)`,
@@ -637,7 +637,11 @@ export async function instrumentAgentLoop(opts) {
637
637
  catch { }
638
638
  let llmCallCount = 0;
639
639
  if (usage || runStatus === "error") {
640
- llmCallCount = 1;
640
+ llmCallCount =
641
+ usage?.llmCalls ??
642
+ // Compatibility for custom loop implementations that predate the
643
+ // attempt counter: a measured run still counts as one call.
644
+ 1;
641
645
  const generationUsage = usage ?? {
642
646
  inputTokens: 0,
643
647
  outputTokens: 0,
@@ -75,10 +75,9 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
- message?: undefined;
78
+ deleted?: undefined;
79
79
  id?: undefined;
80
80
  provider?: undefined;
81
- deleted?: undefined;
82
81
  providers: {
83
82
  id: string;
84
83
  label: string;
@@ -92,41 +91,42 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
92
91
  found?: undefined;
93
92
  registered?: undefined;
94
93
  label?: undefined;
95
- } | {
96
94
  message?: undefined;
97
- count?: undefined;
98
- id?: undefined;
95
+ } | {
99
96
  deleted?: undefined;
97
+ id?: undefined;
100
98
  providers?: undefined;
99
+ count?: undefined;
101
100
  found: boolean;
102
101
  provider: import("../custom-registry.js").CustomProviderConfig;
103
102
  registered?: undefined;
104
103
  label?: undefined;
105
- } | {
106
104
  message?: undefined;
107
- count?: undefined;
108
- provider?: undefined;
105
+ } | {
109
106
  deleted?: undefined;
107
+ provider?: undefined;
110
108
  providers?: undefined;
109
+ count?: undefined;
111
110
  found: boolean;
112
111
  id: string;
113
112
  registered?: undefined;
114
113
  label?: undefined;
115
- } | {
116
114
  message?: undefined;
117
- count?: undefined;
115
+ } | {
118
116
  provider?: undefined;
119
117
  providers?: undefined;
118
+ count?: undefined;
120
119
  found?: undefined;
121
120
  deleted: boolean;
122
121
  id: string;
123
122
  registered?: undefined;
124
123
  label?: undefined;
124
+ message?: undefined;
125
125
  } | {
126
- count?: undefined;
127
- provider?: undefined;
128
126
  deleted?: undefined;
127
+ provider?: undefined;
129
128
  providers?: undefined;
129
+ count?: undefined;
130
130
  found?: undefined;
131
131
  registered: boolean;
132
132
  id: string;
@@ -311,8 +311,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
311
311
  notes?: string | undefined;
312
312
  scope?: "org" | "user" | undefined;
313
313
  }, {
314
- deleted?: undefined;
314
+ message?: undefined;
315
315
  id?: undefined;
316
+ deleted?: undefined;
316
317
  providers: {
317
318
  id: string;
318
319
  label: string;
@@ -327,41 +328,40 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
327
328
  found?: undefined;
328
329
  registered?: undefined;
329
330
  label?: undefined;
330
- message?: undefined;
331
331
  } | {
332
- deleted?: undefined;
332
+ message?: undefined;
333
+ count?: undefined;
333
334
  id?: undefined;
335
+ deleted?: undefined;
334
336
  providers?: undefined;
335
- count?: undefined;
336
337
  found: boolean;
337
338
  provider: import("../custom-registry.js").CustomProviderConfig;
338
339
  registered?: undefined;
339
340
  label?: undefined;
340
- message?: undefined;
341
341
  } | {
342
+ message?: undefined;
343
+ count?: undefined;
342
344
  deleted?: undefined;
343
345
  providers?: undefined;
344
- count?: undefined;
345
346
  provider?: undefined;
346
347
  found: boolean;
347
348
  id: string;
348
349
  registered?: undefined;
349
350
  label?: undefined;
350
- message?: undefined;
351
351
  } | {
352
- providers?: undefined;
352
+ message?: undefined;
353
353
  count?: undefined;
354
+ providers?: undefined;
354
355
  provider?: undefined;
355
356
  found?: undefined;
356
357
  deleted: boolean;
357
358
  id: string;
358
359
  registered?: undefined;
359
360
  label?: undefined;
360
- message?: undefined;
361
361
  } | {
362
+ count?: undefined;
362
363
  deleted?: undefined;
363
364
  providers?: undefined;
364
- count?: undefined;
365
365
  provider?: undefined;
366
366
  found?: undefined;
367
367
  registered: boolean;
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
48
48
  }>;
49
49
  /** DELETE /_agent-native/resources/:id — delete a resource */
50
50
  export declare function handleDeleteResource(event: any): Promise<{
51
- error: string;
52
51
  ok?: undefined;
52
+ error: string;
53
53
  } | {
54
54
  error?: undefined;
55
55
  ok: boolean;
@@ -14,12 +14,14 @@ export type ActionRouteResolvedCaller = AgentRunOwnerContext & {
14
14
  * Org to scope the request to, verified from the same credential as the
15
15
  * caller identity (e.g. the A2A token's org claim). When omitted, the org
16
16
  * is derived from the verified owner email via the framework's owner→org
17
- * membership lookup. The ambient session/org state on the request is never
18
- * consulted for adapter-resolved callers: a request can carry both a valid
19
- * A2A bearer and an unrelated browser cookie, and the cookie user's org
20
- * must not leak into the token caller's request context.
17
+ * membership lookup. An explicit `null` means the verified caller has no
18
+ * org and must not fall back to another membership. The ambient session/org
19
+ * state on the request is never consulted for adapter-resolved callers: a
20
+ * request can carry both a valid A2A bearer and an unrelated browser cookie,
21
+ * and the cookie user's org must not leak into the token caller's request
22
+ * context.
21
23
  */
22
- orgId?: string;
24
+ orgId?: string | null;
23
25
  /** Verified A2A correlation and issuer metadata for the audit row. */
24
26
  delegationJti?: string;
25
27
  delegationIssuer?: string;
@@ -3,6 +3,7 @@ import { verifyA2ATokenWithClaims } from "../a2a-claims.js";
3
3
  import { isActionContractError, isAgentActionStopError } from "../action.js";
4
4
  import { isTransientDatabaseError } from "../db/client.js";
5
5
  import { declaresFeatureFlagDelegation } from "../feature-flags/a2a-action-route.js";
6
+ import { isFeatureFlagAdminEmail } from "../feature-flags/permissions.js";
6
7
  import { resolveOrgByDomain, resolveOrgIdForEmail } from "../org/context.js";
7
8
  import { readBody } from "../server/h3-helpers.js";
8
9
  import { EMBED_TARGET_HEADER } from "../shared/embed-auth.js";
@@ -55,7 +56,7 @@ async function resolveFeatureFlagA2ACaller(event, actionName) {
55
56
  if (!claims || !claims.scope.includes(required))
56
57
  throw new Error("Invalid feature flag delegation");
57
58
  const localOrg = await resolveOrgByDomain(claims.orgDomain);
58
- if (!localOrg)
59
+ if (!localOrg && !isFeatureFlagAdminEmail(claims.email))
59
60
  throw new Error("Invalid feature flag delegation");
60
61
  if (actionName === "set-feature-flag" &&
61
62
  (await consumeOneTimeJti(claims.jti))) {
@@ -63,7 +64,7 @@ async function resolveFeatureFlagA2ACaller(event, actionName) {
63
64
  }
64
65
  return {
65
66
  owner: claims.email,
66
- orgId: localOrg.orgId,
67
+ orgId: localOrg?.orgId ?? null,
67
68
  anonymous: false,
68
69
  delegationJti: claims.jti,
69
70
  delegationIssuer: claims.issuer,
@@ -362,7 +363,8 @@ export function mountActionRoutes(nitroApp, actions, options) {
362
363
  }
363
364
  // Org scoping. For adapter-resolved callers the org must come
364
365
  // exclusively from the verified credential: the adapter-asserted
365
- // orgId when present, otherwise the owner-email membership lookup.
366
+ // orgId when present, explicit null when the caller has no org,
367
+ // otherwise the owner-email membership lookup.
366
368
  // The request's ambient session/org state (`resolveOrgId`, usually
367
369
  // getSession-backed) is deliberately NOT consulted — a request can
368
370
  // carry both a valid A2A bearer and an unrelated same-origin browser
@@ -372,7 +374,10 @@ export function mountActionRoutes(nitroApp, actions, options) {
372
374
  let orgId;
373
375
  if (resolvedCaller) {
374
376
  orgId = normalizeOrgId(resolvedCaller.orgId);
375
- if (!orgId && resolvedCaller.owner && !resolvedCaller.anonymous) {
377
+ if (resolvedCaller.orgId !== null &&
378
+ !orgId &&
379
+ resolvedCaller.owner &&
380
+ !resolvedCaller.anonymous) {
376
381
  orgId = await storedActiveOrgId(resolvedCaller.owner);
377
382
  }
378
383
  }
@@ -854,6 +854,10 @@ async function createBetterAuthInstance(config) {
854
854
  basePath,
855
855
  baseURL: appUrl,
856
856
  database,
857
+ // Auth schema relations are intentionally not registered here. Keep the
858
+ // experimental relational-query path off so a bundled Drizzle adapter
859
+ // cannot recurse while resolving a session or account join.
860
+ experimental: { joins: false },
857
861
  secret,
858
862
  emailAndPassword: {
859
863
  enabled: true,
@@ -233,10 +233,10 @@ export declare class AppSyncState {
233
233
  * bounded index range scan.
234
234
  *
235
235
  * The `lastDurablePrune` throttle below is per-process, so every serverless
236
- * worker prunes on its first poll. That was survivable only once the work
237
- * itself became bounded and indexed; if concurrency is still a problem,
238
- * the next step is a `pg_try_advisory_xact_lock` so one worker prunes at a
239
- * time. Deliberately not done yet — measure before adding a lock.
236
+ * worker prunes on its first poll. Postgres workers also take a
237
+ * transaction-scoped advisory lease for each batch: only one worker deletes
238
+ * at a time, but each batch still commits independently so a killed worker
239
+ * cannot hold a long transaction open.
240
240
  */
241
241
  private pruneDurableEvents;
242
242
  persistSyncEvent(event: ChangeEvent, dedupeKey?: string, presetId?: string): Promise<void>;
@@ -42,6 +42,12 @@ const DURABLE_RETENTION_MS = 24 * 60 * 60 * 1000;
42
42
  */
43
43
  const DURABLE_PRUNE_BATCH = 10_000;
44
44
  const DURABLE_PRUNE_MAX_BATCHES = 40;
45
+ /**
46
+ * Postgres advisory-lock key for the per-database retention worker. The lock
47
+ * is transaction-scoped so a serverless worker that is killed cannot leave a
48
+ * lease behind for another worker to clear.
49
+ */
50
+ const DURABLE_PRUNE_LOCK_KEY = "agent-native:sync-events-prune";
45
51
  /**
46
52
  * How far back a cold-started process replays durable action markers.
47
53
  * Covers the gap between a separate action process writing its marker and this
@@ -455,10 +461,10 @@ export class AppSyncState {
455
461
  * bounded index range scan.
456
462
  *
457
463
  * The `lastDurablePrune` throttle below is per-process, so every serverless
458
- * worker prunes on its first poll. That was survivable only once the work
459
- * itself became bounded and indexed; if concurrency is still a problem,
460
- * the next step is a `pg_try_advisory_xact_lock` so one worker prunes at a
461
- * time. Deliberately not done yet — measure before adding a lock.
464
+ * worker prunes on its first poll. Postgres workers also take a
465
+ * transaction-scoped advisory lease for each batch: only one worker deletes
466
+ * at a time, but each batch still commits independently so a killed worker
467
+ * cannot hold a long transaction open.
462
468
  */
463
469
  async pruneDurableEvents(client) {
464
470
  const now = Date.now();
@@ -469,17 +475,43 @@ export class AppSyncState {
469
475
  let deleted = 0;
470
476
  try {
471
477
  for (let batch = 0; batch < DURABLE_PRUNE_MAX_BATCHES; batch++) {
472
- const result = await client.execute({
478
+ const runBatch = async (tx) => {
479
+ if (this.isPg()) {
480
+ const lockResult = await tx.execute({
481
+ sql: "SELECT pg_try_advisory_xact_lock(hashtextextended(?, 0::bigint)) AS acquired",
482
+ args: [DURABLE_PRUNE_LOCK_KEY],
483
+ });
484
+ const acquired = lockResult.rows[0]?.acquired;
485
+ if (acquired !== true && acquired !== "t")
486
+ return null;
487
+ }
473
488
  // `id IN (...)` rather than ctid/rowid: both are dialect-specific,
474
489
  // and the primary key is indexed on every dialect we ship.
475
- sql: `DELETE FROM sync_events WHERE id IN (
476
- SELECT id FROM sync_events WHERE created_at < ?
477
- ORDER BY created_at, id LIMIT ?
478
- )`,
479
- args: [cutoff, DURABLE_PRUNE_BATCH],
480
- });
481
- deleted += result.rowsAffected;
482
- if (result.rowsAffected < DURABLE_PRUNE_BATCH)
490
+ const result = await tx.execute({
491
+ sql: `DELETE FROM sync_events WHERE id IN (
492
+ SELECT id FROM sync_events WHERE created_at < ?
493
+ ORDER BY created_at, id LIMIT ?
494
+ )`,
495
+ args: [cutoff, DURABLE_PRUNE_BATCH],
496
+ });
497
+ return result.rowsAffected;
498
+ };
499
+ // A Postgres advisory transaction lock is only useful when the lock
500
+ // and delete share a connection and transaction. Never fall back to
501
+ // an uncoordinated delete on a transactionless client.
502
+ const rowsAffected = this.isPg()
503
+ ? client.transaction
504
+ ? await client.transaction(runBatch)
505
+ : null
506
+ : await runBatch(client);
507
+ if (rowsAffected == null) {
508
+ if (this.isPg() && !client.transaction) {
509
+ throw new Error("Postgres prune requires a database transaction");
510
+ }
511
+ break;
512
+ }
513
+ deleted += rowsAffected;
514
+ if (rowsAffected < DURABLE_PRUNE_BATCH)
483
515
  break;
484
516
  }
485
517
  this.durablePruneFailures = 0;
@@ -25,7 +25,7 @@ export const REALTIME_VOICE_CAPABILITY_HEADER = "X-Agent-Native-Realtime-Capabil
25
25
  const OPENAI_REALTIME_CALLS_URL = "https://api.openai.com/v1/realtime/calls";
26
26
  const DEFAULT_MODEL = "gpt-realtime-2.1";
27
27
  const DEFAULT_VOICE = "marin";
28
- const DEFAULT_INSTRUCTIONS = "You are the live voice interface for this Agent Native app. Speak naturally, briefly, and conversationally. Use the available function tools when the user asks you to navigate or take an action. Never claim an action succeeded until its tool result confirms success. If a tool requires approval, explain that the user must approve it in chat.";
28
+ const DEFAULT_INSTRUCTIONS = "You are the live voice interface for this Agent Native app. Speak naturally, briefly, and conversationally. Use the available function tools when the user asks you to navigate or take an action. When the user asks about a previous conversation, saved chat details, or something they told you before, search with the `chat-history` tool before saying you cannot access it. Summarize a matching result and open a thread only when the user asks. If the user repeats a request, acknowledge the prior attempt and finish or correct the missing part instead of restarting from scratch or asking the same clarification again. Never claim an action succeeded until its tool result confirms success. If a tool requires approval, explain that the user must approve it in chat.";
29
29
  const MAX_INSTRUCTIONS_CHARS = 16_000;
30
30
  const MAX_TOOL_DESCRIPTION_CHARS = 2_000;
31
31
  const MAX_APPROVAL_KEY_CHARS = 1_024;
@@ -61,6 +61,7 @@ const REALTIME_VOICE_PRIORITY_TOOLS = [
61
61
  "set-url-path",
62
62
  "set-search-params",
63
63
  "view-screen",
64
+ "chat-history",
64
65
  TOOL_SEARCH_ACTION_NAME,
65
66
  ];
66
67
  function readSafeHeader(event, name) {
@@ -1,11 +1,15 @@
1
1
  import { CONTEXT_XRAY_MIGRATIONS } from "../agent/context-xray/migrations.js";
2
+ import { AGENT_HARNESS_SESSION_MIGRATIONS, AGENT_HARNESS_SESSION_MIGRATIONS_TABLE, } from "../agent/harness/migrations.js";
2
3
  import { OBSERVATIONAL_MEMORY_MIGRATIONS } from "../agent/observational-memory/migrations.js";
4
+ import { AGENT_RUN_MIGRATIONS, AGENT_RUN_MIGRATIONS_TABLE, } from "../agent/run-migrations.js";
3
5
  import { AGENT_TOOL_APPROVAL_MIGRATIONS, AGENT_TOOL_APPROVAL_MIGRATIONS_TABLE, } from "../agent/tool-approval-migrations.js";
6
+ import { CHAT_THREAD_SCHEMA_MIGRATIONS, CHAT_THREAD_SCHEMA_MIGRATIONS_TABLE, } from "../chat-threads/schema-migrations.js";
4
7
  import { runMigrations } from "../db/migrations.js";
5
8
  import { runAutomationRunMigrations } from "../jobs/run-history.js";
6
9
  import { runAutomationSchedulerHealthMigrations } from "../jobs/scheduler-health.js";
7
10
  import { OAUTH_TOKEN_MIGRATIONS, OAUTH_TOKEN_MIGRATIONS_TABLE, } from "../oauth-tokens/migrations.js";
8
11
  import { ORG_MIGRATIONS } from "../org/migrations.js";
12
+ import { USAGE_ALERT_MIGRATIONS, USAGE_ALERT_MIGRATIONS_TABLE, } from "../usage/migrations.js";
9
13
  import { runBetterAuthMigrations } from "./better-auth-migrations.js";
10
14
  import { IDENTITY_SSO_MIGRATIONS } from "./identity-sso-migrations.js";
11
15
  /**
@@ -23,6 +27,18 @@ export async function runFrameworkReleaseMigrations(nitroApp) {
23
27
  await runMigrations(OAUTH_TOKEN_MIGRATIONS, {
24
28
  table: OAUTH_TOKEN_MIGRATIONS_TABLE,
25
29
  })(nitroApp);
30
+ await runMigrations(CHAT_THREAD_SCHEMA_MIGRATIONS, {
31
+ table: CHAT_THREAD_SCHEMA_MIGRATIONS_TABLE,
32
+ })(nitroApp);
33
+ await runMigrations(AGENT_RUN_MIGRATIONS, {
34
+ table: AGENT_RUN_MIGRATIONS_TABLE,
35
+ })(nitroApp);
36
+ await runMigrations(AGENT_HARNESS_SESSION_MIGRATIONS, {
37
+ table: AGENT_HARNESS_SESSION_MIGRATIONS_TABLE,
38
+ })(nitroApp);
39
+ await runMigrations(USAGE_ALERT_MIGRATIONS, {
40
+ table: USAGE_ALERT_MIGRATIONS_TABLE,
41
+ })(nitroApp);
26
42
  await runMigrations(ORG_MIGRATIONS, { table: "_org_migrations" })(nitroApp);
27
43
  await runMigrations(IDENTITY_SSO_MIGRATIONS, {
28
44
  table: "_identity_sso_migrations",
@@ -0,0 +1,4 @@
1
+ import type { MigrationEntry } from "../db/migrations.js";
2
+ export declare const USAGE_ALERT_MIGRATIONS_TABLE = "_usage_alert_migrations";
3
+ /** Authoritative release-time schema for usage alert rules and events. */
4
+ export declare const USAGE_ALERT_MIGRATIONS: MigrationEntry[];
@@ -0,0 +1,45 @@
1
+ export const USAGE_ALERT_MIGRATIONS_TABLE = "_usage_alert_migrations";
2
+ /** Authoritative release-time schema for usage alert rules and events. */
3
+ export const USAGE_ALERT_MIGRATIONS = [
4
+ {
5
+ version: 1,
6
+ name: "usage-alert-rules-and-events-tables",
7
+ sql: `
8
+ CREATE TABLE IF NOT EXISTS usage_alert_rules (
9
+ id TEXT PRIMARY KEY,
10
+ scope TEXT NOT NULL,
11
+ owner_email TEXT NOT NULL,
12
+ org_id TEXT,
13
+ app_id TEXT,
14
+ unit TEXT NOT NULL,
15
+ period TEXT NOT NULL,
16
+ limit_value INTEGER NOT NULL,
17
+ channels TEXT NOT NULL,
18
+ enabled INTEGER NOT NULL DEFAULT 1,
19
+ dismissed_window_start INTEGER,
20
+ created_at INTEGER NOT NULL,
21
+ updated_at INTEGER NOT NULL
22
+ );
23
+ CREATE TABLE IF NOT EXISTS usage_alert_events (
24
+ id TEXT PRIMARY KEY,
25
+ rule_id TEXT NOT NULL,
26
+ window_start INTEGER NOT NULL,
27
+ created_at INTEGER NOT NULL
28
+ );
29
+ CREATE INDEX IF NOT EXISTS idx_usage_alert_rules_owner
30
+ ON usage_alert_rules (owner_email, org_id, scope);
31
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_usage_alert_events_rule_window
32
+ ON usage_alert_events (rule_id, window_start)
33
+ `,
34
+ },
35
+ {
36
+ version: 2,
37
+ name: "usage-alert-default-column",
38
+ sql: `ALTER TABLE usage_alert_rules ADD COLUMN IF NOT EXISTS is_default INTEGER NOT NULL DEFAULT 0`,
39
+ },
40
+ {
41
+ version: 3,
42
+ name: "usage-alert-notification-column",
43
+ sql: "ALTER TABLE usage_alert_events ADD COLUMN IF NOT EXISTS notification_id TEXT",
44
+ },
45
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.157.7",
3
+ "version": "0.157.9",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -385,7 +385,7 @@
385
385
  "@tanstack/react-table": "^8.21.3",
386
386
  "@uiw/react-codemirror": "^4.25.10",
387
387
  "ajv": "^8.20.0",
388
- "better-auth": "1.6.16",
388
+ "better-auth": "1.6.28",
389
389
  "better-sqlite3": "^12.8.0",
390
390
  "botframework-connector": "^4.23.3",
391
391
  "clsx": "^2.1.1",
@@ -426,7 +426,7 @@
426
426
  "yjs": "^13.6.31",
427
427
  "zod": "^4.3.6",
428
428
  "@agent-native/recap-cli": "0.5.4",
429
- "@agent-native/toolkit": "^0.16.1"
429
+ "@agent-native/toolkit": "^0.16.2"
430
430
  },
431
431
  "devDependencies": {
432
432
  "@ai-sdk/anthropic": "^3.0.71",