@agent-native/core 0.122.1 → 0.122.3

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 (114) hide show
  1. package/README.md +1 -13
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +12 -0
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +65 -1
  6. package/corpus/core/src/agent/run-loop-with-resume.ts +12 -0
  7. package/corpus/core/src/agent/run-store.ts +61 -1
  8. package/corpus/core/src/integrations/a2a-continuation-processor.ts +182 -0
  9. package/corpus/core/src/integrations/a2a-continuations-store.ts +122 -12
  10. package/corpus/core/src/integrations/integration-campaign-recovery.ts +127 -0
  11. package/corpus/core/src/integrations/integration-campaigns-store.ts +1039 -0
  12. package/corpus/core/src/integrations/integration-durable-dispatch.ts +28 -3
  13. package/corpus/core/src/integrations/pending-tasks-retry-job.ts +8 -0
  14. package/corpus/core/src/integrations/plugin.ts +453 -35
  15. package/corpus/core/src/integrations/webhook-handler.ts +469 -11
  16. package/corpus/core/src/server/builder-design-systems.ts +1 -1
  17. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +15 -0
  18. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +3 -3
  19. package/corpus/templates/analytics/changelog/2026-07-25-analytics-dashboards-and-scheduled-reports-are-faster-and-mo.md +6 -0
  20. package/corpus/templates/analytics/server/db/schema.ts +12 -0
  21. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +8 -4
  22. package/corpus/templates/analytics/server/lib/dashboard-report.ts +136 -5
  23. package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +250 -1
  24. package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +167 -0
  25. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +34 -9
  26. package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +105 -23
  27. package/corpus/templates/analytics/server/lib/first-party-unbounded-panel-repair.ts +169 -0
  28. package/corpus/templates/analytics/server/plugins/db.ts +41 -1
  29. package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +6 -3
  30. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +7 -3
  31. package/corpus/templates/content/app/components/editor/body-hydration.ts +10 -0
  32. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -0
  33. package/corpus/templates/content/app/root.tsx +12 -1
  34. package/corpus/templates/content/changelog/2026-07-23-new-pages-open-immediately-while-slow-network-work-continues.md +6 -0
  35. package/corpus/templates/content/changelog/2026-07-25-database-preview-actions-open-reliably.md +6 -0
  36. package/corpus/templates/content/e2e/playwright.config.ts +1 -1
  37. package/corpus/templates/design/actions/edit-design.ts +66 -27
  38. package/corpus/templates/design/changelog/2026-07-25-design-edits-retry-concurrent-changes.md +6 -0
  39. package/corpus/templates/design/server/source-workspace.ts +5 -1
  40. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +6 -4
  41. package/corpus/templates/forms/actions/create-form.ts +5 -1
  42. package/corpus/templates/forms/actions/export-responses.ts +22 -11
  43. package/corpus/templates/forms/actions/update-form.ts +5 -1
  44. package/corpus/templates/forms/changelog/2026-07-25-fields-can-be-created-without-ids.md +6 -0
  45. package/corpus/templates/forms/changelog/2026-07-25-response-exports-use-file-storage.md +6 -0
  46. package/corpus/templates/forms/server/lib/validate-fields.ts +43 -0
  47. package/corpus/templates/slides/actions/extract-pdf.ts +4 -4
  48. package/corpus/templates/slides/actions/import-file.ts +3 -4
  49. package/corpus/templates/slides/changelog/2026-07-25-gemini-image-models-updated.md +6 -0
  50. package/corpus/templates/slides/changelog/2026-07-25-pdf-imports-tolerate-missing-canvas.md +6 -0
  51. package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +5 -2
  52. package/corpus/templates/slides/server/lib/pdf-parse-setup.ts +150 -0
  53. package/dist/agent/production-agent.d.ts.map +1 -1
  54. package/dist/agent/production-agent.js +63 -1
  55. package/dist/agent/production-agent.js.map +1 -1
  56. package/dist/agent/run-loop-with-resume.d.ts +7 -1
  57. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  58. package/dist/agent/run-loop-with-resume.js +7 -0
  59. package/dist/agent/run-loop-with-resume.js.map +1 -1
  60. package/dist/agent/run-store.d.ts.map +1 -1
  61. package/dist/agent/run-store.js +49 -0
  62. package/dist/agent/run-store.js.map +1 -1
  63. package/dist/collab/routes.d.ts +1 -1
  64. package/dist/collab/struct-routes.d.ts +1 -1
  65. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  66. package/dist/integrations/a2a-continuation-processor.d.ts +14 -0
  67. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  68. package/dist/integrations/a2a-continuation-processor.js +134 -1
  69. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  70. package/dist/integrations/a2a-continuations-store.d.ts +23 -0
  71. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  72. package/dist/integrations/a2a-continuations-store.js +99 -13
  73. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  74. package/dist/integrations/integration-campaign-recovery.d.ts +16 -0
  75. package/dist/integrations/integration-campaign-recovery.d.ts.map +1 -0
  76. package/dist/integrations/integration-campaign-recovery.js +94 -0
  77. package/dist/integrations/integration-campaign-recovery.js.map +1 -0
  78. package/dist/integrations/integration-campaigns-store.d.ts +121 -0
  79. package/dist/integrations/integration-campaigns-store.d.ts.map +1 -0
  80. package/dist/integrations/integration-campaigns-store.js +784 -0
  81. package/dist/integrations/integration-campaigns-store.js.map +1 -0
  82. package/dist/integrations/integration-durable-dispatch.d.ts +3 -0
  83. package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -1
  84. package/dist/integrations/integration-durable-dispatch.js +22 -3
  85. package/dist/integrations/integration-durable-dispatch.js.map +1 -1
  86. package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
  87. package/dist/integrations/pending-tasks-retry-job.js +6 -0
  88. package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
  89. package/dist/integrations/plugin.d.ts.map +1 -1
  90. package/dist/integrations/plugin.js +325 -18
  91. package/dist/integrations/plugin.js.map +1 -1
  92. package/dist/integrations/webhook-handler.d.ts +16 -1
  93. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  94. package/dist/integrations/webhook-handler.js +368 -13
  95. package/dist/integrations/webhook-handler.js.map +1 -1
  96. package/dist/observability/routes.d.ts +1 -1
  97. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  98. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  99. package/dist/secrets/routes.d.ts +9 -9
  100. package/dist/server/builder-design-systems.js +1 -1
  101. package/dist/server/builder-design-systems.js.map +1 -1
  102. package/package.json +3 -3
  103. package/src/agent/production-agent.ts +65 -1
  104. package/src/agent/run-loop-with-resume.ts +12 -0
  105. package/src/agent/run-store.ts +61 -1
  106. package/src/integrations/a2a-continuation-processor.ts +182 -0
  107. package/src/integrations/a2a-continuations-store.ts +122 -12
  108. package/src/integrations/integration-campaign-recovery.ts +127 -0
  109. package/src/integrations/integration-campaigns-store.ts +1039 -0
  110. package/src/integrations/integration-durable-dispatch.ts +28 -3
  111. package/src/integrations/pending-tasks-retry-job.ts +8 -0
  112. package/src/integrations/plugin.ts +453 -35
  113. package/src/integrations/webhook-handler.ts +469 -11
  114. package/src/server/builder-design-systems.ts +1 -1
@@ -18,6 +18,7 @@ import { z } from "zod";
18
18
  import { getDb, schema } from "../server/db/index.js";
19
19
  import {
20
20
  readLiveSourceFile,
21
+ SourceWorkspaceEditConflictError,
21
22
  writeInlineSourceFile,
22
23
  type SourceWorkspaceFile,
23
24
  } from "../server/source-workspace.js";
@@ -303,36 +304,28 @@ export default defineAction({
303
304
  );
304
305
  }
305
306
 
306
- // Read the LIVE base (collab text when present, else the SQL row) right
307
- // before transforming, and carry its versionHash through to the write
308
- // below. writeInlineSourceFile re-reads the live text immediately before
309
- // its own applyText/DB write and rejects if it no longer matches this
310
- // hash — closing the race window where a concurrent editor/agent write
311
- // lands between this read and the persist (the same stale-diff-base bug
312
- // fixed for insert-design-native-asset.ts and insert-asset.ts: a diff
313
- // computed from a stale base, char-diffed into a collab doc that has
314
- // since moved on, corrupts or drops the other writer's change).
315
- const workspaceFile: SourceWorkspaceFile = {
316
- id: file.id,
317
- designId: file.designId,
318
- filename: file.filename ?? "",
319
- fileType: file.fileType ?? "html",
320
- content: file.content,
321
- createdAt: null,
322
- updatedAt: null,
323
- };
324
- const live = await readLiveSourceFile(workspaceFile);
325
- const base = live.content;
326
-
327
307
  const resolvedMode =
328
308
  mode ??
329
309
  (replacementContent !== undefined ? "replace-file" : "search-replace");
330
- const { content: nextContent, applied } =
331
- resolvedMode === "replace-file"
332
- ? { content: replacementContent ?? "", applied: 0 }
333
- : applySearchReplaceEdits(base, edits ?? []);
334
- const changed = nextContent !== base;
335
310
 
311
+ // A concurrent human/agent write can land between reading the live file
312
+ // and persisting this edit (writeInlineSourceFile throws
313
+ // SourceWorkspaceEditConflictError when that happens — see its own
314
+ // comment for the CAS/collab details). search-replace edits are anchored
315
+ // to specific text rather than a stale full snapshot, so on conflict we
316
+ // can just re-read the fresh content and reapply the SAME edits against
317
+ // it instead of forcing the agent to make a separate get-design-snapshot
318
+ // round trip and guess again. replace-file sends a full document computed
319
+ // from a point-in-time snapshot — retrying that blind could silently
320
+ // clobber whatever the concurrent writer did, so it still fails closed on
321
+ // the first conflict.
322
+ const MAX_EDIT_CONFLICT_RETRIES = 2;
323
+
324
+ let live: Awaited<ReturnType<typeof readLiveSourceFile>>;
325
+ let base = "";
326
+ let nextContent = "";
327
+ let applied = 0;
328
+ let changed = false;
336
329
  let creativeContext:
337
330
  | {
338
331
  contextMode: "off" | "auto" | "pinned";
@@ -352,7 +345,43 @@ export default defineAction({
352
345
  }
353
346
  | undefined;
354
347
 
355
- if (changed) {
348
+ for (let attempt = 0; ; attempt += 1) {
349
+ // Refetch the SQL content on retries — readLiveSourceFile only falls
350
+ // back to this when no collab doc exists yet, so a conflict caused by
351
+ // a plain SQL writer (no live collab session) needs a fresh row here
352
+ // or every retry would recompute the exact same stale versionHash.
353
+ const currentContent =
354
+ attempt === 0
355
+ ? file.content
356
+ : (
357
+ await db
358
+ .select({ content: schema.designFiles.content })
359
+ .from(schema.designFiles)
360
+ .where(eq(schema.designFiles.id, file.id))
361
+ .limit(1)
362
+ )[0]?.content;
363
+
364
+ const workspaceFile: SourceWorkspaceFile = {
365
+ id: file.id,
366
+ designId: file.designId,
367
+ filename: file.filename ?? "",
368
+ fileType: file.fileType ?? "html",
369
+ content: currentContent,
370
+ createdAt: null,
371
+ updatedAt: null,
372
+ };
373
+ live = await readLiveSourceFile(workspaceFile);
374
+ base = live.content;
375
+
376
+ ({ content: nextContent, applied } =
377
+ resolvedMode === "replace-file"
378
+ ? { content: replacementContent ?? "", applied: 0 }
379
+ : applySearchReplaceEdits(base, edits ?? []));
380
+ changed = nextContent !== base;
381
+ creativeContext = undefined;
382
+
383
+ if (!changed) break;
384
+
356
385
  const previous =
357
386
  contextModeOverride === "off"
358
387
  ? null
@@ -439,6 +468,15 @@ export default defineAction({
439
468
  content: nextContent,
440
469
  expectedVersionHash: live.versionHash,
441
470
  });
471
+ } catch (error) {
472
+ if (
473
+ error instanceof SourceWorkspaceEditConflictError &&
474
+ resolvedMode === "search-replace" &&
475
+ attempt < MAX_EDIT_CONFLICT_RETRIES
476
+ ) {
477
+ continue;
478
+ }
479
+ throw error;
442
480
  } finally {
443
481
  agentLeaveDocument(file.id);
444
482
  }
@@ -448,6 +486,7 @@ export default defineAction({
448
486
  artifactId: designId,
449
487
  ...creativeContext,
450
488
  });
489
+ break;
451
490
  }
452
491
 
453
492
  return {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-25
4
+ ---
5
+
6
+ Design edits now retry safely when another collaborator changes the file.
@@ -331,7 +331,11 @@ export async function writeInlineSourceFile(args: {
331
331
  args.expectedVersionHash &&
332
332
  args.expectedVersionHash !== current.versionHash
333
333
  ) {
334
- throw new Error(
334
+ // Typed so callers can catch-and-retry the same way they already do for
335
+ // the other two conflict sites below (see apply-shader-fill.ts /
336
+ // apply-component-prop-edit.ts / edit-design.ts) instead of only
337
+ // matching on message text.
338
+ throw new SourceWorkspaceEditConflictError(
335
339
  "Source file changed since it was read. Re-read the file and retry.",
336
340
  );
337
341
  }
@@ -48,14 +48,16 @@ status, visibility, and an "Open editor" action.
48
48
 
49
49
  ## Exporting Responses
50
50
 
51
- Use `export-responses` to export to CSV or JSON:
51
+ Use `export-responses` to export to CSV or JSON. The export is uploaded to
52
+ configured file storage (never written to local disk — serverless hosts have
53
+ a read-only filesystem) and the action returns the resulting file URL:
52
54
 
53
55
  ```bash
54
56
  # CSV export (default)
55
- pnpm action export-responses --form <form-id> --output data/export.csv
57
+ pnpm action export-responses --form <form-id>
56
58
 
57
59
  # JSON export
58
- pnpm action export-responses --form <form-id> --output data/export.json --format json
60
+ pnpm action export-responses --form <form-id> --format json
59
61
  ```
60
62
 
61
63
  The CSV includes headers derived from field labels. Array values (multiselect) are joined with semicolons.
@@ -113,7 +115,7 @@ To analyze responses, the workflow is:
113
115
  | ---------------------- | ---------------------------------------------------------------------------- |
114
116
  | "@Form setup?" | `preview-form --formId <id>` and answer from the returned fields/settings |
115
117
  | "How many responses?" | `response-insights --formId <id>` and report `summary.responses` |
116
- | "Export to CSV" | `export-responses --form <id> --output data/export.csv` |
118
+ | "Export to CSV" | `export-responses --form <id>` |
117
119
  | "Submissions by day" | `response-insights --formId <id> --days 30` |
118
120
  | "Summarize feedback" | `response-insights`, then `list-responses` if more detail is needed |
119
121
  | "Average rating" | `list-responses`, compute from rating fields and state the sampled row count |
@@ -9,7 +9,10 @@ import { z } from "zod";
9
9
 
10
10
  import { getDb, schema } from "../server/db/index.js";
11
11
  import { assertIntegrationUrlsAllowed } from "../server/lib/integrations.js";
12
- import { assertValidFields } from "../server/lib/validate-fields.js";
12
+ import {
13
+ assertValidFields,
14
+ normalizeFieldIds,
15
+ } from "../server/lib/validate-fields.js";
13
16
  import type { FormField, FormSettings } from "../shared/types.js";
14
17
  import { assertPublishableForm } from "./lib/assert-publishable-form.js";
15
18
 
@@ -88,6 +91,7 @@ export default defineAction({
88
91
  fields = args.fields as unknown as FormField[];
89
92
  }
90
93
  }
94
+ fields = normalizeFieldIds(fields) as FormField[];
91
95
  assertValidFields(fields);
92
96
 
93
97
  const defaultSettings: FormSettings = {
@@ -1,6 +1,6 @@
1
- import fs from "fs";
2
-
3
1
  import { defineAction } from "@agent-native/core";
2
+ import { uploadFile } from "@agent-native/core/file-upload";
3
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
4
4
  import { assertAccess } from "@agent-native/core/sharing";
5
5
  import { eq, desc } from "drizzle-orm";
6
6
  import { z } from "zod";
@@ -12,7 +12,6 @@ export default defineAction({
12
12
  description: "Export form responses to CSV or JSON file.",
13
13
  schema: z.object({
14
14
  form: z.string().describe("Form ID (required)"),
15
- output: z.string().optional().describe("Output file path"),
16
15
  format: z.enum(["csv", "json"]).optional().describe("Export format"),
17
16
  }),
18
17
  http: false,
@@ -28,10 +27,10 @@ export default defineAction({
28
27
  .orderBy(desc(schema.responses.submittedAt));
29
28
 
30
29
  const fields = JSON.parse(form.fields);
31
- const fmt =
32
- args.format || (args.output?.endsWith(".json") ? "json" : "csv");
33
- const outputPath =
34
- args.output || `data/export-${formId}.${fmt === "json" ? "json" : "csv"}`;
30
+ const fmt = args.format || "csv";
31
+ const filename = `export-${formId}.${fmt === "json" ? "json" : "csv"}`;
32
+ const ownerEmail = getRequestUserEmail() ?? undefined;
33
+ let fileBody: string;
35
34
 
36
35
  if (fmt === "json") {
37
36
  const data = responses.map((r) => ({
@@ -42,7 +41,7 @@ export default defineAction({
42
41
  clientSurface: r.clientSurface ?? null,
43
42
  ...JSON.parse(r.data),
44
43
  }));
45
- fs.writeFileSync(outputPath, JSON.stringify(data, null, 2));
44
+ fileBody = JSON.stringify(data, null, 2);
46
45
  } else {
47
46
  const headers = [
48
47
  "ID",
@@ -74,17 +73,29 @@ export default defineAction({
74
73
  // with a single quote so spreadsheets treat it as literal text.
75
74
  const neutralize = (cell: string) =>
76
75
  /^[=+\-@\t\r]/.test(cell) ? `'${cell}` : cell;
77
- const csv = [headers, ...rows]
76
+ fileBody = [headers, ...rows]
78
77
  .map((row) =>
79
78
  row
80
79
  .map((cell: string) => `"${neutralize(cell).replace(/"/g, '""')}"`)
81
80
  .join(","),
82
81
  )
83
82
  .join("\n");
83
+ }
84
+
85
+ const uploaded = await uploadFile({
86
+ data: Buffer.from(fileBody, "utf8"),
87
+ mimeType: fmt === "json" ? "application/json" : "text/csv",
88
+ filename,
89
+ ownerEmail,
90
+ }).catch(() => null);
84
91
 
85
- fs.writeFileSync(outputPath, csv);
92
+ if (!uploaded) {
93
+ throw new Error(
94
+ "Export was generated but not saved because file storage is not configured. " +
95
+ "Connect or reconnect Builder.io in Settings → File uploads, or register a custom provider.",
96
+ );
86
97
  }
87
98
 
88
- return `Exported ${responses.length} responses to ${outputPath}`;
99
+ return `Exported ${responses.length} responses to ${uploaded.url}`;
89
100
  },
90
101
  });
@@ -6,7 +6,10 @@ import { z } from "zod";
6
6
  import { getDb, schema } from "../server/db/index.js";
7
7
  import { assertIntegrationUrlsAllowed } from "../server/lib/integrations.js";
8
8
  import { invalidatePublicFormCache } from "../server/lib/public-form-ssr.js";
9
- import { assertValidFields } from "../server/lib/validate-fields.js";
9
+ import {
10
+ assertValidFields,
11
+ normalizeFieldIds,
12
+ } from "../server/lib/validate-fields.js";
10
13
  import type { FormField, FormSettings } from "../shared/types.js";
11
14
  import { assertPublishableForm } from "./lib/assert-publishable-form.js";
12
15
 
@@ -81,6 +84,7 @@ export default defineAction({
81
84
  } else {
82
85
  parsedFields = args.fields;
83
86
  }
87
+ parsedFields = normalizeFieldIds(parsedFields);
84
88
  assertValidFields(parsedFields);
85
89
  updates.fields = JSON.stringify(parsedFields);
86
90
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-25
4
+ ---
5
+
6
+ Forms can create and update fields without manually supplied IDs.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-25
4
+ ---
5
+
6
+ Response exports now save to file storage and return a downloadable URL.
@@ -6,6 +6,49 @@
6
6
  export const FIELD_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
7
7
  const CONDITIONAL_OPERATORS = new Set(["equals", "not_equals", "contains"]);
8
8
 
9
+ /**
10
+ * Generates a safe id for a whole-array field replacement (create-form,
11
+ * update-form) when the model omits one — the #1 create-form failure in
12
+ * the 2026-07-25 reliability sweep ("field #1 has an invalid id undefined").
13
+ * Never used by patch-form-fields: there a missing id on an upsert op is
14
+ * ambiguous (new field vs. a forgotten reference to an existing one), so it
15
+ * must keep failing loud rather than risk silently creating a duplicate.
16
+ * Ids are slugified from `label` through the same FIELD_ID_PATTERN charset
17
+ * `assertValidFields` enforces, so a generated id can never fail that check.
18
+ */
19
+ export function normalizeFieldIds(fields: unknown): unknown {
20
+ if (!Array.isArray(fields)) return fields;
21
+ const usedIds = new Set(
22
+ fields
23
+ .map((f) =>
24
+ f && typeof f === "object" ? (f as Record<string, unknown>).id : null,
25
+ )
26
+ .filter(
27
+ (id): id is string =>
28
+ typeof id === "string" && FIELD_ID_PATTERN.test(id),
29
+ ),
30
+ );
31
+ return fields.map((field) => {
32
+ if (field == null || typeof field !== "object") return field;
33
+ const f = field as Record<string, unknown>;
34
+ if (typeof f.id === "string" && FIELD_ID_PATTERN.test(f.id)) return field;
35
+ const label = typeof f.label === "string" ? f.label : "";
36
+ const base =
37
+ label
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9_-]+/g, "_")
40
+ .replace(/^_+|_+$/g, "")
41
+ .slice(0, 40) || "field";
42
+ let candidate = base;
43
+ let suffix = 1;
44
+ while (usedIds.has(candidate)) {
45
+ candidate = `${base}_${++suffix}`;
46
+ }
47
+ usedIds.add(candidate);
48
+ return { ...f, id: candidate };
49
+ });
50
+ }
51
+
9
52
  export function assertValidFields(fields: unknown): void {
10
53
  if (!Array.isArray(fields)) {
11
54
  throw new Error("fields must be an array");
@@ -2,6 +2,8 @@ import fs from "fs";
2
2
 
3
3
  import { parseArgs } from "@agent-native/core";
4
4
 
5
+ import { setupPdfParse } from "../server/lib/pdf-parse-setup.js";
6
+
5
7
  export default async function (args: string[]) {
6
8
  const { path: pdfPath } = parseArgs(args);
7
9
  if (!pdfPath) {
@@ -10,12 +12,10 @@ export default async function (args: string[]) {
10
12
  }
11
13
 
12
14
  const buf = fs.readFileSync(pdfPath);
13
- const { CanvasFactory, getData } = await import("pdf-parse/worker");
14
- const { PDFParse } = await import("pdf-parse");
15
- PDFParse.setWorker(getData());
15
+ const { PDFParse, canvasFactory } = await setupPdfParse();
16
16
  const pdf = new PDFParse({
17
17
  data: new Uint8Array(buf),
18
- CanvasFactory,
18
+ CanvasFactory: canvasFactory,
19
19
  });
20
20
  const result = await pdf.getText().finally(() => pdf.destroy());
21
21
  const pages = result.pages || [];
@@ -14,6 +14,7 @@ import { z } from "zod";
14
14
  import { getDb, schema } from "../server/db/index.js";
15
15
  import { notifyClients } from "../server/handlers/decks.js";
16
16
  import { upsertBuilderProxyDesignSystem } from "../server/lib/builder-design-system-proxy.js";
17
+ import { setupPdfParse } from "../server/lib/pdf-parse-setup.js";
17
18
  import { readUserUploadedFile } from "./_uploaded-files.js";
18
19
 
19
20
  const DEFAULT_MAX_SOURCE_CHARS = 60_000;
@@ -211,14 +212,12 @@ export default defineAction({
211
212
  }
212
213
 
213
214
  if (detectedFormat === "pdf") {
214
- const { CanvasFactory, getData } = await import("pdf-parse/worker");
215
- const { PDFParse } = await import("pdf-parse");
216
- PDFParse.setWorker(getData());
215
+ const { PDFParse, canvasFactory } = await setupPdfParse();
217
216
  const { convertSectionsToSlides } =
218
217
  await import("../server/handlers/import/html-converter.js");
219
218
  const pdf = new PDFParse({
220
219
  data: new Uint8Array(fileBuffer),
221
- CanvasFactory,
220
+ CanvasFactory: canvasFactory,
222
221
  });
223
222
  const result = await pdf.getText().finally(() => pdf.destroy());
224
223
  const pages = normalizePdfPages(result);
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-25
4
+ ---
5
+
6
+ AI image generation uses the current Gemini image models.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-25
4
+ ---
5
+
6
+ PDF imports keep extracting text when native canvas bindings are unavailable.
@@ -74,9 +74,12 @@ export class GeminiProvider implements ImageProvider {
74
74
  imageConfig.aspectRatio = config.aspectRatio;
75
75
  }
76
76
 
77
+ // Google retired the "-preview" aliases (shut down 2026-06-25); the GA
78
+ // model ids dropped the suffix. Sending the old preview ids 404s every
79
+ // call before falling through to gemini-2.5-flash-image.
77
80
  const geminiModels = [
78
- "gemini-3.1-flash-image-preview",
79
- "gemini-3-pro-image-preview",
81
+ "gemini-3.1-flash-image",
82
+ "gemini-3-pro-image",
80
83
  "gemini-2.5-flash-image",
81
84
  ];
82
85
  let lastError: Error | null = null;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Prod incident (2026-07-25 reliability sweep): `import-file`'s PDF path
3
+ * crashed with "DOMMatrix is not defined" 21 times in 14 days. `pdf-parse`'s
4
+ * `/worker` submodule unconditionally imports `@napi-rs/canvas` (a native
5
+ * N-API binary) as a side effect of the import itself — just to read its
6
+ * bundled worker-script blob (`getData()`) — and assigns its `DOMMatrix`
7
+ * export onto `globalThis` for pdfjs-dist's text-transform math to use. That
8
+ * native binary is fragile in serverless Lambda-style environments (missing
9
+ * platform binding, wrong glibc/musl target) and can silently fail to
10
+ * populate `globalThis.DOMMatrix` without the surrounding import itself
11
+ * throwing, so the crash only surfaces later, deep inside `getText()`.
12
+ *
13
+ * `getText()` never renders to a canvas — `CanvasFactory` is documented
14
+ * optional on `LoadParameters` and is only consumed by `getImage()`/
15
+ * `getScreenshot()`. So for pure text extraction we don't need the canvas
16
+ * import to succeed; we only need SOME `DOMMatrix` implementation to exist
17
+ * for pdfjs-dist's 2D transform math. This installs a minimal, spec-correct
18
+ * 2D-affine `DOMMatrix` polyfill (PDF content-stream matrices are always the
19
+ * 2D 6-value form, never 3D) as a fallback, then attempts the real
20
+ * `@napi-rs/canvas`-backed setup for the CanvasFactory/worker-script wiring,
21
+ * catching failure instead of letting it crash the whole import.
22
+ */
23
+
24
+ interface Minimal2DMatrix {
25
+ a: number;
26
+ b: number;
27
+ c: number;
28
+ d: number;
29
+ e: number;
30
+ f: number;
31
+ is2D: boolean;
32
+ isIdentity: boolean;
33
+ multiply(other: Minimal2DMatrix): Minimal2DMatrix;
34
+ translate(tx?: number, ty?: number): Minimal2DMatrix;
35
+ scale(sx?: number, sy?: number): Minimal2DMatrix;
36
+ inverse(): Minimal2DMatrix;
37
+ toString(): string;
38
+ }
39
+
40
+ function installDomMatrixPolyfillIfMissing(): void {
41
+ if (
42
+ typeof (globalThis as { DOMMatrix?: unknown }).DOMMatrix !== "undefined"
43
+ ) {
44
+ return;
45
+ }
46
+
47
+ class PolyfillDOMMatrix implements Minimal2DMatrix {
48
+ a = 1;
49
+ b = 0;
50
+ c = 0;
51
+ d = 1;
52
+ e = 0;
53
+ f = 0;
54
+
55
+ constructor(init?: number[] | string) {
56
+ if (Array.isArray(init) && init.length >= 6) {
57
+ [this.a, this.b, this.c, this.d, this.e, this.f] = init;
58
+ }
59
+ }
60
+
61
+ get is2D(): boolean {
62
+ return true;
63
+ }
64
+
65
+ get isIdentity(): boolean {
66
+ return (
67
+ this.a === 1 &&
68
+ this.b === 0 &&
69
+ this.c === 0 &&
70
+ this.d === 1 &&
71
+ this.e === 0 &&
72
+ this.f === 0
73
+ );
74
+ }
75
+
76
+ multiply(other: Minimal2DMatrix): PolyfillDOMMatrix {
77
+ return new PolyfillDOMMatrix([
78
+ this.a * other.a + this.c * other.b,
79
+ this.b * other.a + this.d * other.b,
80
+ this.a * other.c + this.c * other.d,
81
+ this.b * other.c + this.d * other.d,
82
+ this.a * other.e + this.c * other.f + this.e,
83
+ this.b * other.e + this.d * other.f + this.f,
84
+ ]);
85
+ }
86
+
87
+ translate(tx = 0, ty = 0): PolyfillDOMMatrix {
88
+ return this.multiply(new PolyfillDOMMatrix([1, 0, 0, 1, tx, ty]));
89
+ }
90
+
91
+ scale(sx = 1, sy = sx): PolyfillDOMMatrix {
92
+ return this.multiply(new PolyfillDOMMatrix([sx, 0, 0, sy, 0, 0]));
93
+ }
94
+
95
+ inverse(): PolyfillDOMMatrix {
96
+ const det = this.a * this.d - this.b * this.c;
97
+ if (det === 0) return new PolyfillDOMMatrix();
98
+ const invDet = 1 / det;
99
+ return new PolyfillDOMMatrix([
100
+ this.d * invDet,
101
+ -this.b * invDet,
102
+ -this.c * invDet,
103
+ this.a * invDet,
104
+ (this.c * this.f - this.d * this.e) * invDet,
105
+ (this.b * this.e - this.a * this.f) * invDet,
106
+ ]);
107
+ }
108
+
109
+ toString(): string {
110
+ return `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})`;
111
+ }
112
+ }
113
+
114
+ (globalThis as { DOMMatrix?: unknown }).DOMMatrix = PolyfillDOMMatrix;
115
+ }
116
+
117
+ /** Result of `setupPdfParse` — pass `canvasFactory` to `new PDFParse(...)` when present. */
118
+ export interface PdfParseSetup {
119
+ PDFParse: typeof import("pdf-parse").PDFParse;
120
+ canvasFactory: object | undefined;
121
+ }
122
+
123
+ /**
124
+ * Sets up `pdf-parse` for text extraction, tolerating a broken native-canvas
125
+ * dependency. Always installs the DOMMatrix polyfill FIRST (cheap, no
126
+ * native deps, spec-correct for the 2D case pdfjs-dist actually uses) so
127
+ * `getText()` can never hit "DOMMatrix is not defined" regardless of
128
+ * whether the canvas worker setup below succeeds.
129
+ */
130
+ export async function setupPdfParse(): Promise<PdfParseSetup> {
131
+ installDomMatrixPolyfillIfMissing();
132
+
133
+ const { PDFParse } = await import("pdf-parse");
134
+ let canvasFactory: object | undefined;
135
+ try {
136
+ const { CanvasFactory, getData } = await import("pdf-parse/worker");
137
+ PDFParse.setWorker(getData());
138
+ canvasFactory = CanvasFactory;
139
+ } catch (err) {
140
+ // Native @napi-rs/canvas binding unavailable in this runtime — fine for
141
+ // text-only extraction. Leave the worker script unconfigured (a
142
+ // documented no-op read, see PDFParse.setWorker) and CanvasFactory
143
+ // undefined; pdfjs-dist runs in-process without a separate worker.
144
+ console.warn(
145
+ "[import-file] pdf-parse canvas worker setup failed, continuing without it (text-only extraction is unaffected):",
146
+ err instanceof Error ? err.message : String(err),
147
+ );
148
+ }
149
+ return { PDFParse, canvasFactory };
150
+ }