@standardagents/builder 0.25.4 → 0.25.6
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/built-in-routes.js +6 -1
- package/dist/built-in-routes.js.map +1 -1
- package/dist/{index-BsS0cHzL.d.ts → index-CX9Ti98s.d.ts} +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +304 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +30 -2
- package/dist/runtime.js +304 -1
- package/dist/runtime.js.map +1 -1
- package/dist/test.d.ts +1 -1
- package/package.json +5 -5
package/dist/runtime.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { e as ThreadEnv, A as Agent, h as ThreadMetadata, i as ToolResult, M as Message, l as FileRecord, F as FlowState, n as FileStats, G as GrepResult, m as AttachmentRef, r as MessageContent, E as Env, g as ThreadInstance } from './index-
|
|
2
|
-
export { f as BuilderThreadEndpointHandler, B as Controller, a as ControllerContext, j as FlowResult, p as ImageContentPart, I as ImageMetadata, L as LLMResponse, q as MultimodalContent, R as RequestContext, S as StorageBackend, k as TelemetryEvent, o as TextContentPart, b as ThreadEndpointContext, T as ToolCall, c as createThreadEndpointHandler, d as defineController } from './index-
|
|
1
|
+
import { e as ThreadEnv, A as Agent, h as ThreadMetadata, i as ToolResult, M as Message, l as FileRecord, F as FlowState, n as FileStats, G as GrepResult, m as AttachmentRef, r as MessageContent, E as Env, g as ThreadInstance } from './index-CX9Ti98s.js';
|
|
2
|
+
export { f as BuilderThreadEndpointHandler, B as Controller, a as ControllerContext, j as FlowResult, p as ImageContentPart, I as ImageMetadata, L as LLMResponse, q as MultimodalContent, R as RequestContext, S as StorageBackend, k as TelemetryEvent, o as TextContentPart, b as ThreadEndpointContext, T as ToolCall, c as createThreadEndpointHandler, d as defineController } from './index-CX9Ti98s.js';
|
|
3
3
|
import { CodeExecutionOptions, CodeExecutionResult, SubagentRegistryEntry, InjectMessageInput, QueueMessageInput, ClientToolRequest, ClientToolResponse, SkillMetadata, SkillFileInput, ModelDefinition as ModelDefinition$1, ToolArgs, PromptTextPart, PromptEnvPart, VariableDefinition, SubpromptConfig as SubpromptConfig$1, PromptToolConfig as PromptToolConfig$1, SubagentToolConfig as SubagentToolConfig$1, ReasoningConfig, SideConfig as SideConfig$1, LLMProviderInterface, ContentPart, TextPart, ImagePart, FilePart, NamespaceContext, DefinitionLoader } from '@standardagents/spec';
|
|
4
4
|
export { AgentType, DefinitionLoader, GlobalNamespaceContext, HookSignatures, ImageContent, ModelCapabilities, ModelProvider, NamespaceContext, PackageSignature, PackedExports, PackedMeta, PackedMetadata, PackedNamespaceContext, PromptInput, PromptTextPart, ProviderAssistantMessage, ProviderError, ProviderErrorCode, ProviderFactory, ProviderFactoryConfig, ProviderFinishReason, ProviderGeneratedImage, ProviderMessage, ProviderMessageContent, ModelCapabilities as ProviderModelCapabilities, ProviderReasoningDetail, ProviderRequest, ProviderResponse, ProviderStreamChunk, ProviderSystemMessage, ProviderTool, ProviderToolCallPart, ProviderToolMessage, ProviderToolResultContent, ProviderUsage, ProviderUserMessage, ReasoningConfig, StructuredPrompt, TextContent, Tool, ToolArgs, ToolArgsNode, ToolArgsRawShape, ToolContent, belongsToPackage, defineAgent, defineHook, defineModel, definePrompt, defineTool, isPacked, isVisibleInNamespace, mapReasoningLevel } from '@standardagents/spec';
|
|
5
5
|
import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
|
|
@@ -184,6 +184,11 @@ declare class DurableThread<Env extends ThreadEnv = ThreadEnv> extends DurableOb
|
|
|
184
184
|
private alarmQueueRearmed;
|
|
185
185
|
private currentAbortController;
|
|
186
186
|
private isExecuting;
|
|
187
|
+
private lastProgressAt;
|
|
188
|
+
private watchdogWedgeStrikes;
|
|
189
|
+
private readonly watchdogIntervalMs;
|
|
190
|
+
private readonly watchdogResumeWindowMs;
|
|
191
|
+
private readonly watchdogMaxResumesPerWindow;
|
|
187
192
|
private codeExecutionBridges;
|
|
188
193
|
constructor(ctx: DurableObjectState, env: Env);
|
|
189
194
|
/**
|
|
@@ -1080,6 +1085,29 @@ declare class DurableThread<Env extends ThreadEnv = ThreadEnv> extends DurableOb
|
|
|
1080
1085
|
* the alarm chain. All errors are caught and logged.
|
|
1081
1086
|
*/
|
|
1082
1087
|
alarm(): Promise<void>;
|
|
1088
|
+
/** Heartbeat: called by LLMRequest on each attempt and streamed chunk. */
|
|
1089
|
+
noteExecutionProgress(): void;
|
|
1090
|
+
/** Arm the standing watchdog for an execution start. One pending row max. */
|
|
1091
|
+
private armExecutionWatchdog;
|
|
1092
|
+
/** Disarm on clean execution end. Completed check rows are noise, not history. */
|
|
1093
|
+
private disarmExecutionWatchdog;
|
|
1094
|
+
/**
|
|
1095
|
+
* The standing check. Evaluates the thread's state AT FIRE TIME — it knows
|
|
1096
|
+
* nothing about whichever request was in flight when it was armed.
|
|
1097
|
+
*/
|
|
1098
|
+
private watchdogCheck;
|
|
1099
|
+
/** Mark logs the dead executor abandoned, and tell connected clients. */
|
|
1100
|
+
private finalizeLostExecutionLogs;
|
|
1101
|
+
/**
|
|
1102
|
+
* A subagent whose execution the watchdog gave up on leaves its parent
|
|
1103
|
+
* waiting forever for a completion message. Deliver the failure the same way
|
|
1104
|
+
* a sessionFail would, so the parent's turn wakes and can route around it.
|
|
1105
|
+
* Explicit-communication children (e.g. the compaction agent) stay quiet by
|
|
1106
|
+
* contract — only the registry status updates for those.
|
|
1107
|
+
*/
|
|
1108
|
+
private notifyParentOfLostExecution;
|
|
1109
|
+
/** At most N auto-resumes per rolling window, tracked durably. */
|
|
1110
|
+
private consumeWatchdogResumeBudget;
|
|
1083
1111
|
private normalizePlainRecord;
|
|
1084
1112
|
private buildExecutionArguments;
|
|
1085
1113
|
/**
|
package/dist/runtime.js
CHANGED
|
@@ -1057,6 +1057,19 @@ function resolveStreamTimeoutMs(env, key, fallback) {
|
|
|
1057
1057
|
const parsed = typeof raw === "number" ? raw : typeof raw === "string" ? Number.parseInt(raw, 10) : NaN;
|
|
1058
1058
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1059
1059
|
}
|
|
1060
|
+
function resolveWatchdogStaleCeilingMs(env) {
|
|
1061
|
+
const firstChunk = resolveStreamTimeoutMs(
|
|
1062
|
+
env,
|
|
1063
|
+
"STANDARD_AGENTS_STREAM_FIRST_CHUNK_TIMEOUT_MS",
|
|
1064
|
+
STREAM_FIRST_CHUNK_TIMEOUT_MS
|
|
1065
|
+
);
|
|
1066
|
+
const stall = resolveStreamTimeoutMs(
|
|
1067
|
+
env,
|
|
1068
|
+
"STANDARD_AGENTS_STREAM_STALL_TIMEOUT_MS",
|
|
1069
|
+
STREAM_STALL_TIMEOUT_MS
|
|
1070
|
+
);
|
|
1071
|
+
return Math.max(firstChunk, stall) + 6e4;
|
|
1072
|
+
}
|
|
1060
1073
|
function modelSupportsImageInput(modelDef) {
|
|
1061
1074
|
const supportsImages = modelDef.capabilities?.supportsImages;
|
|
1062
1075
|
if (typeof supportsImages === "boolean") {
|
|
@@ -1569,6 +1582,7 @@ var init_LLMRequest = __esm({
|
|
|
1569
1582
|
attempts++;
|
|
1570
1583
|
try {
|
|
1571
1584
|
logId = await this.logRequest(state, actualModelId, context, modelDef, previousLogId);
|
|
1585
|
+
state.thread.instance.noteExecutionProgress?.();
|
|
1572
1586
|
state.emitTelemetry?.({
|
|
1573
1587
|
type: "llm_request",
|
|
1574
1588
|
model: actualModelId,
|
|
@@ -1835,6 +1849,7 @@ var init_LLMRequest = __esm({
|
|
|
1835
1849
|
}
|
|
1836
1850
|
if (next.done) break;
|
|
1837
1851
|
const chunk = next.value;
|
|
1852
|
+
state.thread.instance.noteExecutionProgress?.();
|
|
1838
1853
|
if (firstChunkAt === 0) {
|
|
1839
1854
|
firstChunkAt = Date.now();
|
|
1840
1855
|
emitLogPatch({ time_to_first_token_ms: firstChunkAt - requestSentAt });
|
|
@@ -2370,8 +2385,11 @@ var init_LLMRequest = __esm({
|
|
|
2370
2385
|
*/
|
|
2371
2386
|
static async logError(state, error, errorType, modelId, startTime, existingLogId) {
|
|
2372
2387
|
try {
|
|
2373
|
-
|
|
2388
|
+
let rawErrorMessage = error instanceof Error ? error.message : String(error);
|
|
2374
2389
|
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
2390
|
+
if (error instanceof Error && error.name === "AbortError" && /operation was aborted/i.test(rawErrorMessage)) {
|
|
2391
|
+
rawErrorMessage = state.abortController?.signal?.aborted ? "Aborted: the execution this request belonged to was cancelled (user stop, watchdog takeover, or a restart) \u2014 retries and fallbacks of a cancelled execution fail immediately." : "Aborted: the request's connection was cancelled mid-flight (provider disconnect or executor restart).";
|
|
2392
|
+
}
|
|
2375
2393
|
let errorMessage = rawErrorMessage;
|
|
2376
2394
|
let parsedProviderBody = null;
|
|
2377
2395
|
const trimmedRaw = rawErrorMessage.trim();
|
|
@@ -15023,6 +15041,38 @@ var AlarmQueue = class {
|
|
|
15023
15041
|
async ensureAlarmScheduled() {
|
|
15024
15042
|
await this.rescheduleAlarm();
|
|
15025
15043
|
}
|
|
15044
|
+
/**
|
|
15045
|
+
* Recover queue items orphaned in 'processing' by a dead predecessor
|
|
15046
|
+
* instance.
|
|
15047
|
+
*
|
|
15048
|
+
* processNext marks an item 'processing' before executing it. If the DO is
|
|
15049
|
+
* evicted or a deploy restarts it mid-execution, that row used to stay
|
|
15050
|
+
* 'processing' FOREVER — only 'pending' rows are ever picked up again — so
|
|
15051
|
+
* queued subagent executions, queued messages, and scheduled effects that
|
|
15052
|
+
* were mid-flight at a restart were silently lost (a subagent that "just
|
|
15053
|
+
* died half way through" with nothing to revive it).
|
|
15054
|
+
*
|
|
15055
|
+
* MUST ONLY be called from the once-per-instance wake path (DurableThread's
|
|
15056
|
+
* alarmQueueRearmed guard): at that moment this fresh instance is executing
|
|
15057
|
+
* nothing, so ANY 'processing' row provably belongs to a dead predecessor.
|
|
15058
|
+
* Calling it at any other time would re-queue items that are legitimately
|
|
15059
|
+
* mid-execution on this very instance.
|
|
15060
|
+
*/
|
|
15061
|
+
async recoverOrphanedProcessing(maxAttempts = 3) {
|
|
15062
|
+
const now = Date.now() * 1e3;
|
|
15063
|
+
await this.sql.exec(
|
|
15064
|
+
`UPDATE alarm_queue
|
|
15065
|
+
SET status = 'failed',
|
|
15066
|
+
error = 'Lost mid-execution (the executor restarted) and retry attempts were exhausted.',
|
|
15067
|
+
completed_at = ?
|
|
15068
|
+
WHERE status = 'processing' AND attempts >= ?`,
|
|
15069
|
+
now,
|
|
15070
|
+
maxAttempts
|
|
15071
|
+
);
|
|
15072
|
+
await this.sql.exec(
|
|
15073
|
+
`UPDATE alarm_queue SET status = 'pending' WHERE status = 'processing'`
|
|
15074
|
+
);
|
|
15075
|
+
}
|
|
15026
15076
|
// ============================================================
|
|
15027
15077
|
// Effect-specific Methods
|
|
15028
15078
|
// ============================================================
|
|
@@ -15287,6 +15337,7 @@ var AlarmQueue = class {
|
|
|
15287
15337
|
|
|
15288
15338
|
// src/durable-objects/DurableThread.ts
|
|
15289
15339
|
init_StreamManager();
|
|
15340
|
+
init_LLMRequest();
|
|
15290
15341
|
|
|
15291
15342
|
// src/packing/resolver.ts
|
|
15292
15343
|
var NamespaceResolutionError = class extends Error {
|
|
@@ -15774,6 +15825,18 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
15774
15825
|
alarmQueueRearmed = false;
|
|
15775
15826
|
currentAbortController = null;
|
|
15776
15827
|
isExecuting = false;
|
|
15828
|
+
// ── Execution watchdog state (see watchdogCheck) ─────────────────────────
|
|
15829
|
+
// In-memory heartbeat: bumped by noteExecutionProgress() on every streamed
|
|
15830
|
+
// chunk and request attempt. Deliberately NOT persisted — after a DO restart
|
|
15831
|
+
// the isExecuting flag alone proves the executor is gone, and while the DO
|
|
15832
|
+
// is alive the in-memory value is authoritative. Zero hot-path storage cost.
|
|
15833
|
+
lastProgressAt = 0;
|
|
15834
|
+
// Consecutive watchdog firings that found a live-but-silent executor. Strike
|
|
15835
|
+
// one aborts the in-flight request; strike two forces takeover.
|
|
15836
|
+
watchdogWedgeStrikes = 0;
|
|
15837
|
+
watchdogIntervalMs = 3e4;
|
|
15838
|
+
watchdogResumeWindowMs = 15 * 60 * 1e3;
|
|
15839
|
+
watchdogMaxResumesPerWindow = 2;
|
|
15777
15840
|
codeExecutionBridges = /* @__PURE__ */ new Map();
|
|
15778
15841
|
constructor(ctx, env) {
|
|
15779
15842
|
super(ctx, env);
|
|
@@ -15809,6 +15872,9 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
15809
15872
|
case "continueExecution":
|
|
15810
15873
|
await this.doContinueExecution(args.threadId, args.side);
|
|
15811
15874
|
break;
|
|
15875
|
+
case "watchdogCheck":
|
|
15876
|
+
await this.watchdogCheck(args.threadId);
|
|
15877
|
+
break;
|
|
15812
15878
|
default:
|
|
15813
15879
|
throw new Error(`Unknown method: ${method}`);
|
|
15814
15880
|
}
|
|
@@ -16929,6 +16995,9 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
16929
16995
|
}
|
|
16930
16996
|
this.migratedToVersion = LATEST_SCHEMA_VERSION;
|
|
16931
16997
|
if (!this.alarmQueueRearmed && typeof this.alarmQueue.ensureAlarmScheduled === "function") {
|
|
16998
|
+
if (typeof this.alarmQueue.recoverOrphanedProcessing === "function") {
|
|
16999
|
+
await this.alarmQueue.recoverOrphanedProcessing();
|
|
17000
|
+
}
|
|
16932
17001
|
await this.alarmQueue.ensureAlarmScheduled();
|
|
16933
17002
|
this.alarmQueueRearmed = true;
|
|
16934
17003
|
}
|
|
@@ -19561,6 +19630,234 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19561
19630
|
console.error("[DurableThread] Alarm handler failed:", error);
|
|
19562
19631
|
}
|
|
19563
19632
|
}
|
|
19633
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
19634
|
+
// Execution watchdog
|
|
19635
|
+
//
|
|
19636
|
+
// The in-flight stream guards (LLMRequest's first-chunk/stall timeouts) live
|
|
19637
|
+
// inside the execution's promise chain — they die with the DO. A deploy or
|
|
19638
|
+
// eviction mid-request therefore used to leave a durable log row stuck at
|
|
19639
|
+
// is_complete = 0 forever: the thread looked "working", queued messages
|
|
19640
|
+
// never played, and nothing woke it (finalizeOrphanedLogs only runs at the
|
|
19641
|
+
// START of the next execution). Persisted DO alarms are the one timer that
|
|
19642
|
+
// survives restarts and fires without inbound traffic, so a standing
|
|
19643
|
+
// per-thread watchdog alarm re-checks the thread's CURRENT state every 30s
|
|
19644
|
+
// while work is active.
|
|
19645
|
+
//
|
|
19646
|
+
// Design invariants (do not weaken):
|
|
19647
|
+
// - Progress, never duration: only silence ages. A response can stream for
|
|
19648
|
+
// ten minutes; every chunk bumps the in-memory heartbeat.
|
|
19649
|
+
// - Per-thread, not per-request: the check carries no request identity. By
|
|
19650
|
+
// the time it fires the thread may be several requests further along —
|
|
19651
|
+
// it always evaluates fresh SQL + live flags, never a stale snapshot.
|
|
19652
|
+
// - Strictly second line: the staleness ceiling is DERIVED from the stream
|
|
19653
|
+
// timeouts (resolveWatchdogStaleCeilingMs = max(first-chunk, stall) + 60s)
|
|
19654
|
+
// so the in-flight guards always get to act first. The watchdog only
|
|
19655
|
+
// moves when the guard that should have fired didn't.
|
|
19656
|
+
// - Restart proof: isExecuting is volatile; an incomplete log with
|
|
19657
|
+
// isExecuting === false is PROOF the executor died (executions cannot
|
|
19658
|
+
// survive a restart). No heartbeat needed for that branch.
|
|
19659
|
+
// - Nonce re-read: every await inside the check is a yield point; state is
|
|
19660
|
+
// re-read immediately before acting so a healthy thread racing the
|
|
19661
|
+
// watchdog always wins.
|
|
19662
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
19663
|
+
/** Heartbeat: called by LLMRequest on each attempt and streamed chunk. */
|
|
19664
|
+
noteExecutionProgress() {
|
|
19665
|
+
this.lastProgressAt = Date.now();
|
|
19666
|
+
}
|
|
19667
|
+
/** Arm the standing watchdog for an execution start. One pending row max. */
|
|
19668
|
+
async armExecutionWatchdog(threadId) {
|
|
19669
|
+
try {
|
|
19670
|
+
this.lastProgressAt = Date.now();
|
|
19671
|
+
this.watchdogWedgeStrikes = 0;
|
|
19672
|
+
const pending = (await this.ctx.storage.sql.exec(
|
|
19673
|
+
`SELECT id FROM alarm_queue WHERE method = 'watchdogCheck' AND status = 'pending' LIMIT 1`
|
|
19674
|
+
)).toArray();
|
|
19675
|
+
if (pending.length > 0) return;
|
|
19676
|
+
await this.alarmQueue.enqueue("watchdogCheck", { threadId }, this.watchdogIntervalMs);
|
|
19677
|
+
} catch (error) {
|
|
19678
|
+
console.error("[DurableThread] Failed to arm execution watchdog (non-fatal):", error);
|
|
19679
|
+
}
|
|
19680
|
+
}
|
|
19681
|
+
/** Disarm on clean execution end. Completed check rows are noise, not history. */
|
|
19682
|
+
async disarmExecutionWatchdog() {
|
|
19683
|
+
try {
|
|
19684
|
+
await this.ctx.storage.sql.exec(
|
|
19685
|
+
`DELETE FROM alarm_queue WHERE method = 'watchdogCheck' AND status IN ('pending', 'completed')`
|
|
19686
|
+
);
|
|
19687
|
+
await this.alarmQueue.ensureAlarmScheduled();
|
|
19688
|
+
} catch (error) {
|
|
19689
|
+
console.error("[DurableThread] Failed to disarm execution watchdog (non-fatal):", error);
|
|
19690
|
+
}
|
|
19691
|
+
}
|
|
19692
|
+
/**
|
|
19693
|
+
* The standing check. Evaluates the thread's state AT FIRE TIME — it knows
|
|
19694
|
+
* nothing about whichever request was in flight when it was armed.
|
|
19695
|
+
*/
|
|
19696
|
+
async watchdogCheck(threadId) {
|
|
19697
|
+
const rearm = () => this.alarmQueue.enqueue("watchdogCheck", { threadId }, this.watchdogIntervalMs);
|
|
19698
|
+
try {
|
|
19699
|
+
await this.ctx.storage.sql.exec(
|
|
19700
|
+
`DELETE FROM alarm_queue WHERE method = 'watchdogCheck' AND status = 'completed'`
|
|
19701
|
+
);
|
|
19702
|
+
const readIncomplete = async () => (await this.ctx.storage.sql.exec(
|
|
19703
|
+
`SELECT id FROM logs WHERE is_complete = 0`
|
|
19704
|
+
)).toArray();
|
|
19705
|
+
let incomplete = await readIncomplete();
|
|
19706
|
+
const ceiling = resolveWatchdogStaleCeilingMs(this.env);
|
|
19707
|
+
const silenceMs = this.lastProgressAt === 0 ? Number.POSITIVE_INFINITY : Date.now() - this.lastProgressAt;
|
|
19708
|
+
if (this.isExecuting) {
|
|
19709
|
+
if (incomplete.length === 0) {
|
|
19710
|
+
this.watchdogWedgeStrikes = 0;
|
|
19711
|
+
await rearm();
|
|
19712
|
+
return;
|
|
19713
|
+
}
|
|
19714
|
+
if (silenceMs <= ceiling) {
|
|
19715
|
+
this.watchdogWedgeStrikes = 0;
|
|
19716
|
+
await rearm();
|
|
19717
|
+
return;
|
|
19718
|
+
}
|
|
19719
|
+
this.watchdogWedgeStrikes++;
|
|
19720
|
+
if (this.watchdogWedgeStrikes < 2) {
|
|
19721
|
+
console.warn(
|
|
19722
|
+
`[DurableThread] Watchdog: execution silent for ${Math.round(silenceMs / 1e3)}s (ceiling ${Math.round(ceiling / 1e3)}s) \u2014 observing; takeover next cycle if still silent`
|
|
19723
|
+
);
|
|
19724
|
+
await rearm();
|
|
19725
|
+
return;
|
|
19726
|
+
}
|
|
19727
|
+
console.error(
|
|
19728
|
+
`[DurableThread] Watchdog: wedged execution (${Math.round(silenceMs / 1e3)}s silent across two cycles) \u2014 forcing takeover`
|
|
19729
|
+
);
|
|
19730
|
+
try {
|
|
19731
|
+
this.currentAbortController?.abort();
|
|
19732
|
+
} catch {
|
|
19733
|
+
}
|
|
19734
|
+
this.isExecuting = false;
|
|
19735
|
+
this.currentAbortController = null;
|
|
19736
|
+
} else if (incomplete.length > 0 && silenceMs <= ceiling) {
|
|
19737
|
+
await rearm();
|
|
19738
|
+
return;
|
|
19739
|
+
}
|
|
19740
|
+
incomplete = await readIncomplete();
|
|
19741
|
+
if (this.isExecuting) {
|
|
19742
|
+
await rearm();
|
|
19743
|
+
return;
|
|
19744
|
+
}
|
|
19745
|
+
if (incomplete.length > 0) {
|
|
19746
|
+
await this.finalizeLostExecutionLogs(incomplete.map((row) => row.id));
|
|
19747
|
+
}
|
|
19748
|
+
const stopped = await this.shouldStop();
|
|
19749
|
+
const queued = (await this.ctx.storage.sql.exec(
|
|
19750
|
+
`SELECT COUNT(*) AS c FROM message_queue`
|
|
19751
|
+
)).toArray()[0]?.c ?? 0;
|
|
19752
|
+
if (stopped || incomplete.length === 0 && queued === 0) {
|
|
19753
|
+
await this.disarmExecutionWatchdog();
|
|
19754
|
+
return;
|
|
19755
|
+
}
|
|
19756
|
+
if (await this.consumeWatchdogResumeBudget()) {
|
|
19757
|
+
const side = await this.getLatestQueuedMessageSide() ?? "a";
|
|
19758
|
+
await this.alarmQueue.enqueue("continueExecution", { threadId, side }, 0);
|
|
19759
|
+
console.warn(
|
|
19760
|
+
`[DurableThread] Watchdog: lost execution recovered \u2014 resuming thread ${threadId}`
|
|
19761
|
+
);
|
|
19762
|
+
} else {
|
|
19763
|
+
console.error(
|
|
19764
|
+
`[DurableThread] Watchdog: resume budget exhausted for thread ${threadId} \u2014 leaving thread stopped`
|
|
19765
|
+
);
|
|
19766
|
+
await this.notifyParentOfLostExecution(threadId);
|
|
19767
|
+
}
|
|
19768
|
+
await this.disarmExecutionWatchdog();
|
|
19769
|
+
} catch (error) {
|
|
19770
|
+
console.error("[DurableThread] Watchdog check failed (non-fatal):", error);
|
|
19771
|
+
try {
|
|
19772
|
+
await rearm();
|
|
19773
|
+
} catch {
|
|
19774
|
+
}
|
|
19775
|
+
}
|
|
19776
|
+
}
|
|
19777
|
+
/** Mark logs the dead executor abandoned, and tell connected clients. */
|
|
19778
|
+
async finalizeLostExecutionLogs(logIds) {
|
|
19779
|
+
const error = "Request lost \u2014 the executor restarted (deploy or eviction) or hung before finishing. The thread watchdog finalized it.";
|
|
19780
|
+
const errorType = "execution_lost";
|
|
19781
|
+
await this.ctx.storage.sql.exec(
|
|
19782
|
+
`UPDATE logs SET is_complete = 1, error = ?1, error_type = ?2 WHERE is_complete = 0`,
|
|
19783
|
+
error,
|
|
19784
|
+
errorType
|
|
19785
|
+
);
|
|
19786
|
+
for (const id of logIds) {
|
|
19787
|
+
this.broadcastLog({
|
|
19788
|
+
type: "log_data",
|
|
19789
|
+
log_id: id,
|
|
19790
|
+
data: { id, is_complete: true, error, error_type: errorType }
|
|
19791
|
+
});
|
|
19792
|
+
}
|
|
19793
|
+
}
|
|
19794
|
+
/**
|
|
19795
|
+
* A subagent whose execution the watchdog gave up on leaves its parent
|
|
19796
|
+
* waiting forever for a completion message. Deliver the failure the same way
|
|
19797
|
+
* a sessionFail would, so the parent's turn wakes and can route around it.
|
|
19798
|
+
* Explicit-communication children (e.g. the compaction agent) stay quiet by
|
|
19799
|
+
* contract — only the registry status updates for those.
|
|
19800
|
+
*/
|
|
19801
|
+
async notifyParentOfLostExecution(threadId) {
|
|
19802
|
+
try {
|
|
19803
|
+
const agentBuilderId = this.env.AGENT_BUILDER.idFromName("singleton");
|
|
19804
|
+
const agentBuilder = this.env.AGENT_BUILDER.get(agentBuilderId);
|
|
19805
|
+
const meta = await agentBuilder.getThread(threadId);
|
|
19806
|
+
const parentThreadId = meta?.parent ?? null;
|
|
19807
|
+
if (!parentThreadId) return;
|
|
19808
|
+
const parentId = this.env.AGENT_BUILDER_THREAD.idFromName(parentThreadId);
|
|
19809
|
+
const parentStub = this.env.AGENT_BUILDER_THREAD.get(parentId);
|
|
19810
|
+
const parentCommunication = await this.getParentCommunicationModeForChild(
|
|
19811
|
+
parentStub,
|
|
19812
|
+
parentThreadId,
|
|
19813
|
+
threadId
|
|
19814
|
+
);
|
|
19815
|
+
if (parentCommunication !== "explicit") {
|
|
19816
|
+
await parentStub.queueMessage(parentThreadId, {
|
|
19817
|
+
role: "user",
|
|
19818
|
+
content: `Subagent (reference: ${threadId}) has reported a failure:
|
|
19819
|
+
|
|
19820
|
+
Its execution was lost (the executor restarted or hung) and automatic recovery was exhausted. Treat this subagent's task as failed \u2014 do not wait for it; retry the work another way or report the failure.`,
|
|
19821
|
+
attachments: [],
|
|
19822
|
+
silent: true,
|
|
19823
|
+
metadata: { subagent_id: threadId }
|
|
19824
|
+
});
|
|
19825
|
+
}
|
|
19826
|
+
await parentStub.updateChildRegistryStatus(parentThreadId, threadId, "idle");
|
|
19827
|
+
} catch (error) {
|
|
19828
|
+
console.error(
|
|
19829
|
+
"[DurableThread] Watchdog: failed to notify parent of lost execution (non-fatal):",
|
|
19830
|
+
error
|
|
19831
|
+
);
|
|
19832
|
+
}
|
|
19833
|
+
}
|
|
19834
|
+
/** At most N auto-resumes per rolling window, tracked durably. */
|
|
19835
|
+
async consumeWatchdogResumeBudget() {
|
|
19836
|
+
const now = Date.now();
|
|
19837
|
+
let count = 0;
|
|
19838
|
+
let windowStart = now;
|
|
19839
|
+
const row = (await this.ctx.storage.sql.exec(
|
|
19840
|
+
`SELECT value FROM execution_state WHERE key = 'watchdog_resume_state'`
|
|
19841
|
+
)).toArray()[0];
|
|
19842
|
+
if (row) {
|
|
19843
|
+
try {
|
|
19844
|
+
const parsed = JSON.parse(row.value);
|
|
19845
|
+
if (typeof parsed.count === "number" && typeof parsed.windowStart === "number" && now - parsed.windowStart < this.watchdogResumeWindowMs) {
|
|
19846
|
+
count = parsed.count;
|
|
19847
|
+
windowStart = parsed.windowStart;
|
|
19848
|
+
}
|
|
19849
|
+
} catch {
|
|
19850
|
+
}
|
|
19851
|
+
}
|
|
19852
|
+
if (count >= this.watchdogMaxResumesPerWindow) return false;
|
|
19853
|
+
const value = JSON.stringify({ count: count + 1, windowStart });
|
|
19854
|
+
await this.ctx.storage.sql.exec(
|
|
19855
|
+
`INSERT INTO execution_state (key, value) VALUES ('watchdog_resume_state', ?1)
|
|
19856
|
+
ON CONFLICT(key) DO UPDATE SET value = ?1`,
|
|
19857
|
+
value
|
|
19858
|
+
);
|
|
19859
|
+
return true;
|
|
19860
|
+
}
|
|
19564
19861
|
normalizePlainRecord(value) {
|
|
19565
19862
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
19566
19863
|
return null;
|
|
@@ -19586,6 +19883,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19586
19883
|
await this.assertNotTerminated();
|
|
19587
19884
|
this.isExecuting = true;
|
|
19588
19885
|
this.currentAbortController = new AbortController();
|
|
19886
|
+
await this.armExecutionWatchdog(threadId);
|
|
19589
19887
|
try {
|
|
19590
19888
|
const agentDef = await this.loadAgent(agentName);
|
|
19591
19889
|
const packageId = agentDef.__package?.packageId;
|
|
@@ -19716,6 +20014,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19716
20014
|
} finally {
|
|
19717
20015
|
this.isExecuting = false;
|
|
19718
20016
|
this.currentAbortController = null;
|
|
20017
|
+
await this.disarmExecutionWatchdog();
|
|
19719
20018
|
}
|
|
19720
20019
|
}
|
|
19721
20020
|
/**
|
|
@@ -19762,6 +20061,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19762
20061
|
}
|
|
19763
20062
|
this.isExecuting = true;
|
|
19764
20063
|
this.currentAbortController = new AbortController();
|
|
20064
|
+
await this.armExecutionWatchdog(threadId);
|
|
19765
20065
|
try {
|
|
19766
20066
|
const agentBuilderId = this.env.AGENT_BUILDER.idFromName("singleton");
|
|
19767
20067
|
const agentBuilder = this.env.AGENT_BUILDER.get(agentBuilderId);
|
|
@@ -19900,6 +20200,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19900
20200
|
} finally {
|
|
19901
20201
|
this.isExecuting = false;
|
|
19902
20202
|
this.currentAbortController = null;
|
|
20203
|
+
await this.disarmExecutionWatchdog();
|
|
19903
20204
|
}
|
|
19904
20205
|
}
|
|
19905
20206
|
/**
|
|
@@ -19910,6 +20211,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19910
20211
|
await this.assertNotTerminated();
|
|
19911
20212
|
this.isExecuting = true;
|
|
19912
20213
|
this.currentAbortController = new AbortController();
|
|
20214
|
+
await this.armExecutionWatchdog(threadId);
|
|
19913
20215
|
try {
|
|
19914
20216
|
const agentBuilderId = this.env.AGENT_BUILDER.idFromName("singleton");
|
|
19915
20217
|
const agentBuilder = this.env.AGENT_BUILDER.get(agentBuilderId);
|
|
@@ -20018,6 +20320,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
20018
20320
|
} finally {
|
|
20019
20321
|
this.isExecuting = false;
|
|
20020
20322
|
this.currentAbortController = null;
|
|
20323
|
+
await this.disarmExecutionWatchdog();
|
|
20021
20324
|
const latestQueuedSide = await this.getLatestQueuedMessageSide();
|
|
20022
20325
|
if (latestQueuedSide) {
|
|
20023
20326
|
await this.ensureQueuedMessageExecution(threadId, latestQueuedSide);
|