aiden-runtime 4.1.5 → 4.6.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/README.md +265 -847
- package/dist/api/server.js +32 -5
- package/dist/cli/v4/aidenCLI.js +536 -152
- package/dist/cli/v4/callbacks.js +170 -0
- package/dist/cli/v4/chatSession.js +245 -3
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
- package/dist/cli/v4/commands/browserDepth.js +45 -0
- package/dist/cli/v4/commands/cron.js +264 -0
- package/dist/cli/v4/commands/daemon.js +541 -0
- package/dist/cli/v4/commands/daemonStatus.js +253 -0
- package/dist/cli/v4/commands/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +13 -0
- package/dist/cli/v4/commands/index.js +35 -1
- package/dist/cli/v4/commands/mcp.js +80 -54
- package/dist/cli/v4/commands/plannerGuard.js +53 -0
- package/dist/cli/v4/commands/recovery.js +122 -0
- package/dist/cli/v4/commands/runs.js +223 -0
- package/dist/cli/v4/commands/sandbox.js +48 -0
- package/dist/cli/v4/commands/spawnPause.js +93 -0
- package/dist/cli/v4/commands/suggestions.js +68 -0
- package/dist/cli/v4/commands/tce.js +41 -0
- package/dist/cli/v4/commands/trigger.js +378 -0
- package/dist/cli/v4/commands/update.js +95 -3
- package/dist/cli/v4/daemonAgentBuilder.js +145 -0
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/cli/v4/display/capabilityCard.js +26 -0
- package/dist/cli/v4/display.js +18 -8
- package/dist/cli/v4/replyRenderer.js +31 -23
- package/dist/cli/v4/updateBootPrompt.js +170 -0
- package/dist/core/playwrightBridge.js +129 -0
- package/dist/core/v4/aidenAgent.js +527 -5
- package/dist/core/v4/browserState.js +436 -0
- package/dist/core/v4/checkpoint.js +79 -0
- package/dist/core/v4/daemon/bootstrap.js +651 -0
- package/dist/core/v4/daemon/cleanShutdown.js +154 -0
- package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
- package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
- package/dist/core/v4/daemon/cron/migration.js +199 -0
- package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
- package/dist/core/v4/daemon/daemonConfig.js +90 -0
- package/dist/core/v4/daemon/db/connection.js +106 -0
- package/dist/core/v4/daemon/db/migrations.js +362 -0
- package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
- package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
- package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
- package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
- package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
- package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
- package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
- package/dist/core/v4/daemon/dispatcher/index.js +53 -0
- package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
- package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
- package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
- package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
- package/dist/core/v4/daemon/drain.js +156 -0
- package/dist/core/v4/daemon/eventLoopLag.js +73 -0
- package/dist/core/v4/daemon/health.js +159 -0
- package/dist/core/v4/daemon/idempotencyStore.js +204 -0
- package/dist/core/v4/daemon/index.js +179 -0
- package/dist/core/v4/daemon/instanceTracker.js +99 -0
- package/dist/core/v4/daemon/resourceRegistry.js +150 -0
- package/dist/core/v4/daemon/restartCode.js +32 -0
- package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
- package/dist/core/v4/daemon/runStore.js +144 -0
- package/dist/core/v4/daemon/runtimeLock.js +167 -0
- package/dist/core/v4/daemon/signals.js +50 -0
- package/dist/core/v4/daemon/supervisor.js +272 -0
- package/dist/core/v4/daemon/triggerBus.js +279 -0
- package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
- package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
- package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
- package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
- package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
- package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
- package/dist/core/v4/daemon/triggers/email/index.js +332 -0
- package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
- package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
- package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
- package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
- package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
- package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
- package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
- package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
- package/dist/core/v4/daemon/triggers/webhook.js +376 -0
- package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
- package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
- package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
- package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
- package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
- package/dist/core/v4/daemon/types.js +15 -0
- package/dist/core/v4/dockerSession.js +461 -0
- package/dist/core/v4/dryRun.js +117 -0
- package/dist/core/v4/failureClassifier.js +779 -0
- package/dist/core/v4/providerFallback.js +35 -2
- package/dist/core/v4/recoveryReport.js +449 -0
- package/dist/core/v4/runtimeToggles.js +214 -0
- package/dist/core/v4/sandboxConfig.js +285 -0
- package/dist/core/v4/sandboxFs.js +316 -0
- package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
- package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
- package/dist/core/v4/subagent/childBuilder.js +391 -0
- package/dist/core/v4/subagent/fanout.js +75 -51
- package/dist/core/v4/subagent/spawnPause.js +191 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
- package/dist/core/v4/suggestionCatalog.js +41 -0
- package/dist/core/v4/suggestionEngine.js +210 -0
- package/dist/core/v4/toolRegistry.js +37 -3
- package/dist/core/v4/turnState.js +587 -0
- package/dist/core/v4/update/checkUpdate.js +63 -3
- package/dist/core/v4/update/installMethodDetect.js +115 -0
- package/dist/core/v4/update/registryClient.js +121 -0
- package/dist/core/v4/update/skipState.js +75 -0
- package/dist/core/v4/verifier.js +448 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +29 -0
- package/dist/providers/v4/anthropicAdapter.js +31 -3
- package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
- package/dist/providers/v4/codexResponsesAdapter.js +25 -2
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
- package/dist/tools/v4/browser/_observer.js +224 -0
- package/dist/tools/v4/browser/browserBlocker.js +396 -0
- package/dist/tools/v4/browser/browserClick.js +18 -1
- package/dist/tools/v4/browser/browserClose.js +18 -1
- package/dist/tools/v4/browser/browserExtract.js +5 -1
- package/dist/tools/v4/browser/browserFill.js +17 -1
- package/dist/tools/v4/browser/browserGetUrl.js +5 -1
- package/dist/tools/v4/browser/browserNavigate.js +16 -1
- package/dist/tools/v4/browser/browserScreenshot.js +5 -1
- package/dist/tools/v4/browser/browserScroll.js +18 -1
- package/dist/tools/v4/browser/browserType.js +17 -1
- package/dist/tools/v4/browser/captchaCheck.js +5 -1
- package/dist/tools/v4/executeCode.js +1 -0
- package/dist/tools/v4/files/fileCopy.js +56 -2
- package/dist/tools/v4/files/fileDelete.js +38 -1
- package/dist/tools/v4/files/fileList.js +12 -1
- package/dist/tools/v4/files/fileMove.js +59 -2
- package/dist/tools/v4/files/filePatch.js +43 -1
- package/dist/tools/v4/files/fileRead.js +12 -1
- package/dist/tools/v4/files/fileWrite.js +41 -1
- package/dist/tools/v4/index.js +88 -61
- package/dist/tools/v4/memory/memoryAdd.js +14 -0
- package/dist/tools/v4/memory/memoryRemove.js +14 -0
- package/dist/tools/v4/memory/memoryReplace.js +15 -0
- package/dist/tools/v4/memory/sessionSummary.js +12 -0
- package/dist/tools/v4/process/processKill.js +19 -0
- package/dist/tools/v4/process/processList.js +1 -0
- package/dist/tools/v4/process/processLogRead.js +1 -0
- package/dist/tools/v4/process/processSpawn.js +13 -0
- package/dist/tools/v4/process/processWait.js +1 -0
- package/dist/tools/v4/sessions/recallSession.js +1 -0
- package/dist/tools/v4/sessions/sessionList.js +1 -0
- package/dist/tools/v4/sessions/sessionSearch.js +1 -0
- package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
- package/dist/tools/v4/skills/skillManage.js +13 -0
- package/dist/tools/v4/skills/skillView.js +1 -0
- package/dist/tools/v4/skills/skillsList.js +1 -0
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +54 -1
- package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
- package/dist/tools/v4/system/appClose.js +13 -0
- package/dist/tools/v4/system/appInput.js +13 -0
- package/dist/tools/v4/system/appLaunch.js +13 -0
- package/dist/tools/v4/system/clipboardRead.js +1 -0
- package/dist/tools/v4/system/clipboardWrite.js +14 -0
- package/dist/tools/v4/system/mediaKey.js +12 -0
- package/dist/tools/v4/system/mediaSessions.js +1 -0
- package/dist/tools/v4/system/mediaTransport.js +13 -0
- package/dist/tools/v4/system/naturalEvents.js +1 -0
- package/dist/tools/v4/system/nowPlaying.js +1 -0
- package/dist/tools/v4/system/osProcessList.js +1 -0
- package/dist/tools/v4/system/screenshot.js +1 -0
- package/dist/tools/v4/system/systemInfo.js +1 -0
- package/dist/tools/v4/system/volumeSet.js +17 -0
- package/dist/tools/v4/terminal/shellExec.js +81 -9
- package/dist/tools/v4/web/deepResearch.js +1 -0
- package/dist/tools/v4/web/openUrl.js +1 -0
- package/dist/tools/v4/web/webFetch.js +1 -0
- package/dist/tools/v4/web/webPage.js +1 -0
- package/dist/tools/v4/web/webSearch.js +1 -0
- package/dist/tools/v4/web/youtubeSearch.js +1 -0
- package/package.json +13 -3
|
@@ -36,21 +36,22 @@
|
|
|
36
36
|
* - destructive tool exposure: caller filters the schemas array
|
|
37
37
|
* based on `AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE`
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
* AidenAgent
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* v4.6 Phase 2Q-A — the orchestrator was previously decoupled from
|
|
40
|
+
* AidenAgent via a `runChild` callback (one closure per fanout call
|
|
41
|
+
* built the child agent). Post-2Q-A every child flows through the
|
|
42
|
+
* `spawnSubAgent` primitive, so the legacy `runChild` callback is
|
|
43
|
+
* gone (deleted in 2R). Behavioural test fakes inject a stub
|
|
44
|
+
* `SpawnSubAgentDeps` (real `toolRegistry` + mock provider) — see
|
|
45
|
+
* `tests/v4/subagent/fanout.behavioral.test.ts` for the pattern.
|
|
45
46
|
*/
|
|
46
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
48
|
exports.runFanout = runFanout;
|
|
48
|
-
const node_crypto_1 = require("node:crypto");
|
|
49
49
|
const factory_1 = require("../logger/factory");
|
|
50
50
|
const budget_1 = require("./budget");
|
|
51
51
|
const providerRotation_1 = require("./providerRotation");
|
|
52
52
|
const merger_1 = require("./merger");
|
|
53
53
|
const diagnostics_1 = require("./diagnostics");
|
|
54
|
+
const spawnSubAgent_1 = require("./spawnSubAgent");
|
|
54
55
|
// ── Orchestrator ─────────────────────────────────────────────────────────
|
|
55
56
|
async function runFanout(opts) {
|
|
56
57
|
const logger = (opts.logger ?? (0, factory_1.noopLogger)()).child('subagent');
|
|
@@ -103,19 +104,21 @@ async function runFanout(opts) {
|
|
|
103
104
|
const children = [];
|
|
104
105
|
for (let i = 0; i < opts.n; i += 1) {
|
|
105
106
|
const provider = rotation.assignments[i];
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const role = opts.mode === 'partition' ? opts.tasks[i].role : undefined;
|
|
110
|
-
children.push(spawnOne({
|
|
107
|
+
const task = opts.mode === 'partition' ? opts.tasks[i] : null;
|
|
108
|
+
const role = task?.role;
|
|
109
|
+
children.push(spawnViaPrimitive({
|
|
111
110
|
index: i,
|
|
112
|
-
|
|
111
|
+
query: opts.mode === 'ensemble' ? opts.query : task.goal,
|
|
112
|
+
context: task?.context,
|
|
113
113
|
role,
|
|
114
114
|
provider,
|
|
115
|
+
singleProviderWarning: rotation.singleProviderWarning,
|
|
115
116
|
maxIterations: budget.maxIterations,
|
|
116
117
|
perTimeoutMs: budget.perSubagentTimeoutMs,
|
|
117
118
|
wallSignal: wallController.signal,
|
|
118
|
-
|
|
119
|
+
spawnDeps: opts.spawnDeps,
|
|
120
|
+
parentRunId: opts.parentRunId,
|
|
121
|
+
parentSessionId: opts.parentSessionId,
|
|
119
122
|
logger: logger.child(`#${i}:${provider.providerId}`),
|
|
120
123
|
now,
|
|
121
124
|
}));
|
|
@@ -157,60 +160,81 @@ async function runFanout(opts) {
|
|
|
157
160
|
});
|
|
158
161
|
return { results, merged: merge.merged, diagnostics };
|
|
159
162
|
}
|
|
160
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Spawn one child via the `spawnSubAgent` primitive and adapt its
|
|
165
|
+
* envelope to the merger's `SubagentResult` shape. Centralises the
|
|
166
|
+
* fanout-layer → primitive-layer conversion in one place — every
|
|
167
|
+
* call site goes through here so a future envelope-shape change
|
|
168
|
+
* has a single edit point.
|
|
169
|
+
*
|
|
170
|
+
* Envelope → SubagentResult mapping:
|
|
171
|
+
* - `envelope.summary` → `output` (empty string on failure;
|
|
172
|
+
* the merger uses `output.length === 0` for the failed test).
|
|
173
|
+
* - `envelope.error` → `error` (undefined when ok).
|
|
174
|
+
* - `envelope.metrics.durationMs` → ignored; we capture wall-clock
|
|
175
|
+
* at this layer for diagnostics consistency with v4.1's shape.
|
|
176
|
+
*/
|
|
177
|
+
async function spawnViaPrimitive(args) {
|
|
161
178
|
const startedAt = args.now();
|
|
162
|
-
// Per-child controller, aborted on wall-cap OR per-child timeout.
|
|
163
|
-
const childController = new AbortController();
|
|
164
|
-
const timer = setTimeout(() => childController.abort(), args.perTimeoutMs);
|
|
165
|
-
const wallHandler = () => childController.abort();
|
|
166
|
-
if (args.wallSignal.aborted)
|
|
167
|
-
childController.abort();
|
|
168
|
-
else
|
|
169
|
-
args.wallSignal.addEventListener('abort', wallHandler, { once: true });
|
|
170
|
-
const id = (0, node_crypto_1.randomUUID)();
|
|
171
179
|
args.logger.info('child: spawned', {
|
|
172
|
-
id,
|
|
173
180
|
provider: `${args.provider.providerId}:${args.provider.modelId}`,
|
|
174
181
|
role: args.role,
|
|
175
182
|
timeoutMs: args.perTimeoutMs,
|
|
176
183
|
});
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
// v4.6 Phase 2Q-A-FIX — only forward the per-spawn provider
|
|
185
|
+
// override when rotation has real diversity (>= 2 distinct
|
|
186
|
+
// providerIds). For single-provider pools, every child would be
|
|
187
|
+
// assigned the same providerId, so the override path adds nothing
|
|
188
|
+
// — and worse, it trips 2P's `resolveChildProvider` rejection when
|
|
189
|
+
// the parent is a non-FallbackAdapter ("single-provider
|
|
190
|
+
// configuration" branch). Omitting it lets the child inherit the
|
|
191
|
+
// parent's adapter, which is the correct effective behavior.
|
|
192
|
+
const spec = {
|
|
193
|
+
goal: args.role ? `[role: ${args.role}] ${args.query}` : args.query,
|
|
194
|
+
context: args.context,
|
|
195
|
+
maxIterations: args.maxIterations,
|
|
196
|
+
timeoutMs: args.perTimeoutMs,
|
|
197
|
+
provider: args.singleProviderWarning ? undefined : args.provider.providerId,
|
|
198
|
+
};
|
|
199
|
+
let envelope;
|
|
179
200
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
provider: args.provider,
|
|
185
|
-
signal: childController.signal,
|
|
186
|
-
maxIterations: args.maxIterations,
|
|
187
|
-
logger: args.logger,
|
|
201
|
+
envelope = await (0, spawnSubAgent_1.spawnSubAgent)(spec, args.spawnDeps, {
|
|
202
|
+
signal: args.wallSignal,
|
|
203
|
+
parentRunId: args.parentRunId,
|
|
204
|
+
parentSessionId: args.parentSessionId,
|
|
188
205
|
});
|
|
189
206
|
}
|
|
190
207
|
catch (err) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
// spawnSubAgent's contract says it never throws — but defend in
|
|
209
|
+
// depth: a thrown error from the primitive would otherwise sink
|
|
210
|
+
// the whole Promise.all, which would silently kill sibling
|
|
211
|
+
// children. Surface as a failed SubagentResult instead.
|
|
212
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
213
|
+
args.logger.warn('child: primitive threw', { error });
|
|
214
|
+
return {
|
|
215
|
+
index: args.index,
|
|
216
|
+
providerId: args.provider.providerId,
|
|
217
|
+
modelId: args.provider.modelId,
|
|
218
|
+
output: '',
|
|
219
|
+
error,
|
|
220
|
+
elapsedMs: args.now() - startedAt,
|
|
221
|
+
};
|
|
200
222
|
}
|
|
201
223
|
const elapsedMs = args.now() - startedAt;
|
|
202
|
-
|
|
224
|
+
const error = envelope.error ?? undefined;
|
|
225
|
+
args.logger.info('child: done', {
|
|
226
|
+
elapsedMs,
|
|
227
|
+
ok: envelope.ok,
|
|
228
|
+
status: envelope.status,
|
|
229
|
+
exitReason: envelope.exitReason,
|
|
230
|
+
childRunId: envelope.childRunId,
|
|
231
|
+
});
|
|
203
232
|
return {
|
|
204
233
|
index: args.index,
|
|
205
234
|
providerId: args.provider.providerId,
|
|
206
235
|
modelId: args.provider.modelId,
|
|
207
|
-
output:
|
|
236
|
+
output: envelope.ok ? (envelope.summary ?? '') : '',
|
|
208
237
|
error,
|
|
209
238
|
elapsedMs,
|
|
210
239
|
};
|
|
211
240
|
}
|
|
212
|
-
function buildPartitionPrompt(task) {
|
|
213
|
-
const role = task.role ? `Role: ${task.role}\n` : '';
|
|
214
|
-
const context = task.context ? `\nContext:\n${task.context}\n` : '';
|
|
215
|
-
return `${role}Goal: ${task.goal}${context}`;
|
|
216
|
-
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/subagent/spawnPause.ts — v4.6 Phase 3A.
|
|
10
|
+
*
|
|
11
|
+
* Operator kill-switch for sub-agent spawning. When PAUSED, any new
|
|
12
|
+
* `spawn_sub_agent` or `subagent_fanout` invocation returns a typed
|
|
13
|
+
* failure envelope (`errorCode: 'SUBAGENT_SPAWN_PAUSED'`) BEFORE any
|
|
14
|
+
* runs row is written, child agent built, or provider hit. In-flight
|
|
15
|
+
* children continue uninterrupted — the gate is at tool-handler
|
|
16
|
+
* entry only.
|
|
17
|
+
*
|
|
18
|
+
* Storage: a file marker at `$aidenHome/spawn.paused` (the
|
|
19
|
+
* `paths.root` returned by `resolveAidenPaths()`). This choice
|
|
20
|
+
* differs deliberately from the reference multi-agent system, whose
|
|
21
|
+
* pause flag is an in-process boolean — Aiden's REPL, daemon, and
|
|
22
|
+
* MCP server can all coexist on the same machine, so a single
|
|
23
|
+
* shared marker file is the cheapest way to coordinate pause state
|
|
24
|
+
* across all three runtimes. The marker survives process restart;
|
|
25
|
+
* the boot card surfaces a "spawn-paused" indicator so an operator
|
|
26
|
+
* who forgot they paused last week doesn't sit confused.
|
|
27
|
+
*
|
|
28
|
+
* Marker format: a single-line JSON document
|
|
29
|
+
* { pausedAt: number; reason: string | null; pausedBy: string }
|
|
30
|
+
*
|
|
31
|
+
* Atomic writes: every `pause()` writes to a sibling `.tmp` path
|
|
32
|
+
* and renames atomically so a concurrent reader can never observe
|
|
33
|
+
* a half-written file. `status()` tolerates an unreadable marker
|
|
34
|
+
* (returns `{paused: true}` with no metadata) rather than crashing
|
|
35
|
+
* — the marker EXISTING is the durable fact; the JSON payload is
|
|
36
|
+
* forensic detail.
|
|
37
|
+
*
|
|
38
|
+
* Module-level singleton: `initSpawnPause({aidenHome})` then
|
|
39
|
+
* `getSpawnPause()`. Mirrors the `runtimeToggles` pattern in
|
|
40
|
+
* `core/v4/runtimeToggles.ts` but does NOT route through it —
|
|
41
|
+
* runtimeToggles is config-yaml backed (no per-toggle metadata
|
|
42
|
+
* field), and the reason/pausedAt/pausedBy fields are first-class
|
|
43
|
+
* here.
|
|
44
|
+
*/
|
|
45
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
46
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
47
|
+
};
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.SpawnPauseState = void 0;
|
|
50
|
+
exports.initSpawnPause = initSpawnPause;
|
|
51
|
+
exports.getSpawnPause = getSpawnPause;
|
|
52
|
+
exports._resetSpawnPauseForTests = _resetSpawnPauseForTests;
|
|
53
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
54
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
55
|
+
// ── Implementation ───────────────────────────────────────────────────────
|
|
56
|
+
const MARKER_FILENAME = 'spawn.paused';
|
|
57
|
+
/**
|
|
58
|
+
* File-marker-backed pause state. Concurrent processes (REPL, daemon,
|
|
59
|
+
* MCP server) all read/write the same marker, so flipping pause from
|
|
60
|
+
* a REPL slash command is observed by an MCP-mode `subagent_fanout`
|
|
61
|
+
* call within milliseconds (next read).
|
|
62
|
+
*/
|
|
63
|
+
class SpawnPauseState {
|
|
64
|
+
constructor(opts) {
|
|
65
|
+
this.markerPath = node_path_1.default.join(opts.aidenHome, MARKER_FILENAME);
|
|
66
|
+
this.now = opts.now ?? (() => Date.now());
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Hot path — called at the top of every `spawn_sub_agent` and
|
|
70
|
+
* `subagent_fanout` invocation. MUST stay cheap (single
|
|
71
|
+
* `fs.existsSync`). The metadata read is deferred to `status()`.
|
|
72
|
+
*
|
|
73
|
+
* Any error (FS busy, permission, etc.) silently returns
|
|
74
|
+
* `false` — failing-open is the right default because a paused
|
|
75
|
+
* state that operators can't query/clear due to FS hiccups would
|
|
76
|
+
* brick the whole spawning surface.
|
|
77
|
+
*/
|
|
78
|
+
isPaused() {
|
|
79
|
+
try {
|
|
80
|
+
return node_fs_1.default.existsSync(this.markerPath);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Apply the pause marker. Atomic via tmp-file + rename so a
|
|
88
|
+
* mid-write status read never sees corrupt JSON. Idempotent —
|
|
89
|
+
* pausing while already paused just overwrites the marker (which
|
|
90
|
+
* is the right semantic for "re-pause with a fresh reason").
|
|
91
|
+
*/
|
|
92
|
+
pause(opts) {
|
|
93
|
+
const payload = {
|
|
94
|
+
pausedAt: this.now(),
|
|
95
|
+
reason: opts.reason ?? null,
|
|
96
|
+
pausedBy: opts.pausedBy,
|
|
97
|
+
};
|
|
98
|
+
const tmpPath = `${this.markerPath}.tmp`;
|
|
99
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(this.markerPath), { recursive: true });
|
|
100
|
+
node_fs_1.default.writeFileSync(tmpPath, JSON.stringify(payload), { encoding: 'utf8' });
|
|
101
|
+
node_fs_1.default.renameSync(tmpPath, this.markerPath);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Clear the pause marker. Idempotent — ENOENT (already resumed)
|
|
105
|
+
* is treated as success, so two operators calling resume back-
|
|
106
|
+
* to-back don't error on the second.
|
|
107
|
+
*/
|
|
108
|
+
resume() {
|
|
109
|
+
try {
|
|
110
|
+
node_fs_1.default.unlinkSync(this.markerPath);
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
const code = e.code;
|
|
114
|
+
if (code !== 'ENOENT')
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Read the current pause state with metadata. When the marker
|
|
120
|
+
* exists but is unreadable / malformed JSON, returns
|
|
121
|
+
* `{paused: true}` with no metadata fields — the EXISTENCE of
|
|
122
|
+
* the marker is the durable contract; the JSON payload is best-
|
|
123
|
+
* effort forensic detail.
|
|
124
|
+
*/
|
|
125
|
+
status() {
|
|
126
|
+
if (!this.isPaused()) {
|
|
127
|
+
return { paused: false };
|
|
128
|
+
}
|
|
129
|
+
let raw;
|
|
130
|
+
try {
|
|
131
|
+
raw = node_fs_1.default.readFileSync(this.markerPath, 'utf8');
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return { paused: true };
|
|
135
|
+
}
|
|
136
|
+
let parsed;
|
|
137
|
+
try {
|
|
138
|
+
parsed = JSON.parse(raw);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { paused: true };
|
|
142
|
+
}
|
|
143
|
+
const pausedAt = typeof parsed.pausedAt === 'number' ? parsed.pausedAt : undefined;
|
|
144
|
+
return {
|
|
145
|
+
paused: true,
|
|
146
|
+
pausedAt,
|
|
147
|
+
reason: parsed.reason ?? null,
|
|
148
|
+
pausedBy: parsed.pausedBy ?? 'unknown',
|
|
149
|
+
durationMs: pausedAt !== undefined ? Math.max(0, this.now() - pausedAt) : undefined,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.SpawnPauseState = SpawnPauseState;
|
|
154
|
+
// ── Module-level singleton ───────────────────────────────────────────────
|
|
155
|
+
let _singleton = null;
|
|
156
|
+
/**
|
|
157
|
+
* Initialize the process-wide pause state. Called once at boot
|
|
158
|
+
* (REPL: `buildAgentRuntime`; daemon: dispatcher bootstrap; MCP:
|
|
159
|
+
* `wireSubagentFanout`). Subsequent calls REPLACE the singleton —
|
|
160
|
+
* tests rely on this to swap the marker dir cleanly.
|
|
161
|
+
*/
|
|
162
|
+
function initSpawnPause(opts) {
|
|
163
|
+
_singleton = new SpawnPauseState(opts);
|
|
164
|
+
return _singleton;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Read the current singleton. Throws if `initSpawnPause` hasn't
|
|
168
|
+
* been called yet — the spawn / fanout tool handlers cannot
|
|
169
|
+
* function without it, and a silent fallback to "not paused" would
|
|
170
|
+
* defeat the kill-switch's purpose. Boot wiring is responsible for
|
|
171
|
+
* calling init before any tool handler can fire.
|
|
172
|
+
*
|
|
173
|
+
* For environments that genuinely don't have a marker dir (some
|
|
174
|
+
* test contexts), call `initSpawnPause({aidenHome: <tmp>})` with
|
|
175
|
+
* a throwaway path.
|
|
176
|
+
*/
|
|
177
|
+
function getSpawnPause() {
|
|
178
|
+
if (!_singleton) {
|
|
179
|
+
throw new Error('spawnPause: not initialized — call initSpawnPause({aidenHome}) at boot. ' +
|
|
180
|
+
'This usually means a sub-agent tool handler fired before runtime wiring completed.');
|
|
181
|
+
}
|
|
182
|
+
return _singleton;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Test-only — reset the singleton so the next `initSpawnPause`
|
|
186
|
+
* call wires a fresh state. Production callers should never need
|
|
187
|
+
* this; `initSpawnPause` is already idempotent for re-init.
|
|
188
|
+
*/
|
|
189
|
+
function _resetSpawnPauseForTests() {
|
|
190
|
+
_singleton = null;
|
|
191
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/subagent/spawnSubAgent.ts — v4.6 Phase 1.
|
|
10
|
+
*
|
|
11
|
+
* Public spawn primitive. Synchronously runs one child agent to handle
|
|
12
|
+
* a delegated sub-task, returns a structured `SubAgentResult` envelope.
|
|
13
|
+
* NEVER throws — every error path produces an envelope with the
|
|
14
|
+
* appropriate `status` + `error` fields so the parent's LLM can
|
|
15
|
+
* reason about the failure.
|
|
16
|
+
*
|
|
17
|
+
* Contract per `docs/v4.6/phase-1-design.md` §3, §6, §8.
|
|
18
|
+
*
|
|
19
|
+
* - Single child (Phase 1; batch is Phase 2 via subagent_fanout
|
|
20
|
+
* refactor)
|
|
21
|
+
* - Synchronous: the parent's tool dispatch awaits this Promise
|
|
22
|
+
* - Cooperative cancellation: parent's AbortSignal cascades to
|
|
23
|
+
* child via a linked AbortController
|
|
24
|
+
* - Wall-clock timeout: hard cap via setTimeout → child interrupt
|
|
25
|
+
* - Persistence: writes a `runs` row with `spawned_from_run_id` +
|
|
26
|
+
* `spawned_from_session_id` linking back to the parent
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.spawnSubAgent = spawnSubAgent;
|
|
30
|
+
const node_crypto_1 = require("node:crypto");
|
|
31
|
+
const childBuilder_1 = require("./childBuilder");
|
|
32
|
+
const factory_1 = require("../logger/factory");
|
|
33
|
+
// ── Constants ─────────────────────────────────────────────────────────────
|
|
34
|
+
const DEFAULT_TIMEOUT_MS = 600000;
|
|
35
|
+
const MIN_TIMEOUT_MS = 1000;
|
|
36
|
+
const MAX_TIMEOUT_MS = 3600000;
|
|
37
|
+
const DEFAULT_MAX_ITERATIONS = 50;
|
|
38
|
+
const MIN_MAX_ITERATIONS = 1;
|
|
39
|
+
const MAX_MAX_ITERATIONS = 200;
|
|
40
|
+
// ── Implementation ────────────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Spawn one child agent. Always returns an envelope; never throws.
|
|
43
|
+
*
|
|
44
|
+
* Lifecycle (per §6 state machine):
|
|
45
|
+
*
|
|
46
|
+
* 1. Generate child sessionId (flat UUID).
|
|
47
|
+
* 2. Insert `runs` row with `status: 'running'` + lineage columns.
|
|
48
|
+
* 3. Build child agent (clones FallbackAdapter, intersects toolsets,
|
|
49
|
+
* filters blocklist, fresh ApprovalEngine with auto-deny).
|
|
50
|
+
* 4. Construct linked AbortController: parent's signal feeds into
|
|
51
|
+
* it; a setTimeout on `timeoutMs` also aborts it.
|
|
52
|
+
* 5. Run `child.runConversation(history, { signal: childCtrl.signal })`.
|
|
53
|
+
* 6. On return / throw / timeout, classify into the envelope's
|
|
54
|
+
* status + exitReason and update the runs row's status.
|
|
55
|
+
* 7. Clean up timer + signal listener.
|
|
56
|
+
*/
|
|
57
|
+
async function spawnSubAgent(spec, deps, ctx) {
|
|
58
|
+
const startedAt = Date.now();
|
|
59
|
+
// ── 1. Clamp inputs ─────────────────────────────────────────────────────
|
|
60
|
+
const maxIterations = clamp(spec.maxIterations ?? DEFAULT_MAX_ITERATIONS, MIN_MAX_ITERATIONS, MAX_MAX_ITERATIONS);
|
|
61
|
+
const timeoutMs = clamp(spec.timeoutMs ?? DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
62
|
+
// ── 2. Fresh sessionId + run row ────────────────────────────────────────
|
|
63
|
+
const childSessionId = (0, node_crypto_1.randomUUID)();
|
|
64
|
+
// Pre-create the child run row in 'running' state so the envelope
|
|
65
|
+
// always carries a valid childRunId — even if buildChildAgent throws.
|
|
66
|
+
let childRunId;
|
|
67
|
+
try {
|
|
68
|
+
childRunId = deps.runStore.create({
|
|
69
|
+
sessionId: childSessionId,
|
|
70
|
+
instanceId: deps.instanceId,
|
|
71
|
+
status: 'running',
|
|
72
|
+
startedAt,
|
|
73
|
+
spawnedFromRunId: ctx.parentRunId,
|
|
74
|
+
spawnedFromSessionId: ctx.parentSessionId,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
// Persistence failed before we even started — surface as failed
|
|
79
|
+
// envelope with a synthetic id of '0' so the contract holds.
|
|
80
|
+
return failureEnvelope({
|
|
81
|
+
childRunId: '0',
|
|
82
|
+
childSessionId,
|
|
83
|
+
error: `Failed to create child run row: ${errorMessage(err)}`,
|
|
84
|
+
durationMs: Date.now() - startedAt,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// ── 3. Build child agent ────────────────────────────────────────────────
|
|
88
|
+
const logger = deps.logger ?? (0, factory_1.noopLogger)();
|
|
89
|
+
let agentBundle;
|
|
90
|
+
try {
|
|
91
|
+
agentBundle = (0, childBuilder_1.buildChildAgent)({
|
|
92
|
+
...deps,
|
|
93
|
+
// v4.6 Phase 1 observability — pass runStore + childRunId
|
|
94
|
+
// through so childBuilder can wire onToolCall → run_events
|
|
95
|
+
// for the child's tool dispatches. Both are optional in
|
|
96
|
+
// ChildBuilderDeps so unit tests of buildChildAgent stay
|
|
97
|
+
// dependency-light.
|
|
98
|
+
runStore: deps.runStore,
|
|
99
|
+
childRunId,
|
|
100
|
+
logger,
|
|
101
|
+
}, {
|
|
102
|
+
sessionId: childSessionId,
|
|
103
|
+
goal: spec.goal,
|
|
104
|
+
context: spec.context,
|
|
105
|
+
requestedToolsets: spec.toolsets,
|
|
106
|
+
maxIterations,
|
|
107
|
+
// v4.6 Phase 2P — per-spawn provider override (per design doc §12.2).
|
|
108
|
+
providerOverride: spec.provider,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
// v4.6 Phase 2P — distinguish provider-not-found from other build
|
|
113
|
+
// failures. ProviderNotFoundError carries the failing name + the
|
|
114
|
+
// list of valid alternatives, surfaced verbatim to the LLM in the
|
|
115
|
+
// envelope so it can pick a real provider next time. Other build
|
|
116
|
+
// failures (constructor throws, registry issues, etc.) collapse
|
|
117
|
+
// to the generic 'error' exitReason.
|
|
118
|
+
if (err instanceof childBuilder_1.ProviderNotFoundError) {
|
|
119
|
+
deps.runStore.setStatus(childRunId, 'failed', { finishReason: 'provider_not_found' });
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
status: 'failed',
|
|
123
|
+
summary: null,
|
|
124
|
+
error: err.message,
|
|
125
|
+
exitReason: 'provider_not_found',
|
|
126
|
+
metrics: { apiCalls: 0, durationMs: Date.now() - startedAt, tokensIn: 0, tokensOut: 0 },
|
|
127
|
+
childRunId: String(childRunId),
|
|
128
|
+
childSessionId,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
deps.runStore.setStatus(childRunId, 'failed', { finishReason: 'error' });
|
|
132
|
+
return failureEnvelope({
|
|
133
|
+
childRunId: String(childRunId),
|
|
134
|
+
childSessionId,
|
|
135
|
+
error: `Failed to build child agent: ${errorMessage(err)}`,
|
|
136
|
+
durationMs: Date.now() - startedAt,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// v4.6 Phase 1 observability — log the child's actual tool catalog
|
|
140
|
+
// so we can see whether the toolsets-resolution path produced a
|
|
141
|
+
// sensible set or stripped everything. The single most-load-bearing
|
|
142
|
+
// diagnostic for the "child returned 0" class of bugs.
|
|
143
|
+
const childToolNames = agentBundle.agent.tools.map((t) => t.name);
|
|
144
|
+
logger.info('spawn_sub_agent child built', {
|
|
145
|
+
childRunId: String(childRunId),
|
|
146
|
+
childSessionId,
|
|
147
|
+
toolCount: childToolNames.length,
|
|
148
|
+
toolNames: childToolNames,
|
|
149
|
+
requestedToolsets: spec.toolsets ?? null,
|
|
150
|
+
maxIterations,
|
|
151
|
+
timeoutMs,
|
|
152
|
+
});
|
|
153
|
+
// ── 4. Linked AbortController ───────────────────────────────────────────
|
|
154
|
+
// Two abort sources cascade into the child's signal:
|
|
155
|
+
// (a) parent signal aborts — child aborts.
|
|
156
|
+
// (b) timeoutMs elapses — child aborts.
|
|
157
|
+
// Track which one fired so we can label the envelope as
|
|
158
|
+
// 'interrupted' vs 'timeout' (the spec distinguishes them).
|
|
159
|
+
const childCtrl = new AbortController();
|
|
160
|
+
let timedOut = false;
|
|
161
|
+
const timer = setTimeout(() => {
|
|
162
|
+
timedOut = true;
|
|
163
|
+
childCtrl.abort();
|
|
164
|
+
}, timeoutMs);
|
|
165
|
+
let parentAbortHandler = null;
|
|
166
|
+
if (ctx.signal) {
|
|
167
|
+
if (ctx.signal.aborted) {
|
|
168
|
+
childCtrl.abort();
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
parentAbortHandler = () => childCtrl.abort();
|
|
172
|
+
ctx.signal.addEventListener('abort', parentAbortHandler, { once: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const cleanupAbortWiring = () => {
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
if (parentAbortHandler && ctx.signal) {
|
|
178
|
+
ctx.signal.removeEventListener('abort', parentAbortHandler);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
// ── 5. Run the child ─────────────────────────────────────────────────────
|
|
182
|
+
// `child.runConversation` propagates the signal into the loop's
|
|
183
|
+
// between-iteration + pre-tool-call abort checks via the prep
|
|
184
|
+
// dispatch (commit fd62f96d).
|
|
185
|
+
let summary = null;
|
|
186
|
+
let error = null;
|
|
187
|
+
let status = 'completed';
|
|
188
|
+
let exitReason = 'completed';
|
|
189
|
+
let apiCalls = 0;
|
|
190
|
+
let tokensIn = 0;
|
|
191
|
+
let tokensOut = 0;
|
|
192
|
+
try {
|
|
193
|
+
const result = await agentBundle.agent.runConversation(agentBundle.history, { signal: childCtrl.signal });
|
|
194
|
+
apiCalls = result.turnCount; // one provider call per turn
|
|
195
|
+
tokensIn = result.totalUsage.inputTokens;
|
|
196
|
+
tokensOut = result.totalUsage.outputTokens;
|
|
197
|
+
// Classify the result per design doc §8.
|
|
198
|
+
if (result.finishReason === 'interrupted') {
|
|
199
|
+
// Distinguish timeout from parent-interrupt by which source fired.
|
|
200
|
+
if (timedOut) {
|
|
201
|
+
status = 'timeout';
|
|
202
|
+
exitReason = 'timeout';
|
|
203
|
+
error = `Sub-agent timed out after ${timeoutMs}ms (maxIterations=${maxIterations})`;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
status = 'interrupted';
|
|
207
|
+
exitReason = 'interrupted';
|
|
208
|
+
error = 'Parent interrupted — child did not finish in time';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (result.finishReason === 'budget_exhausted') {
|
|
212
|
+
// Hit maxIterations. If the model produced a partial final reply,
|
|
213
|
+
// we ship it as a 'completed/max_iterations' (partial summary);
|
|
214
|
+
// otherwise it's a failure.
|
|
215
|
+
if (result.finalContent && result.finalContent.length > 0) {
|
|
216
|
+
status = 'completed';
|
|
217
|
+
exitReason = 'max_iterations';
|
|
218
|
+
summary = result.finalContent;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
status = 'failed';
|
|
222
|
+
exitReason = 'error';
|
|
223
|
+
error = `Sub-agent hit max_iterations (${maxIterations}) without producing a summary`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else if (result.finishReason === 'error') {
|
|
227
|
+
status = 'failed';
|
|
228
|
+
exitReason = 'error';
|
|
229
|
+
error = 'Sub-agent loop reported an internal error';
|
|
230
|
+
}
|
|
231
|
+
else if (result.finishReason === 'tool_loop') {
|
|
232
|
+
// TCE surfaced a tool loop — treat as a failure with structured
|
|
233
|
+
// payload buried in error string (Phase 1 doesn't yet ship the
|
|
234
|
+
// capability-card detail into the envelope).
|
|
235
|
+
status = 'failed';
|
|
236
|
+
exitReason = 'error';
|
|
237
|
+
error = `Sub-agent detected a tool loop and stopped: ${result.toolLoopCard?.title ?? 'tool_loop'}`;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// 'stop' → natural completion.
|
|
241
|
+
status = 'completed';
|
|
242
|
+
exitReason = 'completed';
|
|
243
|
+
summary = result.finalContent;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
// child.runConversation threw — typically only happens when the
|
|
248
|
+
// provider call fails after exhausting fallback chain. Surface as
|
|
249
|
+
// a failed envelope with the error string.
|
|
250
|
+
status = 'failed';
|
|
251
|
+
exitReason = 'error';
|
|
252
|
+
error = `Sub-agent threw: ${errorMessage(err)}`;
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
cleanupAbortWiring();
|
|
256
|
+
}
|
|
257
|
+
// ── 6. Update run row + emit envelope ────────────────────────────────────
|
|
258
|
+
const dbStatus = status === 'completed' ? 'completed'
|
|
259
|
+
: status === 'interrupted' ? 'interrupted'
|
|
260
|
+
: 'failed';
|
|
261
|
+
deps.runStore.setStatus(childRunId, dbStatus, { finishReason: exitReason });
|
|
262
|
+
const durationMs = Date.now() - startedAt;
|
|
263
|
+
const ok = status === 'completed' && exitReason !== 'error';
|
|
264
|
+
return {
|
|
265
|
+
ok,
|
|
266
|
+
status,
|
|
267
|
+
summary,
|
|
268
|
+
error,
|
|
269
|
+
exitReason,
|
|
270
|
+
metrics: {
|
|
271
|
+
apiCalls,
|
|
272
|
+
durationMs,
|
|
273
|
+
tokensIn,
|
|
274
|
+
tokensOut,
|
|
275
|
+
},
|
|
276
|
+
childRunId: String(childRunId),
|
|
277
|
+
childSessionId,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
281
|
+
function clamp(n, lo, hi) {
|
|
282
|
+
if (!Number.isFinite(n))
|
|
283
|
+
return lo;
|
|
284
|
+
return Math.max(lo, Math.min(hi, Math.floor(n)));
|
|
285
|
+
}
|
|
286
|
+
function errorMessage(err) {
|
|
287
|
+
return err instanceof Error ? err.message : String(err);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Build a failed envelope for pre-run errors (run-row creation,
|
|
291
|
+
* agent construction). Always carries `summary: null`, `ok: false`,
|
|
292
|
+
* `status: 'failed'`, `exitReason: 'error'`.
|
|
293
|
+
*/
|
|
294
|
+
function failureEnvelope(opts) {
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
status: 'failed',
|
|
298
|
+
summary: null,
|
|
299
|
+
error: opts.error,
|
|
300
|
+
exitReason: 'error',
|
|
301
|
+
metrics: {
|
|
302
|
+
apiCalls: 0,
|
|
303
|
+
durationMs: opts.durationMs,
|
|
304
|
+
tokensIn: 0,
|
|
305
|
+
tokensOut: 0,
|
|
306
|
+
},
|
|
307
|
+
childRunId: opts.childRunId,
|
|
308
|
+
childSessionId: opts.childSessionId,
|
|
309
|
+
};
|
|
310
|
+
}
|