@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.
- package/README.md +1 -13
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +65 -1
- package/corpus/core/src/agent/run-loop-with-resume.ts +12 -0
- package/corpus/core/src/agent/run-store.ts +61 -1
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +182 -0
- package/corpus/core/src/integrations/a2a-continuations-store.ts +122 -12
- package/corpus/core/src/integrations/integration-campaign-recovery.ts +127 -0
- package/corpus/core/src/integrations/integration-campaigns-store.ts +1039 -0
- package/corpus/core/src/integrations/integration-durable-dispatch.ts +28 -3
- package/corpus/core/src/integrations/pending-tasks-retry-job.ts +8 -0
- package/corpus/core/src/integrations/plugin.ts +453 -35
- package/corpus/core/src/integrations/webhook-handler.ts +469 -11
- package/corpus/core/src/server/builder-design-systems.ts +1 -1
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +15 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +3 -3
- package/corpus/templates/analytics/changelog/2026-07-25-analytics-dashboards-and-scheduled-reports-are-faster-and-mo.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +12 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +8 -4
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +136 -5
- package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +250 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +167 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +34 -9
- package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +105 -23
- package/corpus/templates/analytics/server/lib/first-party-unbounded-panel-repair.ts +169 -0
- package/corpus/templates/analytics/server/plugins/db.ts +41 -1
- package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +6 -3
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +7 -3
- package/corpus/templates/content/app/components/editor/body-hydration.ts +10 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -0
- package/corpus/templates/content/app/root.tsx +12 -1
- package/corpus/templates/content/changelog/2026-07-23-new-pages-open-immediately-while-slow-network-work-continues.md +6 -0
- package/corpus/templates/content/changelog/2026-07-25-database-preview-actions-open-reliably.md +6 -0
- package/corpus/templates/content/e2e/playwright.config.ts +1 -1
- package/corpus/templates/design/actions/edit-design.ts +66 -27
- package/corpus/templates/design/changelog/2026-07-25-design-edits-retry-concurrent-changes.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +5 -1
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +6 -4
- package/corpus/templates/forms/actions/create-form.ts +5 -1
- package/corpus/templates/forms/actions/export-responses.ts +22 -11
- package/corpus/templates/forms/actions/update-form.ts +5 -1
- package/corpus/templates/forms/changelog/2026-07-25-fields-can-be-created-without-ids.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-25-response-exports-use-file-storage.md +6 -0
- package/corpus/templates/forms/server/lib/validate-fields.ts +43 -0
- package/corpus/templates/slides/actions/extract-pdf.ts +4 -4
- package/corpus/templates/slides/actions/import-file.ts +3 -4
- package/corpus/templates/slides/changelog/2026-07-25-gemini-image-models-updated.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-25-pdf-imports-tolerate-missing-canvas.md +6 -0
- package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +5 -2
- package/corpus/templates/slides/server/lib/pdf-parse-setup.ts +150 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +63 -1
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +7 -1
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +7 -0
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +49 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/integrations/a2a-continuation-processor.d.ts +14 -0
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +134 -1
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts +23 -0
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +99 -13
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/integration-campaign-recovery.d.ts +16 -0
- package/dist/integrations/integration-campaign-recovery.d.ts.map +1 -0
- package/dist/integrations/integration-campaign-recovery.js +94 -0
- package/dist/integrations/integration-campaign-recovery.js.map +1 -0
- package/dist/integrations/integration-campaigns-store.d.ts +121 -0
- package/dist/integrations/integration-campaigns-store.d.ts.map +1 -0
- package/dist/integrations/integration-campaigns-store.js +784 -0
- package/dist/integrations/integration-campaigns-store.js.map +1 -0
- package/dist/integrations/integration-durable-dispatch.d.ts +3 -0
- package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -1
- package/dist/integrations/integration-durable-dispatch.js +22 -3
- package/dist/integrations/integration-durable-dispatch.js.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.js +6 -0
- package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +325 -18
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts +16 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +368 -13
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/builder-design-systems.js +1 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- package/package.json +3 -3
- package/src/agent/production-agent.ts +65 -1
- package/src/agent/run-loop-with-resume.ts +12 -0
- package/src/agent/run-store.ts +61 -1
- package/src/integrations/a2a-continuation-processor.ts +182 -0
- package/src/integrations/a2a-continuations-store.ts +122 -12
- package/src/integrations/integration-campaign-recovery.ts +127 -0
- package/src/integrations/integration-campaigns-store.ts +1039 -0
- package/src/integrations/integration-durable-dispatch.ts +28 -3
- package/src/integrations/pending-tasks-retry-job.ts +8 -0
- package/src/integrations/plugin.ts +453 -35
- package/src/integrations/webhook-handler.ts +469 -11
- 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
|
|
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
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -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
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.122.
|
|
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 && !
|
|
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,
|