@agent-native/core 0.220.0-nightly-20261024033422 → 0.220.0
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/handlers.js +7 -6
- package/dist/a2a/types.d.ts +1 -0
- package/dist/agent/engine/tool-call-journal-seed.js +24 -20
- package/dist/agent/execution-fence.d.ts +1 -1
- package/dist/agent/execution-fence.js +8 -1
- package/dist/agent/production-agent.js +31 -6
- package/dist/agent/reaped-run-thread.js +38 -3
- package/dist/agent/run-migrations.js +22 -0
- package/dist/agent/run-store.d.ts +19 -2
- package/dist/agent/run-store.fencing.worker.js +21 -1
- package/dist/agent/run-store.js +359 -35
- package/dist/agent/tool-approval-store.d.ts +13 -0
- package/dist/agent/tool-approval-store.js +64 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/background-agent-session.d.ts +4 -0
- package/dist/client/background-agent-session.js +17 -1
- package/dist/client/chat/runtime.js +38 -6
- package/dist/collab/routes.d.ts +2 -2
- package/dist/db/schema-snapshot.generated.js +14 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/jobs/automation-recovery.d.ts +4 -0
- package/dist/jobs/automation-recovery.js +7 -0
- package/dist/jobs/background-automation-runner.js +19 -2
- package/dist/jobs/scheduler.js +3 -1
- package/dist/mcp/org-directory.d.ts +8 -7
- package/dist/mcp/org-directory.js +82 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +7 -7
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/routes.d.ts +10 -10
- package/dist/server/agent-chat-plugin.d.ts +10 -1
- package/dist/server/agent-chat-plugin.js +178 -11
- package/dist/server/agent-discovery.js +11 -49
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/usage/store.js +36 -33
- package/package.json +4 -4
package/dist/agent/run-store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MAX_BACKGROUND_RUN_CONTINUATIONS, TURN_RUN_LEDGER_SLACK, } from "../app-config/run-lifecycle-invariants.js";
|
|
2
2
|
import { isArtifactReceipt, } from "../artifacts/detect.js";
|
|
3
|
-
import { getDbExec } from "../db/client.js";
|
|
3
|
+
import { getDbExec, getScopedTransactionDbExec } from "../db/client.js";
|
|
4
4
|
import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
|
|
5
5
|
import { defineStore } from "../db/store-registry.js";
|
|
6
6
|
import { fromMigrationEntries } from "../db/versioned-store-migrations.js";
|
|
@@ -192,7 +192,9 @@ export const agentRunsStore = defineStore({
|
|
|
192
192
|
dispatch_payload TEXT,
|
|
193
193
|
peak_rss_mb BIGINT,
|
|
194
194
|
continuation_order BIGINT,
|
|
195
|
-
thread_projection_pending BOOLEAN NOT NULL DEFAULT FALSE
|
|
195
|
+
thread_projection_pending BOOLEAN NOT NULL DEFAULT FALSE,
|
|
196
|
+
thread_projection_retry_order BIGINT,
|
|
197
|
+
thread_projection_error_code TEXT
|
|
196
198
|
)
|
|
197
199
|
`;
|
|
198
200
|
const agentRunEventsCreateSql = `
|
|
@@ -257,6 +259,8 @@ export const agentRunsStore = defineStore({
|
|
|
257
259
|
["continuation_order", "BIGINT"],
|
|
258
260
|
["auto_continue_of", "TEXT"],
|
|
259
261
|
["thread_projection_pending", "BOOLEAN NOT NULL DEFAULT FALSE"],
|
|
262
|
+
["thread_projection_retry_order", "BIGINT"],
|
|
263
|
+
["thread_projection_error_code", "TEXT"],
|
|
260
264
|
]) {
|
|
261
265
|
await ensureColumnExists("agent_runs", col, `ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS ${col} ${colType}`);
|
|
262
266
|
}
|
|
@@ -286,6 +290,22 @@ export const agentRunsStore = defineStore({
|
|
|
286
290
|
},
|
|
287
291
|
},
|
|
288
292
|
...fromMigrationEntries(AGENT_RUN_MIGRATIONS_TABLE, AGENT_RUN_MIGRATIONS),
|
|
293
|
+
{
|
|
294
|
+
name: "thread-projection-retry-quarantine-runtime-schema",
|
|
295
|
+
run: async (exec) => {
|
|
296
|
+
await exec.execute({
|
|
297
|
+
sql: "ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS thread_projection_error_code TEXT",
|
|
298
|
+
});
|
|
299
|
+
await exec.execute({
|
|
300
|
+
sql: "CREATE SEQUENCE IF NOT EXISTS agent_run_projection_retry_order_seq",
|
|
301
|
+
});
|
|
302
|
+
await exec.execute({
|
|
303
|
+
sql: `CREATE INDEX IF NOT EXISTS agent_runs_thread_projection_retry_order_idx
|
|
304
|
+
ON agent_runs (thread_projection_retry_order ASC NULLS FIRST, completed_at ASC, id ASC)
|
|
305
|
+
WHERE thread_projection_pending IS TRUE`,
|
|
306
|
+
});
|
|
307
|
+
},
|
|
308
|
+
},
|
|
289
309
|
],
|
|
290
310
|
});
|
|
291
311
|
export function ensureRunTables() {
|
|
@@ -613,6 +633,101 @@ export async function markAgentExecutionStepDispatched(intent) {
|
|
|
613
633
|
throw new Error(`Execution step ${intent.stepId} was not prepared`);
|
|
614
634
|
}
|
|
615
635
|
}
|
|
636
|
+
export async function resetAgentExecutionStepBeforeInvocation(intent) {
|
|
637
|
+
await ensureRunTables();
|
|
638
|
+
const expectedFence = {
|
|
639
|
+
threadId: intent.threadId,
|
|
640
|
+
turnId: intent.turnId,
|
|
641
|
+
runId: intent.runId,
|
|
642
|
+
generation: intent.generation,
|
|
643
|
+
};
|
|
644
|
+
try {
|
|
645
|
+
const result = await withAgentExecutionStepIntentFence(intent, (tx) => tx.execute({
|
|
646
|
+
sql: `UPDATE agent_execution_steps
|
|
647
|
+
SET status = 'prepared', dispatched_at = NULL,
|
|
648
|
+
updated_at = clock_timestamp()
|
|
649
|
+
WHERE thread_id = ? AND turn_id = ? AND step_id = ?
|
|
650
|
+
AND run_id = ? AND generation = CAST(? AS BIGINT)
|
|
651
|
+
AND status IN ('prepared', 'dispatched')`,
|
|
652
|
+
args: [
|
|
653
|
+
intent.threadId,
|
|
654
|
+
intent.turnId,
|
|
655
|
+
intent.stepId,
|
|
656
|
+
intent.runId,
|
|
657
|
+
intent.generation,
|
|
658
|
+
],
|
|
659
|
+
}));
|
|
660
|
+
if (result.rowsAffected !== 1) {
|
|
661
|
+
throw new Error(`Execution step ${intent.stepId} was not reset before invocation`);
|
|
662
|
+
}
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
if (!(error instanceof StaleExecutionFenceError))
|
|
667
|
+
throw error;
|
|
668
|
+
const activeFence = currentExecutionFence();
|
|
669
|
+
if (!activeFence ||
|
|
670
|
+
activeFence.threadId !== expectedFence.threadId ||
|
|
671
|
+
activeFence.turnId !== expectedFence.turnId ||
|
|
672
|
+
activeFence.runId !== expectedFence.runId ||
|
|
673
|
+
activeFence.generation !== expectedFence.generation) {
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const reconcileWithCurrentLease = async (tx) => {
|
|
678
|
+
await lockAndAssertCurrentExecutionFences(tx, expectedFence);
|
|
679
|
+
const currentLease = await lockTurnExecutionLease(tx, intent.threadId, intent.turnId);
|
|
680
|
+
if (!currentLease?.isLive) {
|
|
681
|
+
throw new StaleExecutionFenceError(expectedFence);
|
|
682
|
+
}
|
|
683
|
+
const currentOwner = await tx.execute({
|
|
684
|
+
sql: `SELECT status, worker_generation FROM agent_runs
|
|
685
|
+
WHERE id = ? FOR UPDATE`,
|
|
686
|
+
args: [currentLease.ownerRunId],
|
|
687
|
+
});
|
|
688
|
+
const owner = currentOwner.rows[0];
|
|
689
|
+
const currentFence = {
|
|
690
|
+
threadId: intent.threadId,
|
|
691
|
+
turnId: intent.turnId,
|
|
692
|
+
runId: currentLease.ownerRunId,
|
|
693
|
+
generation: currentLease.generation,
|
|
694
|
+
};
|
|
695
|
+
if (owner?.status !== "running" ||
|
|
696
|
+
String(owner.worker_generation) !== currentLease.generation) {
|
|
697
|
+
throw new StaleExecutionFenceError(currentFence);
|
|
698
|
+
}
|
|
699
|
+
const reconciled = await tx.execute({
|
|
700
|
+
sql: `UPDATE agent_execution_steps
|
|
701
|
+
SET status = 'prepared', run_id = ?,
|
|
702
|
+
generation = CAST(? AS BIGINT), dispatched_at = NULL,
|
|
703
|
+
updated_at = clock_timestamp()
|
|
704
|
+
WHERE thread_id = ? AND turn_id = ? AND step_id = ?
|
|
705
|
+
AND run_id = ? AND generation = CAST(? AS BIGINT)
|
|
706
|
+
AND status IN ('prepared', 'dispatched', 'unknown')`,
|
|
707
|
+
args: [
|
|
708
|
+
currentLease.ownerRunId,
|
|
709
|
+
currentLease.generation,
|
|
710
|
+
intent.threadId,
|
|
711
|
+
intent.turnId,
|
|
712
|
+
intent.stepId,
|
|
713
|
+
intent.runId,
|
|
714
|
+
intent.generation,
|
|
715
|
+
],
|
|
716
|
+
});
|
|
717
|
+
if (reconciled.rowsAffected !== 1) {
|
|
718
|
+
throw new Error(`Execution step ${intent.stepId} was not reconciled under the current lease`);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
const client = getDbExec();
|
|
722
|
+
if (getScopedTransactionDbExec() === client) {
|
|
723
|
+
await reconcileWithCurrentLease(client);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (!client.transaction) {
|
|
727
|
+
throw new Error("Execution step reconciliation requires transaction support");
|
|
728
|
+
}
|
|
729
|
+
await client.transaction(reconcileWithCurrentLease);
|
|
730
|
+
}
|
|
616
731
|
export async function completeAgentExecutionStep(intent, resultJson, errorJson) {
|
|
617
732
|
await ensureRunTables();
|
|
618
733
|
const result = await withFencedExecutionWrite((tx) => tx.execute({
|
|
@@ -714,16 +829,28 @@ export async function getCompletedAgentExecutionSteps(threadId, turnId, stepIds)
|
|
|
714
829
|
AND step_id IN (${batch.map(() => "?").join(", ")})`,
|
|
715
830
|
args: [threadId, turnId, ...batch],
|
|
716
831
|
});
|
|
717
|
-
for (const raw of rows.rows) {
|
|
832
|
+
for (const [batchRowIndex, raw] of rows.rows.entries()) {
|
|
833
|
+
const rowIndex = offset + batchRowIndex;
|
|
834
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
835
|
+
throw new AgentRunJournalUnreadableError(threadId, turnId, rowIndex, "invalid_execution_step", {
|
|
836
|
+
cause: new Error("Completed execution-step lookup returned an invalid row"),
|
|
837
|
+
});
|
|
838
|
+
}
|
|
718
839
|
const row = raw;
|
|
719
840
|
if (typeof row.step_id !== "string" || !batch.includes(row.step_id)) {
|
|
720
|
-
throw new
|
|
841
|
+
throw new AgentRunJournalUnreadableError(threadId, turnId, rowIndex, "invalid_execution_step", {
|
|
842
|
+
cause: new Error("Completed execution-step lookup returned an invalid step id"),
|
|
843
|
+
});
|
|
721
844
|
}
|
|
722
845
|
if (typeof row.operation_id !== "string" ||
|
|
846
|
+
row.operation_id.trim().length === 0 ||
|
|
723
847
|
typeof row.tool_name !== "string" ||
|
|
848
|
+
row.tool_name.trim().length === 0 ||
|
|
724
849
|
typeof row.input_json !== "string" ||
|
|
725
850
|
typeof row.result_json !== "string") {
|
|
726
|
-
throw new
|
|
851
|
+
throw new AgentRunJournalUnreadableError(threadId, turnId, rowIndex, "invalid_execution_step", {
|
|
852
|
+
cause: new Error(`Completed execution step ${row.step_id} has an incomplete stored intent or result`),
|
|
853
|
+
});
|
|
727
854
|
}
|
|
728
855
|
completed.push({
|
|
729
856
|
stepId: row.step_id,
|
|
@@ -986,7 +1113,7 @@ async function lockContinuationOrder(db, threadId, turnId) {
|
|
|
986
1113
|
async function nextContinuationOrder(db, threadId, turnId) {
|
|
987
1114
|
await lockContinuationOrder(db, threadId, turnId);
|
|
988
1115
|
const { rows } = await db.execute({
|
|
989
|
-
sql: `SELECT MAX(continuation_order) AS max_order
|
|
1116
|
+
sql: `SELECT MAX(COALESCE(continuation_order, 0)) AS max_order
|
|
990
1117
|
FROM agent_runs
|
|
991
1118
|
WHERE thread_id = ? AND COALESCE(turn_id, id) = ?`,
|
|
992
1119
|
args: [threadId, turnId],
|
|
@@ -1056,6 +1183,31 @@ function terminalRunEventExclusionSql(runIdColumn = "id") {
|
|
|
1056
1183
|
function livenessBasisSql() {
|
|
1057
1184
|
return `(CASE WHEN COALESCE(last_progress_at, started_at) > COALESCE(heartbeat_at, started_at) THEN COALESCE(last_progress_at, started_at) ELSE COALESCE(heartbeat_at, started_at) END)`;
|
|
1058
1185
|
}
|
|
1186
|
+
function earlierTurnProjectionPendingSql() {
|
|
1187
|
+
return `NOT EXISTS (
|
|
1188
|
+
SELECT 1 FROM agent_runs pending_projection
|
|
1189
|
+
WHERE pending_projection.thread_id = agent_runs.thread_id
|
|
1190
|
+
AND COALESCE(pending_projection.turn_id, pending_projection.id) =
|
|
1191
|
+
COALESCE(agent_runs.turn_id, agent_runs.id)
|
|
1192
|
+
AND pending_projection.id <> agent_runs.id
|
|
1193
|
+
AND pending_projection.thread_projection_pending IS TRUE
|
|
1194
|
+
AND (
|
|
1195
|
+
COALESCE(pending_projection.continuation_order, 0) <
|
|
1196
|
+
COALESCE(agent_runs.continuation_order, 0)
|
|
1197
|
+
OR (
|
|
1198
|
+
COALESCE(pending_projection.continuation_order, 0) =
|
|
1199
|
+
COALESCE(agent_runs.continuation_order, 0)
|
|
1200
|
+
AND (
|
|
1201
|
+
pending_projection.started_at < agent_runs.started_at
|
|
1202
|
+
OR (
|
|
1203
|
+
pending_projection.started_at = agent_runs.started_at
|
|
1204
|
+
AND pending_projection.id < agent_runs.id
|
|
1205
|
+
)
|
|
1206
|
+
)
|
|
1207
|
+
)
|
|
1208
|
+
)
|
|
1209
|
+
)`;
|
|
1210
|
+
}
|
|
1059
1211
|
export async function claimBackgroundRun(runId) {
|
|
1060
1212
|
await ensureRunTables();
|
|
1061
1213
|
const client = getDbExec();
|
|
@@ -1064,7 +1216,8 @@ export async function claimBackgroundRun(runId) {
|
|
|
1064
1216
|
SET dispatch_mode = 'background-processing'
|
|
1065
1217
|
WHERE id = ?
|
|
1066
1218
|
AND status = 'running'
|
|
1067
|
-
AND dispatch_mode = 'background'
|
|
1219
|
+
AND dispatch_mode = 'background'
|
|
1220
|
+
AND ${earlierTurnProjectionPendingSql()}`,
|
|
1068
1221
|
args: [runId],
|
|
1069
1222
|
});
|
|
1070
1223
|
return (rowsAffected ?? 0) > 0;
|
|
@@ -1115,6 +1268,7 @@ export async function listUnclaimedBackgroundRunIds() {
|
|
|
1115
1268
|
sql: `SELECT id FROM agent_runs
|
|
1116
1269
|
WHERE status = 'running'
|
|
1117
1270
|
AND dispatch_mode = 'background'
|
|
1271
|
+
AND ${earlierTurnProjectionPendingSql()}
|
|
1118
1272
|
AND COALESCE(heartbeat_at, started_at) < (CAST(? AS BIGINT) - ${UNCLAIMED_BACKGROUND_RUN_GRACE_MS})`,
|
|
1119
1273
|
args: [Date.now()],
|
|
1120
1274
|
});
|
|
@@ -1140,6 +1294,7 @@ export async function listUnclaimedBackgroundRunRows(options) {
|
|
|
1140
1294
|
sql: `SELECT id, started_at, (dispatch_payload IS NOT NULL) AS has_dispatch_payload FROM agent_runs
|
|
1141
1295
|
WHERE status = 'running'
|
|
1142
1296
|
AND dispatch_mode = 'background'
|
|
1297
|
+
AND ${earlierTurnProjectionPendingSql()}
|
|
1143
1298
|
AND COALESCE(heartbeat_at, started_at) < (CAST(? AS BIGINT) - ${UNCLAIMED_BACKGROUND_RUN_GRACE_MS})
|
|
1144
1299
|
ORDER BY COALESCE(heartbeat_at, started_at) ASC, started_at ASC
|
|
1145
1300
|
LIMIT ${limit}`,
|
|
@@ -1391,6 +1546,15 @@ export async function tryClaimRunSlot(threadId, runId, maxStaleMs, options) {
|
|
|
1391
1546
|
};
|
|
1392
1547
|
}
|
|
1393
1548
|
}
|
|
1549
|
+
if (await options?.hasPendingApproval?.(tx)) {
|
|
1550
|
+
return {
|
|
1551
|
+
result: {
|
|
1552
|
+
claimed: false,
|
|
1553
|
+
activeRunId: null,
|
|
1554
|
+
approvalPending: true,
|
|
1555
|
+
},
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1394
1558
|
// Admitted under the thread's slot lock, so two tabs continuing the same
|
|
1395
1559
|
// stop start one run and the count of continuations stays exact.
|
|
1396
1560
|
if (continueOf) {
|
|
@@ -1723,6 +1887,7 @@ export async function reconcileTerminalRunFromEvents(runId, options = {}) {
|
|
|
1723
1887
|
error_code = ?,
|
|
1724
1888
|
error_detail = ?,
|
|
1725
1889
|
terminal_reason = ?,
|
|
1890
|
+
thread_projection_retry_order = CASE WHEN ? THEN COALESCE(thread_projection_retry_order, nextval('agent_run_projection_retry_order_seq')) ELSE thread_projection_retry_order END,
|
|
1726
1891
|
thread_projection_pending = CASE WHEN ? THEN TRUE ELSE thread_projection_pending END
|
|
1727
1892
|
WHERE id = ?
|
|
1728
1893
|
AND (
|
|
@@ -1737,6 +1902,7 @@ export async function reconcileTerminalRunFromEvents(runId, options = {}) {
|
|
|
1737
1902
|
errorDetail,
|
|
1738
1903
|
terminalReason,
|
|
1739
1904
|
options.threadProjectionPending === true,
|
|
1905
|
+
options.threadProjectionPending === true,
|
|
1740
1906
|
runId,
|
|
1741
1907
|
STALE_RUN_ERROR_EVENT.errorCode,
|
|
1742
1908
|
...(allowMatchingTerminalRepair ? [status, terminalReason] : []),
|
|
@@ -2043,6 +2209,7 @@ async function reconcileTerminalRunInRecoveryTransaction(tx, runId) {
|
|
|
2043
2209
|
error_detail = ?,
|
|
2044
2210
|
terminal_reason = ?
|
|
2045
2211
|
WHERE id = ? AND status = 'running'
|
|
2212
|
+
AND ${earlierTurnProjectionPendingSql()}
|
|
2046
2213
|
${ENDED_RUN_RETURNING}`,
|
|
2047
2214
|
args: [
|
|
2048
2215
|
status,
|
|
@@ -2075,6 +2242,7 @@ async function reapSingleStaleRun(runId, source, maxStaleMs) {
|
|
|
2075
2242
|
terminal_reason = ?
|
|
2076
2243
|
WHERE id = ?
|
|
2077
2244
|
AND status = 'running'
|
|
2245
|
+
AND ${earlierTurnProjectionPendingSql()}
|
|
2078
2246
|
AND ${terminalRunEventExclusionSql()}
|
|
2079
2247
|
AND ${staleClause}
|
|
2080
2248
|
${ENDED_RUN_RETURNING}`;
|
|
@@ -2127,15 +2295,31 @@ async function reapSingleStaleRun(runId, source, maxStaleMs) {
|
|
|
2127
2295
|
args: [runScope.thread_id, runScope.turn_id, runId, lease.generation],
|
|
2128
2296
|
});
|
|
2129
2297
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2298
|
+
const executionLeaseChanged = !generationMatches && (lease !== null || run.worker_generation != null);
|
|
2299
|
+
const shouldProjectRepairedRun = generationMatches || (!lease && run.worker_generation == null);
|
|
2300
|
+
const outcome = isContinuationTerminalReason(repaired.ending.terminalReason) &&
|
|
2301
|
+
!executionLeaseChanged
|
|
2302
|
+
? await attemptStaleRunRecovery(tx, runId)
|
|
2303
|
+
: null;
|
|
2304
|
+
if (shouldProjectRepairedRun) {
|
|
2305
|
+
const projection = await tx.execute({
|
|
2132
2306
|
sql: `UPDATE agent_runs
|
|
2133
|
-
SET thread_projection_pending = TRUE
|
|
2134
|
-
|
|
2135
|
-
|
|
2307
|
+
SET thread_projection_pending = TRUE,
|
|
2308
|
+
thread_projection_retry_order = COALESCE(thread_projection_retry_order, nextval('agent_run_projection_retry_order_seq'))
|
|
2309
|
+
WHERE id = ? AND status = ?
|
|
2310
|
+
RETURNING id`,
|
|
2311
|
+
args: [runId, repaired.ending.status],
|
|
2136
2312
|
});
|
|
2313
|
+
if ((projection.rowsAffected ?? projection.rows.length) !== 1) {
|
|
2314
|
+
throw new Error(`Repaired run ${runId} did not retain its thread projection retry marker`);
|
|
2315
|
+
}
|
|
2137
2316
|
}
|
|
2138
|
-
return {
|
|
2317
|
+
return {
|
|
2318
|
+
reaped: false,
|
|
2319
|
+
outcome,
|
|
2320
|
+
repaired,
|
|
2321
|
+
shouldProjectRepairedRun,
|
|
2322
|
+
};
|
|
2139
2323
|
}
|
|
2140
2324
|
const { rows, rowsAffected } = await tx.execute({
|
|
2141
2325
|
sql: updateSql,
|
|
@@ -2161,7 +2345,8 @@ async function reapSingleStaleRun(runId, source, maxStaleMs) {
|
|
|
2161
2345
|
if (outcome.outcome !== "execution_lease_changed") {
|
|
2162
2346
|
const projection = await tx.execute({
|
|
2163
2347
|
sql: `UPDATE agent_runs
|
|
2164
|
-
SET thread_projection_pending = TRUE
|
|
2348
|
+
SET thread_projection_pending = TRUE,
|
|
2349
|
+
thread_projection_retry_order = COALESCE(thread_projection_retry_order, nextval('agent_run_projection_retry_order_seq'))
|
|
2165
2350
|
WHERE id = ? AND status = 'errored'
|
|
2166
2351
|
RETURNING id`,
|
|
2167
2352
|
args: [runId],
|
|
@@ -2248,11 +2433,16 @@ async function reapSingleStaleRun(runId, source, maxStaleMs) {
|
|
|
2248
2433
|
// first would have this older run's steps land after its, and take its
|
|
2249
2434
|
// error and status.
|
|
2250
2435
|
let projectionFailed = false;
|
|
2251
|
-
if (
|
|
2436
|
+
if ("repaired" in reapResult && reapResult.shouldProjectRepairedRun) {
|
|
2437
|
+
projectionFailed = !(await finalizeStaleRun(runId, source));
|
|
2438
|
+
}
|
|
2439
|
+
else if (reaped) {
|
|
2252
2440
|
// A newer generation owns the turn, so it must remain the sole thread projector.
|
|
2253
2441
|
projectionFailed = !(await finalizeStaleRun(runId, source, outcome?.outcome !== "execution_lease_changed"));
|
|
2254
2442
|
}
|
|
2255
|
-
if (reaped
|
|
2443
|
+
if ((reaped || "repaired" in reapResult) &&
|
|
2444
|
+
outcome &&
|
|
2445
|
+
outcome.outcome !== "not_background") {
|
|
2256
2446
|
const outcomeDetail = outcome.outcome === "recovered"
|
|
2257
2447
|
? `recovered successorRunId=${outcome.successorRunId}`
|
|
2258
2448
|
: `declined reason=${outcome.outcome}`;
|
|
@@ -2260,7 +2450,7 @@ async function reapSingleStaleRun(runId, source, maxStaleMs) {
|
|
|
2260
2450
|
? `${outcomeDetail} ${priorStageInfo}`
|
|
2261
2451
|
: outcomeDetail;
|
|
2262
2452
|
await recordRunDiagnostic(runId, RUN_DIAG_STAGE.staleRunRecoveryAttempted, forensics ? `${detail} ${forensics}` : detail).catch(() => { });
|
|
2263
|
-
if (outcome.outcome === "recovered") {
|
|
2453
|
+
if (outcome.outcome === "recovered" && !projectionFailed) {
|
|
2264
2454
|
attemptStaleRunRecoveryDispatch(outcome.successorRunId);
|
|
2265
2455
|
}
|
|
2266
2456
|
}
|
|
@@ -2298,6 +2488,7 @@ export async function reapUnclaimedBackgroundRun(runId) {
|
|
|
2298
2488
|
AND status = 'running'
|
|
2299
2489
|
AND dispatch_mode = 'background'
|
|
2300
2490
|
AND COALESCE(heartbeat_at, started_at) < ?
|
|
2491
|
+
AND ${earlierTurnProjectionPendingSql()}
|
|
2301
2492
|
${ENDED_RUN_RETURNING}`,
|
|
2302
2493
|
args: [
|
|
2303
2494
|
completedAt,
|
|
@@ -2347,6 +2538,7 @@ export async function updateRunStatusIfRunning(runId, status, options = {}) {
|
|
|
2347
2538
|
const terminalReason = options.terminalReason?.slice(0, 200);
|
|
2348
2539
|
const { rowsAffected } = await withFencedExecutionWrite((tx) => tx.execute({
|
|
2349
2540
|
sql: `UPDATE agent_runs SET status = ?, completed_at = ?, dispatch_payload = NULL,
|
|
2541
|
+
thread_projection_retry_order = CASE WHEN ? THEN COALESCE(thread_projection_retry_order, nextval('agent_run_projection_retry_order_seq')) ELSE thread_projection_retry_order END,
|
|
2350
2542
|
thread_projection_pending = CASE WHEN ? THEN TRUE ELSE thread_projection_pending END
|
|
2351
2543
|
${terminalReason === undefined ? "" : ", terminal_reason = ?"}
|
|
2352
2544
|
WHERE id = ? AND status = 'running'`,
|
|
@@ -2354,6 +2546,7 @@ export async function updateRunStatusIfRunning(runId, status, options = {}) {
|
|
|
2354
2546
|
status,
|
|
2355
2547
|
Date.now(),
|
|
2356
2548
|
options.threadProjectionPending === true,
|
|
2549
|
+
options.threadProjectionPending === true,
|
|
2357
2550
|
...(terminalReason === undefined ? [] : [terminalReason]),
|
|
2358
2551
|
runId,
|
|
2359
2552
|
],
|
|
@@ -3279,7 +3472,7 @@ export async function getRunById(runId) {
|
|
|
3279
3472
|
await ensureRunTables();
|
|
3280
3473
|
const client = getDbExec();
|
|
3281
3474
|
const { rows } = await client.execute({
|
|
3282
|
-
sql: `SELECT id, thread_id, status, started_at, error_code, error_detail, terminal_reason FROM agent_runs WHERE id = ?`,
|
|
3475
|
+
sql: `SELECT id, thread_id, status, started_at, error_code, error_detail, terminal_reason, thread_projection_error_code FROM agent_runs WHERE id = ?`,
|
|
3283
3476
|
args: [runId],
|
|
3284
3477
|
});
|
|
3285
3478
|
if (rows.length === 0)
|
|
@@ -3293,6 +3486,7 @@ export async function getRunById(runId) {
|
|
|
3293
3486
|
errorCode: r.error_code ?? null,
|
|
3294
3487
|
errorDetail: r.error_detail ?? null,
|
|
3295
3488
|
terminalReason: r.terminal_reason ?? null,
|
|
3489
|
+
threadProjectionErrorCode: r.thread_projection_error_code ?? null,
|
|
3296
3490
|
};
|
|
3297
3491
|
}
|
|
3298
3492
|
/**
|
|
@@ -3328,8 +3522,21 @@ export async function readStoppedRunForThreadFold(runId) {
|
|
|
3328
3522
|
if (!row || row.status === "running")
|
|
3329
3523
|
return null;
|
|
3330
3524
|
const events = [];
|
|
3331
|
-
for (const { seq, eventData } of await getRunEventsSince(runId, 0)) {
|
|
3332
|
-
|
|
3525
|
+
for (const [rowIndex, { seq, eventData }] of (await getRunEventsSince(runId, 0)).entries()) {
|
|
3526
|
+
if (!Number.isSafeInteger(seq) || seq < 0) {
|
|
3527
|
+
throw new AgentRunJournalUnreadableError(row.thread_id, row.turn_id, rowIndex, "invalid_row");
|
|
3528
|
+
}
|
|
3529
|
+
let event;
|
|
3530
|
+
try {
|
|
3531
|
+
event = JSON.parse(eventData);
|
|
3532
|
+
}
|
|
3533
|
+
catch (cause) {
|
|
3534
|
+
throw new AgentRunJournalUnreadableError(row.thread_id, row.turn_id, rowIndex, "invalid_event_json", { cause });
|
|
3535
|
+
}
|
|
3536
|
+
if (!isReadableJournalEvent(event)) {
|
|
3537
|
+
throw new AgentRunJournalUnreadableError(row.thread_id, row.turn_id, rowIndex, "invalid_event");
|
|
3538
|
+
}
|
|
3539
|
+
events.push({ seq, event });
|
|
3333
3540
|
}
|
|
3334
3541
|
return {
|
|
3335
3542
|
threadId: row.thread_id,
|
|
@@ -3481,7 +3688,7 @@ export async function listRunsForThread(threadId, options = {}) {
|
|
|
3481
3688
|
? ` AND id IN (${batch.map(() => "?").join(", ")})`
|
|
3482
3689
|
: "";
|
|
3483
3690
|
return {
|
|
3484
|
-
sql: `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, error_code, abort_reason, dispatch_mode, terminal_reason, diag_stage, in_flight_since
|
|
3691
|
+
sql: `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, error_code, abort_reason, dispatch_mode, terminal_reason, diag_stage, thread_projection_error_code, in_flight_since
|
|
3485
3692
|
FROM agent_runs
|
|
3486
3693
|
WHERE thread_id = ?${runIdFilter}${batch ? "" : " ORDER BY started_at DESC LIMIT ?"}`,
|
|
3487
3694
|
args: [threadId, ...(batch ?? []), ...(batch ? [] : [limit])],
|
|
@@ -3524,6 +3731,7 @@ export async function listRunsForThread(threadId, options = {}) {
|
|
|
3524
3731
|
dispatchMode: row.dispatch_mode ?? null,
|
|
3525
3732
|
terminalReason: row.terminal_reason ?? null,
|
|
3526
3733
|
diagStage: row.diag_stage ?? null,
|
|
3734
|
+
threadProjectionErrorCode: row.thread_projection_error_code ?? null,
|
|
3527
3735
|
inFlightSince: row.in_flight_since == null ? null : Number(row.in_flight_since),
|
|
3528
3736
|
};
|
|
3529
3737
|
});
|
|
@@ -3759,6 +3967,7 @@ export async function reapAllStaleRuns() {
|
|
|
3759
3967
|
const scanned = await client.execute({
|
|
3760
3968
|
sql: `SELECT id FROM agent_runs
|
|
3761
3969
|
WHERE status = 'running'
|
|
3970
|
+
AND ${earlierTurnProjectionPendingSql()}
|
|
3762
3971
|
AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}
|
|
3763
3972
|
ORDER BY started_at ASC
|
|
3764
3973
|
LIMIT ${REAP_ALL_STALE_BATCH_LIMIT + 1}`,
|
|
@@ -4138,17 +4347,7 @@ async function finalizeStaleRun(runId, source, projectThread = true) {
|
|
|
4138
4347
|
if (!projectThread)
|
|
4139
4348
|
return true;
|
|
4140
4349
|
try {
|
|
4141
|
-
|
|
4142
|
-
const disposition = await foldReapedRunIntoThread(runId);
|
|
4143
|
-
if (disposition === "retry")
|
|
4144
|
-
return false;
|
|
4145
|
-
await getDbExec().execute({
|
|
4146
|
-
sql: `UPDATE agent_runs
|
|
4147
|
-
SET thread_projection_pending = FALSE
|
|
4148
|
-
WHERE id = ? AND thread_projection_pending IS TRUE`,
|
|
4149
|
-
args: [runId],
|
|
4150
|
-
});
|
|
4151
|
-
return true;
|
|
4350
|
+
return (await projectRunThread(runId)) === "projected";
|
|
4152
4351
|
}
|
|
4153
4352
|
catch (error) {
|
|
4154
4353
|
captureError(error, {
|
|
@@ -4162,6 +4361,122 @@ async function finalizeStaleRun(runId, source, projectThread = true) {
|
|
|
4162
4361
|
return false;
|
|
4163
4362
|
}
|
|
4164
4363
|
}
|
|
4364
|
+
async function bumpRunThreadProjectionRetryOrder(runId) {
|
|
4365
|
+
await getDbExec().execute({
|
|
4366
|
+
sql: `UPDATE agent_runs
|
|
4367
|
+
SET thread_projection_retry_order = nextval('agent_run_projection_retry_order_seq')
|
|
4368
|
+
WHERE id = ? AND thread_projection_pending IS TRUE`,
|
|
4369
|
+
args: [runId],
|
|
4370
|
+
});
|
|
4371
|
+
}
|
|
4372
|
+
async function projectRunThread(runId) {
|
|
4373
|
+
const { foldReapedRunIntoThread } = await import("./reaped-run-thread.js");
|
|
4374
|
+
let disposition;
|
|
4375
|
+
try {
|
|
4376
|
+
disposition = await foldReapedRunIntoThread(runId);
|
|
4377
|
+
}
|
|
4378
|
+
catch (error) {
|
|
4379
|
+
if (error instanceof AgentRunJournalUnreadableError) {
|
|
4380
|
+
await getDbExec().execute({
|
|
4381
|
+
sql: `UPDATE agent_runs
|
|
4382
|
+
SET thread_projection_error_code = 'tool_call_journal_unreadable',
|
|
4383
|
+
thread_projection_retry_order = NULL
|
|
4384
|
+
WHERE id = ? AND thread_projection_pending IS TRUE`,
|
|
4385
|
+
args: [runId],
|
|
4386
|
+
});
|
|
4387
|
+
throw error;
|
|
4388
|
+
}
|
|
4389
|
+
try {
|
|
4390
|
+
await bumpRunThreadProjectionRetryOrder(runId);
|
|
4391
|
+
}
|
|
4392
|
+
catch (retryOrderError) {
|
|
4393
|
+
throw new AggregateError([error, retryOrderError], `Run ${runId} projection failed and its retry order could not be advanced`, { cause: error });
|
|
4394
|
+
}
|
|
4395
|
+
throw error;
|
|
4396
|
+
}
|
|
4397
|
+
if (disposition === "retry") {
|
|
4398
|
+
await bumpRunThreadProjectionRetryOrder(runId);
|
|
4399
|
+
return "deferred";
|
|
4400
|
+
}
|
|
4401
|
+
const client = getDbExec();
|
|
4402
|
+
if (!client.transaction) {
|
|
4403
|
+
throw new Error("Run thread projection completion requires transaction support");
|
|
4404
|
+
}
|
|
4405
|
+
await client.transaction(async (tx) => {
|
|
4406
|
+
const { rows } = await tx.execute({
|
|
4407
|
+
sql: `UPDATE agent_runs
|
|
4408
|
+
SET thread_projection_pending = FALSE,
|
|
4409
|
+
thread_projection_retry_order = NULL,
|
|
4410
|
+
thread_projection_error_code = NULL
|
|
4411
|
+
WHERE id = ? AND thread_projection_pending IS TRUE
|
|
4412
|
+
RETURNING id`,
|
|
4413
|
+
args: [runId],
|
|
4414
|
+
});
|
|
4415
|
+
if (rows.length === 0)
|
|
4416
|
+
return;
|
|
4417
|
+
// Give queued same-turn successors a dispatch window when their gate opens.
|
|
4418
|
+
await tx.execute({
|
|
4419
|
+
sql: `UPDATE agent_runs AS successor
|
|
4420
|
+
SET heartbeat_at = ?
|
|
4421
|
+
FROM agent_runs AS predecessor
|
|
4422
|
+
WHERE predecessor.id = ?
|
|
4423
|
+
AND successor.thread_id = predecessor.thread_id
|
|
4424
|
+
AND COALESCE(successor.turn_id, successor.id) =
|
|
4425
|
+
COALESCE(predecessor.turn_id, predecessor.id)
|
|
4426
|
+
AND successor.id <> predecessor.id
|
|
4427
|
+
AND successor.status = 'running'
|
|
4428
|
+
AND successor.dispatch_mode = 'background'
|
|
4429
|
+
AND (
|
|
4430
|
+
COALESCE(predecessor.continuation_order, 0) <
|
|
4431
|
+
COALESCE(successor.continuation_order, 0)
|
|
4432
|
+
OR (
|
|
4433
|
+
COALESCE(predecessor.continuation_order, 0) =
|
|
4434
|
+
COALESCE(successor.continuation_order, 0)
|
|
4435
|
+
AND (
|
|
4436
|
+
predecessor.started_at < successor.started_at
|
|
4437
|
+
OR (
|
|
4438
|
+
predecessor.started_at = successor.started_at
|
|
4439
|
+
AND predecessor.id < successor.id
|
|
4440
|
+
)
|
|
4441
|
+
)
|
|
4442
|
+
)
|
|
4443
|
+
)`,
|
|
4444
|
+
args: [Date.now(), runId],
|
|
4445
|
+
});
|
|
4446
|
+
});
|
|
4447
|
+
return "projected";
|
|
4448
|
+
}
|
|
4449
|
+
/** Retry after repairing persisted source data; until success, the marker keeps later same-turn runs gated. */
|
|
4450
|
+
export async function retryRunThreadProjection(runId) {
|
|
4451
|
+
await ensureRunTables();
|
|
4452
|
+
const retried = await getDbExec().execute({
|
|
4453
|
+
sql: `UPDATE agent_runs
|
|
4454
|
+
SET thread_projection_error_code = NULL
|
|
4455
|
+
WHERE id = ?
|
|
4456
|
+
AND thread_projection_pending IS TRUE
|
|
4457
|
+
AND ${earlierTurnProjectionPendingSql()}
|
|
4458
|
+
RETURNING id`,
|
|
4459
|
+
args: [runId],
|
|
4460
|
+
});
|
|
4461
|
+
if (retried.rows.length > 0) {
|
|
4462
|
+
return { state: await projectRunThread(runId) };
|
|
4463
|
+
}
|
|
4464
|
+
const { rows } = await getDbExec().execute({
|
|
4465
|
+
sql: `SELECT thread_projection_pending FROM agent_runs WHERE id = ?`,
|
|
4466
|
+
args: [runId],
|
|
4467
|
+
});
|
|
4468
|
+
// Outcome pruning retains every run whose projection marker is still set.
|
|
4469
|
+
if (rows.length === 0)
|
|
4470
|
+
return { state: "untracked" };
|
|
4471
|
+
const pending = rows[0]?.thread_projection_pending;
|
|
4472
|
+
if (pending === true || pending === "t" || pending === 1) {
|
|
4473
|
+
return { state: "deferred" };
|
|
4474
|
+
}
|
|
4475
|
+
if (pending === false || pending === "f" || pending === 0) {
|
|
4476
|
+
return { state: "already_projected" };
|
|
4477
|
+
}
|
|
4478
|
+
throw new Error(`Run ${runId} has an unreadable thread projection marker`);
|
|
4479
|
+
}
|
|
4165
4480
|
class AbortTerminalEventWriteError extends Error {
|
|
4166
4481
|
runId;
|
|
4167
4482
|
constructor(runId, cause) {
|
|
@@ -4309,9 +4624,13 @@ async function appendTerminalRunEventInTransaction(tx, runId, event, replaceExis
|
|
|
4309
4624
|
}
|
|
4310
4625
|
async function retryPendingStaleRunThreadProjections(client) {
|
|
4311
4626
|
const { rows } = await client.execute({
|
|
4312
|
-
sql: `SELECT id FROM agent_runs
|
|
4627
|
+
sql: `SELECT id, thread_projection_error_code FROM agent_runs
|
|
4313
4628
|
WHERE thread_projection_pending IS TRUE
|
|
4314
|
-
|
|
4629
|
+
AND (thread_projection_error_code IS NOT NULL
|
|
4630
|
+
OR ${earlierTurnProjectionPendingSql()})
|
|
4631
|
+
ORDER BY CASE WHEN thread_projection_error_code IS NULL THEN 0 ELSE 1 END,
|
|
4632
|
+
thread_projection_retry_order ASC NULLS FIRST,
|
|
4633
|
+
completed_at ASC, id ASC
|
|
4315
4634
|
LIMIT ${REAP_ALL_STALE_BATCH_LIMIT + 1}`,
|
|
4316
4635
|
args: [],
|
|
4317
4636
|
});
|
|
@@ -4319,7 +4638,12 @@ async function retryPendingStaleRunThreadProjections(client) {
|
|
|
4319
4638
|
const pending = truncated ? rows.slice(0, REAP_ALL_STALE_BATCH_LIMIT) : rows;
|
|
4320
4639
|
let failed = 0;
|
|
4321
4640
|
for (const row of pending) {
|
|
4322
|
-
const
|
|
4641
|
+
const projection = row;
|
|
4642
|
+
if (projection.thread_projection_error_code != null) {
|
|
4643
|
+
failed += 1;
|
|
4644
|
+
continue;
|
|
4645
|
+
}
|
|
4646
|
+
const runId = projection.id;
|
|
4323
4647
|
if (typeof runId !== "string")
|
|
4324
4648
|
continue;
|
|
4325
4649
|
if (!(await finalizeStaleRun(runId, "stale-projection-retry")))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DbExec } from "../db/client.js";
|
|
1
2
|
export { hashAgentToolApprovalKey } from "./approval-key.js";
|
|
2
3
|
export declare const agentToolApprovalsStore: import("../db/store-registry.js").Store;
|
|
3
4
|
export declare function ensureAgentToolApprovalTable(): Promise<void>;
|
|
@@ -27,6 +28,18 @@ export interface AgentToolApprovalPolicyBinding {
|
|
|
27
28
|
export declare class AgentToolApprovalNotFoundError extends Error {
|
|
28
29
|
constructor();
|
|
29
30
|
}
|
|
31
|
+
export declare function hasPendingAgentToolApproval(tx: Pick<DbExec, "execute">, input: {
|
|
32
|
+
ownerEmail: string;
|
|
33
|
+
orgId?: string | null;
|
|
34
|
+
threadId: string;
|
|
35
|
+
}): Promise<boolean>;
|
|
36
|
+
/** Denial is a durable resolution so queued turns can resume after a pause. */
|
|
37
|
+
export declare function denyAgentToolApproval(input: {
|
|
38
|
+
approvalId: string;
|
|
39
|
+
ownerEmail: string;
|
|
40
|
+
orgId?: string | null;
|
|
41
|
+
threadId: string;
|
|
42
|
+
}): Promise<"denied" | "already_resolved" | "not_applied">;
|
|
30
43
|
/**
|
|
31
44
|
* Saves "always allow" for one tool name. With `approvalId`, the grant takes
|
|
32
45
|
* the revision of that approval row, the card the user clicked, so it never
|