@agent-native/core 0.109.0 → 0.109.2

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 +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/docs/content/integrations.mdx +87 -253
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/a2a/client.ts +24 -2
  6. package/corpus/core/src/a2a/correlation.ts +50 -0
  7. package/corpus/core/src/a2a/handlers.ts +140 -14
  8. package/corpus/core/src/a2a/index.ts +1 -0
  9. package/corpus/core/src/a2a/invoke.ts +10 -1
  10. package/corpus/core/src/a2a/task-store.ts +146 -6
  11. package/corpus/core/src/a2a/types.ts +16 -0
  12. package/corpus/core/src/action.ts +2 -0
  13. package/corpus/core/src/agent/production-agent.ts +23 -0
  14. package/corpus/core/src/client/blocks/library/diagram.config.ts +10 -1
  15. package/corpus/core/src/client/blocks/library/diagram.tsx +9 -4
  16. package/corpus/core/src/client/use-run-stuck-detection.ts +27 -1
  17. package/corpus/core/src/db/client.ts +55 -0
  18. package/corpus/core/src/deploy/build.ts +63 -3
  19. package/corpus/core/src/observability/traces.ts +172 -19
  20. package/corpus/core/src/scripts/call-agent.ts +53 -2
  21. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +72 -25
  22. package/corpus/core/src/server/agent-chat-plugin.ts +107 -6
  23. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
  24. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
  25. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  26. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
  27. package/corpus/templates/analytics/changelog/2026-07-17-analytics-now-includes-a-default-alert-for-spikes-in-stuck-agent-chats.md +6 -0
  28. package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +23 -14
  29. package/corpus/templates/analytics/docs/schemas/tracked-events.md +5 -1
  30. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +3 -3
  31. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +209 -87
  32. package/corpus/templates/clips/.agents/skills/dictate/SKILL.md +28 -5
  33. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -0
  34. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +23 -0
  35. package/corpus/templates/clips/AGENTS.md +10 -0
  36. package/corpus/templates/clips/actions/create-dictation.ts +29 -4
  37. package/corpus/templates/clips/actions/finalize-recording.ts +542 -65
  38. package/corpus/templates/clips/actions/get-recording-player-data.ts +16 -6
  39. package/corpus/templates/clips/actions/set-mobile-capture-state.ts +37 -0
  40. package/corpus/templates/clips/actions/update-dictation.ts +9 -1
  41. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +17 -2
  42. package/corpus/templates/clips/app/components/player/video-player.tsx +14 -2
  43. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +2 -2
  44. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +14 -3
  45. package/corpus/templates/clips/app/routes/record.tsx +3 -3
  46. package/corpus/templates/clips/app/routes/share.$shareId.tsx +19 -5
  47. package/corpus/templates/clips/changelog/2026-07-17-capture-dictation-background-meeting-audio-and-camera-videos.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-17-clips-now-retries-storage-verification-in-the-background-ins.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-17-delayed-upload-verification-keeps-local-backups-retryable.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-17-fixed-broken-thumbnail-icons-appearing-over-playable-clip-em.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-17-playback-comments-stay-readable-at-faster-playback-speeds.md +6 -0
  52. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +3 -0
  53. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +3 -3
  54. package/corpus/templates/clips/desktop/src/app.tsx +35 -6
  55. package/corpus/templates/clips/desktop/src/lib/processing-backup-recovery.ts +19 -0
  56. package/corpus/templates/clips/desktop/src/lib/recorder.ts +141 -18
  57. package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +28 -7
  58. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -4
  59. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +32 -15
  60. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +59 -18
  61. package/corpus/templates/clips/server/db/schema.ts +9 -1
  62. package/corpus/templates/clips/server/jobs/media-verification.ts +130 -0
  63. package/corpus/templates/clips/server/lib/builder-media-compression.ts +74 -7
  64. package/corpus/templates/clips/server/lib/media-verification-state.ts +55 -0
  65. package/corpus/templates/clips/server/lib/player-thumbnail-url.ts +31 -0
  66. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +10 -3
  67. package/corpus/templates/clips/server/plugins/auth.ts +5 -5
  68. package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
  69. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +12 -2
  70. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +13 -1
  71. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +340 -0
  72. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +24 -7
  73. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +91 -7
  74. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +22 -1
  75. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +14 -0
  76. package/corpus/templates/clips/shared/finalize-recovery.ts +61 -2
  77. package/corpus/templates/clips/shared/share-meta.ts +1 -1
  78. package/corpus/templates/plan/changelog/2026-07-17-diagrams-can-opt-into-a-polished-clean-rendering-mode-withou.md +6 -0
  79. package/corpus/templates/plan/shared/plan-content.ts +3 -0
  80. package/dist/a2a/client.d.ts +11 -2
  81. package/dist/a2a/client.d.ts.map +1 -1
  82. package/dist/a2a/client.js +16 -3
  83. package/dist/a2a/client.js.map +1 -1
  84. package/dist/a2a/correlation.d.ts +10 -0
  85. package/dist/a2a/correlation.d.ts.map +1 -0
  86. package/dist/a2a/correlation.js +40 -0
  87. package/dist/a2a/correlation.js.map +1 -0
  88. package/dist/a2a/handlers.d.ts.map +1 -1
  89. package/dist/a2a/handlers.js +100 -15
  90. package/dist/a2a/handlers.js.map +1 -1
  91. package/dist/a2a/index.d.ts +1 -1
  92. package/dist/a2a/index.d.ts.map +1 -1
  93. package/dist/a2a/index.js.map +1 -1
  94. package/dist/a2a/invoke.d.ts +4 -1
  95. package/dist/a2a/invoke.d.ts.map +1 -1
  96. package/dist/a2a/invoke.js +3 -0
  97. package/dist/a2a/invoke.js.map +1 -1
  98. package/dist/a2a/task-store.d.ts +12 -1
  99. package/dist/a2a/task-store.d.ts.map +1 -1
  100. package/dist/a2a/task-store.js +96 -6
  101. package/dist/a2a/task-store.js.map +1 -1
  102. package/dist/a2a/types.d.ts +15 -0
  103. package/dist/a2a/types.d.ts.map +1 -1
  104. package/dist/a2a/types.js.map +1 -1
  105. package/dist/action.d.ts +2 -0
  106. package/dist/action.d.ts.map +1 -1
  107. package/dist/action.js.map +1 -1
  108. package/dist/agent/production-agent.d.ts +9 -0
  109. package/dist/agent/production-agent.d.ts.map +1 -1
  110. package/dist/agent/production-agent.js +14 -0
  111. package/dist/agent/production-agent.js.map +1 -1
  112. package/dist/client/blocks/library/diagram.config.d.ts +2 -0
  113. package/dist/client/blocks/library/diagram.config.d.ts.map +1 -1
  114. package/dist/client/blocks/library/diagram.config.js +8 -1
  115. package/dist/client/blocks/library/diagram.config.js.map +1 -1
  116. package/dist/client/blocks/library/diagram.d.ts.map +1 -1
  117. package/dist/client/blocks/library/diagram.js +9 -4
  118. package/dist/client/blocks/library/diagram.js.map +1 -1
  119. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  120. package/dist/client/use-run-stuck-detection.js +25 -1
  121. package/dist/client/use-run-stuck-detection.js.map +1 -1
  122. package/dist/collab/routes.d.ts +1 -1
  123. package/dist/db/client.d.ts +7 -0
  124. package/dist/db/client.d.ts.map +1 -1
  125. package/dist/db/client.js +33 -0
  126. package/dist/db/client.js.map +1 -1
  127. package/dist/deploy/build.d.ts +3 -0
  128. package/dist/deploy/build.d.ts.map +1 -1
  129. package/dist/deploy/build.js +45 -1
  130. package/dist/deploy/build.js.map +1 -1
  131. package/dist/observability/routes.d.ts +1 -1
  132. package/dist/observability/traces.d.ts +9 -0
  133. package/dist/observability/traces.d.ts.map +1 -1
  134. package/dist/observability/traces.js +137 -19
  135. package/dist/observability/traces.js.map +1 -1
  136. package/dist/progress/routes.d.ts +1 -1
  137. package/dist/scripts/call-agent.d.ts.map +1 -1
  138. package/dist/scripts/call-agent.js +35 -3
  139. package/dist/scripts/call-agent.js.map +1 -1
  140. package/dist/server/agent-chat/action-filters-a2a.d.ts +18 -2
  141. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  142. package/dist/server/agent-chat/action-filters-a2a.js +35 -7
  143. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  144. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  145. package/dist/server/agent-chat-plugin.js +93 -6
  146. package/dist/server/agent-chat-plugin.js.map +1 -1
  147. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
  148. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
  149. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  150. package/docs/content/integrations.mdx +87 -253
  151. package/package.json +1 -1
  152. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
  153. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
  154. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
@@ -2481,6 +2481,9 @@ export interface ExecuteAgentToolCallOptions {
2481
2481
  orgId?: string | null;
2482
2482
  /** Audit/action attribution for this externally selected call. */
2483
2483
  caller?: ActionCaller;
2484
+ networkProtocol?: "a2a" | "mcp" | "provider-api";
2485
+ networkId?: string;
2486
+ networkPeer?: string;
2484
2487
  threadId?: string;
2485
2488
  turnId?: string;
2486
2489
  approvedToolCalls?: string[];
@@ -2576,6 +2579,9 @@ export async function executeAgentToolCall(
2576
2579
  ownerEmail: options.ownerEmail,
2577
2580
  orgId: options.orgId,
2578
2581
  actionCaller: options.caller,
2582
+ networkProtocol: options.networkProtocol,
2583
+ networkId: options.networkId,
2584
+ networkPeer: options.networkPeer,
2579
2585
  executionMode: "act",
2580
2586
  maxIterations: 2,
2581
2587
  threadId: options.threadId,
@@ -3157,6 +3163,12 @@ export async function runAgentLoop(opts: {
3157
3163
  orgId?: string | null;
3158
3164
  /** Action invocation attribution. Defaults to the normal agent tool loop. */
3159
3165
  actionCaller?: ActionCaller;
3166
+ /** Concrete execution id used for cross-app trace correlation. */
3167
+ runId?: string;
3168
+ /** Verified/telemetry-only delegated lineage supplied by the transport. */
3169
+ networkProtocol?: "a2a" | "mcp" | "provider-api";
3170
+ networkId?: string;
3171
+ networkPeer?: string;
3160
3172
  /**
3161
3173
  * Attachments submitted with this turn (pasted text, files, images), passed
3162
3174
  * through to each tool's `ActionRunContext.attachments` so actions can
@@ -4215,6 +4227,9 @@ export async function runAgentLoop(opts: {
4215
4227
  userEmail: getRequestUserEmail(),
4216
4228
  orgId: getRequestOrgId() ?? null,
4217
4229
  caller: opts.actionCaller ?? "tool",
4230
+ networkProtocol: opts.networkProtocol,
4231
+ networkId: opts.networkId,
4232
+ networkPeer: opts.networkPeer,
4218
4233
  }),
4219
4234
  )
4220
4235
  : actionEntry.needsApproval === true;
@@ -4249,6 +4264,9 @@ export async function runAgentLoop(opts: {
4249
4264
  ctx: {
4250
4265
  actionName: toolCall.name,
4251
4266
  caller: opts.actionCaller ?? "tool",
4267
+ networkProtocol: opts.networkProtocol,
4268
+ networkId: opts.networkId,
4269
+ networkPeer: opts.networkPeer,
4252
4270
  userEmail: getRequestUserEmail(),
4253
4271
  orgId: getRequestOrgId() ?? null,
4254
4272
  ...(opts.threadId ? { threadId: opts.threadId } : {}),
@@ -4645,12 +4663,16 @@ export async function runAgentLoop(opts: {
4645
4663
  userEmail: actionUserEmail ?? undefined,
4646
4664
  orgId: actionOrgId,
4647
4665
  caller: opts.actionCaller ?? "tool",
4666
+ networkProtocol: opts.networkProtocol,
4667
+ networkId: opts.networkId,
4668
+ networkPeer: opts.networkPeer,
4648
4669
  attachments: opts.attachments,
4649
4670
  signal,
4650
4671
  // Audit attribution: the action name + the agent thread/turn that
4651
4672
  // triggered this call, so a mutation can be traced to its run.
4652
4673
  actionName: toolCall.name,
4653
4674
  ...(opts.threadId ? { threadId: opts.threadId } : {}),
4675
+ ...(opts.runId ? { runId: opts.runId } : {}),
4654
4676
  ...(opts.turnId ? { turnId: opts.turnId } : {}),
4655
4677
  };
4656
4678
  const requestContext = getRequestContext();
@@ -8004,6 +8026,7 @@ export function createProductionAgentHandler(
8004
8026
  const agentLoopOpts = {
8005
8027
  engine,
8006
8028
  model: effectiveModel,
8029
+ runId,
8007
8030
  systemPrompt: requestSystemPrompt,
8008
8031
  tools: requestTools,
8009
8032
  availableTools: availableRequestTools,
@@ -44,6 +44,8 @@ export interface DiagramData {
44
44
  */
45
45
  html?: string;
46
46
  css?: string;
47
+ /** `design` forces clean HTML/CSS rendering without the sketch overlay. */
48
+ renderMode?: "wireframe" | "design";
47
49
  caption?: string;
48
50
  /** Outer surface frame. `auto` lets the host choose the right default. */
49
51
  frame?: BlockVisualFrame;
@@ -147,6 +149,7 @@ export const diagramSchema = z
147
149
  message: "Diagram css must not include document or script tags.",
148
150
  })
149
151
  .optional(),
152
+ renderMode: z.enum(["wireframe", "design"]).optional(),
150
153
  caption: z.string().trim().max(600).optional(),
151
154
  frame: visualFrameSchema.optional(),
152
155
  nodes: z.array(diagramNodeSchema).max(80).optional(),
@@ -173,7 +176,7 @@ function graphDataForAttr(data: DiagramData): DiagramData | undefined {
173
176
  if (data.notes?.length) graph.notes = data.notes;
174
177
  if (Object.keys(graph).length > 0) return graph;
175
178
  if (hasChildFenceData(data)) return undefined;
176
- const { frame: _frame, ...dataForAttr } = data;
179
+ const { frame: _frame, renderMode: _renderMode, ...dataForAttr } = data;
177
180
  return Object.keys(dataForAttr).length > 0 ? dataForAttr : undefined;
178
181
  }
179
182
 
@@ -193,6 +196,7 @@ export const diagramMdx: BlockMdxConfig<DiagramData> = {
193
196
  | undefined,
194
197
  caption: data.caption,
195
198
  frame: data.frame,
199
+ renderMode: data.renderMode,
196
200
  }),
197
201
  fromAttrs: (attrs) => ({
198
202
  ...(attrs.object<DiagramData>("data") ?? {}),
@@ -202,6 +206,11 @@ export const diagramMdx: BlockMdxConfig<DiagramData> = {
202
206
  ...(attrs.string("frame") !== undefined
203
207
  ? { frame: attrs.string("frame") as BlockVisualFrame }
204
208
  : {}),
209
+ ...(attrs.string("renderMode") !== undefined
210
+ ? {
211
+ renderMode: attrs.string("renderMode") as DiagramData["renderMode"],
212
+ }
213
+ : {}),
205
214
  }),
206
215
  serializeChildren: (data) =>
207
216
  serializeChildCodeFenceFields(data, { html: "html", css: "css" }),
@@ -88,7 +88,9 @@ function HtmlDiagram({
88
88
  }) {
89
89
  const ref = useRef<HTMLDivElement>(null);
90
90
  const isDark = useIsDark();
91
- const style = useWireframeStyle();
91
+ const preferredStyle = useWireframeStyle();
92
+ const designMode = data.renderMode === "design";
93
+ const style = designMode ? "clean" : preferredStyle;
92
94
  const showFrame = resolveVisualFrame(data.frame, ctx);
93
95
  const scopeId = useId().replace(/[^a-zA-Z0-9_-]/g, "");
94
96
  // Sanitize author HTML/CSS at the render point (defense-in-depth against
@@ -127,7 +129,7 @@ function HtmlDiagram({
127
129
  </div>
128
130
  <RoughOverlay
129
131
  scopeRef={ref}
130
- enabled={style === "sketchy"}
132
+ enabled={!designMode && style === "sketchy"}
131
133
  drawFrame={showFrame}
132
134
  selector={DIAGRAM_ROUGH_SELECTOR}
133
135
  />
@@ -650,7 +652,8 @@ function ExpandableDiagramBody({
650
652
  compact?: boolean;
651
653
  }) {
652
654
  const [expanded, setExpanded] = useState(false);
653
- const supportsStyleToggle = Boolean(data.html);
655
+ const designMode = data.renderMode === "design";
656
+ const supportsStyleToggle = Boolean(data.html) && !designMode;
654
657
  const style = useWireframeStyle();
655
658
  const copy = useBlockCopy();
656
659
  const sketchy = style === "sketchy";
@@ -794,7 +797,9 @@ export function DiagramEdit({
794
797
  onChange({
795
798
  html: html.trim() || undefined,
796
799
  css: css.trim() || undefined,
800
+ renderMode: data.renderMode,
797
801
  caption: caption.trim() || undefined,
802
+ frame: data.frame,
798
803
  nodes: data.nodes,
799
804
  edges: data.edges,
800
805
  notes: data.notes,
@@ -946,7 +951,7 @@ export const diagramBlock = defineBlock<DiagramData>({
946
951
  editSurface: "panel",
947
952
  label: "Diagram",
948
953
  description:
949
- "A flexible inline architecture/code diagram. Prefer html/css with SVG or semantic HTML for polished two-dimensional layouts; use .diagram-* primitives and --wf-* tokens for theme/sketch compatibility. Legacy nodes/edges are only for simple previews.",
954
+ "A flexible inline architecture/code diagram. Prefer html/css with SVG or semantic HTML for polished two-dimensional layouts; use renderMode design for clean presentation or .diagram-* primitives and --wf-* tokens for theme/sketch compatibility. Legacy nodes/edges are only for simple previews.",
950
955
  // Seed the legacy fallback shape so a fresh block validates while agents can
951
956
  // replace it with html/css when layout quality matters.
952
957
  empty: () => ({ nodes: [{ id: "n1", label: "Module" }], edges: [] }),
@@ -80,6 +80,7 @@ export const DEFAULT_BACKGROUND_STUCK_THRESHOLD_MS = 180_000;
80
80
  export const DEFAULT_LIVE_BACKGROUND_STUCK_THRESHOLD_MS = 13 * 60_000;
81
81
  const DEFAULT_POLL_INTERVAL_MS = 5_000;
82
82
  const IDLE_BACKOFF_INTERVAL_MS = 15_000;
83
+ const MAX_POLL_ERROR_BACKOFF_MS = 30_000;
83
84
  const FRESH_BACKGROUND_HEARTBEAT_MS = 30_000;
84
85
 
85
86
  interface ActiveRunResponse {
@@ -129,6 +130,22 @@ export function useRunStuckDetection({
129
130
  let timer: ReturnType<typeof setTimeout> | null = null;
130
131
  let snapshotTransitionTimer: ReturnType<typeof setTimeout> | null = null;
131
132
  let snapshotVersion = 0;
133
+ let consecutivePollFailures = 0;
134
+
135
+ const pollFailureDelay = () => {
136
+ consecutivePollFailures += 1;
137
+ return Math.min(
138
+ Math.max(pollIntervalMs, 1) * 2 ** consecutivePollFailures,
139
+ MAX_POLL_ERROR_BACKOFF_MS,
140
+ );
141
+ };
142
+
143
+ const clearObservedRun = () => {
144
+ snapshotVersion += 1;
145
+ if (snapshotTransitionTimer) clearTimeout(snapshotTransitionTimer);
146
+ snapshotTransitionTimer = null;
147
+ setState(EMPTY_STATE);
148
+ };
132
149
 
133
150
  type RunHealthSnapshot = {
134
151
  active: boolean;
@@ -269,6 +286,7 @@ export function useRunStuckDetection({
269
286
  );
270
287
  if (cancelled) return;
271
288
  if (res.ok) {
289
+ consecutivePollFailures = 0;
272
290
  const data = (await res.json()) as ActiveRunResponse;
273
291
  const lastProgressAt = data.lastProgressAt ?? null;
274
292
  // Measure elapsed against the SERVER clock (serverNow) rather than the
@@ -336,9 +354,17 @@ export function useRunStuckDetection({
336
354
  if (!data.active || data.status !== "running") {
337
355
  nextDelay = IDLE_BACKOFF_INTERVAL_MS;
338
356
  }
357
+ } else if (res.status === 429 || res.status >= 500) {
358
+ nextDelay = pollFailureDelay();
359
+ } else {
360
+ consecutivePollFailures = 0;
361
+ clearObservedRun();
362
+ nextDelay = MAX_POLL_ERROR_BACKOFF_MS;
339
363
  }
340
364
  } catch {
341
- // Network blip — leave previous state. Next tick will retry.
365
+ // Network/503 blip — leave previous state and ease polling pressure
366
+ // while the server or database recovers.
367
+ nextDelay = pollFailureDelay();
342
368
  }
343
369
  if (!cancelled) {
344
370
  timer = setTimeout(poll, nextDelay);
@@ -669,6 +669,61 @@ export function isConnectionError(err: any): boolean {
669
669
  );
670
670
  }
671
671
 
672
+ /**
673
+ * Classify database failures that should temporarily shed request load.
674
+ * Statement timeouts are not included in isConnectionError() because retrying
675
+ * every timed-out mutation would not be safe, but request handlers can still
676
+ * return a retryable service-unavailable response for them.
677
+ */
678
+ export function isTransientDatabaseError(err: unknown): boolean {
679
+ const error = err as {
680
+ code?: unknown;
681
+ name?: unknown;
682
+ message?: unknown;
683
+ stack?: unknown;
684
+ cause?: {
685
+ code?: unknown;
686
+ name?: unknown;
687
+ message?: unknown;
688
+ stack?: unknown;
689
+ };
690
+ };
691
+ const code = String(error?.code ?? error?.cause?.code ?? "");
692
+ if (
693
+ code === "ECHECKOUTTIMEOUT" ||
694
+ code === "53300" ||
695
+ code === "57014" ||
696
+ /^08/.test(code) ||
697
+ /^57P0[123]$/.test(code)
698
+ ) {
699
+ return true;
700
+ }
701
+
702
+ const message = [error?.message, error?.cause?.message]
703
+ .filter((value): value is string => typeof value === "string")
704
+ .join(" ");
705
+ if (
706
+ /\bstatement timeout\b|\bdb (?:query|connect) timed out\b/i.test(message)
707
+ ) {
708
+ return true;
709
+ }
710
+
711
+ const databaseSurface = [
712
+ error?.name,
713
+ error?.cause?.name,
714
+ error?.stack,
715
+ error?.cause?.stack,
716
+ ]
717
+ .filter((value): value is string => typeof value === "string")
718
+ .join(" ");
719
+ return (
720
+ isConnectionError(error) &&
721
+ /@neondatabase|@libsql|\bpostgres(?:ql)?\b|\bpg-pool\b|drizzle-orm|\/db\/client\.[cm]?[jt]s/i.test(
722
+ databaseSurface,
723
+ )
724
+ );
725
+ }
726
+
672
727
  export async function retryOnConnectionError<T>(
673
728
  fn: () => Promise<T>,
674
729
  maxAttempts = 3,
@@ -138,6 +138,14 @@ export const CLOUDFLARE_WORKER_STUB_MODULES: Record<string, string> = {
138
138
  "export default { Resvg };",
139
139
  "",
140
140
  ].join("\n"),
141
+ playwright: [
142
+ "const unavailable = async () => { throw new Error('playwright unavailable in Cloudflare Pages worker'); };",
143
+ "export const chromium = { launch: unavailable, connect: unavailable, connectOverCDP: unavailable };",
144
+ "export const firefox = { launch: unavailable, connect: unavailable };",
145
+ "export const webkit = { launch: unavailable, connect: unavailable };",
146
+ "export default { chromium, firefox, webkit };",
147
+ "",
148
+ ].join("\n"),
141
149
  "playwright-core": [
142
150
  "const unavailable = async () => { throw new Error('playwright-core unavailable in Cloudflare Pages worker'); };",
143
151
  "export const chromium = { launch: unavailable };",
@@ -187,6 +195,50 @@ export const CLOUDFLARE_WORKER_STUB_MODULES: Record<string, string> = {
187
195
  "export default async () => ({ elements: [], files: {} });\n",
188
196
  };
189
197
 
198
+ export const CLOUDFLARE_WORKER_STUB_SUBPATH_MODULES: Record<string, string> = {
199
+ "pdf-parse/worker": [
200
+ "const unavailable = async () => { throw new Error('pdf-parse/worker unavailable in Cloudflare Pages worker'); };",
201
+ "export class CanvasFactory {}",
202
+ "export const getData = unavailable;",
203
+ "export default { CanvasFactory, getData };",
204
+ "",
205
+ ].join("\n"),
206
+ };
207
+
208
+ export function cloudflareWorkerStubAliasArgs(stubDir: string): string[] {
209
+ const subpathAliases = Object.keys(CLOUDFLARE_WORKER_STUB_SUBPATH_MODULES)
210
+ .sort((a, b) => b.length - a.length)
211
+ .map(
212
+ (mod) =>
213
+ `--alias:${mod}=${path.join(stubDir, `${mod.replace(/\//g, "__")}.js`)}`,
214
+ );
215
+ const packageAliases = Object.keys(CLOUDFLARE_WORKER_STUB_MODULES)
216
+ .sort((a, b) => b.length - a.length)
217
+ .map((mod) => `--alias:${mod}=${path.join(stubDir, mod, "index.js")}`);
218
+ return [...subpathAliases, ...packageAliases];
219
+ }
220
+
221
+ export function assertNoCloudflareWorkerStubDynamicImports(
222
+ code: string,
223
+ sourceName: string,
224
+ ): void {
225
+ const stubbedModules = [
226
+ ...Object.keys(CLOUDFLARE_WORKER_STUB_SUBPATH_MODULES),
227
+ ...Object.keys(CLOUDFLARE_WORKER_STUB_MODULES),
228
+ ];
229
+ const pattern = stubbedModules
230
+ .sort((a, b) => b.length - a.length)
231
+ .map((moduleName) => moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
232
+ .join("|");
233
+ const unresolvedImport = code.match(
234
+ new RegExp(`\\bimport\\s*\\(\\s*(["'])(${pattern})\\1\\s*\\)`),
235
+ );
236
+ if (!unresolvedImport) return;
237
+ throw new Error(
238
+ `Cloudflare worker output ${sourceName} retained a dynamic import for stubbed module "${unresolvedImport[2]}". Use a literal import so the worker bundler can apply its fail-closed stub.`,
239
+ );
240
+ }
241
+
190
242
  function cloudflareNodeBuiltinStubSource(
191
243
  moduleName: string,
192
244
  namedExports: string[],
@@ -1666,9 +1718,15 @@ async function buildCloudflarePages() {
1666
1718
  JSON.stringify({ name: mod, main: "index.js", type: "module" }),
1667
1719
  );
1668
1720
  }
1669
- const stubAliases = Object.keys(CLOUDFLARE_WORKER_STUB_MODULES).map(
1670
- (mod) => `--alias:${mod}=${path.join(stubDir, mod, "index.js")}`,
1671
- );
1721
+ for (const [mod, source] of Object.entries(
1722
+ CLOUDFLARE_WORKER_STUB_SUBPATH_MODULES,
1723
+ )) {
1724
+ fs.writeFileSync(
1725
+ path.join(stubDir, `${mod.replace(/\//g, "__")}.js`),
1726
+ source,
1727
+ );
1728
+ }
1729
+ const stubAliases = cloudflareWorkerStubAliasArgs(stubDir);
1672
1730
  const nodeBuiltinStubDir = path.join(tmpDir, "node-builtin-stubs");
1673
1731
  fs.mkdirSync(nodeBuiltinStubDir, { recursive: true });
1674
1732
  const nodeBuiltinStubAliases: string[] = [];
@@ -1850,6 +1908,8 @@ async function buildCloudflarePages() {
1850
1908
  );
1851
1909
  }
1852
1910
 
1911
+ assertNoCloudflareWorkerStubDynamicImports(code, jsFile);
1912
+
1853
1913
  fs.writeFileSync(jsFile, code);
1854
1914
  }
1855
1915