@agent-native/core 0.120.3 → 0.121.0

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 (71) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +22 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/use-db-sync.ts +23 -21
  5. package/corpus/core/src/deploy/build.ts +1 -0
  6. package/corpus/core/src/server/core-routes-plugin.ts +3 -0
  7. package/corpus/core/src/server/gateway-access-check.ts +67 -0
  8. package/corpus/core/src/server/poll.ts +295 -9
  9. package/corpus/core/src/server/short-lived-token.ts +122 -0
  10. package/corpus/templates/content/actions/update-document.ts +145 -91
  11. package/corpus/templates/content/app/components/editor/CommentComposer.tsx +4 -1
  12. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +366 -164
  13. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +51 -28
  14. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +19 -8
  15. package/corpus/templates/content/app/i18n-data.ts +15 -0
  16. package/corpus/templates/content/changelog/2026-07-23-shared-pages-now-keep-durable-content-visible-and-comment-th.md +6 -0
  17. package/corpus/templates/content/changelog/2026-07-24-comment-drafts-now-stay-open-when-saving-fails-and-long-live.md +6 -0
  18. package/corpus/templates/design/actions/update-file.ts +85 -18
  19. package/corpus/templates/design/app/lib/design-save-outbox.ts +39 -0
  20. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +29 -4
  21. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +365 -9
  22. package/corpus/templates/slides/app/components/editor/bullet-editing.ts +350 -0
  23. package/corpus/templates/slides/app/context/DeckContext.tsx +1 -3
  24. package/corpus/templates/slides/app/i18n/en-US.ts +2 -0
  25. package/corpus/templates/slides/app/pages/DeckEditor.tsx +5 -0
  26. package/corpus/templates/slides/changelog/2026-07-23-new-bullet-rows-created-with-enter-now-keep-the-list-item-s-.md +6 -0
  27. package/corpus/templates/slides/changelog/2026-07-23-pressing-enter-in-generated-checkbox-shape-marker-lists-now-.md +6 -0
  28. package/corpus/templates/slides/changelog/2026-07-24-typing-a-markdown-style-dash-space-at-the-start-of-a-text-bl.md +6 -0
  29. package/corpus/toolkit/CHANGELOG.md +6 -0
  30. package/corpus/toolkit/package.json +1 -1
  31. package/corpus/toolkit/src/editor/useCollabReconcile.ts +83 -15
  32. package/dist/client/use-db-sync.d.ts.map +1 -1
  33. package/dist/client/use-db-sync.js +27 -22
  34. package/dist/client/use-db-sync.js.map +1 -1
  35. package/dist/collab/awareness.d.ts +2 -2
  36. package/dist/collab/awareness.d.ts.map +1 -1
  37. package/dist/deploy/build.d.ts.map +1 -1
  38. package/dist/deploy/build.js +1 -0
  39. package/dist/deploy/build.js.map +1 -1
  40. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  41. package/dist/notifications/routes.d.ts +1 -1
  42. package/dist/observability/routes.d.ts +3 -3
  43. package/dist/progress/routes.d.ts +1 -1
  44. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  45. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  46. package/dist/resources/handlers.d.ts +1 -1
  47. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  48. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  49. package/dist/server/core-routes-plugin.js +3 -0
  50. package/dist/server/core-routes-plugin.js.map +1 -1
  51. package/dist/server/gateway-access-check.d.ts +12 -0
  52. package/dist/server/gateway-access-check.d.ts.map +1 -0
  53. package/dist/server/gateway-access-check.js +49 -0
  54. package/dist/server/gateway-access-check.js.map +1 -0
  55. package/dist/server/poll.d.ts +55 -1
  56. package/dist/server/poll.d.ts.map +1 -1
  57. package/dist/server/poll.js +254 -7
  58. package/dist/server/poll.js.map +1 -1
  59. package/dist/server/realtime-token.d.ts +1 -1
  60. package/dist/server/short-lived-token.d.ts +28 -0
  61. package/dist/server/short-lived-token.d.ts.map +1 -1
  62. package/dist/server/short-lived-token.js +78 -0
  63. package/dist/server/short-lived-token.js.map +1 -1
  64. package/dist/server/transcribe-voice.d.ts +1 -1
  65. package/package.json +2 -2
  66. package/src/client/use-db-sync.ts +23 -21
  67. package/src/deploy/build.ts +1 -0
  68. package/src/server/core-routes-plugin.ts +3 -0
  69. package/src/server/gateway-access-check.ts +67 -0
  70. package/src/server/poll.ts +295 -9
  71. package/src/server/short-lived-token.ts +122 -0
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `GET /_agent-native/can-see` — the hosted Realtime Gateway's sharee-visibility
3
+ * check. Verifies a gateway access-check token (rationale in
4
+ * short-lived-token.ts), runs the app's own `resolveAccess`, answers
5
+ * `{ allowed }`, and fails closed.
6
+ */
7
+ export declare function createGatewayAccessCheckHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
8
+ error: string;
9
+ } | {
10
+ allowed: boolean;
11
+ }>>;
12
+ //# sourceMappingURL=gateway-access-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway-access-check.d.ts","sourceRoot":"","sources":["../../src/server/gateway-access-check.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiBH,wBAAgB,+BAA+B;;;;IA4C9C"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `GET /_agent-native/can-see` — the hosted Realtime Gateway's sharee-visibility
3
+ * check. Verifies a gateway access-check token (rationale in
4
+ * short-lived-token.ts), runs the app's own `resolveAccess`, answers
5
+ * `{ allowed }`, and fails closed.
6
+ */
7
+ import { defineEventHandler, getMethod, getQuery, setResponseHeader, setResponseStatus, } from "h3";
8
+ import { resolveAccess } from "../sharing/access.js";
9
+ import { getBuilderBranchProjectId } from "./builder-browser.js";
10
+ import { getRealtimeSigningSecret } from "./realtime-token.js";
11
+ import { runWithRequestContext } from "./request-context.js";
12
+ import { verifyGatewayAccessToken } from "./short-lived-token.js";
13
+ export function createGatewayAccessCheckHandler() {
14
+ return defineEventHandler(async (event) => {
15
+ setResponseHeader(event, "Cache-Control", "private, no-store");
16
+ if (getMethod(event) !== "GET") {
17
+ setResponseStatus(event, 405);
18
+ return { error: "Method not allowed" };
19
+ }
20
+ const secret = getRealtimeSigningSecret();
21
+ if (!secret) {
22
+ setResponseStatus(event, 404);
23
+ return { error: "Realtime gateway not configured" };
24
+ }
25
+ const token = getQuery(event).token;
26
+ // Sync, env-only: binds the token's channel when this app's project id is
27
+ // known, and no-ops (undefined) for scoped-secret apps where it isn't.
28
+ const expectedProjectId = getBuilderBranchProjectId() || undefined;
29
+ const verified = typeof token === "string"
30
+ ? verifyGatewayAccessToken(token, secret, expectedProjectId)
31
+ : { ok: false, reason: "missing" };
32
+ if (!verified.ok) {
33
+ setResponseStatus(event, 401);
34
+ return { error: "Unauthorized" };
35
+ }
36
+ const { resourceType, resourceId, userEmail, orgId } = verified;
37
+ return runWithRequestContext({ userEmail, orgId }, async () => {
38
+ try {
39
+ const access = await resolveAccess(resourceType, resourceId, { userEmail, orgId }, { skipResourceBody: true });
40
+ return { allowed: access != null };
41
+ }
42
+ catch {
43
+ // Unknown resource type or lookup failure: fail closed.
44
+ return { allowed: false };
45
+ }
46
+ });
47
+ });
48
+ }
49
+ //# sourceMappingURL=gateway-access-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway-access-check.js","sourceRoot":"","sources":["../../src/server/gateway-access-check.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,QAAQ,EAER,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,IAAI,CAAC;AAEZ,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAElE,MAAM,UAAU,+BAA+B;IAC7C,OAAO,kBAAkB,CAAC,KAAK,EAAE,KAAc,EAAE,EAAE;QACjD,iBAAiB,CAAC,KAAK,EAAE,eAAe,EAAE,mBAAmB,CAAC,CAAC;QAE/D,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,KAAK,EAAE,CAAC;YAC/B,iBAAiB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAC9B,OAAO,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;QACzC,CAAC;QAED,MAAM,MAAM,GAAG,wBAAwB,EAAE,CAAC;QAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,iBAAiB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAC9B,OAAO,EAAE,KAAK,EAAE,iCAAiC,EAAE,CAAC;QACtD,CAAC;QAED,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC;QACpC,0EAA0E;QAC1E,uEAAuE;QACvE,MAAM,iBAAiB,GAAG,yBAAyB,EAAE,IAAI,SAAS,CAAC;QACnE,MAAM,QAAQ,GACZ,OAAO,KAAK,KAAK,QAAQ;YACvB,CAAC,CAAC,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,iBAAiB,CAAC;YAC5D,CAAC,CAAE,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAY,CAAC;QAClD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,iBAAiB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAC9B,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC;QACnC,CAAC;QAED,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,QAAQ,CAAC;QAChE,OAAO,qBAAqB,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,KAAK,IAAI,EAAE;YAC5D,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,aAAa,CAChC,YAAY,EACZ,UAAU,EACV,EAAE,SAAS,EAAE,KAAK,EAAE,EACpB,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAC3B,CAAC;gBACF,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC;YACrC,CAAC;YAAC,MAAM,CAAC;gBACP,wDAAwD;gBACxD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YAC5B,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * `GET /_agent-native/can-see` — the hosted Realtime Gateway's sharee-visibility\n * check. Verifies a gateway access-check token (rationale in\n * short-lived-token.ts), runs the app's own `resolveAccess`, answers\n * `{ allowed }`, and fails closed.\n */\n\nimport {\n defineEventHandler,\n getMethod,\n getQuery,\n type H3Event,\n setResponseHeader,\n setResponseStatus,\n} from \"h3\";\n\nimport { resolveAccess } from \"../sharing/access.js\";\nimport { getBuilderBranchProjectId } from \"./builder-browser.js\";\nimport { getRealtimeSigningSecret } from \"./realtime-token.js\";\nimport { runWithRequestContext } from \"./request-context.js\";\nimport { verifyGatewayAccessToken } from \"./short-lived-token.js\";\n\nexport function createGatewayAccessCheckHandler() {\n return defineEventHandler(async (event: H3Event) => {\n setResponseHeader(event, \"Cache-Control\", \"private, no-store\");\n\n if (getMethod(event) !== \"GET\") {\n setResponseStatus(event, 405);\n return { error: \"Method not allowed\" };\n }\n\n const secret = getRealtimeSigningSecret();\n if (!secret) {\n setResponseStatus(event, 404);\n return { error: \"Realtime gateway not configured\" };\n }\n\n const token = getQuery(event).token;\n // Sync, env-only: binds the token's channel when this app's project id is\n // known, and no-ops (undefined) for scoped-secret apps where it isn't.\n const expectedProjectId = getBuilderBranchProjectId() || undefined;\n const verified =\n typeof token === \"string\"\n ? verifyGatewayAccessToken(token, secret, expectedProjectId)\n : ({ ok: false, reason: \"missing\" } as const);\n if (!verified.ok) {\n setResponseStatus(event, 401);\n return { error: \"Unauthorized\" };\n }\n\n const { resourceType, resourceId, userEmail, orgId } = verified;\n return runWithRequestContext({ userEmail, orgId }, async () => {\n try {\n const access = await resolveAccess(\n resourceType,\n resourceId,\n { userEmail, orgId },\n { skipResourceBody: true },\n );\n return { allowed: access != null };\n } catch {\n // Unknown resource type or lookup failure: fail closed.\n return { allowed: false };\n }\n });\n });\n}\n"]}
@@ -98,6 +98,22 @@ export interface AppSyncStateOptions {
98
98
  * across instances.
99
99
  */
100
100
  deterministicEventIds?: boolean;
101
+ /**
102
+ * Allocate durable-event versions from the app's Postgres DB (a one-row
103
+ * allocator advanced with `GREATEST(v + 1, epoch_ms_now)` inside the insert
104
+ * statement) instead of the per-writer in-memory clock counter.
105
+ *
106
+ * Off by default: a single-writer app's clock counter is already monotonic.
107
+ * Hosted realtime has MULTIPLE writers per app DB (the app's serverless
108
+ * instances plus gateway instances), where clock skew can assign a LOWER
109
+ * version to a LATER event — a client whose cursor passed the higher value
110
+ * then filters the later event out forever. DB allocation serializes on the
111
+ * allocator row's lock, so version order equals commit order across all
112
+ * writers. Versions stay on the epoch-ms scale (existing cursors, the
113
+ * detector's timestamp-mixed seed, and lag metrics all assume it).
114
+ * Postgres only; ignored on SQLite.
115
+ */
116
+ dbAssignedVersions?: boolean;
101
117
  }
102
118
  /**
103
119
  * Per-app change-tracking state and read path. One instance per deployed app.
@@ -110,6 +126,13 @@ export declare class AppSyncState {
110
126
  private readonly isPg;
111
127
  private readonly resolveAccessFn;
112
128
  private readonly deterministicEventIds;
129
+ private readonly dbAssignedVersions;
130
+ /** Serializes gated recordChange calls so buffer/emit order matches
131
+ * allocation order (interleaved allocations could buffer out of order). */
132
+ private recordChain;
133
+ /** Count of DB-allocation failures that fell back to clock versions. */
134
+ private dbVersionFallbacks;
135
+ private warnedListenerThrow;
113
136
  private version;
114
137
  private readonly buffer;
115
138
  private readonly pollEmitter;
@@ -173,7 +196,28 @@ export declare class AppSyncState {
173
196
  wireLocalEmitters(): void;
174
197
  ensureSyncEventsTable(): Promise<boolean>;
175
198
  private pruneDurableEvents;
176
- persistSyncEvent(event: ChangeEvent, dedupeKey?: string): Promise<void>;
199
+ persistSyncEvent(event: ChangeEvent, dedupeKey?: string, presetId?: string): Promise<void>;
200
+ /**
201
+ * Persist with a DB-allocated version (see ALLOCATING_INSERT_SQL). Returns
202
+ * the allocated version — for a deterministic-id dedupe loser, the winner's
203
+ * existing version — or null when persistence is unavailable (caller falls
204
+ * back to clock allocation). Unlike `persistSyncEvent`, errors PROPAGATE to
205
+ * the caller: silence here would silently drop the event entirely, since the
206
+ * buffer/emit are deferred behind this call.
207
+ */
208
+ private persistWithDbAssignedVersion;
209
+ /** Read back the version of an already-committed row (a failed allocating
210
+ * call whose statement actually committed). Null when absent/unreachable. */
211
+ private recoverCommittedVersion;
212
+ /**
213
+ * Lift the allocator to at least `floor`. Client cursors are max-only, so
214
+ * any value that can become a cursor (notably the seed's app-clock
215
+ * `updated_at` domain) must never exceed the allocator — a cursor above it
216
+ * would filter later, lower-allocated events out permanently. Failure is
217
+ * swallowed: alignment degrades to the same soft guarantee as the clock
218
+ * fallback.
219
+ */
220
+ private alignVersionAllocator;
177
221
  readMaxSyncEventVersion(): Promise<number>;
178
222
  /**
179
223
  * Oldest retained durable version. Used to detect a reconnect cursor that
@@ -232,6 +276,16 @@ export declare class AppSyncState {
232
276
  }, opts?: {
233
277
  dedupeKey?: string;
234
278
  }): void;
279
+ /** Buffer + emit. Shared by both version-allocation modes. */
280
+ private commitEntry;
281
+ private recordWithDbVersion;
282
+ /**
283
+ * `commitEntry` for the deferred (chained) path: the buffer push must land
284
+ * and a throwing pollEmitter listener must not reject the chain — the
285
+ * synchronous path propagates such throws to the recordChange caller, but
286
+ * here there is no caller to propagate to, only the chain to poison.
287
+ */
288
+ private commitEntryForChain;
235
289
  private recordExtensionChanges;
236
290
  private recordActionChanges;
237
291
  /** Get all changes after a given version. */
@@ -1 +1 @@
1
- {"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../../src/server/poll.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../../src/server/poll.ts"],"names":[],"mappings":"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"}
@@ -48,6 +48,41 @@ const ACCESS_CACHE_DENY_TTL_MS = 5_000;
48
48
  /** Max cache entries before FIFO eviction kicks in. */
49
49
  const ACCESS_CACHE_MAX = 500;
50
50
  const SCREEN_REFRESH_KEY = "__screen_refresh__";
51
+ /**
52
+ * DB-assigned version allocation (see `AppSyncStateOptions.dbAssignedVersions`).
53
+ * The allocator is a one-row table advanced with `GREATEST(v + 1, epoch_ms)` —
54
+ * the SQL analog of the in-memory `Math.max(version + 1, Date.now())` — inside
55
+ * the same autocommit statement as the event insert. The row lock is held to
56
+ * the statement's implicit commit, so version order equals commit order across
57
+ * every writer, closing the cross-writer clock-skew hole no sequence closes
58
+ * (sequences serialize allocation, not commit visibility). Single-statement is
59
+ * required: the gateway reaches the app DB through a pooled (transactionless)
60
+ * connection. `ON CONFLICT DO UPDATE` (not DO NOTHING) so a deterministic-id
61
+ * dedupe loser still gets the winner's version back and never emits a phantom
62
+ * version to its own clients. The stored JSON is restamped with the allocated
63
+ * version; the read path's column-wins override makes that belt-and-suspenders.
64
+ */
65
+ const SEED_SYNC_VERSION_SQL = `
66
+ INSERT INTO sync_version (id, v)
67
+ SELECT 1, GREATEST(
68
+ COALESCE((SELECT MAX(version) FROM sync_events), 0),
69
+ (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::BIGINT
70
+ )
71
+ ON CONFLICT (id) DO NOTHING
72
+ `;
73
+ const ALLOCATING_INSERT_SQL = `
74
+ WITH alloc AS (
75
+ UPDATE sync_version
76
+ SET v = GREATEST(v + 1, (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::BIGINT, (?)::BIGINT)
77
+ WHERE id = 1
78
+ RETURNING v
79
+ )
80
+ INSERT INTO sync_events (id, version, event_json, source, type, event_key, owner, org_id, resource_type, resource_id, created_at)
81
+ SELECT ?, alloc.v, jsonb_set((?)::jsonb, '{version}', to_jsonb(alloc.v))::text, ?, ?, ?, ?, ?, ?, ?, ?
82
+ FROM alloc
83
+ ON CONFLICT (id) DO UPDATE SET id = excluded.id
84
+ RETURNING version
85
+ `;
51
86
  function timestampValue(value) {
52
87
  if (typeof value === "number" && Number.isFinite(value))
53
88
  return value;
@@ -196,6 +231,13 @@ export class AppSyncState {
196
231
  isPg;
197
232
  resolveAccessFn;
198
233
  deterministicEventIds;
234
+ dbAssignedVersions;
235
+ /** Serializes gated recordChange calls so buffer/emit order matches
236
+ * allocation order (interleaved allocations could buffer out of order). */
237
+ recordChain = Promise.resolve();
238
+ /** Count of DB-allocation failures that fell back to clock versions. */
239
+ dbVersionFallbacks = 0;
240
+ warnedListenerThrow = false;
199
241
  // Timestamp-aligned versions so all serverless instances produce values in
200
242
  // the same range (seeded from DB, then incremented via Date.now). Plain
201
243
  // ++counter diverges across cold starts.
@@ -248,6 +290,7 @@ export class AppSyncState {
248
290
  this.isPg = options.isPostgres ?? isPostgres;
249
291
  this.resolveAccessFn = options.resolveAccess ?? defaultResolveAccess;
250
292
  this.deterministicEventIds = options.deterministicEventIds ?? false;
293
+ this.dbAssignedVersions = options.dbAssignedVersions ?? false;
251
294
  this.pollEmitter.setMaxListeners(0);
252
295
  }
253
296
  /**
@@ -337,6 +380,12 @@ export class AppSyncState {
337
380
  await ensureIndexExists("sync_events_version_idx", "CREATE INDEX IF NOT EXISTS sync_events_version_idx ON sync_events (version)", guardOptions);
338
381
  await ensureIndexExists("sync_events_owner_version_idx", "CREATE INDEX IF NOT EXISTS sync_events_owner_version_idx ON sync_events (owner, version)", guardOptions);
339
382
  await ensureIndexExists("sync_events_org_version_idx", "CREATE INDEX IF NOT EXISTS sync_events_org_version_idx ON sync_events (org_id, version)", guardOptions);
383
+ if (this.dbAssignedVersions) {
384
+ await ensureTableExists("sync_version", "CREATE TABLE IF NOT EXISTS sync_version (id INT PRIMARY KEY, v BIGINT NOT NULL)", guardOptions);
385
+ // Seed at/above both the existing durable max and wall clock, so
386
+ // allocated versions never land below live client cursors.
387
+ await client.execute(SEED_SYNC_VERSION_SQL);
388
+ }
340
389
  return true;
341
390
  }
342
391
  await client.execute(createSql);
@@ -372,7 +421,7 @@ export class AppSyncState {
372
421
  })
373
422
  .catch(() => { });
374
423
  }
375
- async persistSyncEvent(event, dedupeKey) {
424
+ async persistSyncEvent(event, dedupeKey, presetId) {
376
425
  if (!(await this.ensureSyncEventsTable()))
377
426
  return;
378
427
  const client = this.getDb();
@@ -385,7 +434,9 @@ export class AppSyncState {
385
434
  : `INSERT OR IGNORE INTO sync_events (id, version, event_json, source, type, event_key, owner, org_id, resource_type, resource_id, created_at)
386
435
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
387
436
  args: [
388
- this.durableEventId(event, dedupeKey),
437
+ // presetId lets a gated fallback reuse the allocating attempt's id,
438
+ // so a commit-then-timeout can't produce the same event twice.
439
+ presetId ?? this.durableEventId(event, dedupeKey),
389
440
  event.version,
390
441
  JSON.stringify(event),
391
442
  event.source,
@@ -401,6 +452,83 @@ export class AppSyncState {
401
452
  .catch(() => { });
402
453
  await this.pruneDurableEvents(client);
403
454
  }
455
+ /**
456
+ * Persist with a DB-allocated version (see ALLOCATING_INSERT_SQL). Returns
457
+ * the allocated version — for a deterministic-id dedupe loser, the winner's
458
+ * existing version — or null when persistence is unavailable (caller falls
459
+ * back to clock allocation). Unlike `persistSyncEvent`, errors PROPAGATE to
460
+ * the caller: silence here would silently drop the event entirely, since the
461
+ * buffer/emit are deferred behind this call.
462
+ */
463
+ async persistWithDbAssignedVersion(event, id) {
464
+ if (!(await this.ensureSyncEventsTable()))
465
+ return null;
466
+ const client = this.getDb();
467
+ const args = [
468
+ // Local monotonicity floor: after a clock fallback, the next allocation
469
+ // must not land at/below versions this writer already emitted.
470
+ this.version + 1,
471
+ id,
472
+ // jsonb (unlike the legacy TEXT write) rejects \\u0000 escapes; strip
473
+ // them rather than diverting those events to the clock fallback.
474
+ JSON.stringify(event).split("\\u0000").join(""),
475
+ event.source,
476
+ event.type,
477
+ event.key ?? null,
478
+ event.owner ?? null,
479
+ event.orgId ?? null,
480
+ event.resourceType ?? null,
481
+ event.resourceId ?? null,
482
+ Date.now(),
483
+ ];
484
+ let result = await client.execute({ sql: ALLOCATING_INSERT_SQL, args });
485
+ if (result.rows.length === 0) {
486
+ // Allocator row missing (e.g. wiped after ensure): reseed, retry once.
487
+ await client.execute(SEED_SYNC_VERSION_SQL).catch(() => { });
488
+ result = await client.execute({ sql: ALLOCATING_INSERT_SQL, args });
489
+ }
490
+ const version = timestampValue(result.rows[0]?.version);
491
+ await this.pruneDurableEvents(client);
492
+ return version > 0 ? version : null;
493
+ }
494
+ /** Read back the version of an already-committed row (a failed allocating
495
+ * call whose statement actually committed). Null when absent/unreachable. */
496
+ async recoverCommittedVersion(id) {
497
+ try {
498
+ const result = await this.getDb().execute({
499
+ sql: "SELECT version FROM sync_events WHERE id = ?",
500
+ args: [id],
501
+ });
502
+ const version = timestampValue(result.rows[0]?.version);
503
+ return version > 0 ? version : null;
504
+ }
505
+ catch {
506
+ return null;
507
+ }
508
+ }
509
+ /**
510
+ * Lift the allocator to at least `floor`. Client cursors are max-only, so
511
+ * any value that can become a cursor (notably the seed's app-clock
512
+ * `updated_at` domain) must never exceed the allocator — a cursor above it
513
+ * would filter later, lower-allocated events out permanently. Failure is
514
+ * swallowed: alignment degrades to the same soft guarantee as the clock
515
+ * fallback.
516
+ */
517
+ async alignVersionAllocator(floor) {
518
+ if (floor <= 0)
519
+ return;
520
+ try {
521
+ if (!(await this.ensureSyncEventsTable()))
522
+ return;
523
+ await this.getDb().execute({
524
+ sql: "UPDATE sync_version SET v = GREATEST(v, (?)::BIGINT) WHERE id = 1",
525
+ args: [floor],
526
+ });
527
+ }
528
+ catch {
529
+ // Soft guarantee under DB failure, matching the clock fallback.
530
+ }
531
+ }
404
532
  async readMaxSyncEventVersion() {
405
533
  if (!(await this.ensureSyncEventsTable()))
406
534
  return 0;
@@ -559,14 +687,98 @@ export class AppSyncState {
559
687
  * concern and never stored on the event object.
560
688
  */
561
689
  recordChange(event, opts) {
690
+ if (this.dbAssignedVersions && this.isPg() && !syncEventsDisabled()) {
691
+ // No provisional version may reach clients: cursors are max-only, so an
692
+ // emitted clock version above a later DB allocation would recreate the
693
+ // skew bug. Buffer/emit happen only after the DB returns the version,
694
+ // serialized so entries land in allocation order. The catch is a
695
+ // backstop: one rejected link must never poison the chain and silently
696
+ // drop every later event.
697
+ this.recordChain = this.recordChain
698
+ .then(() => this.recordWithDbVersion(event, opts?.dedupeKey))
699
+ .catch(() => { });
700
+ return;
701
+ }
562
702
  this.version = Math.max(this.version + 1, Date.now());
563
703
  const entry = { ...event, version: this.version };
704
+ this.commitEntry(entry);
705
+ void this.persistSyncEvent(entry, opts?.dedupeKey);
706
+ }
707
+ /** Buffer + emit. Shared by both version-allocation modes. */
708
+ commitEntry(entry) {
564
709
  this.buffer.push(entry);
565
710
  if (this.buffer.length > MAX_BUFFER) {
566
711
  this.buffer.splice(0, this.buffer.length - MAX_BUFFER);
567
712
  }
568
713
  this.pollEmitter.emit(POLL_CHANGE_EVENT, entry);
569
- void this.persistSyncEvent(entry, opts?.dedupeKey);
714
+ }
715
+ async recordWithDbVersion(event, dedupeKey) {
716
+ // One id for both the allocating attempt AND any fallback persist: if the
717
+ // allocating INSERT commits server-side but the call fails (e.g. a query
718
+ // timeout after commit), the fallback's ON CONFLICT dedupes against the
719
+ // committed row instead of writing the same event twice under two ids.
720
+ // Deterministic ids exclude version by design; the random fallback id gets
721
+ // a wall-clock prefix instead of its usual version prefix (the prefix is
722
+ // debuggability, not semantics — nothing parses row ids).
723
+ const id = this.deterministicEventIds && dedupeKey !== undefined
724
+ ? this.durableEventId({ ...event, version: 0 }, dedupeKey)
725
+ : `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
726
+ let version = null;
727
+ try {
728
+ version = await this.persistWithDbAssignedVersion(event, id);
729
+ }
730
+ catch {
731
+ version = null;
732
+ }
733
+ if (version == null) {
734
+ // The statement may have COMMITTED even though the call failed (e.g. a
735
+ // timeout after commit). Recover the durable row's version by the shared
736
+ // id so clients see the allocator-assigned value, never a divergent
737
+ // clock one.
738
+ version = await this.recoverCommittedVersion(id);
739
+ }
740
+ if (version == null) {
741
+ // Availability fallback: a DB blip must not stall the in-process fast
742
+ // path. Clock-allocate and emit — but first make a best effort to lift
743
+ // the allocator to the fallback value, so other writers' next
744
+ // allocations land above the cursors this emit will advance (max-only
745
+ // cursors above the allocator would filter those events permanently).
746
+ // If the DB is down the lift fails too; the ordering guarantee is soft
747
+ // exactly while the DB is unhealthy.
748
+ this.dbVersionFallbacks++;
749
+ if (this.dbVersionFallbacks === 1) {
750
+ console.warn("[agent-native] sync version allocation failed; falling back to clock-assigned versions");
751
+ }
752
+ this.version = Math.max(this.version + 1, Date.now());
753
+ const entry = { ...event, version: this.version };
754
+ await this.alignVersionAllocator(this.version);
755
+ this.commitEntryForChain(entry);
756
+ void this.persistSyncEvent(entry, dedupeKey, id);
757
+ return;
758
+ }
759
+ // A deterministic-id dedupe loser adopts the winner's (possibly older)
760
+ // version here — buffer filtering is a full scan, so a non-tail version is
761
+ // delivered correctly and the version-keyed combined-read dedupe collapses
762
+ // it against the durable row.
763
+ this.version = Math.max(this.version, version);
764
+ this.commitEntryForChain({ ...event, version });
765
+ }
766
+ /**
767
+ * `commitEntry` for the deferred (chained) path: the buffer push must land
768
+ * and a throwing pollEmitter listener must not reject the chain — the
769
+ * synchronous path propagates such throws to the recordChange caller, but
770
+ * here there is no caller to propagate to, only the chain to poison.
771
+ */
772
+ commitEntryForChain(entry) {
773
+ try {
774
+ this.commitEntry(entry);
775
+ }
776
+ catch (err) {
777
+ if (!this.warnedListenerThrow) {
778
+ this.warnedListenerThrow = true;
779
+ console.warn("[agent-native] poll listener threw during deferred emit", err);
780
+ }
781
+ }
570
782
  }
571
783
  recordExtensionChanges(targets, dedupeKey) {
572
784
  const uniqueTargets = new Map();
@@ -785,8 +997,16 @@ export class AppSyncState {
785
997
  for (const row of refreshResult.rows) {
786
998
  refreshTs = Math.max(refreshTs, timestampValue(row.updated_at));
787
999
  }
1000
+ const seedMax = Math.max(syncEventsTs, appTs, settingsTs, extensionsTs, extensionMarkerTs, actionMarkerTs);
1001
+ // The seed mixes app-clock `updated_at` values that can sit AHEAD of the
1002
+ // allocator (a skew-fast writer's rows). Lift the allocator to the seed
1003
+ // BEFORE the seed can reach this.version — and through it, client
1004
+ // cursors — so no later allocation lands below a seeded cursor.
1005
+ if (this.dbAssignedVersions && this.isPg() && !syncEventsDisabled()) {
1006
+ await this.alignVersionAllocator(seedMax);
1007
+ }
788
1008
  // Seed version — never decrease an already-set value
789
- this.version = Math.max(this.version, syncEventsTs, appTs, settingsTs, extensionsTs, extensionMarkerTs, actionMarkerTs);
1009
+ this.version = Math.max(this.version, seedMax);
790
1010
  // Set baselines so checkExternalDbChanges detects future writes
791
1011
  this.lastAppStateTs = appTs;
792
1012
  this.lastSettingsTs = settingsTs;
@@ -829,7 +1049,18 @@ export class AppSyncState {
829
1049
  if (this.checkPromise)
830
1050
  return this.checkPromise;
831
1051
  this.lastDbCheck = now;
832
- this.checkPromise = this.doCheckExternalDbChanges().finally(() => {
1052
+ this.checkPromise = this.doCheckExternalDbChanges()
1053
+ .then(() => {
1054
+ // Gated: the detector's recordChange calls only SCHEDULED allocations.
1055
+ // The poll handler awaits this check so detected cross-process writes
1056
+ // land in the same response — drain the chain here to keep that
1057
+ // contract (and so a serverless instance frozen after responding
1058
+ // can't strand them). The chain never rejects (backstopped).
1059
+ if (this.dbAssignedVersions && this.isPg() && !syncEventsDisabled()) {
1060
+ return this.recordChain;
1061
+ }
1062
+ })
1063
+ .finally(() => {
833
1064
  this.checkPromise = null;
834
1065
  });
835
1066
  return this.checkPromise;
@@ -998,13 +1229,29 @@ export class AppSyncState {
998
1229
  }
999
1230
  }
1000
1231
  let _defaultState;
1232
+ /**
1233
+ * Hosted-realtime gate for the default instance, env-only. Mirrors the
1234
+ * fail-closed transport-AND-url pair in `sentry-config.ts`'s
1235
+ * `resolveRealtimeClientConfig` (not imported — poll.ts is import-cycle
1236
+ * sensitive). Hosted apps are multi-writer (their own serverless instances
1237
+ * plus gateway instances share one DB), so THEY must DB-allocate versions too
1238
+ * — gating only the gateway would leave the app's user-event writes
1239
+ * clock-versioned and the cross-writer skew hole open.
1240
+ */
1241
+ function hostedRealtimeTransportEnabled() {
1242
+ return (process.env.AGENT_NATIVE_REALTIME_TRANSPORT?.trim() === "hosted" &&
1243
+ !!process.env.AGENT_NATIVE_REALTIME_GATEWAY_URL?.trim());
1244
+ }
1001
1245
  /**
1002
1246
  * The process-wide default instance, bound to the global DB. All module-level
1003
1247
  * exports delegate here so self-hosted apps run exactly one code path.
1004
1248
  */
1005
1249
  export function getDefaultAppSyncState() {
1006
- if (!_defaultState)
1007
- _defaultState = new AppSyncState();
1250
+ if (!_defaultState) {
1251
+ _defaultState = new AppSyncState({
1252
+ dbAssignedVersions: hostedRealtimeTransportEnabled(),
1253
+ });
1254
+ }
1008
1255
  return _defaultState;
1009
1256
  }
1010
1257
  /** Get the current global version counter. */