@agent-native/core 0.145.6 → 0.145.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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: 7887
34
+ - template files: 7888
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-07
4
+ ---
5
+
6
+ Analytics no longer stalls under bursts of event ingest — key bookkeeping writes no longer serialize the whole pipeline
@@ -1,6 +1,6 @@
1
1
  import { getDbExec } from "@agent-native/core/db";
2
2
  import { runWithRequestContext } from "@agent-native/core/server";
3
- import { and, eq, isNull, or } from "drizzle-orm";
3
+ import { and, eq, isNull, lt, or } from "drizzle-orm";
4
4
 
5
5
  import { FIRST_PARTY_ANALYTICS_QUERY_TIMEOUT_MS } from "../../shared/dashboard-report-timeouts.js";
6
6
  import { getDb, schema } from "../db/index.js";
@@ -181,6 +181,68 @@ export async function listAnalyticsPublicKeys(
181
181
  }));
182
182
  }
183
183
 
184
+ /** How stale the last-used stamp must be before a request pays to refresh it. */
185
+ const LAST_USED_AT_REFRESH_MS = 60_000;
186
+
187
+ /**
188
+ * Refresh a public key's last-used stamp without serializing ingest behind it.
189
+ *
190
+ * This UPDATE used to sit inside the ingest transaction, so every concurrent
191
+ * request for one public key took an exclusive row lock on that key's row and
192
+ * held it until the transaction committed — which meant through the rollup
193
+ * upsert. Production stacked 36 writers on three hot rows waiting 38-57s each;
194
+ * that exhausted the connection pool, and the whole app stopped loading while
195
+ * Postgres reported "no server connection available, client being queued". The
196
+ * stamp is bookkeeping: it needs neither atomicity with the events nor
197
+ * second-precision, and losing one is harmless.
198
+ *
199
+ * The staleness predicate throttles in SQL rather than in the caller, because
200
+ * Postgres only locks rows an UPDATE actually matches — a request whose key was
201
+ * stamped seconds ago matches nothing and takes no lock at all. Doing the same
202
+ * check in JS would reintroduce the convoy, since every racing request would
203
+ * still issue its own unconditional write.
204
+ *
205
+ * `last_used_at` is TEXT holding ISO-8601 UTC (always `Z`-suffixed), so `lt` is
206
+ * a lexicographic comparison that happens to be chronological. Storing a local
207
+ * or offset-bearing timestamp here would silently break this ordering.
208
+ */
209
+ export async function touchPublicKeyLastUsedAt(
210
+ keyId: string,
211
+ receivedAt: string,
212
+ ): Promise<void> {
213
+ const parsed = Date.parse(receivedAt);
214
+ if (!Number.isFinite(parsed)) {
215
+ console.warn(
216
+ "[first-party-analytics] Skipping last-used stamp: unparseable receivedAt",
217
+ receivedAt,
218
+ );
219
+ return;
220
+ }
221
+ const staleBefore = new Date(parsed - LAST_USED_AT_REFRESH_MS).toISOString();
222
+ try {
223
+ const db = await getDb();
224
+ await db
225
+ .update(schema.analyticsPublicKeys)
226
+ .set({ lastUsedAt: receivedAt })
227
+ .where(
228
+ and(
229
+ eq(schema.analyticsPublicKeys.id, keyId),
230
+ or(
231
+ isNull(schema.analyticsPublicKeys.lastUsedAt),
232
+ lt(schema.analyticsPublicKeys.lastUsedAt, staleBefore),
233
+ ),
234
+ ),
235
+ );
236
+ } catch (error) {
237
+ // Best-effort by design: a failed stamp must not reject an ingest whose
238
+ // events already committed. Loud enough to see if it starts failing always.
239
+ console.warn(
240
+ "[first-party-analytics] Failed to refresh key last-used stamp:",
241
+ error,
242
+ );
243
+ }
244
+ }
245
+
184
246
  function parseReplayAllowedOrigins(value: unknown): string[] {
185
247
  if (Array.isArray(value)) {
186
248
  return value.filter((item): item is string => typeof item === "string");
@@ -539,18 +601,11 @@ export async function recordAnalyticsEvents(
539
601
  if (rows.length && backend.sink !== "bigquery") {
540
602
  await db.transaction(async (tx: any) => {
541
603
  await tx.insert(schema.analyticsEvents).values(rows);
542
- await tx
543
- .update(schema.analyticsPublicKeys)
544
- .set({ lastUsedAt: receivedAt })
545
- .where(eq(schema.analyticsPublicKeys.id, key.id));
546
604
  await upsertFirstPartyAnalyticsRollups(rows, tx);
547
605
  });
548
606
  }
549
- if (rows.length && backend.sink === "bigquery") {
550
- await db
551
- .update(schema.analyticsPublicKeys)
552
- .set({ lastUsedAt: receivedAt })
553
- .where(eq(schema.analyticsPublicKeys.id, key.id));
607
+ if (rows.length) {
608
+ await touchPublicKeyLastUsedAt(key.id, receivedAt);
554
609
  }
555
610
 
556
611
  // Fork captured exceptions into the dedicated error-capture tables. This is
@@ -39,7 +39,10 @@ import {
39
39
  SESSION_REPLAY_NETWORK_EVENT_TAG,
40
40
  } from "../../shared/session-replay-diagnostics.js";
41
41
  import { getDb, schema } from "../db/index.js";
42
- import { resolveAnalyticsEventDimensions } from "./first-party-analytics.js";
42
+ import {
43
+ resolveAnalyticsEventDimensions,
44
+ touchPublicKeyLastUsedAt,
45
+ } from "./first-party-analytics.js";
43
46
 
44
47
  export type ReplayRange = "24h" | "7d" | "30d" | "90d" | "all";
45
48
 
@@ -1654,10 +1657,7 @@ export async function recordSessionReplayChunks(
1654
1657
  })
1655
1658
  .where(eq(schema.sessionRecordings.id, recording.id));
1656
1659
 
1657
- await db
1658
- .update(schema.analyticsPublicKeys)
1659
- .set({ lastUsedAt: ingestedAt })
1660
- .where(eq(schema.analyticsPublicKeys.id, key.id));
1660
+ await touchPublicKeyLastUsedAt(key.id, ingestedAt);
1661
1661
 
1662
1662
  recordChange({
1663
1663
  source: "session-recordings",
@@ -335,6 +335,10 @@ class SyncTransport {
335
335
  // collab on its fast presence cadence against the local stream. Subscribers
336
336
  // are re-notified via the close/connect cycle below.
337
337
  this.capabilities = [];
338
+ // The failure count was earned against the gateway. Carrying it over would
339
+ // back the local endpoint off by 2^failures (already 8min at the threshold)
340
+ // before its first poll, even though it just served this page.
341
+ this.consecutiveFailures = 0;
338
342
  if (this.gatewayReconnectTimer) {
339
343
  clearTimeout(this.gatewayReconnectTimer);
340
344
  this.gatewayReconnectTimer = null;
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- error: string;
30
29
  ok?: undefined;
30
+ error: string;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
17
17
  id?: undefined;
18
18
  provider?: undefined;
19
19
  } | {
20
- error?: undefined;
21
20
  configured?: undefined;
22
21
  connectPath?: undefined;
23
22
  url: string;
24
23
  id: string;
25
24
  provider: string;
25
+ error?: undefined;
26
26
  }>;
27
27
  export default _default;
@@ -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;
66
65
  ok: boolean;
66
+ error?: undefined;
67
67
  }>>;
@@ -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
- ok?: undefined;
52
51
  error: string;
52
+ ok?: undefined;
53
53
  } | {
54
54
  error?: undefined;
55
55
  ok: boolean;
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.145.6",
3
+ "version": "0.145.7",
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": {