aiden-runtime 4.5.0 → 4.6.1

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 (50) hide show
  1. package/README.md +17 -2
  2. package/dist/cli/v4/aidenCLI.js +207 -100
  3. package/dist/cli/v4/chatSession.js +120 -0
  4. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +2 -0
  5. package/dist/cli/v4/commands/fanout.js +42 -59
  6. package/dist/cli/v4/commands/help.js +8 -0
  7. package/dist/cli/v4/commands/index.js +21 -1
  8. package/dist/cli/v4/commands/mcp.js +80 -54
  9. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  10. package/dist/cli/v4/commands/recovery.js +122 -0
  11. package/dist/cli/v4/commands/runs.js +22 -2
  12. package/dist/cli/v4/commands/spawnPause.js +93 -0
  13. package/dist/cli/v4/commands/walkthrough.js +140 -0
  14. package/dist/cli/v4/daemonAgentBuilder.js +4 -1
  15. package/dist/cli/v4/defaultSoul.js +1 -1
  16. package/dist/cli/v4/onboarding/disclaimer.js +162 -0
  17. package/dist/cli/v4/onboarding/loading.js +208 -0
  18. package/dist/cli/v4/onboarding/providerPicker.js +126 -0
  19. package/dist/cli/v4/onboarding/successScreen.js +68 -0
  20. package/dist/cli/v4/repl/firstRunHint.js +107 -0
  21. package/dist/cli/v4/setupWizard.js +201 -31
  22. package/dist/core/v4/aidenAgent.js +219 -1
  23. package/dist/core/v4/daemon/bootstrap.js +47 -0
  24. package/dist/core/v4/daemon/db/migrations.js +66 -0
  25. package/dist/core/v4/daemon/runStore.js +33 -3
  26. package/dist/core/v4/providerFallback.js +35 -2
  27. package/dist/core/v4/providers/modelFetch.js +179 -0
  28. package/dist/core/v4/providers/probe.js +275 -0
  29. package/dist/core/v4/runtimeToggles.js +30 -3
  30. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  31. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  32. package/dist/core/v4/subagent/childBuilder.js +391 -0
  33. package/dist/core/v4/subagent/fanout.js +75 -51
  34. package/dist/core/v4/subagent/spawnPause.js +191 -0
  35. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  36. package/dist/core/v4/toolRegistry.js +19 -3
  37. package/dist/core/v4/ui/banner.js +133 -0
  38. package/dist/core/v4/ui/theme.js +164 -0
  39. package/dist/core/version.js +1 -1
  40. package/dist/moat/plannerGuard.js +29 -0
  41. package/dist/providers/v4/anthropicAdapter.js +31 -3
  42. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  43. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  44. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  45. package/dist/tools/v4/index.js +17 -3
  46. package/dist/tools/v4/skills/lookupToolSchema.js +6 -1
  47. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  48. package/dist/tools/v4/subagent/subagentFanout.js +53 -1
  49. package/dist/tools/v4/ui/_uiSmokeTool.js +60 -0
  50. package/package.json +7 -3
@@ -0,0 +1,164 @@
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/ui/theme.ts — ONB1 (v4.7 onboarding rework).
10
+ *
11
+ * Self-contained theme module for the redesigned first-run experience.
12
+ * Lives alongside, NOT instead of, the existing skin engine
13
+ * (cli/v4/skinEngine.ts). The skin engine drives the REPL / boot card
14
+ * / every post-onboarding surface; this theme drives only the
15
+ * onboarding screens (slices 1–10 of dispatch ONB1).
16
+ *
17
+ * Why a separate module:
18
+ * - Onboarding palette is specified to a different muted/text spec
19
+ * than the existing skin (e.g. cool-grey #71717A vs warm-tan
20
+ * #B8A89A). Swapping the skin would re-paint every chat turn the
21
+ * user sees afterwards, surprising the eye on the *second* boot.
22
+ * - Onboarding is a single-shot surface — no per-user customisation,
23
+ * no YAML loader, no `monochrome`/`light` variants needed beyond
24
+ * graceful colour-depth degradation.
25
+ *
26
+ * Truecolor → 256 → 16 detection runs once at module load and is
27
+ * cached. Set `AIDEN_FORCE_COLOR_DEPTH=truecolor|256|16|none` to
28
+ * override (smoke tests rely on this).
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.SEP_LIGHT = exports.SEP_HEAVY = exports.dim = exports.italic = exports.bold = exports.c = exports.PALETTE = void 0;
32
+ exports.getColorDepth = getColorDepth;
33
+ exports.paint = paint;
34
+ exports.separator = separator;
35
+ exports.termWidth = termWidth;
36
+ /**
37
+ * The 8-colour onboarding palette. Hex strings are the source of
38
+ * truth; the emit functions below convert per detected depth.
39
+ */
40
+ exports.PALETTE = {
41
+ primary: '#FF6B35', // brand orange — Aiden hero
42
+ accent: '#FFB088', // light orange — highlights
43
+ success: '#4ADE80', // green checkmarks
44
+ warning: '#FBBF24', // amber warnings
45
+ error: '#EF4444', // red errors
46
+ text: '#F5F5F5', // bright white — headers/titles
47
+ muted: '#71717A', // dim grey — secondary text/hints
48
+ rule: '#27272A', // dark grey — separators
49
+ };
50
+ /** Parse a `#RRGGBB` hex string into [r,g,b]. */
51
+ function hexToRgb(hex) {
52
+ const m = /^#?([0-9a-fA-F]{6})$/.exec(hex);
53
+ if (!m)
54
+ return [255, 255, 255];
55
+ const n = parseInt(m[1], 16);
56
+ return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
57
+ }
58
+ /**
59
+ * Map a 24-bit RGB triple to the closest xterm-256 colour index.
60
+ * Uses the standard 6×6×6 cube + grey-ramp approximation.
61
+ */
62
+ function rgbTo256(r, g, b) {
63
+ // Grey-ramp fast path: when r==g==b within 8, prefer the 24-step ramp.
64
+ if (Math.abs(r - g) < 8 && Math.abs(g - b) < 8) {
65
+ if (r < 8)
66
+ return 16;
67
+ if (r > 248)
68
+ return 231;
69
+ return Math.round(((r - 8) / 247) * 24) + 232;
70
+ }
71
+ const q = (v) => Math.round(v / 51);
72
+ return 16 + 36 * q(r) + 6 * q(g) + q(b);
73
+ }
74
+ /**
75
+ * Map a 24-bit RGB triple to a low-fidelity 16-colour ANSI code
76
+ * (30–37 / 90–97). Picks the closest of the 16 standard slots.
77
+ */
78
+ function rgbTo16(r, g, b) {
79
+ const STD = [
80
+ [30, 0, 0, 0], [31, 205, 49, 49], [32, 13, 188, 121], [33, 229, 229, 16],
81
+ [34, 36, 114, 200], [35, 188, 63, 188], [36, 17, 168, 205], [37, 229, 229, 229],
82
+ [90, 102, 102, 102], [91, 241, 76, 76], [92, 35, 209, 139], [93, 245, 245, 67],
83
+ [94, 59, 142, 234], [95, 214, 112, 214], [96, 41, 184, 219], [97, 229, 229, 229],
84
+ ];
85
+ let best = STD[0];
86
+ let bestDist = Infinity;
87
+ for (const cand of STD) {
88
+ const [, cr, cg, cb] = cand;
89
+ const d = (r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2;
90
+ if (d < bestDist) {
91
+ bestDist = d;
92
+ best = cand;
93
+ }
94
+ }
95
+ return best[0];
96
+ }
97
+ /** Detect the terminal's effective colour depth. Cached at module load. */
98
+ function detectColorDepth() {
99
+ const forced = process.env.AIDEN_FORCE_COLOR_DEPTH?.toLowerCase();
100
+ if (forced === 'truecolor' || forced === '256' || forced === '16' || forced === 'none') {
101
+ return forced;
102
+ }
103
+ if (process.env.NO_COLOR && process.env.NO_COLOR !== '')
104
+ return 'none';
105
+ if (!process.stdout.isTTY)
106
+ return 'none';
107
+ const ct = (process.env.COLORTERM ?? '').toLowerCase();
108
+ if (ct === 'truecolor' || ct === '24bit')
109
+ return 'truecolor';
110
+ const term = (process.env.TERM ?? '').toLowerCase();
111
+ if (term.includes('256'))
112
+ return '256';
113
+ if (term === 'dumb' || term === '')
114
+ return 'none';
115
+ return '16';
116
+ }
117
+ const COLOR_DEPTH = detectColorDepth();
118
+ /** Public: report the depth (smoke tests + diagnostics). */
119
+ function getColorDepth() { return COLOR_DEPTH; }
120
+ /** Wrap `text` in the SGR sequence for `kind`, degrading per depth. */
121
+ function paint(text, kind) {
122
+ if (COLOR_DEPTH === 'none')
123
+ return text;
124
+ const [r, g, b] = hexToRgb(exports.PALETTE[kind]);
125
+ if (COLOR_DEPTH === 'truecolor')
126
+ return `\x1b[38;2;${r};${g};${b}m${text}\x1b[39m`;
127
+ if (COLOR_DEPTH === '256')
128
+ return `\x1b[38;5;${rgbTo256(r, g, b)}m${text}\x1b[39m`;
129
+ return `\x1b[${rgbTo16(r, g, b)}m${text}\x1b[39m`;
130
+ }
131
+ /** Convenience helpers — one per palette key. */
132
+ exports.c = {
133
+ primary: (s) => paint(s, 'primary'),
134
+ accent: (s) => paint(s, 'accent'),
135
+ success: (s) => paint(s, 'success'),
136
+ warning: (s) => paint(s, 'warning'),
137
+ error: (s) => paint(s, 'error'),
138
+ text: (s) => paint(s, 'text'),
139
+ muted: (s) => paint(s, 'muted'),
140
+ rule: (s) => paint(s, 'rule'),
141
+ };
142
+ /** SGR helpers for emphasis. Italic gracefully degrades when unsupported. */
143
+ const bold = (s) => (COLOR_DEPTH === 'none' ? s : `\x1b[1m${s}\x1b[22m`);
144
+ exports.bold = bold;
145
+ const italic = (s) => (COLOR_DEPTH === 'none' ? s : `\x1b[3m${s}\x1b[23m`);
146
+ exports.italic = italic;
147
+ const dim = (s) => (COLOR_DEPTH === 'none' ? s : `\x1b[2m${s}\x1b[22m`);
148
+ exports.dim = dim;
149
+ /**
150
+ * Common ornaments — single source so onboarding screens share rhythm.
151
+ */
152
+ exports.SEP_HEAVY = '━';
153
+ exports.SEP_LIGHT = '─';
154
+ /** Render a full-width separator in RULE colour, optionally heavy. */
155
+ function separator(width, heavy = true) {
156
+ const w = Math.max(8, Math.min(width, 100));
157
+ const ch = heavy ? exports.SEP_HEAVY : exports.SEP_LIGHT;
158
+ return exports.c.rule(ch.repeat(w));
159
+ }
160
+ /** Effective terminal width clamped to a sane band. */
161
+ function termWidth() {
162
+ const raw = process.stdout.columns ?? 80;
163
+ return Math.max(40, Math.min(raw, 100));
164
+ }
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
4
  // AUTO-GENERATED by scripts/inject-version.js — do not edit by hand
5
- exports.VERSION = '4.5.0';
5
+ exports.VERSION = '4.6.1';
@@ -87,10 +87,39 @@ const RULES = [
87
87
  toolsets: ['execute'],
88
88
  },
89
89
  // Process registry
90
+ //
91
+ // Note: the bare word "spawn" appears in this rule's keyword list
92
+ // (legacy — predates v4.6 sub-agents). The dedicated 'subagent'
93
+ // rule below ALSO matches "spawn" via a tighter delegation
94
+ // vocabulary, so a message like "spawn a background server" hits
95
+ // BOTH rules and adds both toolsets — UNION semantics make this
96
+ // additive, not conflicting.
90
97
  {
91
98
  keywords: /\b(process|background|long.?running|server|spawn|kill|daemon)\b/i,
92
99
  toolsets: ['process'],
93
100
  },
101
+ // v4.6 Phase 1 — sub-agent delegation surface (spawn_sub_agent +
102
+ // subagent_fanout). Both tools live in toolset `'subagent'`, which
103
+ // no pre-v4.6 rule mapped to — so PlannerGuard's per-turn narrowing
104
+ // silently stripped them from the model's catalog whenever any
105
+ // other rule fired. The model could see them via lookup_tool_schema
106
+ // but failed to actually invoke them because the provider tool list
107
+ // (post-narrow) didn't include them — see Dispatch 2H diagnostic.
108
+ //
109
+ // Regex notes:
110
+ // - `spawn_sub_agent` and `subagent_fanout` literals are listed
111
+ // explicitly because `\bspawn\b` does NOT match within
112
+ // `spawn_sub_agent` (underscore is a word char in JS regex,
113
+ // so there's no word boundary between `n` and `_`). Users who
114
+ // name the tool directly hit the literal arm.
115
+ // - The free-form vocabulary arm (`spawn`, `delegate`, etc.)
116
+ // catches natural-language delegation intent. UNION semantics
117
+ // with other rules let "spawn a child to read files" surface
118
+ // both 'subagent' AND 'files'.
119
+ {
120
+ keywords: /\b(spawn_sub_agent|subagent_fanout|spawn|subagent|sub.?agent|delegate|fanout|fan.?out|child.?agent|parallel|isolated)\b/i,
121
+ toolsets: ['subagent'],
122
+ },
94
123
  // Media playback control (v4.1.4-media)
95
124
  //
96
125
  // Without this, intents like "list media sessions" matched the
@@ -92,14 +92,14 @@ class AnthropicAdapter {
92
92
  // ── Public: non-streaming ────────────────────────────────────────────────
93
93
  async call(input) {
94
94
  const body = this.buildBody(input, /* streaming */ false);
95
- const reply = await this.dispatch(body, /* streaming */ false);
95
+ const reply = await this.dispatch(body, /* streaming */ false, input.signal);
96
96
  const json = (await reply.json());
97
97
  return decodeResponse(json);
98
98
  }
99
99
  // ── Public: streaming ────────────────────────────────────────────────────
100
100
  async *callStream(input) {
101
101
  const body = this.buildBody(input, /* streaming */ true);
102
- const reply = await this.dispatch(body, /* streaming */ true);
102
+ const reply = await this.dispatch(body, /* streaming */ true, input.signal);
103
103
  if (!reply.body) {
104
104
  // Server promised SSE but gave us nothing — fall through to a synthetic
105
105
  // empty done event so the agent loop terminates rather than hangs.
@@ -163,7 +163,7 @@ class AnthropicAdapter {
163
163
  // beta flags, or per-deployment routing tags without forking the adapter.
164
164
  return { ...headers, ...this.extraHeaders };
165
165
  }
166
- async dispatch(body, streaming) {
166
+ async dispatch(body, streaming, externalSignal) {
167
167
  // Resolved once per process via the userAgent module's cache, so paying
168
168
  // for the version detection here is cheap on every retry/turn.
169
169
  const userAgent = await (0, userAgent_1.getClaudeCliUserAgent)();
@@ -174,6 +174,22 @@ class AnthropicAdapter {
174
174
  for (let attempt = 0; attempt < totalTries; attempt++) {
175
175
  const controller = new AbortController();
176
176
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
177
+ // v4.6 prep — forward an external AbortSignal into this attempt's
178
+ // internal controller so a parent agent that aborts mid-flight
179
+ // cancels the in-flight fetch. External aborts surface as a raw
180
+ // AbortError (NOT ProviderTimeoutError) so AidenAgent can route
181
+ // them as `finishReason: 'interrupted'` instead of treating them
182
+ // as a retryable timeout.
183
+ let externalAbortHandler = null;
184
+ if (externalSignal) {
185
+ if (externalSignal.aborted) {
186
+ controller.abort();
187
+ }
188
+ else {
189
+ externalAbortHandler = () => controller.abort();
190
+ externalSignal.addEventListener('abort', externalAbortHandler, { once: true });
191
+ }
192
+ }
177
193
  let response;
178
194
  try {
179
195
  response = await fetch(this.endpoint, {
@@ -185,7 +201,16 @@ class AnthropicAdapter {
185
201
  }
186
202
  catch (err) {
187
203
  clearTimeout(timer);
204
+ if (externalAbortHandler && externalSignal) {
205
+ externalSignal.removeEventListener('abort', externalAbortHandler);
206
+ }
188
207
  if (err?.name === 'AbortError') {
208
+ // v4.6 prep — external abort takes priority over internal
209
+ // timeout. Surface the raw AbortError immediately (no retry)
210
+ // so AidenAgent's catch routes it as 'interrupted'.
211
+ if (externalSignal?.aborted) {
212
+ throw err;
213
+ }
189
214
  // Treat timeout as retryable; only surface ProviderTimeoutError if
190
215
  // we've burned the last attempt.
191
216
  lastErr = new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
@@ -200,6 +225,9 @@ class AnthropicAdapter {
200
225
  throw lastErr;
201
226
  }
202
227
  clearTimeout(timer);
228
+ if (externalAbortHandler && externalSignal) {
229
+ externalSignal.removeEventListener('abort', externalAbortHandler);
230
+ }
203
231
  if (response.ok)
204
232
  return response;
205
233
  // Phase 25.1.5d diagnostic: gated dump of request + response so we
@@ -73,7 +73,7 @@ class ChatCompletionsAdapter {
73
73
  // ── Non-streaming ────────────────────────────────────────────────────
74
74
  async call(input) {
75
75
  const body = this.buildBody(input, /* streaming */ false);
76
- const reply = await this.dispatch(body, /* streaming */ false);
76
+ const reply = await this.dispatch(body, /* streaming */ false, input.signal);
77
77
  const text = await reply.text();
78
78
  let parsed;
79
79
  try {
@@ -91,7 +91,7 @@ class ChatCompletionsAdapter {
91
91
  // ── Streaming ────────────────────────────────────────────────────────
92
92
  async *callStream(input) {
93
93
  const body = this.buildBody(input, /* streaming */ true);
94
- const reply = await this.dispatch(body, /* streaming */ true);
94
+ const reply = await this.dispatch(body, /* streaming */ true, input.signal);
95
95
  if (!reply.body) {
96
96
  yield {
97
97
  type: 'done',
@@ -150,7 +150,7 @@ class ChatCompletionsAdapter {
150
150
  headers['Accept'] = 'text/event-stream';
151
151
  return { ...headers, ...this.extraHeaders };
152
152
  }
153
- async dispatch(body, streaming) {
153
+ async dispatch(body, streaming, externalSignal) {
154
154
  const headers = this.buildHeaders(streaming);
155
155
  const serialised = JSON.stringify(body);
156
156
  const totalTries = this.maxRetries + 1;
@@ -158,6 +158,19 @@ class ChatCompletionsAdapter {
158
158
  for (let attempt = 0; attempt < totalTries; attempt++) {
159
159
  const controller = new AbortController();
160
160
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
161
+ // v4.6 prep — forward external abort into the internal controller.
162
+ // External aborts surface as raw AbortError so AidenAgent routes
163
+ // them as 'interrupted' rather than retrying as ProviderTimeoutError.
164
+ let externalAbortHandler = null;
165
+ if (externalSignal) {
166
+ if (externalSignal.aborted) {
167
+ controller.abort();
168
+ }
169
+ else {
170
+ externalAbortHandler = () => controller.abort();
171
+ externalSignal.addEventListener('abort', externalAbortHandler, { once: true });
172
+ }
173
+ }
161
174
  let response;
162
175
  try {
163
176
  response = await fetch(this.endpoint, {
@@ -169,7 +182,14 @@ class ChatCompletionsAdapter {
169
182
  }
170
183
  catch (err) {
171
184
  clearTimeout(timer);
185
+ if (externalAbortHandler && externalSignal) {
186
+ externalSignal.removeEventListener('abort', externalAbortHandler);
187
+ }
172
188
  if (err?.name === 'AbortError') {
189
+ // v4.6 prep — external abort takes priority over internal timeout.
190
+ if (externalSignal?.aborted) {
191
+ throw err;
192
+ }
173
193
  lastErr = new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
174
194
  }
175
195
  else {
@@ -182,6 +202,9 @@ class ChatCompletionsAdapter {
182
202
  throw lastErr;
183
203
  }
184
204
  clearTimeout(timer);
205
+ if (externalAbortHandler && externalSignal) {
206
+ externalSignal.removeEventListener('abort', externalAbortHandler);
207
+ }
185
208
  if (response.ok)
186
209
  return response;
187
210
  const status = response.status;
@@ -104,7 +104,7 @@ class CodexResponsesAdapter {
104
104
  // ── Public: non-streaming entry ─────────────────────────────────────
105
105
  async call(input) {
106
106
  const body = this.buildBody(input);
107
- const reply = await this.dispatch(body);
107
+ const reply = await this.dispatch(body, input.signal);
108
108
  // Codex backend always streams; aggregate the SSE frames into the
109
109
  // same shape the JSON path returns. Plain api.openai.com path returns
110
110
  // JSON directly.
@@ -175,7 +175,7 @@ class CodexResponsesAdapter {
175
175
  }
176
176
  return { ...headers, ...this.extraHeaders };
177
177
  }
178
- async dispatch(body) {
178
+ async dispatch(body, externalSignal) {
179
179
  const headers = this.buildHeaders();
180
180
  const serialised = JSON.stringify(body);
181
181
  const totalTries = this.maxRetries + 1;
@@ -183,6 +183,19 @@ class CodexResponsesAdapter {
183
183
  for (let attempt = 0; attempt < totalTries; attempt++) {
184
184
  const controller = new AbortController();
185
185
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
186
+ // v4.6 prep — forward external abort into the internal controller.
187
+ // External aborts surface as raw AbortError so AidenAgent routes
188
+ // them as 'interrupted' rather than retrying as ProviderTimeoutError.
189
+ let externalAbortHandler = null;
190
+ if (externalSignal) {
191
+ if (externalSignal.aborted) {
192
+ controller.abort();
193
+ }
194
+ else {
195
+ externalAbortHandler = () => controller.abort();
196
+ externalSignal.addEventListener('abort', externalAbortHandler, { once: true });
197
+ }
198
+ }
186
199
  let response;
187
200
  try {
188
201
  response = await fetch(this.endpoint, {
@@ -194,7 +207,14 @@ class CodexResponsesAdapter {
194
207
  }
195
208
  catch (err) {
196
209
  clearTimeout(timer);
210
+ if (externalAbortHandler && externalSignal) {
211
+ externalSignal.removeEventListener('abort', externalAbortHandler);
212
+ }
197
213
  if (err?.name === 'AbortError') {
214
+ // v4.6 prep — external abort takes priority over internal timeout.
215
+ if (externalSignal?.aborted) {
216
+ throw err;
217
+ }
198
218
  lastErr = new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
199
219
  }
200
220
  else {
@@ -207,6 +227,9 @@ class CodexResponsesAdapter {
207
227
  throw lastErr;
208
228
  }
209
229
  clearTimeout(timer);
230
+ if (externalAbortHandler && externalSignal) {
231
+ externalSignal.removeEventListener('abort', externalAbortHandler);
232
+ }
210
233
  if (response.ok)
211
234
  return response;
212
235
  const status = response.status;
@@ -63,7 +63,7 @@ class OllamaPromptToolsAdapter {
63
63
  let lastError = null;
64
64
  for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {
65
65
  try {
66
- const response = await this.fetchWithTimeout(url, headers, body);
66
+ const response = await this.fetchWithTimeout(url, headers, body, input.signal);
67
67
  if (response.ok) {
68
68
  const json = (await response.json());
69
69
  return this.parseResponse(json);
@@ -134,6 +134,17 @@ class OllamaPromptToolsAdapter {
134
134
  const headers = { 'Content-Type': 'application/json' };
135
135
  const controller = new AbortController();
136
136
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
137
+ // v4.6 prep — forward external abort into the internal controller.
138
+ let externalAbortHandler = null;
139
+ if (input.signal) {
140
+ if (input.signal.aborted) {
141
+ controller.abort();
142
+ }
143
+ else {
144
+ externalAbortHandler = () => controller.abort();
145
+ input.signal.addEventListener('abort', externalAbortHandler, { once: true });
146
+ }
147
+ }
137
148
  let response;
138
149
  try {
139
150
  response = await fetch(url, {
@@ -145,13 +156,23 @@ class OllamaPromptToolsAdapter {
145
156
  }
146
157
  catch (err) {
147
158
  clearTimeout(timer);
159
+ if (externalAbortHandler && input.signal) {
160
+ input.signal.removeEventListener('abort', externalAbortHandler);
161
+ }
148
162
  if (err instanceof Error && err.name === 'AbortError') {
163
+ // v4.6 prep — external abort takes priority over internal timeout.
164
+ if (input.signal?.aborted) {
165
+ throw err;
166
+ }
149
167
  throw new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
150
168
  }
151
169
  throw new errors_1.ProviderError(`Ollama not reachable at ${this.baseUrl}: ${err instanceof Error ? err.message : String(err)}`, this.providerName, undefined, err, true);
152
170
  }
153
171
  if (!response.ok) {
154
172
  clearTimeout(timer);
173
+ if (externalAbortHandler && input.signal) {
174
+ input.signal.removeEventListener('abort', externalAbortHandler);
175
+ }
155
176
  const status = response.status;
156
177
  const rawText = await this.safeReadText(response);
157
178
  // Phase v4.1.1-oauth-fix Phase 5: composeMessage handles body
@@ -160,8 +181,15 @@ class OllamaPromptToolsAdapter {
160
181
  }
161
182
  if (!response.body) {
162
183
  clearTimeout(timer);
184
+ if (externalAbortHandler && input.signal) {
185
+ input.signal.removeEventListener('abort', externalAbortHandler);
186
+ }
163
187
  throw new errors_1.ProviderError(`Provider ${this.providerName} returned an empty stream body`, this.providerName);
164
188
  }
189
+ // Response is good; the stream consumer will run for a while. The
190
+ // controller stays armed (with `externalSignal` still listening) so
191
+ // that mid-stream aborts cancel reader.read() via fetch's signal.
192
+ // Listener cleanup happens in the stream-consumer try/finally below.
165
193
  const reader = response.body.getReader();
166
194
  const decoder = new TextDecoder('utf-8');
167
195
  let lineBuffer = '';
@@ -223,10 +251,18 @@ class OllamaPromptToolsAdapter {
223
251
  }
224
252
  catch (err) {
225
253
  clearTimeout(timer);
254
+ // v4.6 prep — external abort during mid-stream read surfaces as
255
+ // AbortError; re-throw so AidenAgent routes it as 'interrupted'.
256
+ if (err instanceof Error && err.name === 'AbortError' && input.signal?.aborted) {
257
+ throw err;
258
+ }
226
259
  throw new errors_1.ProviderError(`Provider ${this.providerName} stream interrupted: ${err instanceof Error ? err.message : String(err)}`, this.providerName, undefined, err, true);
227
260
  }
228
261
  finally {
229
262
  clearTimeout(timer);
263
+ if (externalAbortHandler && input.signal) {
264
+ input.signal.removeEventListener('abort', externalAbortHandler);
265
+ }
230
266
  try {
231
267
  reader.releaseLock();
232
268
  }
@@ -422,9 +458,20 @@ class OllamaPromptToolsAdapter {
422
458
  const textBefore = firstTagIdx >= 0 ? text.slice(0, firstTagIdx).trim() : text;
423
459
  return { textBefore, toolCalls };
424
460
  }
425
- async fetchWithTimeout(url, headers, body) {
461
+ async fetchWithTimeout(url, headers, body, externalSignal) {
426
462
  const controller = new AbortController();
427
463
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
464
+ // v4.6 prep — forward external abort into the internal controller.
465
+ let externalAbortHandler = null;
466
+ if (externalSignal) {
467
+ if (externalSignal.aborted) {
468
+ controller.abort();
469
+ }
470
+ else {
471
+ externalAbortHandler = () => controller.abort();
472
+ externalSignal.addEventListener('abort', externalAbortHandler, { once: true });
473
+ }
474
+ }
428
475
  try {
429
476
  return await fetch(url, {
430
477
  method: 'POST',
@@ -435,12 +482,20 @@ class OllamaPromptToolsAdapter {
435
482
  }
436
483
  catch (err) {
437
484
  if (err instanceof Error && err.name === 'AbortError') {
485
+ // v4.6 prep — external abort takes priority over internal timeout.
486
+ // Surface the raw AbortError so AidenAgent routes it as 'interrupted'.
487
+ if (externalSignal?.aborted) {
488
+ throw err;
489
+ }
438
490
  throw new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
439
491
  }
440
492
  throw err;
441
493
  }
442
494
  finally {
443
495
  clearTimeout(timer);
496
+ if (externalAbortHandler && externalSignal) {
497
+ externalSignal.removeEventListener('abort', externalAbortHandler);
498
+ }
444
499
  }
445
500
  }
446
501
  async safeReadText(response) {
@@ -90,6 +90,9 @@ const memoryReplace_1 = require("./memory/memoryReplace");
90
90
  const memoryRemove_1 = require("./memory/memoryRemove");
91
91
  const sessionSummary_1 = require("./memory/sessionSummary");
92
92
  const subagentFanout_1 = require("./subagent/subagentFanout");
93
+ // v4.6 Phase 1 — spawn_sub_agent stub registered alongside the
94
+ // fanout stub so the schema is visible at agent construction.
95
+ const spawnSubAgentTool_1 = require("./subagent/spawnSubAgentTool");
93
96
  /**
94
97
  * Register every read-only tool into `registry`. The
95
98
  * `lookup_tool_schema` tool needs a registry reference, so it's
@@ -152,11 +155,25 @@ function registerReadOnlyTools(registry) {
152
155
  // Until then, calling the stub returns a clear "not wired" error
153
156
  // rather than crashing.
154
157
  register(makeSubagentFanoutStub());
158
+ // v4.6 Phase 1 — register a stub for spawn_sub_agent. Same
159
+ // rationale: agent construction at `cli/v4/aidenCLI.ts` snapshots
160
+ // the tool array, so the schema must be in the registry by then.
161
+ // The REPL wiring at `buildAgentRuntime` calls
162
+ // `register(makeSpawnSubAgentTool({...real deps}))` to replace
163
+ // this stub once `parentAgent`, `runStore`, etc. are available.
164
+ // The stub carries `contexts: ['repl']` so it's excluded from the
165
+ // daemon agent's tool catalog via `getSchemas(_, 'daemon')`.
166
+ register((0, spawnSubAgentTool_1.makeSpawnSubAgentStub)());
155
167
  }
156
168
  /** Stub used until the runtime wires real provider / adapter / agent
157
169
  * dependencies. Returns the SAME schema as the real tool so MCP and
158
170
  * /tools see a consistent surface. */
159
171
  function makeSubagentFanoutStub() {
172
+ // v4.6 Phase 2R — `runChild` removed from `SubagentFanoutFactoryOptions`.
173
+ // The stub returns a "no providers configured" error envelope on every
174
+ // call via `resolveProviders: () => []`. Production wires real
175
+ // `spawnDeps` post-runtime build (`cli/v4/aidenCLI.ts` for REPL,
176
+ // `cli/v4/commands/mcp.ts` for MCP serve).
160
177
  return (0, subagentFanout_1.makeSubagentFanoutTool)({
161
178
  resolveProviders: () => [],
162
179
  resolveActiveModel: () => ({ providerId: 'unset', modelId: 'unset' }),
@@ -167,9 +184,6 @@ function makeSubagentFanoutStub() {
167
184
  'Call register(makeSubagentFanoutTool({...})) after buildAgentRuntime.');
168
185
  },
169
186
  },
170
- runChild: async () => {
171
- throw new Error('subagent_fanout: tool not wired — runtime did not replace the stub.');
172
- },
173
187
  });
174
188
  }
175
189
  /**
@@ -64,7 +64,12 @@ function makeLookupToolSchema(registry) {
64
64
  category: handler.category,
65
65
  mutates: handler.mutates,
66
66
  toolset: handler.toolset,
67
- riskTier: 'safe', // v4.4 Phase 1
67
+ // v4.6 Phase 1 — read the queried tool's actual risk tier
68
+ // (was previously hardcoded to 'safe' regardless of the tool,
69
+ // which mis-reported caution/dangerous tools as safe in the
70
+ // /tools surface). Falls back to 'safe' for tools that never
71
+ // annotated their tier — matches the registry-level default.
72
+ riskTier: handler.riskTier ?? 'safe',
68
73
  };
69
74
  },
70
75
  };