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
@@ -40,8 +40,73 @@
40
40
  * `urlProvenance.ts`, `intentPreArm.ts`. Those modules predate this rewrite
41
41
  * and stay as-is.
42
42
  */
43
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
44
+ if (k2 === undefined) k2 = k;
45
+ var desc = Object.getOwnPropertyDescriptor(m, k);
46
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
47
+ desc = { enumerable: true, get: function() { return m[k]; } };
48
+ }
49
+ Object.defineProperty(o, k2, desc);
50
+ }) : (function(o, m, k, k2) {
51
+ if (k2 === undefined) k2 = k;
52
+ o[k2] = m[k];
53
+ }));
54
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
55
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
56
+ }) : function(o, v) {
57
+ o["default"] = v;
58
+ });
59
+ var __importStar = (this && this.__importStar) || (function () {
60
+ var ownKeys = function(o) {
61
+ ownKeys = Object.getOwnPropertyNames || function (o) {
62
+ var ar = [];
63
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
64
+ return ar;
65
+ };
66
+ return ownKeys(o);
67
+ };
68
+ return function (mod) {
69
+ if (mod && mod.__esModule) return mod;
70
+ var result = {};
71
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
72
+ __setModuleDefault(result, mod);
73
+ return result;
74
+ };
75
+ })();
43
76
  Object.defineProperty(exports, "__esModule", { value: true });
44
77
  exports.AidenAgent = void 0;
78
+ // v4.1.6 spike — Task Completion Engine (TCE) per-turn loop detector
79
+ // + recovery controller. Default ON as of v4.2 Phase 6 — set
80
+ // AIDEN_TCE=0 to disable. Zero
81
+ // behavioral change when unset. See core/v4/turnState.ts.
82
+ const turnState_1 = require("./turnState");
83
+ // v4.2 Phase 1 — per-tool result verifier. Same TCE gate as
84
+ // TurnState (default ON, opt-out via AIDEN_TCE=0); classification
85
+ // feeds the recovery controller.
86
+ const verifier_1 = require("./verifier");
87
+ // v4.2 Phase 2 — tool-failure WHY-classifier. Runs after the verifier
88
+ // when verification.ok === false. Records-only; Phase 3 will act.
89
+ const failureClassifier_1 = require("./failureClassifier");
90
+ // v4.2 Phase 3 — structured RecoveryReport. Built ONLY when the
91
+ // recovery controller's surface stage fires (tool_loop); enriches the
92
+ // existing surface card with summary + category breakdown + dominant
93
+ // guidance. Implicitly gated by TCE being enabled (surface only
94
+ // reachable when TurnState is enabled — default ON as of Phase 6).
95
+ const recoveryReport_1 = require("./recoveryReport");
96
+ // v4.6 Phase 3b — self-improvement loop. Durable cross-session
97
+ // failure ledger + recovery report writes. Loaded lazily inside the
98
+ // per-call branch so a missing singleton (test agents without a
99
+ // daemon DB) never blocks the agent loop.
100
+ const signatureBuilder_1 = require("./selfimprovement/signatureBuilder");
101
+ const recoveryStore_1 = require("./selfimprovement/recoveryStore");
102
+ // v4.2 Phase 4 — checkpoint / restore. Lets the recovery controller
103
+ // roll conversation messages + TurnState internals back to before a
104
+ // looping tool started failing, so the model retries from a clean
105
+ // baseline. Hard-blocked on iterations containing mutating tools
106
+ // (never claim to undo executed side effects). All-no-op when
107
+ // TCE is opted out via AIDEN_TCE=0 — capture / mark / find /
108
+ // restore all short-circuit.
109
+ const checkpoint_1 = require("./checkpoint");
45
110
  const skillEnforcement_1 = require("./agent/skillEnforcement");
46
111
  const urlProvenance_1 = require("./agent/urlProvenance");
47
112
  const intentPreArm_1 = require("./agent/intentPreArm");
@@ -58,6 +123,14 @@ class AidenAgent {
58
123
  constructor(opts) {
59
124
  this.skillMinerTurnIdx = 0;
60
125
  // ── Cross-call state ─────────────────────────────────────────────────
126
+ /**
127
+ * v4.6 Phase 1 — current per-turn AbortSignal, exposed to tools that need
128
+ * to construct child signal chains (specifically `spawn_sub_agent`). Set
129
+ * at the top of `runTurnLoop` from `runOptions.signal`, cleared before
130
+ * the loop returns. Read via `getCurrentSignal()`. Per-agent-instance —
131
+ * not shared across agents; a child agent has its own `_currentSignal`.
132
+ */
133
+ this._currentSignal = undefined;
61
134
  /** Cached system prompt — invalidated by setPersonalityOverlay/markMemoryDirty/explicit. */
62
135
  this.cachedSystemPrompt = null;
63
136
  this.compressionEvents = 0;
@@ -92,6 +165,7 @@ class AidenAgent {
92
165
  this.onSkillCandidate = opts.onSkillCandidate;
93
166
  this.resolveVerifiedFlag = opts.resolveVerifiedFlag;
94
167
  this.resolveToolset = opts.resolveToolset;
168
+ this.resolveMutates = opts.resolveMutates;
95
169
  this.promptBuilder = opts.promptBuilder;
96
170
  this.promptBuilderOptions = opts.promptBuilderOptions;
97
171
  this.contextCompressor = opts.contextCompressor;
@@ -108,6 +182,15 @@ class AidenAgent {
108
182
  this.onPromptBuilt = opts.onPromptBuilt;
109
183
  this.onProviderRequestStart = opts.onProviderRequestStart;
110
184
  this.lookupSkillRequiredTools = opts.lookupSkillRequiredTools;
185
+ // v4.5 Phase 7 — explicit sessionId. Existing access path
186
+ // `(this as { sessionId?: string }).sessionId` at line 751–752
187
+ // already reads from `this.sessionId`; setting it here keys
188
+ // docker / browser / TurnState per session for daemon-mode
189
+ // turns. Interactive REPL callers don't pass this and continue
190
+ // hitting the 'session' fallback.
191
+ if (typeof opts.sessionId === 'string' && opts.sessionId.length > 0) {
192
+ this.sessionId = opts.sessionId;
193
+ }
111
194
  // Phase v4.1.2-slice3: optional health registry (constructor-
112
195
  // injected per the slice3 decision tree — no singleton). When
113
196
  // wired, the caller already plumbed trackers into each subsystem
@@ -227,6 +310,17 @@ class AidenAgent {
227
310
  getEmptyResponseMetrics() {
228
311
  return { ...this.emptyResponseMetrics };
229
312
  }
313
+ /**
314
+ * v4.6 Phase 1 — return the AbortSignal currently associated with this
315
+ * agent's active `runTurnLoop`, or `undefined` if the agent is between
316
+ * turns. Used by the `spawn_sub_agent` tool to construct a child signal
317
+ * chain that cascades parent aborts to the child (Flag 1 pattern: tool
318
+ * captures the parent agent reference at construction time and reads
319
+ * the current signal from the instance at dispatch time).
320
+ */
321
+ getCurrentSignal() {
322
+ return this._currentSignal;
323
+ }
230
324
  // ── Main entry: runConversation ──────────────────────────────────────
231
325
  async runConversation(history, options = {}) {
232
326
  // 1. Refresh memory snapshot if the dirty bit was set since last turn.
@@ -304,7 +398,21 @@ class AidenAgent {
304
398
  }
305
399
  }
306
400
  // 10. SkillTeacher post-loop observation + proposal.
401
+ //
402
+ // v4.1.6 Polish 2 — `handleProposal` previously ran INLINE here,
403
+ // awaiting `callbacks.promptUser` (an inquirer modal) before
404
+ // `runConversation` returned. That made the modal fire BEFORE
405
+ // chatSession rendered the agent's reply on screen, so users
406
+ // saw "Save this as a reusable skill?" pop up mid-turn — feels
407
+ // like an interruption.
408
+ //
409
+ // New flow: agent ONLY observes here. When a proposal needs user
410
+ // confirmation (tier_3_propose with a promptUser callback), the
411
+ // proposal is surfaced in `AidenAgentResult.skillProposal` and
412
+ // chatSession handles the prompt + create dance AFTER rendering
413
+ // the reply. Tier_4_auto still runs inline (no prompt needed).
307
414
  let skillCreated;
415
+ let skillProposal;
308
416
  if (this.skillTeacher) {
309
417
  try {
310
418
  const traceForTeacher = loopResult.toolCallTrace.map((entry, i) => ({
@@ -316,9 +424,20 @@ class AidenAgent {
316
424
  }));
317
425
  const proposal = await this.skillTeacher.observeTurn(history, traceForTeacher, loopResult.finishReason !== 'stop');
318
426
  if (proposal) {
319
- const result = await this.skillTeacher.handleProposal(proposal, this.skillTeacherCallbacks);
320
- if (result.created && result.skillName) {
321
- skillCreated = result.skillName;
427
+ // Defer to chatSession only when there's a prompt callback
428
+ // wired (tier_3_propose path). Otherwise run inline to
429
+ // preserve tier_4_auto and tier_off behaviour.
430
+ const hasPromptCallback = typeof this.skillTeacherCallbacks?.promptUser === 'function';
431
+ if (hasPromptCallback) {
432
+ // Surface the proposal back to chatSession; do NOT call
433
+ // handleProposal here.
434
+ skillProposal = proposal;
435
+ }
436
+ else {
437
+ const result = await this.skillTeacher.handleProposal(proposal, this.skillTeacherCallbacks);
438
+ if (result.created && result.skillName) {
439
+ skillCreated = result.skillName;
440
+ }
322
441
  }
323
442
  }
324
443
  }
@@ -369,11 +488,20 @@ class AidenAgent {
369
488
  toolCallTrace: loopResult.toolCallTrace,
370
489
  honestyFindings,
371
490
  skillCreated,
491
+ // v4.1.6 Polish 2 — deferred to chatSession's post-render
492
+ // handler when the SkillTeacher proposal needs user
493
+ // confirmation. Undefined when no proposal, when tier auto-
494
+ // handled inline, or when the teacher's observation faulted.
495
+ skillProposal,
372
496
  compressionEvents: this.compressionEvents,
373
497
  auxiliaryUsage: this.auxiliaryClient?.getUsage() ?? {},
374
498
  skillEnforcement: { ...this.skillEnforcementMetrics },
375
499
  urlProvenance: { ...this.urlProvenanceMetrics },
376
500
  emptyResponse: { ...this.emptyResponseMetrics },
501
+ // v4.1.6 spike (TCE) — surfaced when TurnState hit the surface
502
+ // threshold mid-turn. chatSession reads this to render the
503
+ // structured-failure card; undefined on all other finishReasons.
504
+ toolLoopCard: loopResult.toolLoopCard,
377
505
  };
378
506
  }
379
507
  // ── Private helpers ──────────────────────────────────────────────────
@@ -442,6 +570,23 @@ class AidenAgent {
442
570
  async narrowTools(userMsg, history) {
443
571
  if (!this.plannerGuard)
444
572
  return this.tools;
573
+ // v4.6 Phase 2M — runtime toggle gates the keyword-based narrower.
574
+ // Default OFF: smart models (GPT-5.5, Claude Sonnet 4.5+, Opus)
575
+ // pick tools fine from the full catalog every turn, matching the
576
+ // reference multi-agent system's pattern. Opt in via env
577
+ // (AIDEN_PLANNER_GUARD=1) or `/planner-guard on` for small local
578
+ // models that need help. The toggle is read on each call so a
579
+ // mid-conversation flip takes effect on the next turn without
580
+ // restarting the agent.
581
+ //
582
+ // Lazy `require` to avoid a hard import dependency in the agent
583
+ // core — pure unit tests of AidenAgent that don't initialise the
584
+ // runtime toggles singleton keep working (the lazy getter returns
585
+ // an env-only fallback resolver per runtimeToggles.ts:213).
586
+ const { getRuntimeToggles } = await Promise.resolve().then(() => __importStar(require('./runtimeToggles')));
587
+ if (!getRuntimeToggles().isEnabled('planner_guard')) {
588
+ return this.tools;
589
+ }
445
590
  const decision = await this.plannerGuard.decide(userMsg, history);
446
591
  this.onPlannerGuardDecision?.(decision);
447
592
  const allowed = new Set(decision.selectedTools);
@@ -458,13 +603,33 @@ class AidenAgent {
458
603
  * `runConversation` enriches with post-loop scan output.
459
604
  */
460
605
  async runTurnLoop(initialMessages, tools, trackers, runOptions) {
606
+ // v4.6 Phase 1 — expose the per-turn signal to tools via
607
+ // `getCurrentSignal()`. Set at loop entry; cleared before the return
608
+ // below. Tools that need the parent's signal (e.g. `spawn_sub_agent`
609
+ // building a child cancellation chain) capture the agent reference at
610
+ // construction time and read this field at dispatch time. If the loop
611
+ // throws, the stale value persists until the next call's set —
612
+ // acceptable because the only consumer is in-flight tool dispatch,
613
+ // which can only run while the loop is mid-execution.
614
+ this._currentSignal = runOptions.signal;
461
615
  const messages = [...initialMessages];
462
616
  const toolCallTrace = [];
617
+ // v4.6 Phase 3b — per-turn signature tracker for failure → success
618
+ // transitions. Each entry records the signatureId + failure count
619
+ // observed so far for a given signature THIS turn. When a verifier
620
+ // later reports `ok` for a tool call whose signature has prior
621
+ // failures, we record a recovery report. Keyed by signature string
622
+ // (the canonical `tool:category[:hash]` form).
623
+ const turnFailureTracker = new Map();
463
624
  // Internal trace mirror that retains tool-call arguments — Honesty's
464
625
  // shape doesn't include args, but SkillTeacher needs them. Both live
465
626
  // off the same entry index.
466
627
  const fullTrace = [];
467
628
  const totalUsage = { inputTokens: 0, outputTokens: 0 };
629
+ // v4.2 Phase 3 — turn start timestamp for RecoveryReport duration.
630
+ // Captured here so any code path (early-return / error / surface)
631
+ // can compute wallclock duration consistently.
632
+ const turnStartedAt = Date.now();
468
633
  let turnCount = 0;
469
634
  let toolCallCount = 0;
470
635
  let fallbackActivated = false;
@@ -473,7 +638,37 @@ class AidenAgent {
473
638
  let emptyRetriesUsed = 0;
474
639
  let finishReason = 'stop';
475
640
  let finalContent = '';
641
+ // v4.1.6 spike (TCE) — per-turn loop detection + recovery state.
642
+ // Default ON as of v4.2 Phase 6 — set AIDEN_TCE=0 to disable.
643
+ // When disabled, TurnState.recordToolCall short-circuits with
644
+ // `{kind: 'allow'}` and the entire v4.2 recovery surface stays
645
+ // dormant (zero behavioural change vs v4.1.6).
646
+ const turnState = new turnState_1.TurnState();
647
+ // v4.2 Phase 1 — per-tool verifier registry. Constructed
648
+ // unconditionally (cheap, no side effects) but only used to
649
+ // classify tool outcomes when TCE is enabled; verification args
650
+ // are passed to TurnState only inside the gated branch below.
651
+ const verifierRegistry = (0, verifier_1.buildDefaultRegistry)();
652
+ // v4.2 Phase 2 — per-tool failure classifier. Same gating as
653
+ // the verifier; only runs when verification.ok === false. Phase 2
654
+ // records-only — Phase 3 wires recovery actions off the category.
655
+ const failureClassifier = (0, failureClassifier_1.buildDefaultClassifier)();
656
+ let toolLoopCard = undefined;
476
657
  while (true) {
658
+ // v4.6 prep — between-iteration cooperative-cancellation check.
659
+ // When the caller passed an AbortSignal that has aborted, exit
660
+ // immediately with `finishReason: 'interrupted'`. Delta accumulation
661
+ // on abort is deferred — finalContent stays '' in this prep dispatch
662
+ // (see docs/v4.6/phase-1-design.md §11.0).
663
+ if (runOptions.signal?.aborted) {
664
+ finishReason = 'interrupted';
665
+ finalContent = '';
666
+ break;
667
+ }
668
+ // v4.1.6 spike — decrement cooldown counters once per iteration
669
+ // so cooled-down tools eventually return to the schemas. No-op
670
+ // when TCE is disabled.
671
+ turnState.advanceIteration();
477
672
  if (turnCount >= this.maxTurns) {
478
673
  finishReason = 'budget_exhausted';
479
674
  break;
@@ -491,12 +686,36 @@ class AidenAgent {
491
686
  this.onBudgetWarning?.('warning', turnCount, this.maxTurns);
492
687
  }
493
688
  // ── Provider call (stream or non-stream) ──────────────────────────
689
+ //
690
+ // v4.1.6 spike (TCE) — filter cooled-down tools out of the
691
+ // schemas we send to the provider. The model literally cannot
692
+ // see (and therefore cannot request) a cooled-down tool until
693
+ // its cooldown counter decrements to zero via
694
+ // `turnState.advanceIteration()`. No-op when TCE disabled
695
+ // (`getCooledDownTools()` returns []).
696
+ let effectiveTools = tools;
697
+ const cooledDown = turnState.getCooledDownTools();
698
+ if (cooledDown.length > 0) {
699
+ const cdSet = new Set(cooledDown);
700
+ effectiveTools = tools.filter((t) => !cdSet.has(t.name));
701
+ }
494
702
  let output;
495
703
  try {
496
- output = await this.callProvider(messages, tools, runOptions);
704
+ output = await this.callProvider(messages, effectiveTools, runOptions);
497
705
  }
498
706
  catch (err) {
499
707
  const error = err instanceof Error ? err : new Error(String(err));
708
+ // v4.6 prep — external abort takes priority over fallback. An
709
+ // AbortError surfaced from the adapter when input.signal aborted
710
+ // is NOT a transient transport failure; surface it immediately
711
+ // as `finishReason: 'interrupted'` so the calling spawn primitive
712
+ // can route correctly. Detect via either the live signal flag or
713
+ // the error name (covers both pre-fetch and mid-flight aborts).
714
+ if (runOptions.signal?.aborted || error.name === 'AbortError') {
715
+ finishReason = 'interrupted';
716
+ finalContent = '';
717
+ break;
718
+ }
500
719
  if (this.fallback && !fallbackActivated) {
501
720
  const next = await this.fallback.activate(error, turnCount);
502
721
  if (next) {
@@ -511,6 +730,25 @@ class AidenAgent {
511
730
  }
512
731
  totalUsage.inputTokens += output.usage?.inputTokens ?? 0;
513
732
  totalUsage.outputTokens += output.usage?.outputTokens ?? 0;
733
+ // v4.2 Phase 4 — capture the state going INTO this iteration's
734
+ // tool dispatch. MUST run BEFORE `messages.push(assistantMsg)`
735
+ // so the checkpoint represents "the conversation before the
736
+ // model decided to call this iteration's tools". If rollback
737
+ // fires later, truncating `messages.length` to
738
+ // `checkpoint.messages.length` drops the assistant tool_call
739
+ // message together with its tool result messages — preserving
740
+ // tool_call/tool_result pairing in the rolled-back state.
741
+ //
742
+ // Capturing AFTER the assistant push (the prior placement) was
743
+ // a real bug: rollback would leave the assistant tool_call in
744
+ // history without its tool results, producing strict-provider
745
+ // 400 errors of the form "No tool output found for function
746
+ // call <id>". Tests in tests/v4/core/checkpoint-integration
747
+ // assert the post-rollback messages array contains zero orphan
748
+ // assistant tool_calls — this position is part of the contract.
749
+ //
750
+ // No-op when TCE is disabled (AIDEN_TCE=0) or checkpointDepth=0.
751
+ turnState.captureCheckpoint(messages, turnCount);
514
752
  // ── Append assistant message ──────────────────────────────────────
515
753
  const assistantMsg = output.toolCalls.length > 0
516
754
  ? { role: 'assistant', content: output.content ?? '', toolCalls: output.toolCalls }
@@ -585,8 +823,40 @@ class AidenAgent {
585
823
  }
586
824
  // ── Dispatch tools sequentially ──────────────────────────────────
587
825
  const turnToolMessages = [];
826
+ // v4.1.6 spike (TCE) — set when TurnState surfaces a tool_loop
827
+ // mid-batch. The agent stops dispatching remaining calls in the
828
+ // batch and breaks out of the outer iteration loop cleanly.
829
+ let surfaceDecision = null;
830
+ // v4.2 Phase 4 — set when TurnState's recovery controller asks
831
+ // for a rollback. The agent loop truncates messages + restores
832
+ // TurnState internals + pushes a corrective system message,
833
+ // then continues the outer iteration loop from a clean baseline.
834
+ let rollbackDecision = null;
588
835
  for (const call of output.toolCalls) {
836
+ // v4.6 prep — pre-tool-call cooperative-cancellation check.
837
+ // If the caller aborted between the model emitting tool calls
838
+ // and us dispatching them, skip the remaining calls in this
839
+ // batch. We set finishReason here; the outer-while break is
840
+ // handled after the for-of exits.
841
+ if (runOptions.signal?.aborted) {
842
+ finishReason = 'interrupted';
843
+ finalContent = '';
844
+ break;
845
+ }
589
846
  this.onToolCall?.(call, 'before');
847
+ // v4.2 Phase 4 — mark any active checkpoints as containing a
848
+ // mutating call BEFORE dispatch. Done pre-dispatch (not post)
849
+ // so that even if the tool throws / errors / produces a
850
+ // partial side effect, the mutation flag is set — rollback
851
+ // safety errs on the side of "this iteration mutated state".
852
+ // The mutability resolver is wired from the CLI's tool
853
+ // registry (`resolveMutates`); unknown tools return undefined,
854
+ // which we treat as non-mutating (leave the flag alone).
855
+ // Plugin authors should declare `mutates` honestly on their
856
+ // tool handlers — this is the structural enforcement point.
857
+ if (turnState.isEnabled() && this.resolveMutates?.(call.name) === true) {
858
+ turnState.markMutationOnLiveCheckpoint(call.name);
859
+ }
590
860
  let result;
591
861
  try {
592
862
  result = await this.toolExecutor(call);
@@ -600,11 +870,114 @@ class AidenAgent {
600
870
  };
601
871
  }
602
872
  toolCallCount += 1;
873
+ // v4.2 Phase 1 — verifier classification. Runs only when TCE
874
+ // is enabled; the registry resolves a per-tool verifier or
875
+ // falls back to the heuristic default. Synchronous + pure;
876
+ // no network, no side effects.
877
+ let verification;
878
+ let classification = null;
879
+ if (turnState.isEnabled()) {
880
+ try {
881
+ verification = verifierRegistry.resolve(call.name)(call.name, call.arguments, result);
882
+ }
883
+ catch {
884
+ // Defensive — a buggy verifier never breaks the agent loop.
885
+ verification = undefined;
886
+ }
887
+ // v4.2 Phase 2 — classify WHY when the verifier said !ok.
888
+ // classify(...) returns null for ok results, so happy-path
889
+ // calls incur zero classifier work.
890
+ if (verification && !verification.ok) {
891
+ try {
892
+ classification = failureClassifier.classify(verification, call.name, call.arguments, result);
893
+ }
894
+ catch {
895
+ // Defensive — a buggy classifier never breaks the loop.
896
+ classification = null;
897
+ }
898
+ // v4.6 Phase 3b — write-through to the durable failure
899
+ // ledger. Best-effort: a null/missing store (test agents
900
+ // without a daemon DB wired) silently no-ops. The
901
+ // signature builder is pure + cheap.
902
+ if (classification) {
903
+ try {
904
+ const store = (0, recoveryStore_1.getRecoveryStore)();
905
+ if (store) {
906
+ const sig = (0, signatureBuilder_1.buildFailureSignature)({
907
+ toolName: call.name,
908
+ category: classification.category,
909
+ args: call.arguments,
910
+ });
911
+ const signatureId = store.recordFailureOccurrence({
912
+ signature: sig.signature,
913
+ toolName: call.name,
914
+ category: classification.category,
915
+ argsHash: sig.argsHash,
916
+ });
917
+ if (signatureId > 0) {
918
+ const existing = turnFailureTracker.get(sig.signature);
919
+ turnFailureTracker.set(sig.signature, {
920
+ signatureId,
921
+ failedAttempts: (existing?.failedAttempts ?? 0) + 1,
922
+ });
923
+ }
924
+ }
925
+ }
926
+ catch {
927
+ // Defensive — persistence failure must never break the loop.
928
+ }
929
+ }
930
+ }
931
+ else if (verification && verification.ok) {
932
+ // v4.6 Phase 3b — failure → success transition detection.
933
+ // We don't know the failure CATEGORY for this successful
934
+ // call (the verifier said ok, so classify() wasn't run),
935
+ // but the per-turn tracker remembers every signature seen
936
+ // failing this turn. Walk the tracker; if any entry's
937
+ // signature starts with `<call.name>:`, this tool now
938
+ // succeeded — record a recovery and drop the entry so
939
+ // subsequent successes don't double-count.
940
+ try {
941
+ const store = (0, recoveryStore_1.getRecoveryStore)();
942
+ if (store) {
943
+ const matching = [];
944
+ for (const sig of turnFailureTracker.keys()) {
945
+ if (sig.startsWith(`${call.name}:`))
946
+ matching.push(sig);
947
+ }
948
+ for (const sig of matching) {
949
+ const entry = turnFailureTracker.get(sig);
950
+ if (!entry)
951
+ continue;
952
+ store.recordRecovery({
953
+ signatureId: entry.signatureId,
954
+ sessionId: this.sessionId,
955
+ failedAttempts: entry.failedAttempts,
956
+ successfulStrategy: 'in_turn_retry',
957
+ notes: `${call.name} succeeded after ${entry.failedAttempts} prior failure(s) this turn`,
958
+ });
959
+ turnFailureTracker.delete(sig);
960
+ }
961
+ }
962
+ }
963
+ catch {
964
+ // Defensive — recovery persistence failure must never break the loop.
965
+ }
966
+ }
967
+ }
603
968
  toolCallTrace.push({
604
969
  name: call.name,
605
970
  result: result.result,
606
971
  error: result.error,
607
972
  verified: this.resolveVerifiedFlag?.(result),
973
+ // v4.2 Phase 1 — verification surfaces alongside the trace
974
+ // entry for downstream callers (chatSession, loopTrace,
975
+ // future RecoveryReport). Undefined when TCE is off.
976
+ verification,
977
+ // v4.2 Phase 2 — classification surfaces alongside verification.
978
+ // Undefined for verifier-ok calls (classifier skips them) and
979
+ // when TCE is off.
980
+ classification: classification ?? undefined,
608
981
  });
609
982
  fullTrace.push({ name: call.name, args: call.arguments });
610
983
  // URL ledger ingest — extracts ids from result body for next turn.
@@ -623,6 +996,134 @@ class AidenAgent {
623
996
  ? `[error] ${result.error}`
624
997
  : stringifyToolResult(result.result),
625
998
  });
999
+ // v4.1.6 spike (TCE) — after the tool result lands in the
1000
+ // message history, consult the recovery controller. Returns
1001
+ // `allow` immediately when TCE disabled (zero overhead).
1002
+ // v4.2 Phase 1 — pass the verifier outcome so TurnState's
1003
+ // consecFailed counter can fast-fail on demonstrably failing
1004
+ // tool calls before the slower signature/name counters fire.
1005
+ // v4.2 Phase 2 — also pass the classification so TurnState
1006
+ // records the WHY for Phase 3's RecoveryReport.
1007
+ const recovery = turnState.recordToolCall(call.name, call.arguments, verification, classification);
1008
+ if (recovery.kind === 'hint' && recovery.hintMessage) {
1009
+ // Stage 1: append a corrective system message so the model
1010
+ // sees it on the next provider call. Same pattern as the
1011
+ // existing skill-enforcement + URL-provenance correctives.
1012
+ turnToolMessages.push({
1013
+ role: 'system',
1014
+ content: recovery.hintMessage,
1015
+ });
1016
+ }
1017
+ else if (recovery.kind === 'cooldown_with_rollback' && recovery.rollback) {
1018
+ // v4.2 Phase 4 — controller asks us to roll back. Capture
1019
+ // the decision; we apply it AFTER the inner dispatch loop
1020
+ // exits so we don't leave partial turnToolMessages in a
1021
+ // half-state. Break out of dispatch immediately — no point
1022
+ // running more tools whose results we're about to drop.
1023
+ rollbackDecision = recovery;
1024
+ break;
1025
+ }
1026
+ else if (recovery.kind === 'cooldown' && recovery.cooldownMessage) {
1027
+ // Stage 2: cooldown has already been recorded internally
1028
+ // (next iteration's schema-filter step excludes this tool).
1029
+ // Inject a system message announcing the cooldown so the
1030
+ // model knows why the tool just disappeared from its menu.
1031
+ turnToolMessages.push({
1032
+ role: 'system',
1033
+ content: recovery.cooldownMessage,
1034
+ });
1035
+ }
1036
+ else if (recovery.kind === 'surface' && recovery.surfaceCard) {
1037
+ // Stage 3: structured failure. Stop dispatching the rest of
1038
+ // the batch — anything else is throwing good budget after
1039
+ // bad. The outer loop reads `surfaceDecision` below and
1040
+ // exits cleanly.
1041
+ surfaceDecision = recovery;
1042
+ break;
1043
+ }
1044
+ }
1045
+ // v4.6 prep — if the per-tool-call abort check fired inside the
1046
+ // for-of above, finishReason is now 'interrupted'. Break the outer
1047
+ // while immediately so we don't run another provider call. Done
1048
+ // here (post-for-of) rather than inside the for-of because the
1049
+ // inner `break` only exits the inner loop.
1050
+ if (finishReason === 'interrupted') {
1051
+ break;
1052
+ }
1053
+ // v4.2 Phase 4 — apply rollback if the controller asked for it.
1054
+ // Truncate messages to the captured snapshot length, restore
1055
+ // TurnState internals, then push a corrective system message
1056
+ // and continue the OUTER iteration loop. We deliberately drop
1057
+ // any partial `turnToolMessages` collected before the rollback
1058
+ // trigger — those are the noise we're trying to undo.
1059
+ //
1060
+ // Hard-block invariant: TurnState only emits
1061
+ // `cooldown_with_rollback` when the target checkpoint has
1062
+ // `containedMutations === false`, so we never get here for an
1063
+ // iteration that ran a mutating tool. The optional
1064
+ // `rollback.blockedBy` is empty in Phase 4 (kept on the type
1065
+ // for a Phase 5+ soft-rollback variant).
1066
+ if (rollbackDecision && rollbackDecision.rollback) {
1067
+ const { checkpoint, blockedBy } = rollbackDecision.rollback;
1068
+ // Truncate messages array to the captured length. The captured
1069
+ // items are immutable Message references; we keep them as-is
1070
+ // and just shorten the live array.
1071
+ messages.length = checkpoint.messages.length;
1072
+ // Restore TurnState mutable internals (stage / streaks /
1073
+ // cooledDownTools / arrays). The cooled-down tools map is
1074
+ // preserved as it was at checkpoint time — but the controller
1075
+ // already added the looping tool to `cooledDownTools` before
1076
+ // emitting the decision, so we need to RE-apply that cooldown
1077
+ // after restore to honour the cooldown intent.
1078
+ turnState.restoreInternalsFrom(checkpoint);
1079
+ // Re-cool the tool that triggered the rollback so the next
1080
+ // provider call sees the constrained schema.
1081
+ if (rollbackDecision.toolName) {
1082
+ turnState.reapplyCooldown(rollbackDecision.toolName);
1083
+ }
1084
+ // Inject corrective system message so the model sees what
1085
+ // happened and why the tool just disappeared from its menu.
1086
+ messages.push({
1087
+ role: 'system',
1088
+ content: (0, checkpoint_1.buildRollbackMessage)({
1089
+ iteration: checkpoint.iteration,
1090
+ toolName: rollbackDecision.toolName,
1091
+ blockedBy,
1092
+ }),
1093
+ });
1094
+ // Continue the outer iteration loop from the restored
1095
+ // baseline. The next provider call gets the filtered tool
1096
+ // schema (cooldown applied) and the corrective message.
1097
+ continue;
1098
+ }
1099
+ // v4.1.6 spike (TCE) — terminal surface handling.
1100
+ if (surfaceDecision && surfaceDecision.kind === 'surface') {
1101
+ finishReason = 'tool_loop';
1102
+ // v4.2 Phase 3 — enrich the base surface card with a
1103
+ // structured RecoveryReport. Pure synthesis from TurnState's
1104
+ // diagnostic snapshot + first-user-message goal + duration.
1105
+ // Implicit gating: this branch is only reachable when
1106
+ // TurnState is enabled, so AIDEN_TCE=0 (opt-out) never
1107
+ // builds a report.
1108
+ if (surfaceDecision.surfaceCard) {
1109
+ const report = (0, recoveryReport_1.buildRecoveryReport)({
1110
+ snapshot: turnState.getDiagnosticSnapshot(),
1111
+ goal: (0, recoveryReport_1.extractGoal)(messages),
1112
+ exitReason: 'tool_loop',
1113
+ durationMs: Date.now() - turnStartedAt,
1114
+ });
1115
+ toolLoopCard = (0, recoveryReport_1.enrichCardWithReport)(surfaceDecision.surfaceCard, report);
1116
+ }
1117
+ else {
1118
+ toolLoopCard = surfaceDecision.surfaceCard;
1119
+ }
1120
+ // Push the partial tool messages we collected so honesty +
1121
+ // history downstream see the full sequence including the
1122
+ // loop-trigger call. No final assistant message — the
1123
+ // tool_loop card IS the user-facing surface.
1124
+ messages.push(...turnToolMessages);
1125
+ finalContent = '';
1126
+ break;
626
1127
  }
627
1128
  // ── Iteration-budget injection on the LAST tool message ──────────
628
1129
  if (this.iterationBudgetInjection && turnToolMessages.length > 0) {
@@ -635,6 +1136,11 @@ class AidenAgent {
635
1136
  messages.push(...turnToolMessages);
636
1137
  // Loop continues — provider gets the tool results next iteration.
637
1138
  }
1139
+ // v4.6 Phase 1 — clear the per-turn signal exposure before returning.
1140
+ // No-throw guarantee: if any prior code in this loop threw, the next
1141
+ // call's `this._currentSignal = runOptions.signal` at the top will
1142
+ // overwrite the stale value before any tool can read it.
1143
+ this._currentSignal = undefined;
638
1144
  return {
639
1145
  finalContent,
640
1146
  messages,
@@ -645,6 +1151,7 @@ class AidenAgent {
645
1151
  totalUsage,
646
1152
  toolCallTrace,
647
1153
  fullTrace,
1154
+ toolLoopCard,
648
1155
  };
649
1156
  }
650
1157
  /**
@@ -669,7 +1176,9 @@ class AidenAgent {
669
1176
  }
670
1177
  catch { /* defensive */ }
671
1178
  if (!wantStream) {
672
- return this.provider.call({ messages, tools });
1179
+ // v4.6 prep — forward the abort signal into the provider call so
1180
+ // an in-flight HTTP request can be cancelled mid-flight.
1181
+ return this.provider.call({ messages, tools, signal: runOptions.signal });
673
1182
  }
674
1183
  let firstDeltaFired = false;
675
1184
  let finalOutput = null;
@@ -677,6 +1186,9 @@ class AidenAgent {
677
1186
  messages,
678
1187
  tools,
679
1188
  stream: true,
1189
+ // v4.6 prep — also forward to streaming adapters; mid-stream
1190
+ // aborts cancel the underlying SSE read via the same signal.
1191
+ signal: runOptions.signal,
680
1192
  });
681
1193
  for await (const evt of stream) {
682
1194
  if (evt.type === 'delta') {
@@ -703,6 +1215,16 @@ class AidenAgent {
703
1215
  }
704
1216
  }
705
1217
  if (!finalOutput) {
1218
+ // v4.6 prep — if the stream consumer exited without a `done`
1219
+ // event because the signal was aborted mid-stream, surface a
1220
+ // synthetic AbortError so the outer catch routes it as
1221
+ // 'interrupted' rather than the misleading "closed without done"
1222
+ // generic error.
1223
+ if (runOptions.signal?.aborted) {
1224
+ const abortErr = new Error('Streaming provider aborted before done event');
1225
+ abortErr.name = 'AbortError';
1226
+ throw abortErr;
1227
+ }
706
1228
  throw new Error('Streaming provider closed without a done event');
707
1229
  }
708
1230
  return finalOutput;