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.
Files changed (181) hide show
  1. package/README.md +265 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +536 -152
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +245 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
  7. package/dist/cli/v4/commands/browserDepth.js +45 -0
  8. package/dist/cli/v4/commands/cron.js +264 -0
  9. package/dist/cli/v4/commands/daemon.js +541 -0
  10. package/dist/cli/v4/commands/daemonStatus.js +253 -0
  11. package/dist/cli/v4/commands/fanout.js +42 -59
  12. package/dist/cli/v4/commands/help.js +13 -0
  13. package/dist/cli/v4/commands/index.js +35 -1
  14. package/dist/cli/v4/commands/mcp.js +80 -54
  15. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  16. package/dist/cli/v4/commands/recovery.js +122 -0
  17. package/dist/cli/v4/commands/runs.js +223 -0
  18. package/dist/cli/v4/commands/sandbox.js +48 -0
  19. package/dist/cli/v4/commands/spawnPause.js +93 -0
  20. package/dist/cli/v4/commands/suggestions.js +68 -0
  21. package/dist/cli/v4/commands/tce.js +41 -0
  22. package/dist/cli/v4/commands/trigger.js +378 -0
  23. package/dist/cli/v4/commands/update.js +95 -3
  24. package/dist/cli/v4/daemonAgentBuilder.js +145 -0
  25. package/dist/cli/v4/defaultSoul.js +1 -1
  26. package/dist/cli/v4/display/capabilityCard.js +26 -0
  27. package/dist/cli/v4/display.js +18 -8
  28. package/dist/cli/v4/replyRenderer.js +31 -23
  29. package/dist/cli/v4/updateBootPrompt.js +170 -0
  30. package/dist/core/playwrightBridge.js +129 -0
  31. package/dist/core/v4/aidenAgent.js +527 -5
  32. package/dist/core/v4/browserState.js +436 -0
  33. package/dist/core/v4/checkpoint.js +79 -0
  34. package/dist/core/v4/daemon/bootstrap.js +651 -0
  35. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  36. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  37. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  38. package/dist/core/v4/daemon/cron/migration.js +199 -0
  39. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  40. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  41. package/dist/core/v4/daemon/db/connection.js +106 -0
  42. package/dist/core/v4/daemon/db/migrations.js +362 -0
  43. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  44. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  45. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  46. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  47. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  48. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  49. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  50. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  51. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  52. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  53. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  54. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  55. package/dist/core/v4/daemon/drain.js +156 -0
  56. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  57. package/dist/core/v4/daemon/health.js +159 -0
  58. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  59. package/dist/core/v4/daemon/index.js +179 -0
  60. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  61. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  62. package/dist/core/v4/daemon/restartCode.js +32 -0
  63. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  64. package/dist/core/v4/daemon/runStore.js +144 -0
  65. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  66. package/dist/core/v4/daemon/signals.js +50 -0
  67. package/dist/core/v4/daemon/supervisor.js +272 -0
  68. package/dist/core/v4/daemon/triggerBus.js +279 -0
  69. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  70. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  71. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  72. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  73. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  74. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  75. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  76. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  77. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  78. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  79. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  80. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  81. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  82. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  83. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  84. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  85. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  86. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  87. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  88. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  89. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  90. package/dist/core/v4/daemon/types.js +15 -0
  91. package/dist/core/v4/dockerSession.js +461 -0
  92. package/dist/core/v4/dryRun.js +117 -0
  93. package/dist/core/v4/failureClassifier.js +779 -0
  94. package/dist/core/v4/providerFallback.js +35 -2
  95. package/dist/core/v4/recoveryReport.js +449 -0
  96. package/dist/core/v4/runtimeToggles.js +214 -0
  97. package/dist/core/v4/sandboxConfig.js +285 -0
  98. package/dist/core/v4/sandboxFs.js +316 -0
  99. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  100. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  101. package/dist/core/v4/subagent/childBuilder.js +391 -0
  102. package/dist/core/v4/subagent/fanout.js +75 -51
  103. package/dist/core/v4/subagent/spawnPause.js +191 -0
  104. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  105. package/dist/core/v4/suggestionCatalog.js +41 -0
  106. package/dist/core/v4/suggestionEngine.js +210 -0
  107. package/dist/core/v4/toolRegistry.js +37 -3
  108. package/dist/core/v4/turnState.js +587 -0
  109. package/dist/core/v4/update/checkUpdate.js +63 -3
  110. package/dist/core/v4/update/installMethodDetect.js +115 -0
  111. package/dist/core/v4/update/registryClient.js +121 -0
  112. package/dist/core/v4/update/skipState.js +75 -0
  113. package/dist/core/v4/verifier.js +448 -0
  114. package/dist/core/version.js +1 -1
  115. package/dist/moat/plannerGuard.js +29 -0
  116. package/dist/providers/v4/anthropicAdapter.js +31 -3
  117. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  118. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  119. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  120. package/dist/tools/v4/browser/_observer.js +224 -0
  121. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  122. package/dist/tools/v4/browser/browserClick.js +18 -1
  123. package/dist/tools/v4/browser/browserClose.js +18 -1
  124. package/dist/tools/v4/browser/browserExtract.js +5 -1
  125. package/dist/tools/v4/browser/browserFill.js +17 -1
  126. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  127. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  128. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  129. package/dist/tools/v4/browser/browserScroll.js +18 -1
  130. package/dist/tools/v4/browser/browserType.js +17 -1
  131. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  132. package/dist/tools/v4/executeCode.js +1 -0
  133. package/dist/tools/v4/files/fileCopy.js +56 -2
  134. package/dist/tools/v4/files/fileDelete.js +38 -1
  135. package/dist/tools/v4/files/fileList.js +12 -1
  136. package/dist/tools/v4/files/fileMove.js +59 -2
  137. package/dist/tools/v4/files/filePatch.js +43 -1
  138. package/dist/tools/v4/files/fileRead.js +12 -1
  139. package/dist/tools/v4/files/fileWrite.js +41 -1
  140. package/dist/tools/v4/index.js +88 -61
  141. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  142. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  143. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  144. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  145. package/dist/tools/v4/process/processKill.js +19 -0
  146. package/dist/tools/v4/process/processList.js +1 -0
  147. package/dist/tools/v4/process/processLogRead.js +1 -0
  148. package/dist/tools/v4/process/processSpawn.js +13 -0
  149. package/dist/tools/v4/process/processWait.js +1 -0
  150. package/dist/tools/v4/sessions/recallSession.js +1 -0
  151. package/dist/tools/v4/sessions/sessionList.js +1 -0
  152. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  153. package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
  154. package/dist/tools/v4/skills/skillManage.js +13 -0
  155. package/dist/tools/v4/skills/skillView.js +1 -0
  156. package/dist/tools/v4/skills/skillsList.js +1 -0
  157. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  158. package/dist/tools/v4/subagent/subagentFanout.js +54 -1
  159. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  160. package/dist/tools/v4/system/appClose.js +13 -0
  161. package/dist/tools/v4/system/appInput.js +13 -0
  162. package/dist/tools/v4/system/appLaunch.js +13 -0
  163. package/dist/tools/v4/system/clipboardRead.js +1 -0
  164. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  165. package/dist/tools/v4/system/mediaKey.js +12 -0
  166. package/dist/tools/v4/system/mediaSessions.js +1 -0
  167. package/dist/tools/v4/system/mediaTransport.js +13 -0
  168. package/dist/tools/v4/system/naturalEvents.js +1 -0
  169. package/dist/tools/v4/system/nowPlaying.js +1 -0
  170. package/dist/tools/v4/system/osProcessList.js +1 -0
  171. package/dist/tools/v4/system/screenshot.js +1 -0
  172. package/dist/tools/v4/system/systemInfo.js +1 -0
  173. package/dist/tools/v4/system/volumeSet.js +17 -0
  174. package/dist/tools/v4/terminal/shellExec.js +81 -9
  175. package/dist/tools/v4/web/deepResearch.js +1 -0
  176. package/dist/tools/v4/web/openUrl.js +1 -0
  177. package/dist/tools/v4/web/webFetch.js +1 -0
  178. package/dist/tools/v4/web/webPage.js +1 -0
  179. package/dist/tools/v4/web/webSearch.js +1 -0
  180. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  181. 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
- * The orchestrator itself is INTENTIONALLY decoupled from
40
- * AidenAgent it takes a `runChild` callback that knows how to run
41
- * one subagent. The tool wrapper at tools/v4/subagent/subagentFanout
42
- * supplies the production callback (which constructs an AidenAgent);
43
- * tests inject a stub that returns canned strings without any
44
- * provider plumbing. This is what made the offline smoke tractable.
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 prompt = opts.mode === 'ensemble'
107
- ? opts.query
108
- : buildPartitionPrompt(opts.tasks[i]);
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
- prompt,
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
- runChild: opts.runChild,
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
- async function spawnOne(args) {
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
- let output = '';
178
- let error;
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
- output = await args.runChild({
181
- index: args.index,
182
- prompt: args.prompt,
183
- role: args.role,
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
- error = err instanceof Error ? err.message : String(err);
192
- if (childController.signal.aborted) {
193
- error = `aborted (timeout=${args.perTimeoutMs}ms or parent abort): ${error}`;
194
- }
195
- args.logger.warn('child: errored', { error });
196
- }
197
- finally {
198
- clearTimeout(timer);
199
- args.wallSignal.removeEventListener('abort', wallHandler);
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
- args.logger.info('child: done', { elapsedMs, ok: !error && output.length > 0 });
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: error ? '' : 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
+ }