@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
package/README.md CHANGED
@@ -39,8 +39,6 @@ Fork a working app and let the agent evolve it. **You can customize everything.*
39
39
 
40
40
  **Agent-Native Loom**
41
41
 
42
- Record your screen with auto-transcripts and captured browser debug logs, share a link, and let an agent read the transcript, see timestamped frames, and fix the bug.
43
-
44
42
  </td>
45
43
  <td width="33%" align="center" valign="top">
46
44
 
@@ -50,8 +48,6 @@ Record your screen with auto-transcripts and captured browser debug logs, share
50
48
 
51
49
  **Visual plan mode for coding agents**
52
50
 
53
- Install `/visual-plan` and `/visual-recap` so your coding agent can plan before it builds and recap changes after they land. High-level code reviews with diagrams, wireframes, annotations, and review links.
54
-
55
51
  </td>
56
52
  <td width="33%" align="center" valign="top">
57
53
 
@@ -61,8 +57,6 @@ Install `/visual-plan` and `/visual-recap` so your coding agent can plan before
61
57
 
62
58
  **Agent-Native Figma**
63
59
 
64
- Generate interactive HTML prototypes, compare variants, refine controls, and export the result.
65
-
66
60
  </td>
67
61
  </tr>
68
62
  <tr>
@@ -74,8 +68,6 @@ Generate interactive HTML prototypes, compare variants, refine controls, and exp
74
68
 
75
69
  **Agent-Native Notion/Obsidian**
76
70
 
77
- Edit local Markdown/MDX files, generate rich interactive custom blocks, and draft, rewrite, or publish with an agent.
78
-
79
71
  </td>
80
72
  <td width="33%" align="center" valign="top">
81
73
 
@@ -85,8 +77,6 @@ Edit local Markdown/MDX files, generate rich interactive custom blocks, and draf
85
77
 
86
78
  **Open-Source Alternative to Amplitude and FullStory**
87
79
 
88
- Connect analytics data sources, prompt for real charts, and build reusable dashboards.
89
-
90
80
  </td>
91
81
  <td width="33%" align="center" valign="top">
92
82
 
@@ -96,13 +86,11 @@ Connect analytics data sources, prompt for real charts, and build reusable dashb
96
86
 
97
87
  **A minimal ChatGPT-style app for your own agent**
98
88
 
99
- Chat-first app scaffold with durable threads, actions, auth, live sync, and a clean path to add screens or plug in your own agent backend.
100
-
101
89
  </td>
102
90
  </tr>
103
91
  </table>
104
92
 
105
- View the full app gallery at **[agent-native.com/apps](https://agent-native.com/apps)**, or build from scratch with the **[framework guide](https://agent-native.com/docs/getting-started)**.
93
+ View the [full gallery](https://agent-native.com/apps), or build from scratch with the **[framework guide](https://agent-native.com/docs/getting-started)**.
106
94
 
107
95
  ## Quick Start
108
96
 
package/corpus/README.md CHANGED
@@ -28,6 +28,6 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
 
29
29
  ## Generated Counts
30
30
 
31
- - core files: 1671
31
+ - core files: 1673
32
32
  - toolkit files: 160
33
- - template files: 6461
33
+ - template files: 6472
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.122.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 6dd7ee0: Recover long-running messaging integrations across hosted function boundaries while preserving one logical agent turn and one final platform reply.
8
+
9
+ ## 0.122.2
10
+
11
+ ### Patch Changes
12
+
13
+ - 905bdba: Improve tool-input recovery, stale-run handling, and design-system routing reliability.
14
+
3
15
  ## 0.122.1
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.122.1",
3
+ "version": "0.122.3",
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": {
@@ -3369,6 +3369,59 @@ function normalizeOptionalToolPlaceholders(
3369
3369
  return { input: normalized, changed: true };
3370
3370
  }
3371
3371
 
3372
+ /**
3373
+ * Models routinely JSON-encode a field's value into a string when the tool
3374
+ * schema wants an object/array — e.g. `config: "{\"name\":...}"` instead of
3375
+ * `config: {name:...}`, or `operations: "[...]"` instead of `operations: [...]`.
3376
+ * Seen across every app profiled in the 2026-07-25 reliability sweep
3377
+ * (brain's `update-source` config, analytics' `mutate-dashboard`/
3378
+ * `update-dashboard` operations/ops/config) and the model does NOT
3379
+ * self-correct across repeated identical retries — it burns the full
3380
+ * `MAX_IDENTICAL_TOOL_ERRORS` retry budget on the same wrong shape every
3381
+ * time. Evidence-gated exactly like `normalizeOptionalToolPlaceholders`:
3382
+ * only coerce a field whose CURRENT value fails schema validation (so a
3383
+ * legitimate string value is never touched — a field schema-valid as a
3384
+ * string is never also schema-valid as object/array) and whose parsed form
3385
+ * passes. Never touches values that are already the right shape.
3386
+ */
3387
+ function coerceStringifiedJsonToolValues(
3388
+ schema: RawJsonSchema | undefined,
3389
+ input: unknown,
3390
+ ): { input: unknown; changed: boolean } {
3391
+ if (!schema?.properties || !input || typeof input !== "object") {
3392
+ return { input, changed: false };
3393
+ }
3394
+ if (Array.isArray(input)) return { input, changed: false };
3395
+
3396
+ let normalized: Record<string, unknown> | null = null;
3397
+ for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
3398
+ if (typeof value !== "string" || value.trim().length === 0) continue;
3399
+ const propertySchema = schema.properties[key];
3400
+ if (!propertySchema || typeof propertySchema !== "object") continue;
3401
+ const expectedType = (propertySchema as { type?: unknown }).type;
3402
+ const expectsObjectOrArray =
3403
+ expectedType === "object" ||
3404
+ expectedType === "array" ||
3405
+ (Array.isArray(expectedType) &&
3406
+ (expectedType.includes("object") || expectedType.includes("array")));
3407
+ if (!expectsObjectOrArray) continue;
3408
+ if (schemaAcceptsToolValue(propertySchema, value)) continue;
3409
+ let parsed: unknown;
3410
+ try {
3411
+ parsed = JSON.parse(value);
3412
+ } catch {
3413
+ continue;
3414
+ }
3415
+ if (typeof parsed !== "object" || parsed === null) continue;
3416
+ if (!schemaAcceptsToolValue(propertySchema, parsed)) continue;
3417
+ normalized ??= { ...(input as Record<string, unknown>) };
3418
+ normalized[key] = parsed;
3419
+ }
3420
+ return normalized
3421
+ ? { input: normalized, changed: true }
3422
+ : { input, changed: false };
3423
+ }
3424
+
3372
3425
  function getRawToolInputValidator(schema: RawJsonSchema): ValidateFunction {
3373
3426
  const cached = rawToolInputValidatorCache.get(schema);
3374
3427
  if (cached) return cached;
@@ -4354,6 +4407,17 @@ export async function runAgentLoop(opts: {
4354
4407
  if (placeholderNormalization.changed) {
4355
4408
  toolCall = { ...toolCall, input: placeholderNormalization.input };
4356
4409
  }
4410
+ const jsonStringCoercion = actionEntry
4411
+ ? coerceStringifiedJsonToolValues(
4412
+ actionEntry.tool.parameters,
4413
+ toolCall.input,
4414
+ )
4415
+ : { input: toolCall.input, changed: false };
4416
+ if (jsonStringCoercion.changed) {
4417
+ toolCall = { ...toolCall, input: jsonStringCoercion.input };
4418
+ }
4419
+ const toolInputNormalized =
4420
+ placeholderNormalization.changed || jsonStringCoercion.changed;
4357
4421
  const wireToolInput = JSON.stringify(toolCall.input ?? {});
4358
4422
  const normalizedToolInput = normalizeToolCallInputForHistory(
4359
4423
  toolCall.input,
@@ -4776,7 +4840,7 @@ export async function runAgentLoop(opts: {
4776
4840
  });
4777
4841
 
4778
4842
  const toolCallSchemaError = toolCallErrors.get(toolCall.id);
4779
- if (toolCallSchemaError && !placeholderNormalization.changed) {
4843
+ if (toolCallSchemaError && !toolInputNormalized) {
4780
4844
  const result = finalizeToolErrorResult(
4781
4845
  toolInputSchemaErrorResult(
4782
4846
  toolCall.name,
@@ -124,6 +124,18 @@ async function appendContinuationAndJournal(
124
124
  appendToolCallJournalNote(messages, events);
125
125
  }
126
126
 
127
+ /**
128
+ * Rebuild the same safe continuation context for a logical turn that resumes
129
+ * in a fresh hosted invocation.
130
+ */
131
+ export async function appendDurableContinuationContext(
132
+ messages: EngineMessage[],
133
+ reason: AgentLoopContinuationReason,
134
+ threadId: string,
135
+ ): Promise<void> {
136
+ await appendContinuationAndJournal(messages, reason, threadId);
137
+ }
138
+
127
139
  async function hasCompletedSideEffectToolCallInCurrentTurn(
128
140
  threadId: string | undefined,
129
141
  localEvents: readonly AgentChatEvent[] = [],
@@ -173,6 +173,34 @@ export const UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS = 20_000;
173
173
  */
174
174
  const STALE_RUN_RECOVERY_MAX_TURN_RUNS = 25;
175
175
 
176
+ /**
177
+ * Circuit breaker for a DETERMINISTIC dead-on-arrival loop: some request
178
+ * shapes make the worker hang almost immediately every single time (e.g. an
179
+ * un-timed-out provider fetch that blocks the event loop) rather than merely
180
+ * hitting a transient blip. Because `attemptStaleRunRecovery` replays the
181
+ * SAME captured `dispatch_payload` on every successor (never a fresh
182
+ * request), such a turn was retrying an unwinnable request up to
183
+ * `STALE_RUN_RECOVERY_MAX_TURN_RUNS` (25) times — ~25 * 53s ≈ 22 minutes,
184
+ * each cycle re-billing the full input context — before finally giving up.
185
+ * Confirmed live in prod (assets: one turn cycled 24x, each attempt an
186
+ * identical ~32K-token request that made a token of real progress around
187
+ * ~8s in then went completely silent for the rest of its life until the 45s
188
+ * reap). Stop recovering after this many CONSECUTIVE stale_run reaps that
189
+ * each made near-zero real progress — a single blip never trips it (needs
190
+ * 3 in a row), and a run that's genuinely grinding through long work right
191
+ * up to its heartbeat window is untouched (see `hasNoForwardProgress`).
192
+ */
193
+ const STALE_RUN_RECOVERY_CONSECUTIVE_NO_PROGRESS_LIMIT = 3;
194
+
195
+ /**
196
+ * A reaped run counts as having made no forward progress if it never
197
+ * emitted a real event (`last_progress_at` unset) or died within this many
198
+ * ms of starting — well short of the 45s background reap window, so a run
199
+ * that was legitimately working almost up to the reap boundary is not
200
+ * penalized. See `STALE_RUN_RECOVERY_CONSECUTIVE_NO_PROGRESS_LIMIT`.
201
+ */
202
+ const STALE_RUN_RECOVERY_NO_PROGRESS_WINDOW_MS = 20_000;
203
+
176
204
  /**
177
205
  * Maximum time the stale reapers (`reapIfStale`, `reapAllStaleRuns`,
178
206
  * `cleanupOldRuns`'s heartbeat-stale pass) will suspend reaping a "running"
@@ -1361,7 +1389,8 @@ type StaleRunRecoveryOutcome =
1361
1389
  | { outcome: "not_background" }
1362
1390
  | { outcome: "payload_missing" }
1363
1391
  | { outcome: "newer_run_exists" }
1364
- | { outcome: "budget_exhausted" };
1392
+ | { outcome: "budget_exhausted" }
1393
+ | { outcome: "repeated_no_progress" };
1365
1394
 
1366
1395
  /**
1367
1396
  * Mirrors `production-agent.ts`'s `generateRunId` — duplicated (not
@@ -1477,6 +1506,37 @@ async function attemptStaleRunRecovery(
1477
1506
  return { outcome: "budget_exhausted" };
1478
1507
  }
1479
1508
 
1509
+ // See `STALE_RUN_RECOVERY_CONSECUTIVE_NO_PROGRESS_LIMIT`: a run whose last
1510
+ // N attempts (including the one just reaped, already written by the
1511
+ // caller's UPDATE earlier in this same transaction) all died as stale_run
1512
+ // having made essentially no real progress is retrying an unwinnable
1513
+ // request, not recovering from a blip. Stop far short of the 25-run/~22min
1514
+ // budget above instead of grinding through it.
1515
+ const { rows: recentRows } = await db.execute({
1516
+ sql: `SELECT error_code, started_at, last_progress_at FROM agent_runs WHERE turn_id = ? ORDER BY started_at DESC LIMIT ?`,
1517
+ args: [turnId, STALE_RUN_RECOVERY_CONSECUTIVE_NO_PROGRESS_LIMIT],
1518
+ });
1519
+ const recent = (recentRows ?? []) as Array<{
1520
+ error_code?: string | null;
1521
+ started_at: number | string;
1522
+ last_progress_at: number | string | null;
1523
+ }>;
1524
+ const allDeadOnArrival =
1525
+ recent.length === STALE_RUN_RECOVERY_CONSECUTIVE_NO_PROGRESS_LIMIT &&
1526
+ recent.every((r) => {
1527
+ if (r.error_code !== STALE_RUN_ERROR_EVENT.errorCode) return false;
1528
+ const started = Number(r.started_at) || 0;
1529
+ const progress =
1530
+ r.last_progress_at == null ? null : Number(r.last_progress_at);
1531
+ return (
1532
+ progress === null ||
1533
+ progress - started < STALE_RUN_RECOVERY_NO_PROGRESS_WINDOW_MS
1534
+ );
1535
+ });
1536
+ if (allDeadOnArrival) {
1537
+ return { outcome: "repeated_no_progress" };
1538
+ }
1539
+
1480
1540
  const successorRunId = generateRecoveryRunId();
1481
1541
  const now = Date.now();
1482
1542
  await db.execute({
@@ -19,12 +19,30 @@ import {
19
19
  claimA2AContinuationDelivery,
20
20
  claimDueA2AContinuations,
21
21
  completeA2AContinuation,
22
+ failA2AContinuationsForIntegrationTask,
22
23
  failA2AContinuation,
23
24
  getA2AContinuation,
25
+ hasActiveA2AContinuationsForIntegrationTask,
26
+ listRecoverableA2AIntegrationTasks,
27
+ recoverDueA2AContinuationIds,
24
28
  rescheduleA2AContinuation,
25
29
  type A2AContinuation,
30
+ type RecoverableA2AIntegrationTask,
26
31
  } from "./a2a-continuations-store.js";
32
+ import {
33
+ completeIntegrationCampaignTaskAfterA2A,
34
+ failDisabledIntegrationCampaignTask,
35
+ getIntegrationCampaignForTask,
36
+ } from "./integration-campaigns-store.js";
37
+ import {
38
+ dispatchPendingIntegrationTask,
39
+ isIntegrationDurableDispatchEnabledForTask,
40
+ } from "./integration-durable-dispatch.js";
27
41
  import { signInternalToken } from "./internal-token.js";
42
+ import {
43
+ getNextPendingTaskForThread,
44
+ getPendingTask,
45
+ } from "./pending-tasks-store.js";
28
46
  import { getThreadMapping } from "./thread-mapping-store.js";
29
47
  import type {
30
48
  OutgoingMessage,
@@ -150,10 +168,62 @@ export async function processDueA2AContinuations(options: {
150
168
  }
151
169
  }
152
170
 
171
+ /**
172
+ * Durable scheduler wake-up only: make a bounded set of due/stale rows
173
+ * eligible, then invoke their normal processors. It never polls remote A2A
174
+ * tasks or runs a mutation itself, keeping the scheduled route within its
175
+ * short execution budget. Duplicate wake-ups are safe because each processor
176
+ * still takes the store's atomic claim before it can progress or deliver.
177
+ */
178
+ export async function recoverDueA2AContinuations(options?: {
179
+ limit?: number;
180
+ webhookBaseUrl?: string;
181
+ }): Promise<{ dispatched: number; failed: number }> {
182
+ const limit = options?.limit ?? 5;
183
+ const candidateTasks = await listRecoverableA2AIntegrationTasks(200);
184
+ const eligibleTaskIds: string[] = [];
185
+ for (const task of candidateTasks) {
186
+ const enabled = isIntegrationDurableDispatchEnabledForTask({
187
+ platform: task.platform,
188
+ externalThreadId: task.externalThreadId,
189
+ platformContext: task.dispatchScope
190
+ ? { channelId: task.dispatchScope }
191
+ : undefined,
192
+ });
193
+ if (enabled) {
194
+ eligibleTaskIds.push(task.id);
195
+ if (eligibleTaskIds.length >= limit) break;
196
+ } else {
197
+ await failDisabledDurableA2ATask(task);
198
+ }
199
+ }
200
+ const ids = await recoverDueA2AContinuationIds(limit, eligibleTaskIds);
201
+ let dispatched = 0;
202
+ let failed = 0;
203
+
204
+ await Promise.all(
205
+ ids.map(async (id) => {
206
+ try {
207
+ await dispatchA2AContinuation(id, options?.webhookBaseUrl);
208
+ dispatched += 1;
209
+ } catch (err) {
210
+ failed += 1;
211
+ console.error(
212
+ `[integrations] Failed to recover A2A continuation ${id}:`,
213
+ err,
214
+ );
215
+ }
216
+ }),
217
+ );
218
+
219
+ return { dispatched, failed };
220
+ }
221
+
153
222
  async function processClaimedContinuation(
154
223
  continuation: A2AContinuation,
155
224
  options: { adapters: Map<string, PlatformAdapter> },
156
225
  ): Promise<void> {
226
+ if (!(await durableContinuationScopeStillEnabled(continuation))) return;
157
227
  const adapter = options.adapters.get(continuation.platform);
158
228
  if (!adapter) {
159
229
  await failA2AContinuation(
@@ -300,6 +370,66 @@ async function processClaimedContinuation(
300
370
  );
301
371
  }
302
372
 
373
+ async function durableContinuationScopeStillEnabled(
374
+ continuation: A2AContinuation,
375
+ ): Promise<boolean> {
376
+ const campaign = await getIntegrationCampaignForTask(
377
+ continuation.integrationTaskId,
378
+ );
379
+ if (!campaign) return true;
380
+ if (campaign.status === "completed" || campaign.status === "failed") {
381
+ await failA2AContinuation(
382
+ continuation.id,
383
+ "Owning integration campaign is already terminal",
384
+ );
385
+ return false;
386
+ }
387
+ const task = await getPendingTask(continuation.integrationTaskId);
388
+ const enabled =
389
+ task?.status === "processing" &&
390
+ isIntegrationDurableDispatchEnabledForTask({
391
+ platform: task.platform,
392
+ externalThreadId: task.externalThreadId,
393
+ platformContext: task.dispatchScope
394
+ ? { channelId: task.dispatchScope }
395
+ : undefined,
396
+ });
397
+ if (enabled) return true;
398
+
399
+ await failDisabledDurableA2ATask({
400
+ id: continuation.integrationTaskId,
401
+ platform: task?.platform ?? continuation.platform,
402
+ externalThreadId: task?.externalThreadId ?? continuation.externalThreadId,
403
+ dispatchScope: task?.dispatchScope ?? null,
404
+ status: task?.status ?? "missing",
405
+ });
406
+ return false;
407
+ }
408
+
409
+ async function failDisabledDurableA2ATask(
410
+ task: RecoverableA2AIntegrationTask,
411
+ ): Promise<void> {
412
+ const message = "Durable integration campaign was disabled for this scope";
413
+ await failA2AContinuationsForIntegrationTask(task.id, message);
414
+ await failDisabledIntegrationCampaignTask(task.id, message);
415
+ const nextTask = await getNextPendingTaskForThread(
416
+ task.platform,
417
+ task.externalThreadId,
418
+ );
419
+ if (nextTask) {
420
+ await dispatchPendingIntegrationTask({
421
+ taskId: nextTask.id,
422
+ task: {
423
+ platform: task.platform,
424
+ externalThreadId: task.externalThreadId,
425
+ platformContext: nextTask.dispatchScope
426
+ ? { channelId: nextTask.dispatchScope }
427
+ : undefined,
428
+ },
429
+ });
430
+ }
431
+ }
432
+
303
433
  async function resumeA2AContinuationProgress(
304
434
  continuation: A2AContinuation,
305
435
  adapter: PlatformAdapter,
@@ -372,6 +502,7 @@ async function notifyAndFailA2AContinuation(
372
502
  reason: string,
373
503
  progress: PlatformRunProgress | null = null,
374
504
  ): Promise<void> {
505
+ if (!(await durableContinuationScopeStillEnabled(continuation))) return;
375
506
  const deliveryContinuation = await claimA2AContinuationDelivery(
376
507
  continuation.id,
377
508
  );
@@ -399,9 +530,17 @@ async function notifyAndFailA2AContinuation(
399
530
  `[integrations] Failed to notify ${deliveryContinuation.platform} about failed A2A continuation ${deliveryContinuation.id}:`,
400
531
  err,
401
532
  );
533
+ if (deliveryContinuation.attempts >= MAX_ATTEMPTS) {
534
+ await failA2AContinuation(deliveryContinuation.id, reason);
535
+ await completeParentCampaignAfterTerminalA2A(deliveryContinuation);
536
+ return;
537
+ }
538
+ await rescheduleAndRedispatchA2AContinuation(deliveryContinuation.id);
539
+ return;
402
540
  }
403
541
 
404
542
  await failA2AContinuation(deliveryContinuation.id, reason);
543
+ await completeParentCampaignAfterTerminalA2A(deliveryContinuation);
405
544
  }
406
545
 
407
546
  async function deliverAndCompleteA2AContinuation(
@@ -410,6 +549,7 @@ async function deliverAndCompleteA2AContinuation(
410
549
  text: string,
411
550
  progress: PlatformRunProgress | null = null,
412
551
  ): Promise<void> {
552
+ if (!(await durableContinuationScopeStillEnabled(continuation))) return;
413
553
  const deliveryContinuation = await claimA2AContinuationDelivery(
414
554
  continuation.id,
415
555
  );
@@ -589,6 +729,7 @@ async function completeAfterSuccessfulDelivery(
589
729
  for (let attempt = 0; attempt < COMPLETE_AFTER_DELIVERY_ATTEMPTS; attempt++) {
590
730
  try {
591
731
  await completeA2AContinuation(continuation.id);
732
+ await completeParentCampaignAfterTerminalA2A(continuation);
592
733
  return;
593
734
  } catch (err) {
594
735
  lastError = err;
@@ -602,6 +743,47 @@ async function completeAfterSuccessfulDelivery(
602
743
  );
603
744
  }
604
745
 
746
+ async function completeParentCampaignAfterTerminalA2A(
747
+ continuation: A2AContinuation,
748
+ ): Promise<void> {
749
+ const campaign = await getIntegrationCampaignForTask(
750
+ continuation.integrationTaskId,
751
+ );
752
+ if (!campaign) return;
753
+ if (
754
+ await hasActiveA2AContinuationsForIntegrationTask(
755
+ continuation.integrationTaskId,
756
+ )
757
+ ) {
758
+ return;
759
+ }
760
+ const completed = await completeIntegrationCampaignTaskAfterA2A(
761
+ continuation.integrationTaskId,
762
+ );
763
+ if (!completed) return;
764
+
765
+ const nextTask = await getNextPendingTaskForThread(
766
+ continuation.platform,
767
+ continuation.externalThreadId,
768
+ );
769
+ if (!nextTask) return;
770
+ await dispatchPendingIntegrationTask({
771
+ taskId: nextTask.id,
772
+ task: {
773
+ platform: continuation.platform,
774
+ externalThreadId: continuation.externalThreadId,
775
+ platformContext: nextTask.dispatchScope
776
+ ? { channelId: nextTask.dispatchScope }
777
+ : undefined,
778
+ },
779
+ }).catch((err) => {
780
+ console.error(
781
+ `[integrations] Failed to wake successor ${nextTask.id} after A2A parent completion:`,
782
+ err,
783
+ );
784
+ });
785
+ }
786
+
605
787
  function formatContinuationFailureMessage(
606
788
  continuation: A2AContinuation,
607
789
  reason: string,