@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
|
@@ -257,6 +257,7 @@ interface ThreadInstance {
|
|
|
257
257
|
error?: string;
|
|
258
258
|
}>;
|
|
259
259
|
shouldStop(): Promise<boolean>;
|
|
260
|
+
noteExecutionProgress?(): void;
|
|
260
261
|
tools(): Record<string, () => Promise<NativeToolModule>>;
|
|
261
262
|
hooks(): HookRegistry;
|
|
262
263
|
getValue<T = unknown>(key: string): Promise<T | null>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ModelDefinition, AgentDefinition } from './runtime.js';
|
|
2
2
|
export { AgentBuilderEnv, AuthContext, AuthUser, BroadcastOptions, CodeExecutionBridge, DurableAgentBuilder, DurableThread, FlowStateSdk, FlowStateWithSdk, ImageContextConfig, InjectMessageOptions, LLMProviderInterface, NamespaceResolutionError, PromptContent, PromptDefinition, PromptIncludePart, PromptPart, PromptToolConfig, Provider, ProviderContentPart, ProviderFilePart, ProviderImagePart, ProviderRegistry, ProviderTextPart, SessionToolBinding, SessionToolConfig, SideConfig, SubagentToolConfig, SubpromptConfig, ThreadRegistryEntry, ToolConfig, UpdateThreadParams, User, authenticate, buildImageDescription, cat, createNamespaceContext, emitThreadEvent, enhanceFlowState, exists, find, forceTurn, generateImageDescription, getFileStats, getMessages, getMessagesToSummarize, getShortName, getThumbnail, getUnsummarizedImageAttachments, getVisibleAgentNames, getVisibleModelNames, getVisiblePromptNames, getVisibleToolNames, grep, hasImageAttachments, head, injectMessage, isQualifiedName, linkFile, mkdir, move, optimizeImageContext, parseQualifiedName, qualifyName, queueTool, readFile, readdir, reloadHistory, rename, replaceImagesWithDescriptions, requireAdmin, requireAuth, resolveAgent, resolveHook, resolveModel, resolvePrompt, resolveTool, rmdir, stat, tail, unlink, updateThread, writeFile, writeImage } from './runtime.js';
|
|
3
3
|
export { AgentPluginOptions, agentbuilder, builder } from './plugin.js';
|
|
4
|
-
export { A as Agent, m as AttachmentRef, f as BuilderThreadEndpointHandler, B as Controller, a as ControllerContext, E as Env, l as FileRecord, n as FileStats, j as FlowResult, F as FlowState, G as GrepResult, p as ImageContentPart, I as ImageMetadata, L as LLMResponse, M as Message, r as MessageContent, q as MultimodalContent, R as RequestContext, S as StorageBackend, k as TelemetryEvent, o as TextContentPart, b as ThreadEndpointContext, e as ThreadEnv, g as ThreadInstance, h as ThreadMetadata, T as ToolCall, i as ToolResult, c as createThreadEndpointHandler, d as defineController } from './index-
|
|
4
|
+
export { A as Agent, m as AttachmentRef, f as BuilderThreadEndpointHandler, B as Controller, a as ControllerContext, E as Env, l as FileRecord, n as FileStats, j as FlowResult, F as FlowState, G as GrepResult, p as ImageContentPart, I as ImageMetadata, L as LLMResponse, M as Message, r as MessageContent, q as MultimodalContent, R as RequestContext, S as StorageBackend, k as TelemetryEvent, o as TextContentPart, b as ThreadEndpointContext, e as ThreadEnv, g as ThreadInstance, h as ThreadMetadata, T as ToolCall, i as ToolResult, c as createThreadEndpointHandler, d as defineController } from './index-CX9Ti98s.js';
|
|
5
5
|
export { AgentType, DefinitionLoader, GlobalNamespaceContext, HookSignatures, ImageContent, ModelCapabilities, ModelProvider, NamespaceContext, PackageSignature, PackedExports, PackedMeta, PackedMetadata, PackedNamespaceContext, PromptInput, PromptTextPart, ProviderAssistantMessage, ProviderConfigSlot, ProviderConfigSlots, ProviderConfigValueSource, ProviderDefinition, ProviderError, ProviderErrorCode, ProviderFactory, ProviderFactoryConfig, ProviderFinishReason, ProviderGeneratedImage, ProviderMessage, ProviderMessageContent, ProviderMethodOverrides, ModelCapabilities as ProviderModelCapabilities, ProviderOverride, ProviderOverrideContext, ProviderReasoningDetail, ProviderRequest, ProviderResponse, ProviderStreamChunk, ProviderSyncOverride, ProviderSystemMessage, ProviderTool, ProviderToolCallPart, ProviderToolMessage, ProviderToolResultContent, ProviderUsage, ProviderUserMessage, ReasoningConfig, StructuredPrompt, TextContent, Tool, ToolArgs, ToolArgsNode, ToolArgsRawShape, ToolContent, belongsToPackage, defineAgent, defineHook, defineModel, definePrompt, defineProvider, defineTool, isPacked, isVisibleInNamespace, mapReasoningLevel, providerConst, providerEnv, providerValue } from '@standardagents/spec';
|
|
6
6
|
export { C as ConstituentItem, D as DiscoveredPackage, N as NamespacedRegistry, P as PackingAnalysis, a as PackingOptions, b as PackingResult, c as UnpackOptions, d as UnpackResult } from './types-Bpe7IANZ.js';
|
|
7
7
|
export { D as DiscoveryConfig, P as PackageDiscoveryService, d as discoverPackages } from './discovery-DiMJWisl.js';
|
package/dist/index.js
CHANGED
|
@@ -1066,6 +1066,19 @@ function resolveStreamTimeoutMs(env, key, fallback) {
|
|
|
1066
1066
|
const parsed = typeof raw === "number" ? raw : typeof raw === "string" ? Number.parseInt(raw, 10) : NaN;
|
|
1067
1067
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1068
1068
|
}
|
|
1069
|
+
function resolveWatchdogStaleCeilingMs(env) {
|
|
1070
|
+
const firstChunk = resolveStreamTimeoutMs(
|
|
1071
|
+
env,
|
|
1072
|
+
"STANDARD_AGENTS_STREAM_FIRST_CHUNK_TIMEOUT_MS",
|
|
1073
|
+
STREAM_FIRST_CHUNK_TIMEOUT_MS
|
|
1074
|
+
);
|
|
1075
|
+
const stall = resolveStreamTimeoutMs(
|
|
1076
|
+
env,
|
|
1077
|
+
"STANDARD_AGENTS_STREAM_STALL_TIMEOUT_MS",
|
|
1078
|
+
STREAM_STALL_TIMEOUT_MS
|
|
1079
|
+
);
|
|
1080
|
+
return Math.max(firstChunk, stall) + 6e4;
|
|
1081
|
+
}
|
|
1069
1082
|
function modelSupportsImageInput(modelDef) {
|
|
1070
1083
|
const supportsImages = modelDef.capabilities?.supportsImages;
|
|
1071
1084
|
if (typeof supportsImages === "boolean") {
|
|
@@ -1578,6 +1591,7 @@ var init_LLMRequest = __esm({
|
|
|
1578
1591
|
attempts++;
|
|
1579
1592
|
try {
|
|
1580
1593
|
logId = await this.logRequest(state, actualModelId, context, modelDef, previousLogId);
|
|
1594
|
+
state.thread.instance.noteExecutionProgress?.();
|
|
1581
1595
|
state.emitTelemetry?.({
|
|
1582
1596
|
type: "llm_request",
|
|
1583
1597
|
model: actualModelId,
|
|
@@ -1844,6 +1858,7 @@ var init_LLMRequest = __esm({
|
|
|
1844
1858
|
}
|
|
1845
1859
|
if (next.done) break;
|
|
1846
1860
|
const chunk = next.value;
|
|
1861
|
+
state.thread.instance.noteExecutionProgress?.();
|
|
1847
1862
|
if (firstChunkAt === 0) {
|
|
1848
1863
|
firstChunkAt = Date.now();
|
|
1849
1864
|
emitLogPatch({ time_to_first_token_ms: firstChunkAt - requestSentAt });
|
|
@@ -2379,8 +2394,11 @@ var init_LLMRequest = __esm({
|
|
|
2379
2394
|
*/
|
|
2380
2395
|
static async logError(state, error, errorType, modelId, startTime, existingLogId) {
|
|
2381
2396
|
try {
|
|
2382
|
-
|
|
2397
|
+
let rawErrorMessage = error instanceof Error ? error.message : String(error);
|
|
2383
2398
|
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
2399
|
+
if (error instanceof Error && error.name === "AbortError" && /operation was aborted/i.test(rawErrorMessage)) {
|
|
2400
|
+
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).";
|
|
2401
|
+
}
|
|
2384
2402
|
let errorMessage = rawErrorMessage;
|
|
2385
2403
|
let parsedProviderBody = null;
|
|
2386
2404
|
const trimmedRaw = rawErrorMessage.trim();
|
|
@@ -23314,6 +23332,38 @@ var AlarmQueue = class {
|
|
|
23314
23332
|
async ensureAlarmScheduled() {
|
|
23315
23333
|
await this.rescheduleAlarm();
|
|
23316
23334
|
}
|
|
23335
|
+
/**
|
|
23336
|
+
* Recover queue items orphaned in 'processing' by a dead predecessor
|
|
23337
|
+
* instance.
|
|
23338
|
+
*
|
|
23339
|
+
* processNext marks an item 'processing' before executing it. If the DO is
|
|
23340
|
+
* evicted or a deploy restarts it mid-execution, that row used to stay
|
|
23341
|
+
* 'processing' FOREVER — only 'pending' rows are ever picked up again — so
|
|
23342
|
+
* queued subagent executions, queued messages, and scheduled effects that
|
|
23343
|
+
* were mid-flight at a restart were silently lost (a subagent that "just
|
|
23344
|
+
* died half way through" with nothing to revive it).
|
|
23345
|
+
*
|
|
23346
|
+
* MUST ONLY be called from the once-per-instance wake path (DurableThread's
|
|
23347
|
+
* alarmQueueRearmed guard): at that moment this fresh instance is executing
|
|
23348
|
+
* nothing, so ANY 'processing' row provably belongs to a dead predecessor.
|
|
23349
|
+
* Calling it at any other time would re-queue items that are legitimately
|
|
23350
|
+
* mid-execution on this very instance.
|
|
23351
|
+
*/
|
|
23352
|
+
async recoverOrphanedProcessing(maxAttempts = 3) {
|
|
23353
|
+
const now = Date.now() * 1e3;
|
|
23354
|
+
await this.sql.exec(
|
|
23355
|
+
`UPDATE alarm_queue
|
|
23356
|
+
SET status = 'failed',
|
|
23357
|
+
error = 'Lost mid-execution (the executor restarted) and retry attempts were exhausted.',
|
|
23358
|
+
completed_at = ?
|
|
23359
|
+
WHERE status = 'processing' AND attempts >= ?`,
|
|
23360
|
+
now,
|
|
23361
|
+
maxAttempts
|
|
23362
|
+
);
|
|
23363
|
+
await this.sql.exec(
|
|
23364
|
+
`UPDATE alarm_queue SET status = 'pending' WHERE status = 'processing'`
|
|
23365
|
+
);
|
|
23366
|
+
}
|
|
23317
23367
|
// ============================================================
|
|
23318
23368
|
// Effect-specific Methods
|
|
23319
23369
|
// ============================================================
|
|
@@ -23578,6 +23628,7 @@ var AlarmQueue = class {
|
|
|
23578
23628
|
|
|
23579
23629
|
// src/durable-objects/DurableThread.ts
|
|
23580
23630
|
init_StreamManager();
|
|
23631
|
+
init_LLMRequest();
|
|
23581
23632
|
|
|
23582
23633
|
// src/packing/resolver.ts
|
|
23583
23634
|
var NamespaceResolutionError = class extends Error {
|
|
@@ -24065,6 +24116,18 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
24065
24116
|
alarmQueueRearmed = false;
|
|
24066
24117
|
currentAbortController = null;
|
|
24067
24118
|
isExecuting = false;
|
|
24119
|
+
// ── Execution watchdog state (see watchdogCheck) ─────────────────────────
|
|
24120
|
+
// In-memory heartbeat: bumped by noteExecutionProgress() on every streamed
|
|
24121
|
+
// chunk and request attempt. Deliberately NOT persisted — after a DO restart
|
|
24122
|
+
// the isExecuting flag alone proves the executor is gone, and while the DO
|
|
24123
|
+
// is alive the in-memory value is authoritative. Zero hot-path storage cost.
|
|
24124
|
+
lastProgressAt = 0;
|
|
24125
|
+
// Consecutive watchdog firings that found a live-but-silent executor. Strike
|
|
24126
|
+
// one aborts the in-flight request; strike two forces takeover.
|
|
24127
|
+
watchdogWedgeStrikes = 0;
|
|
24128
|
+
watchdogIntervalMs = 3e4;
|
|
24129
|
+
watchdogResumeWindowMs = 15 * 60 * 1e3;
|
|
24130
|
+
watchdogMaxResumesPerWindow = 2;
|
|
24068
24131
|
codeExecutionBridges = /* @__PURE__ */ new Map();
|
|
24069
24132
|
constructor(ctx, env) {
|
|
24070
24133
|
super(ctx, env);
|
|
@@ -24100,6 +24163,9 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
24100
24163
|
case "continueExecution":
|
|
24101
24164
|
await this.doContinueExecution(args.threadId, args.side);
|
|
24102
24165
|
break;
|
|
24166
|
+
case "watchdogCheck":
|
|
24167
|
+
await this.watchdogCheck(args.threadId);
|
|
24168
|
+
break;
|
|
24103
24169
|
default:
|
|
24104
24170
|
throw new Error(`Unknown method: ${method}`);
|
|
24105
24171
|
}
|
|
@@ -25220,6 +25286,9 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
25220
25286
|
}
|
|
25221
25287
|
this.migratedToVersion = LATEST_SCHEMA_VERSION;
|
|
25222
25288
|
if (!this.alarmQueueRearmed && typeof this.alarmQueue.ensureAlarmScheduled === "function") {
|
|
25289
|
+
if (typeof this.alarmQueue.recoverOrphanedProcessing === "function") {
|
|
25290
|
+
await this.alarmQueue.recoverOrphanedProcessing();
|
|
25291
|
+
}
|
|
25223
25292
|
await this.alarmQueue.ensureAlarmScheduled();
|
|
25224
25293
|
this.alarmQueueRearmed = true;
|
|
25225
25294
|
}
|
|
@@ -27852,6 +27921,234 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27852
27921
|
console.error("[DurableThread] Alarm handler failed:", error);
|
|
27853
27922
|
}
|
|
27854
27923
|
}
|
|
27924
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
27925
|
+
// Execution watchdog
|
|
27926
|
+
//
|
|
27927
|
+
// The in-flight stream guards (LLMRequest's first-chunk/stall timeouts) live
|
|
27928
|
+
// inside the execution's promise chain — they die with the DO. A deploy or
|
|
27929
|
+
// eviction mid-request therefore used to leave a durable log row stuck at
|
|
27930
|
+
// is_complete = 0 forever: the thread looked "working", queued messages
|
|
27931
|
+
// never played, and nothing woke it (finalizeOrphanedLogs only runs at the
|
|
27932
|
+
// START of the next execution). Persisted DO alarms are the one timer that
|
|
27933
|
+
// survives restarts and fires without inbound traffic, so a standing
|
|
27934
|
+
// per-thread watchdog alarm re-checks the thread's CURRENT state every 30s
|
|
27935
|
+
// while work is active.
|
|
27936
|
+
//
|
|
27937
|
+
// Design invariants (do not weaken):
|
|
27938
|
+
// - Progress, never duration: only silence ages. A response can stream for
|
|
27939
|
+
// ten minutes; every chunk bumps the in-memory heartbeat.
|
|
27940
|
+
// - Per-thread, not per-request: the check carries no request identity. By
|
|
27941
|
+
// the time it fires the thread may be several requests further along —
|
|
27942
|
+
// it always evaluates fresh SQL + live flags, never a stale snapshot.
|
|
27943
|
+
// - Strictly second line: the staleness ceiling is DERIVED from the stream
|
|
27944
|
+
// timeouts (resolveWatchdogStaleCeilingMs = max(first-chunk, stall) + 60s)
|
|
27945
|
+
// so the in-flight guards always get to act first. The watchdog only
|
|
27946
|
+
// moves when the guard that should have fired didn't.
|
|
27947
|
+
// - Restart proof: isExecuting is volatile; an incomplete log with
|
|
27948
|
+
// isExecuting === false is PROOF the executor died (executions cannot
|
|
27949
|
+
// survive a restart). No heartbeat needed for that branch.
|
|
27950
|
+
// - Nonce re-read: every await inside the check is a yield point; state is
|
|
27951
|
+
// re-read immediately before acting so a healthy thread racing the
|
|
27952
|
+
// watchdog always wins.
|
|
27953
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
27954
|
+
/** Heartbeat: called by LLMRequest on each attempt and streamed chunk. */
|
|
27955
|
+
noteExecutionProgress() {
|
|
27956
|
+
this.lastProgressAt = Date.now();
|
|
27957
|
+
}
|
|
27958
|
+
/** Arm the standing watchdog for an execution start. One pending row max. */
|
|
27959
|
+
async armExecutionWatchdog(threadId) {
|
|
27960
|
+
try {
|
|
27961
|
+
this.lastProgressAt = Date.now();
|
|
27962
|
+
this.watchdogWedgeStrikes = 0;
|
|
27963
|
+
const pending = (await this.ctx.storage.sql.exec(
|
|
27964
|
+
`SELECT id FROM alarm_queue WHERE method = 'watchdogCheck' AND status = 'pending' LIMIT 1`
|
|
27965
|
+
)).toArray();
|
|
27966
|
+
if (pending.length > 0) return;
|
|
27967
|
+
await this.alarmQueue.enqueue("watchdogCheck", { threadId }, this.watchdogIntervalMs);
|
|
27968
|
+
} catch (error) {
|
|
27969
|
+
console.error("[DurableThread] Failed to arm execution watchdog (non-fatal):", error);
|
|
27970
|
+
}
|
|
27971
|
+
}
|
|
27972
|
+
/** Disarm on clean execution end. Completed check rows are noise, not history. */
|
|
27973
|
+
async disarmExecutionWatchdog() {
|
|
27974
|
+
try {
|
|
27975
|
+
await this.ctx.storage.sql.exec(
|
|
27976
|
+
`DELETE FROM alarm_queue WHERE method = 'watchdogCheck' AND status IN ('pending', 'completed')`
|
|
27977
|
+
);
|
|
27978
|
+
await this.alarmQueue.ensureAlarmScheduled();
|
|
27979
|
+
} catch (error) {
|
|
27980
|
+
console.error("[DurableThread] Failed to disarm execution watchdog (non-fatal):", error);
|
|
27981
|
+
}
|
|
27982
|
+
}
|
|
27983
|
+
/**
|
|
27984
|
+
* The standing check. Evaluates the thread's state AT FIRE TIME — it knows
|
|
27985
|
+
* nothing about whichever request was in flight when it was armed.
|
|
27986
|
+
*/
|
|
27987
|
+
async watchdogCheck(threadId) {
|
|
27988
|
+
const rearm = () => this.alarmQueue.enqueue("watchdogCheck", { threadId }, this.watchdogIntervalMs);
|
|
27989
|
+
try {
|
|
27990
|
+
await this.ctx.storage.sql.exec(
|
|
27991
|
+
`DELETE FROM alarm_queue WHERE method = 'watchdogCheck' AND status = 'completed'`
|
|
27992
|
+
);
|
|
27993
|
+
const readIncomplete = async () => (await this.ctx.storage.sql.exec(
|
|
27994
|
+
`SELECT id FROM logs WHERE is_complete = 0`
|
|
27995
|
+
)).toArray();
|
|
27996
|
+
let incomplete = await readIncomplete();
|
|
27997
|
+
const ceiling = resolveWatchdogStaleCeilingMs(this.env);
|
|
27998
|
+
const silenceMs = this.lastProgressAt === 0 ? Number.POSITIVE_INFINITY : Date.now() - this.lastProgressAt;
|
|
27999
|
+
if (this.isExecuting) {
|
|
28000
|
+
if (incomplete.length === 0) {
|
|
28001
|
+
this.watchdogWedgeStrikes = 0;
|
|
28002
|
+
await rearm();
|
|
28003
|
+
return;
|
|
28004
|
+
}
|
|
28005
|
+
if (silenceMs <= ceiling) {
|
|
28006
|
+
this.watchdogWedgeStrikes = 0;
|
|
28007
|
+
await rearm();
|
|
28008
|
+
return;
|
|
28009
|
+
}
|
|
28010
|
+
this.watchdogWedgeStrikes++;
|
|
28011
|
+
if (this.watchdogWedgeStrikes < 2) {
|
|
28012
|
+
console.warn(
|
|
28013
|
+
`[DurableThread] Watchdog: execution silent for ${Math.round(silenceMs / 1e3)}s (ceiling ${Math.round(ceiling / 1e3)}s) \u2014 observing; takeover next cycle if still silent`
|
|
28014
|
+
);
|
|
28015
|
+
await rearm();
|
|
28016
|
+
return;
|
|
28017
|
+
}
|
|
28018
|
+
console.error(
|
|
28019
|
+
`[DurableThread] Watchdog: wedged execution (${Math.round(silenceMs / 1e3)}s silent across two cycles) \u2014 forcing takeover`
|
|
28020
|
+
);
|
|
28021
|
+
try {
|
|
28022
|
+
this.currentAbortController?.abort();
|
|
28023
|
+
} catch {
|
|
28024
|
+
}
|
|
28025
|
+
this.isExecuting = false;
|
|
28026
|
+
this.currentAbortController = null;
|
|
28027
|
+
} else if (incomplete.length > 0 && silenceMs <= ceiling) {
|
|
28028
|
+
await rearm();
|
|
28029
|
+
return;
|
|
28030
|
+
}
|
|
28031
|
+
incomplete = await readIncomplete();
|
|
28032
|
+
if (this.isExecuting) {
|
|
28033
|
+
await rearm();
|
|
28034
|
+
return;
|
|
28035
|
+
}
|
|
28036
|
+
if (incomplete.length > 0) {
|
|
28037
|
+
await this.finalizeLostExecutionLogs(incomplete.map((row) => row.id));
|
|
28038
|
+
}
|
|
28039
|
+
const stopped = await this.shouldStop();
|
|
28040
|
+
const queued = (await this.ctx.storage.sql.exec(
|
|
28041
|
+
`SELECT COUNT(*) AS c FROM message_queue`
|
|
28042
|
+
)).toArray()[0]?.c ?? 0;
|
|
28043
|
+
if (stopped || incomplete.length === 0 && queued === 0) {
|
|
28044
|
+
await this.disarmExecutionWatchdog();
|
|
28045
|
+
return;
|
|
28046
|
+
}
|
|
28047
|
+
if (await this.consumeWatchdogResumeBudget()) {
|
|
28048
|
+
const side = await this.getLatestQueuedMessageSide() ?? "a";
|
|
28049
|
+
await this.alarmQueue.enqueue("continueExecution", { threadId, side }, 0);
|
|
28050
|
+
console.warn(
|
|
28051
|
+
`[DurableThread] Watchdog: lost execution recovered \u2014 resuming thread ${threadId}`
|
|
28052
|
+
);
|
|
28053
|
+
} else {
|
|
28054
|
+
console.error(
|
|
28055
|
+
`[DurableThread] Watchdog: resume budget exhausted for thread ${threadId} \u2014 leaving thread stopped`
|
|
28056
|
+
);
|
|
28057
|
+
await this.notifyParentOfLostExecution(threadId);
|
|
28058
|
+
}
|
|
28059
|
+
await this.disarmExecutionWatchdog();
|
|
28060
|
+
} catch (error) {
|
|
28061
|
+
console.error("[DurableThread] Watchdog check failed (non-fatal):", error);
|
|
28062
|
+
try {
|
|
28063
|
+
await rearm();
|
|
28064
|
+
} catch {
|
|
28065
|
+
}
|
|
28066
|
+
}
|
|
28067
|
+
}
|
|
28068
|
+
/** Mark logs the dead executor abandoned, and tell connected clients. */
|
|
28069
|
+
async finalizeLostExecutionLogs(logIds) {
|
|
28070
|
+
const error = "Request lost \u2014 the executor restarted (deploy or eviction) or hung before finishing. The thread watchdog finalized it.";
|
|
28071
|
+
const errorType = "execution_lost";
|
|
28072
|
+
await this.ctx.storage.sql.exec(
|
|
28073
|
+
`UPDATE logs SET is_complete = 1, error = ?1, error_type = ?2 WHERE is_complete = 0`,
|
|
28074
|
+
error,
|
|
28075
|
+
errorType
|
|
28076
|
+
);
|
|
28077
|
+
for (const id of logIds) {
|
|
28078
|
+
this.broadcastLog({
|
|
28079
|
+
type: "log_data",
|
|
28080
|
+
log_id: id,
|
|
28081
|
+
data: { id, is_complete: true, error, error_type: errorType }
|
|
28082
|
+
});
|
|
28083
|
+
}
|
|
28084
|
+
}
|
|
28085
|
+
/**
|
|
28086
|
+
* A subagent whose execution the watchdog gave up on leaves its parent
|
|
28087
|
+
* waiting forever for a completion message. Deliver the failure the same way
|
|
28088
|
+
* a sessionFail would, so the parent's turn wakes and can route around it.
|
|
28089
|
+
* Explicit-communication children (e.g. the compaction agent) stay quiet by
|
|
28090
|
+
* contract — only the registry status updates for those.
|
|
28091
|
+
*/
|
|
28092
|
+
async notifyParentOfLostExecution(threadId) {
|
|
28093
|
+
try {
|
|
28094
|
+
const agentBuilderId = this.env.AGENT_BUILDER.idFromName("singleton");
|
|
28095
|
+
const agentBuilder = this.env.AGENT_BUILDER.get(agentBuilderId);
|
|
28096
|
+
const meta = await agentBuilder.getThread(threadId);
|
|
28097
|
+
const parentThreadId = meta?.parent ?? null;
|
|
28098
|
+
if (!parentThreadId) return;
|
|
28099
|
+
const parentId = this.env.AGENT_BUILDER_THREAD.idFromName(parentThreadId);
|
|
28100
|
+
const parentStub = this.env.AGENT_BUILDER_THREAD.get(parentId);
|
|
28101
|
+
const parentCommunication = await this.getParentCommunicationModeForChild(
|
|
28102
|
+
parentStub,
|
|
28103
|
+
parentThreadId,
|
|
28104
|
+
threadId
|
|
28105
|
+
);
|
|
28106
|
+
if (parentCommunication !== "explicit") {
|
|
28107
|
+
await parentStub.queueMessage(parentThreadId, {
|
|
28108
|
+
role: "user",
|
|
28109
|
+
content: `Subagent (reference: ${threadId}) has reported a failure:
|
|
28110
|
+
|
|
28111
|
+
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.`,
|
|
28112
|
+
attachments: [],
|
|
28113
|
+
silent: true,
|
|
28114
|
+
metadata: { subagent_id: threadId }
|
|
28115
|
+
});
|
|
28116
|
+
}
|
|
28117
|
+
await parentStub.updateChildRegistryStatus(parentThreadId, threadId, "idle");
|
|
28118
|
+
} catch (error) {
|
|
28119
|
+
console.error(
|
|
28120
|
+
"[DurableThread] Watchdog: failed to notify parent of lost execution (non-fatal):",
|
|
28121
|
+
error
|
|
28122
|
+
);
|
|
28123
|
+
}
|
|
28124
|
+
}
|
|
28125
|
+
/** At most N auto-resumes per rolling window, tracked durably. */
|
|
28126
|
+
async consumeWatchdogResumeBudget() {
|
|
28127
|
+
const now = Date.now();
|
|
28128
|
+
let count = 0;
|
|
28129
|
+
let windowStart = now;
|
|
28130
|
+
const row = (await this.ctx.storage.sql.exec(
|
|
28131
|
+
`SELECT value FROM execution_state WHERE key = 'watchdog_resume_state'`
|
|
28132
|
+
)).toArray()[0];
|
|
28133
|
+
if (row) {
|
|
28134
|
+
try {
|
|
28135
|
+
const parsed = JSON.parse(row.value);
|
|
28136
|
+
if (typeof parsed.count === "number" && typeof parsed.windowStart === "number" && now - parsed.windowStart < this.watchdogResumeWindowMs) {
|
|
28137
|
+
count = parsed.count;
|
|
28138
|
+
windowStart = parsed.windowStart;
|
|
28139
|
+
}
|
|
28140
|
+
} catch {
|
|
28141
|
+
}
|
|
28142
|
+
}
|
|
28143
|
+
if (count >= this.watchdogMaxResumesPerWindow) return false;
|
|
28144
|
+
const value = JSON.stringify({ count: count + 1, windowStart });
|
|
28145
|
+
await this.ctx.storage.sql.exec(
|
|
28146
|
+
`INSERT INTO execution_state (key, value) VALUES ('watchdog_resume_state', ?1)
|
|
28147
|
+
ON CONFLICT(key) DO UPDATE SET value = ?1`,
|
|
28148
|
+
value
|
|
28149
|
+
);
|
|
28150
|
+
return true;
|
|
28151
|
+
}
|
|
27855
28152
|
normalizePlainRecord(value) {
|
|
27856
28153
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
27857
28154
|
return null;
|
|
@@ -27877,6 +28174,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27877
28174
|
await this.assertNotTerminated();
|
|
27878
28175
|
this.isExecuting = true;
|
|
27879
28176
|
this.currentAbortController = new AbortController();
|
|
28177
|
+
await this.armExecutionWatchdog(threadId);
|
|
27880
28178
|
try {
|
|
27881
28179
|
const agentDef = await this.loadAgent(agentName);
|
|
27882
28180
|
const packageId = agentDef.__package?.packageId;
|
|
@@ -28007,6 +28305,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
28007
28305
|
} finally {
|
|
28008
28306
|
this.isExecuting = false;
|
|
28009
28307
|
this.currentAbortController = null;
|
|
28308
|
+
await this.disarmExecutionWatchdog();
|
|
28010
28309
|
}
|
|
28011
28310
|
}
|
|
28012
28311
|
/**
|
|
@@ -28053,6 +28352,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
28053
28352
|
}
|
|
28054
28353
|
this.isExecuting = true;
|
|
28055
28354
|
this.currentAbortController = new AbortController();
|
|
28355
|
+
await this.armExecutionWatchdog(threadId);
|
|
28056
28356
|
try {
|
|
28057
28357
|
const agentBuilderId = this.env.AGENT_BUILDER.idFromName("singleton");
|
|
28058
28358
|
const agentBuilder = this.env.AGENT_BUILDER.get(agentBuilderId);
|
|
@@ -28191,6 +28491,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
28191
28491
|
} finally {
|
|
28192
28492
|
this.isExecuting = false;
|
|
28193
28493
|
this.currentAbortController = null;
|
|
28494
|
+
await this.disarmExecutionWatchdog();
|
|
28194
28495
|
}
|
|
28195
28496
|
}
|
|
28196
28497
|
/**
|
|
@@ -28201,6 +28502,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
28201
28502
|
await this.assertNotTerminated();
|
|
28202
28503
|
this.isExecuting = true;
|
|
28203
28504
|
this.currentAbortController = new AbortController();
|
|
28505
|
+
await this.armExecutionWatchdog(threadId);
|
|
28204
28506
|
try {
|
|
28205
28507
|
const agentBuilderId = this.env.AGENT_BUILDER.idFromName("singleton");
|
|
28206
28508
|
const agentBuilder = this.env.AGENT_BUILDER.get(agentBuilderId);
|
|
@@ -28309,6 +28611,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
28309
28611
|
} finally {
|
|
28310
28612
|
this.isExecuting = false;
|
|
28311
28613
|
this.currentAbortController = null;
|
|
28614
|
+
await this.disarmExecutionWatchdog();
|
|
28312
28615
|
const latestQueuedSide = await this.getLatestQueuedMessageSide();
|
|
28313
28616
|
if (latestQueuedSide) {
|
|
28314
28617
|
await this.ensureQueuedMessageExecution(threadId, latestQueuedSide);
|