@agent-native/core 0.210.1-nightly-20261020093350 → 0.210.1
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/dist/a2a/server.js +2 -1
- package/dist/agent/contain-principal.js +2 -3
- package/dist/agent/engine/ai-sdk-engine.js +29 -20
- package/dist/agent/engine/anthropic-engine.js +10 -1
- package/dist/agent/engine/builder-engine.js +28 -14
- package/dist/agent/engine/types.d.ts +11 -0
- package/dist/agent/engine/types.js +8 -0
- package/dist/agent/execution-fence.d.ts +5 -0
- package/dist/agent/execution-fence.js +15 -0
- package/dist/agent/harness/background.js +28 -7
- package/dist/agent/production-agent.d.ts +4 -3
- package/dist/agent/production-agent.js +20 -3
- package/dist/agent/run-manager.d.ts +9 -2
- package/dist/agent/run-manager.js +290 -43
- package/dist/agent/run-store.d.ts +3 -2
- package/dist/agent/run-store.fencing.worker.js +19 -1
- package/dist/agent/run-store.js +228 -68
- package/dist/chat-threads/schema-migrations.js +8 -0
- package/dist/chat-threads/store.d.ts +33 -0
- package/dist/chat-threads/store.js +151 -16
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/schema-snapshot.generated.js +2 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/localization/core-messages/supplemental/ar-SA.js +1 -0
- package/dist/localization/core-messages/supplemental/de-DE.js +1 -0
- package/dist/localization/core-messages/supplemental/en-US.js +1 -0
- package/dist/localization/core-messages/supplemental/es-ES.js +1 -0
- package/dist/localization/core-messages/supplemental/fr-FR.js +1 -0
- package/dist/localization/core-messages/supplemental/hi-IN.js +1 -0
- package/dist/localization/core-messages/supplemental/ja-JP.js +1 -0
- package/dist/localization/core-messages/supplemental/ko-KR.js +1 -0
- package/dist/localization/core-messages/supplemental/pt-BR.js +1 -0
- package/dist/localization/core-messages/supplemental/zh-CN.js +1 -0
- package/dist/localization/core-messages/supplemental/zh-TW.js +1 -0
- package/dist/localization/default-messages.d.ts +14 -0
- package/dist/localization/default-messages.js +15 -1
- package/dist/localization/mcp-settings-messages.d.ts +1 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/mcp-client/actions/get-managed-mcp-oauth-readiness.d.ts +5 -0
- package/dist/mcp-client/actions/get-managed-mcp-oauth-readiness.js +17 -0
- package/dist/mcp-client/oauth-routes.d.ts +3 -0
- package/dist/mcp-client/oauth-routes.js +15 -4
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/org/index.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
- package/dist/provider-api/actions/provider-api.d.ts +10 -10
- package/dist/secrets/register-framework-secrets.js +25 -0
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/action-discovery.js +6 -0
- package/dist/server/agent-chat-plugin.js +100 -16
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/agent-teams.d.ts +3 -0
- package/dist/server/agent-teams.js +192 -30
- package/dist/server/google-oauth.d.ts +4 -1
- package/dist/server/google-oauth.js +4 -2
- package/package.json +4 -4
package/dist/agent/run-store.js
CHANGED
|
@@ -14,7 +14,7 @@ import { approvalKeyForHistory } from "./approval-key.js";
|
|
|
14
14
|
import { admitAutoContinue, admitManualContinue, } from "./auto-continue.js";
|
|
15
15
|
import { AGENT_CHAT_RECOVERY_OF_RUN_FIELD } from "./durable-background.js";
|
|
16
16
|
import { LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./engine/credential-errors.js";
|
|
17
|
-
import { currentExecutionFence, ExecutionFenceError, MissingExecutionFenceError, lockAndAssertCurrentExecutionFences, StaleExecutionFenceError, withFencedExecutionWrite, } from "./execution-fence.js";
|
|
17
|
+
import { currentExecutionFence, currentExecutionFenceForScope, ExecutionFenceError, MissingExecutionFenceError, lockAndAssertCurrentExecutionFences, StaleExecutionFenceError, withFencedExecutionWrite, } from "./execution-fence.js";
|
|
18
18
|
import { AGENT_RUN_MIGRATIONS, AGENT_RUN_MIGRATIONS_TABLE, } from "./run-migrations.js";
|
|
19
19
|
import { isConsistentToolCallInputFingerprint, isUnavailableToolCallInput, toolCallInputFingerprint, } from "./tool-call-journal.js";
|
|
20
20
|
import { CONTINUATION_REASONS, isContinuationTerminalReason } from "./types.js";
|
|
@@ -2228,32 +2228,51 @@ export async function reapIfStale(runId, maxStaleMs) {
|
|
|
2228
2228
|
export async function reapUnclaimedBackgroundRun(runId) {
|
|
2229
2229
|
await ensureRunTables();
|
|
2230
2230
|
const client = getDbExec();
|
|
2231
|
+
if (!client.transaction) {
|
|
2232
|
+
throw new Error("Atomic unclaimed-background reaping requires transaction support");
|
|
2233
|
+
}
|
|
2231
2234
|
const completedAt = Date.now();
|
|
2232
2235
|
const cutoff = completedAt - UNCLAIMED_BACKGROUND_RUN_GRACE_MS;
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2236
|
+
const reaped = await client.transaction(async (tx) => {
|
|
2237
|
+
const { rowsAffected } = await tx.execute({
|
|
2238
|
+
sql: `UPDATE agent_runs
|
|
2239
|
+
SET status = 'errored',
|
|
2240
|
+
completed_at = ?,
|
|
2241
|
+
error_code = ?,
|
|
2242
|
+
error_detail = ?,
|
|
2243
|
+
terminal_reason = ?
|
|
2244
|
+
WHERE id = ?
|
|
2245
|
+
AND status = 'running'
|
|
2246
|
+
AND dispatch_mode = 'background'
|
|
2247
|
+
AND COALESCE(heartbeat_at, started_at) < ?`,
|
|
2248
|
+
args: [
|
|
2249
|
+
completedAt,
|
|
2250
|
+
UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT.errorCode,
|
|
2251
|
+
UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT.details,
|
|
2252
|
+
UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT.errorCode,
|
|
2253
|
+
runId,
|
|
2254
|
+
cutoff,
|
|
2255
|
+
],
|
|
2256
|
+
});
|
|
2257
|
+
const updated = (rowsAffected ?? 0) > 0;
|
|
2258
|
+
if (updated) {
|
|
2259
|
+
await appendTerminalRunEventInTransaction(tx, runId, UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT);
|
|
2260
|
+
}
|
|
2261
|
+
return updated;
|
|
2252
2262
|
});
|
|
2253
|
-
const reaped = (rowsAffected ?? 0) > 0;
|
|
2254
2263
|
if (reaped) {
|
|
2255
|
-
|
|
2256
|
-
|
|
2264
|
+
try {
|
|
2265
|
+
await recordRunDiagnostic(runId, RUN_DIAG_STAGE.workerThrew, "unclaimed background dispatch reaped (worker never claimed the run)");
|
|
2266
|
+
}
|
|
2267
|
+
catch (error) {
|
|
2268
|
+
captureError(error, {
|
|
2269
|
+
tags: {
|
|
2270
|
+
component: "agent-run-store",
|
|
2271
|
+
operation: "record-unclaimed-background-reap-diagnostic",
|
|
2272
|
+
},
|
|
2273
|
+
extra: { runId },
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2257
2276
|
}
|
|
2258
2277
|
return reaped;
|
|
2259
2278
|
}
|
|
@@ -2478,7 +2497,7 @@ export function terminalEventForAbortReason(reason) {
|
|
|
2478
2497
|
};
|
|
2479
2498
|
}
|
|
2480
2499
|
export async function markRunAborted(runId, reason) {
|
|
2481
|
-
|
|
2500
|
+
return updateRunAborted(runId, reason, false);
|
|
2482
2501
|
}
|
|
2483
2502
|
/** Abort only when no model or tool work has crossed its dispatch boundary. */
|
|
2484
2503
|
export async function markRunAbortedAtSafeBoundary(runId, reason) {
|
|
@@ -2486,22 +2505,32 @@ export async function markRunAbortedAtSafeBoundary(runId, reason) {
|
|
|
2486
2505
|
}
|
|
2487
2506
|
async function updateRunAborted(runId, reason, requireSafeBoundary) {
|
|
2488
2507
|
await ensureRunTables();
|
|
2508
|
+
const fence = currentExecutionFenceForScope({ runId });
|
|
2489
2509
|
const client = getDbExec();
|
|
2490
2510
|
if (!client.transaction) {
|
|
2491
2511
|
throw new Error("Atomic run cancellation requires transaction support");
|
|
2492
2512
|
}
|
|
2493
|
-
const
|
|
2513
|
+
const abortResult = await client.transaction(async (tx) => {
|
|
2494
2514
|
await lockExecutionLeasesForRuns(tx, [runId]);
|
|
2515
|
+
await lockAndAssertCurrentExecutionFences(tx);
|
|
2516
|
+
const runResult = await tx.execute({
|
|
2517
|
+
sql: `SELECT thread_id, COALESCE(turn_id, id) AS turn_id,
|
|
2518
|
+
worker_generation
|
|
2519
|
+
FROM agent_runs WHERE id = ? FOR UPDATE`,
|
|
2520
|
+
args: [runId],
|
|
2521
|
+
});
|
|
2522
|
+
const lockedRun = runResult.rows[0];
|
|
2523
|
+
if (fence &&
|
|
2524
|
+
(lockedRun?.thread_id !== fence.threadId ||
|
|
2525
|
+
lockedRun.turn_id !== fence.turnId ||
|
|
2526
|
+
lockedRun.worker_generation == null ||
|
|
2527
|
+
String(lockedRun.worker_generation) !== fence.generation)) {
|
|
2528
|
+
throw new StaleExecutionFenceError(fence);
|
|
2529
|
+
}
|
|
2495
2530
|
if (requireSafeBoundary) {
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
args: [runId],
|
|
2500
|
-
});
|
|
2501
|
-
const runRef = run.rows[0];
|
|
2502
|
-
if (runRef) {
|
|
2503
|
-
if (typeof runRef.thread_id !== "string" ||
|
|
2504
|
-
typeof runRef.turn_id !== "string") {
|
|
2531
|
+
if (lockedRun) {
|
|
2532
|
+
if (typeof lockedRun.thread_id !== "string" ||
|
|
2533
|
+
typeof lockedRun.turn_id !== "string") {
|
|
2505
2534
|
throw new Error(`Run ${runId} has an invalid thread or turn ID`);
|
|
2506
2535
|
}
|
|
2507
2536
|
const dispatchedStep = await tx.execute({
|
|
@@ -2509,10 +2538,11 @@ async function updateRunAborted(runId, reason, requireSafeBoundary) {
|
|
|
2509
2538
|
WHERE thread_id = ? AND turn_id = ? AND status = 'dispatched'
|
|
2510
2539
|
AND run_id = ?
|
|
2511
2540
|
LIMIT 1`,
|
|
2512
|
-
args: [
|
|
2541
|
+
args: [lockedRun.thread_id, lockedRun.turn_id, runId],
|
|
2513
2542
|
});
|
|
2514
|
-
if (dispatchedStep.rows.length > 0)
|
|
2515
|
-
return 0;
|
|
2543
|
+
if (dispatchedStep.rows.length > 0) {
|
|
2544
|
+
return { rowsAffected: 0, appendTerminalEvent: false };
|
|
2545
|
+
}
|
|
2516
2546
|
}
|
|
2517
2547
|
}
|
|
2518
2548
|
const result = await tx.execute({
|
|
@@ -2524,15 +2554,32 @@ async function updateRunAborted(runId, reason, requireSafeBoundary) {
|
|
|
2524
2554
|
runId,
|
|
2525
2555
|
],
|
|
2526
2556
|
});
|
|
2527
|
-
|
|
2557
|
+
const rowsAffected = result.rowsAffected ?? 0;
|
|
2558
|
+
if (rowsAffected > 0) {
|
|
2528
2559
|
await revokeExecutionLeasesForRuns(tx, [runId]);
|
|
2560
|
+
return { rowsAffected, appendTerminalEvent: true };
|
|
2529
2561
|
}
|
|
2530
|
-
|
|
2562
|
+
const terminalState = await tx.execute({
|
|
2563
|
+
sql: `SELECT status, terminal_reason, in_flight_since FROM agent_runs WHERE id = ?`,
|
|
2564
|
+
args: [runId],
|
|
2565
|
+
});
|
|
2566
|
+
const terminalRun = terminalState.rows[0];
|
|
2567
|
+
return {
|
|
2568
|
+
rowsAffected,
|
|
2569
|
+
appendTerminalEvent: terminalRun?.status === "aborted" &&
|
|
2570
|
+
terminalRun.terminal_reason === `aborted:${reason ?? "user"}` &&
|
|
2571
|
+
(!requireSafeBoundary || terminalRun.in_flight_since == null),
|
|
2572
|
+
};
|
|
2531
2573
|
});
|
|
2532
|
-
if (
|
|
2533
|
-
|
|
2574
|
+
if (abortResult.appendTerminalEvent) {
|
|
2575
|
+
try {
|
|
2576
|
+
await safeAppendTerminalRunEvent(runId, terminalEventForAbortReason(reason), "mark-aborted");
|
|
2577
|
+
}
|
|
2578
|
+
catch (error) {
|
|
2579
|
+
throw new AgentRunEventNotPersistedError(runId, null, true, error);
|
|
2580
|
+
}
|
|
2534
2581
|
}
|
|
2535
|
-
return
|
|
2582
|
+
return abortResult.rowsAffected > 0 || abortResult.appendTerminalEvent;
|
|
2536
2583
|
}
|
|
2537
2584
|
async function lockExecutionLeasesForRuns(db, runIds) {
|
|
2538
2585
|
const ids = [...new Set(runIds)].sort();
|
|
@@ -2596,6 +2643,10 @@ function turnAbortMarkerRunId(threadId, turnId) {
|
|
|
2596
2643
|
return `turn-abort:${encodeURIComponent(threadId)}:${encodeURIComponent(turnId)}`;
|
|
2597
2644
|
}
|
|
2598
2645
|
export async function markTurnAborted(threadId, turnId, reason = "user") {
|
|
2646
|
+
const fence = currentExecutionFence();
|
|
2647
|
+
if (fence && (fence.threadId !== threadId || fence.turnId !== turnId)) {
|
|
2648
|
+
throw new StaleExecutionFenceError(fence);
|
|
2649
|
+
}
|
|
2599
2650
|
await ensureRunTables();
|
|
2600
2651
|
const now = Date.now();
|
|
2601
2652
|
const client = getDbExec();
|
|
@@ -2608,7 +2659,15 @@ export async function markTurnAborted(threadId, turnId, reason = "user") {
|
|
|
2608
2659
|
sql: "SELECT pg_advisory_xact_lock(hashtextextended(?, 0::bigint))",
|
|
2609
2660
|
args: [`agent-native:run-slot:${threadId}`],
|
|
2610
2661
|
});
|
|
2611
|
-
await
|
|
2662
|
+
await lockAndAssertCurrentExecutionFences(tx);
|
|
2663
|
+
const lease = await lockTurnExecutionLease(tx, threadId, turnId);
|
|
2664
|
+
if (fence &&
|
|
2665
|
+
(!lease ||
|
|
2666
|
+
!lease.isLive ||
|
|
2667
|
+
lease.ownerRunId !== fence.runId ||
|
|
2668
|
+
lease.generation !== fence.generation)) {
|
|
2669
|
+
throw new StaleExecutionFenceError(fence);
|
|
2670
|
+
}
|
|
2612
2671
|
const running = await tx.execute({
|
|
2613
2672
|
sql: `SELECT id FROM agent_runs WHERE thread_id = ? AND COALESCE(turn_id, id) = ? AND status = 'running' AND dispatch_mode IS DISTINCT FROM 'turn-abort'`,
|
|
2614
2673
|
args: [threadId, turnId],
|
|
@@ -2629,7 +2688,7 @@ export async function markTurnAborted(threadId, turnId, reason = "user") {
|
|
|
2629
2688
|
}
|
|
2630
2689
|
if (runIds.length === 0) {
|
|
2631
2690
|
const existing = await tx.execute({
|
|
2632
|
-
sql: `SELECT id, terminal_reason,
|
|
2691
|
+
sql: `SELECT id, status, terminal_reason,
|
|
2633
2692
|
EXISTS (
|
|
2634
2693
|
SELECT 1 FROM agent_run_events
|
|
2635
2694
|
WHERE agent_run_events.run_id = agent_runs.id
|
|
@@ -2650,6 +2709,17 @@ export async function markTurnAborted(threadId, turnId, reason = "user") {
|
|
|
2650
2709
|
],
|
|
2651
2710
|
});
|
|
2652
2711
|
const latest = existing.rows[0];
|
|
2712
|
+
const aborted = await tx.execute({
|
|
2713
|
+
sql: `SELECT id FROM agent_runs
|
|
2714
|
+
WHERE thread_id = ? AND COALESCE(turn_id, id) = ?
|
|
2715
|
+
AND status = 'aborted'
|
|
2716
|
+
AND dispatch_mode IS DISTINCT FROM 'turn-abort'
|
|
2717
|
+
AND terminal_reason = ?`,
|
|
2718
|
+
args: [threadId, turnId, `aborted:${reason}`],
|
|
2719
|
+
});
|
|
2720
|
+
runIds.push(...aborted.rows
|
|
2721
|
+
.map((row) => String(row.id ?? ""))
|
|
2722
|
+
.filter(Boolean));
|
|
2653
2723
|
if (latest &&
|
|
2654
2724
|
latest.continuation_pending !== true &&
|
|
2655
2725
|
!isContinuationTerminalReason(latest.terminal_reason ?? "")) {
|
|
@@ -2681,8 +2751,6 @@ export async function markTurnAborted(threadId, turnId, reason = "user") {
|
|
|
2681
2751
|
});
|
|
2682
2752
|
return "aborted";
|
|
2683
2753
|
});
|
|
2684
|
-
if (outcome === "already_terminal")
|
|
2685
|
-
return outcome;
|
|
2686
2754
|
await Promise.all(runIds.map((runId) => safeAppendTerminalRunEvent(runId, terminalEventForAbortReason(reason), "mark-turn-aborted")));
|
|
2687
2755
|
return outcome;
|
|
2688
2756
|
}
|
|
@@ -2756,40 +2824,100 @@ function persistedRunEventData(eventData, inputSource) {
|
|
|
2756
2824
|
});
|
|
2757
2825
|
}
|
|
2758
2826
|
export class AgentRunEventNotPersistedError extends Error {
|
|
2759
|
-
|
|
2760
|
-
|
|
2827
|
+
terminalRun;
|
|
2828
|
+
constructor(runId, seq, terminalRun = false, cause) {
|
|
2829
|
+
const detail = cause instanceof Error ? `: ${cause.message}` : "";
|
|
2830
|
+
super(seq === null
|
|
2831
|
+
? `Terminal run event was not persisted: ${runId}${detail}`
|
|
2832
|
+
: `Run event was not persisted: ${runId} sequence ${seq}${detail}`);
|
|
2761
2833
|
this.name = "AgentRunEventNotPersistedError";
|
|
2834
|
+
this.terminalRun = terminalRun;
|
|
2835
|
+
if (cause !== undefined) {
|
|
2836
|
+
Object.defineProperty(this, "cause", {
|
|
2837
|
+
value: cause,
|
|
2838
|
+
configurable: true,
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2762
2841
|
}
|
|
2763
2842
|
}
|
|
2764
2843
|
export async function insertRunEvent(runId, seq, eventData, options) {
|
|
2765
2844
|
await ensureRunTables();
|
|
2766
2845
|
const storedEventData = persistedRunEventData(eventData, options?.toolInputSource);
|
|
2846
|
+
const eventAt = options?.eventAt ?? Date.now();
|
|
2767
2847
|
// The reaper must wait for the marker's commit before exposing a successor.
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2848
|
+
let result;
|
|
2849
|
+
try {
|
|
2850
|
+
result = await withFencedExecutionWrite(async (tx) => {
|
|
2851
|
+
const { rowsAffected } = await tx.execute({
|
|
2852
|
+
sql: options?.requireInserted
|
|
2853
|
+
? `WITH running_run AS MATERIALIZED (
|
|
2771
2854
|
SELECT id FROM agent_runs WHERE id = ? AND status = 'running' FOR UPDATE
|
|
2772
2855
|
)
|
|
2773
2856
|
INSERT INTO agent_run_events (run_id, seq, event_at, event_data)
|
|
2774
2857
|
SELECT id, ?, ?, ? FROM running_run LIMIT 1
|
|
2775
2858
|
ON CONFLICT (run_id, seq) DO NOTHING`
|
|
2776
|
-
|
|
2859
|
+
: `INSERT INTO agent_run_events (run_id, seq, event_at, event_data)
|
|
2777
2860
|
SELECT ?, ?, ?, ?
|
|
2778
2861
|
WHERE NOT EXISTS (
|
|
2779
2862
|
SELECT 1 FROM agent_runs
|
|
2780
2863
|
WHERE id = ? AND status <> 'running'
|
|
2781
2864
|
)
|
|
2782
2865
|
ON CONFLICT (run_id, seq) DO NOTHING`,
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2866
|
+
args: [
|
|
2867
|
+
runId,
|
|
2868
|
+
seq,
|
|
2869
|
+
eventAt,
|
|
2870
|
+
storedEventData,
|
|
2871
|
+
...(options?.requireInserted ? [] : [runId]),
|
|
2872
|
+
],
|
|
2873
|
+
});
|
|
2874
|
+
if (!options?.requireInserted || rowsAffected === 1)
|
|
2875
|
+
return {
|
|
2876
|
+
rowsAffected,
|
|
2877
|
+
terminalRun: false,
|
|
2878
|
+
persistedIdentically: rowsAffected === 1,
|
|
2879
|
+
};
|
|
2880
|
+
const existing = await tx.execute({
|
|
2881
|
+
sql: `SELECT event_at, event_data FROM agent_run_events
|
|
2882
|
+
WHERE run_id = ? AND seq = ?`,
|
|
2883
|
+
args: [runId, seq],
|
|
2884
|
+
});
|
|
2885
|
+
const existingRow = existing.rows[0];
|
|
2886
|
+
const persistedIdentically = existingRow !== undefined &&
|
|
2887
|
+
Number(existingRow.event_at) === eventAt &&
|
|
2888
|
+
String(existingRow.event_data) === storedEventData;
|
|
2889
|
+
const run = await tx.execute({
|
|
2890
|
+
sql: "SELECT status FROM agent_runs WHERE id = ?",
|
|
2891
|
+
args: [runId],
|
|
2892
|
+
});
|
|
2893
|
+
const status = run.rows[0]?.status;
|
|
2894
|
+
return {
|
|
2895
|
+
rowsAffected,
|
|
2896
|
+
terminalRun: status !== "running",
|
|
2897
|
+
persistedIdentically,
|
|
2898
|
+
};
|
|
2899
|
+
}, { runId });
|
|
2900
|
+
}
|
|
2901
|
+
catch (error) {
|
|
2902
|
+
if (error instanceof StaleExecutionFenceError) {
|
|
2903
|
+
let status;
|
|
2904
|
+
try {
|
|
2905
|
+
status = await getRunStatus(runId);
|
|
2906
|
+
}
|
|
2907
|
+
catch (statusError) {
|
|
2908
|
+
throw new AggregateError([error, statusError], `Could not confirm run status after a stale execution fence: ${runId}`);
|
|
2909
|
+
}
|
|
2910
|
+
if (status !== null && status !== "running") {
|
|
2911
|
+
throw new AgentRunEventNotPersistedError(runId, seq, true, error);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
throw error;
|
|
2915
|
+
}
|
|
2916
|
+
const { rowsAffected, terminalRun, persistedIdentically } = result;
|
|
2917
|
+
if (options?.requireInserted &&
|
|
2918
|
+
rowsAffected !== 1 &&
|
|
2919
|
+
(!persistedIdentically || terminalRun))
|
|
2920
|
+
throw new AgentRunEventNotPersistedError(runId, seq, terminalRun);
|
|
2793
2921
|
}
|
|
2794
2922
|
// Stays well under PostgreSQL's 65,535 bind-parameter limit at 4 per row.
|
|
2795
2923
|
const RUN_EVENT_BATCH_ROWS = 500;
|
|
@@ -2801,9 +2929,13 @@ export async function insertRunEvents(runId, events) {
|
|
|
2801
2929
|
if (events.length === 0)
|
|
2802
2930
|
return;
|
|
2803
2931
|
await ensureRunTables();
|
|
2804
|
-
|
|
2932
|
+
const writeBatch = async (tx) => {
|
|
2805
2933
|
for (let start = 0; start < events.length; start += RUN_EVENT_BATCH_ROWS) {
|
|
2806
2934
|
const chunk = events.slice(start, start + RUN_EVENT_BATCH_ROWS);
|
|
2935
|
+
const storedChunk = chunk.map((event) => ({
|
|
2936
|
+
...event,
|
|
2937
|
+
eventData: persistedRunEventData(event.eventData),
|
|
2938
|
+
}));
|
|
2807
2939
|
await tx.execute({
|
|
2808
2940
|
sql: `INSERT INTO agent_run_events (run_id, seq, event_at, event_data)
|
|
2809
2941
|
SELECT v.run_id, v.seq, v.event_at, v.event_data
|
|
@@ -2816,17 +2948,41 @@ export async function insertRunEvents(runId, events) {
|
|
|
2816
2948
|
)
|
|
2817
2949
|
ON CONFLICT (run_id, seq) DO NOTHING`,
|
|
2818
2950
|
args: [
|
|
2819
|
-
...
|
|
2951
|
+
...storedChunk.flatMap(({ seq, eventAt, eventData }) => [
|
|
2820
2952
|
runId,
|
|
2821
2953
|
seq,
|
|
2822
2954
|
eventAt,
|
|
2823
|
-
|
|
2955
|
+
eventData,
|
|
2824
2956
|
]),
|
|
2825
2957
|
runId,
|
|
2826
2958
|
],
|
|
2827
2959
|
});
|
|
2960
|
+
const persisted = await tx.execute({
|
|
2961
|
+
sql: `SELECT seq, event_at, event_data
|
|
2962
|
+
FROM agent_run_events
|
|
2963
|
+
WHERE run_id = ? AND seq IN (${chunk.map(() => "?").join(", ")})`,
|
|
2964
|
+
args: [runId, ...chunk.map(({ seq }) => seq)],
|
|
2965
|
+
});
|
|
2966
|
+
const persistedBySeq = new Map(persisted.rows.map((row) => [Number(row.seq), row]));
|
|
2967
|
+
for (const event of storedChunk) {
|
|
2968
|
+
const row = persistedBySeq.get(event.seq);
|
|
2969
|
+
if (!row ||
|
|
2970
|
+
Number(row.event_at) !== event.eventAt ||
|
|
2971
|
+
String(row.event_data) !== event.eventData) {
|
|
2972
|
+
const run = await tx.execute({
|
|
2973
|
+
sql: "SELECT status FROM agent_runs WHERE id = ?",
|
|
2974
|
+
args: [runId],
|
|
2975
|
+
});
|
|
2976
|
+
const status = run.rows[0]
|
|
2977
|
+
?.status;
|
|
2978
|
+
throw new AgentRunEventNotPersistedError(runId, event.seq, status !== "running");
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2828
2981
|
}
|
|
2829
|
-
}
|
|
2982
|
+
};
|
|
2983
|
+
await withFencedExecutionWrite((tx) => currentExecutionFence()
|
|
2984
|
+
? writeBatch(tx)
|
|
2985
|
+
: (tx.transaction?.(writeBatch) ?? writeBatch(tx)), { runId });
|
|
2830
2986
|
}
|
|
2831
2987
|
export const CHECKPOINT_TERMINAL_EVENT_SEQ = 1_000_000_000;
|
|
2832
2988
|
export async function persistRunCheckpointEvent(runId, event, terminalReason) {
|
|
@@ -3670,6 +3826,7 @@ async function safeAppendTerminalRunEvent(runId, event, source) {
|
|
|
3670
3826
|
firstError: firstError instanceof Error ? firstError.message : String(firstError),
|
|
3671
3827
|
},
|
|
3672
3828
|
});
|
|
3829
|
+
throw retryErr;
|
|
3673
3830
|
}
|
|
3674
3831
|
}
|
|
3675
3832
|
async function appendTerminalRunEvent(runId, event) {
|
|
@@ -3696,8 +3853,8 @@ async function appendTerminalRunEvent(runId, event) {
|
|
|
3696
3853
|
}
|
|
3697
3854
|
}
|
|
3698
3855
|
const nextSeq = last ? Number(last.seq ?? -1) + 1 : 0;
|
|
3699
|
-
await client.execute({
|
|
3700
|
-
sql: `INSERT INTO agent_run_events (run_id, seq, event_at, event_data) VALUES (?, ?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING`,
|
|
3856
|
+
const inserted = await client.execute({
|
|
3857
|
+
sql: `INSERT INTO agent_run_events (run_id, seq, event_at, event_data) VALUES (?, ?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING RETURNING seq`,
|
|
3701
3858
|
args: [
|
|
3702
3859
|
runId,
|
|
3703
3860
|
nextSeq,
|
|
@@ -3705,4 +3862,7 @@ async function appendTerminalRunEvent(runId, event) {
|
|
|
3705
3862
|
stripInlineBytesFromJson(JSON.stringify(event), "placeholder"),
|
|
3706
3863
|
],
|
|
3707
3864
|
});
|
|
3865
|
+
if (inserted.rows.length === 0) {
|
|
3866
|
+
throw new Error(`Run ${runId} terminal event conflicted with sequence ${nextSeq}`);
|
|
3867
|
+
}
|
|
3708
3868
|
}
|
|
@@ -101,4 +101,12 @@ export const CHAT_THREAD_SCHEMA_MIGRATIONS = [
|
|
|
101
101
|
ON agent_chat_submission_inbox (state, retry_after, interrupt_active_run DESC, move_to_top_at DESC, queue_order ASC)
|
|
102
102
|
`,
|
|
103
103
|
},
|
|
104
|
+
{
|
|
105
|
+
version: 6,
|
|
106
|
+
name: "chat-thread-inbox-interrupt-replay",
|
|
107
|
+
sql: `
|
|
108
|
+
ALTER TABLE agent_chat_submission_inbox
|
|
109
|
+
ADD COLUMN IF NOT EXISTS interrupt_run_id TEXT
|
|
110
|
+
`,
|
|
111
|
+
},
|
|
104
112
|
];
|
|
@@ -259,6 +259,7 @@ export interface QueuedSubmissionReceipt {
|
|
|
259
259
|
message?: QueuedMessage;
|
|
260
260
|
payloadHash: string;
|
|
261
261
|
submittedRunId?: string;
|
|
262
|
+
interruptRunId?: string;
|
|
262
263
|
retryAfter?: number;
|
|
263
264
|
createdAt: number;
|
|
264
265
|
updatedAt: number;
|
|
@@ -294,6 +295,27 @@ export declare function retryQueuedSubmissionReceipt(threadId: string, messageId
|
|
|
294
295
|
export declare function blockQueuedSubmissionReceipt(threadId: string, messageId: string, options?: {
|
|
295
296
|
claimId?: string;
|
|
296
297
|
}): Promise<boolean>;
|
|
298
|
+
/** Persist the active run before interrupting it so a later inbox claim can finish the abort. */
|
|
299
|
+
export declare function recordQueuedSubmissionInterruptRun(input: {
|
|
300
|
+
threadId: string;
|
|
301
|
+
messageId: string;
|
|
302
|
+
runId: string;
|
|
303
|
+
claimId: string;
|
|
304
|
+
}): Promise<boolean>;
|
|
305
|
+
/** Renews a live interruption claim before its recorded run is aborted. */
|
|
306
|
+
export declare function renewQueuedSubmissionInterruptClaim(input: {
|
|
307
|
+
threadId: string;
|
|
308
|
+
messageId: string;
|
|
309
|
+
runId: string;
|
|
310
|
+
claimId: string;
|
|
311
|
+
}): Promise<boolean>;
|
|
312
|
+
/** Clear a recorded interruption only after its turn abort is durable. */
|
|
313
|
+
export declare function clearQueuedSubmissionInterruptRun(input: {
|
|
314
|
+
threadId: string;
|
|
315
|
+
messageId: string;
|
|
316
|
+
runId: string;
|
|
317
|
+
claimId: string;
|
|
318
|
+
}): Promise<boolean>;
|
|
297
319
|
export type ThreadQueuedMessageMutation = {
|
|
298
320
|
type: "append";
|
|
299
321
|
message: QueuedMessage;
|
|
@@ -321,6 +343,16 @@ export type ThreadQueuedMessageMutation = {
|
|
|
321
343
|
type: "blockReceipt";
|
|
322
344
|
messageId: string;
|
|
323
345
|
claimId?: string;
|
|
346
|
+
} | {
|
|
347
|
+
type: "recordInterruptRun" | "clearInterruptRun";
|
|
348
|
+
messageId: string;
|
|
349
|
+
runId: string;
|
|
350
|
+
claimId: string;
|
|
351
|
+
} | {
|
|
352
|
+
type: "renewClaim";
|
|
353
|
+
messageId: string;
|
|
354
|
+
runId: string;
|
|
355
|
+
claimId: string;
|
|
324
356
|
} | {
|
|
325
357
|
type: "release";
|
|
326
358
|
messageId: string;
|
|
@@ -338,6 +370,7 @@ export interface ThreadQueuedMessageMutationResult {
|
|
|
338
370
|
receiptUnavailable?: boolean;
|
|
339
371
|
receiptConflict?: boolean;
|
|
340
372
|
receiptUpdated?: boolean;
|
|
373
|
+
claimLost?: boolean;
|
|
341
374
|
released?: boolean;
|
|
342
375
|
}
|
|
343
376
|
/** Applies a queue operation to the latest durable thread state on every CAS retry. */
|