aiden-runtime 4.5.0 → 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 (38) hide show
  1. package/README.md +17 -2
  2. package/dist/cli/v4/aidenCLI.js +185 -99
  3. package/dist/cli/v4/chatSession.js +107 -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 +6 -0
  7. package/dist/cli/v4/commands/index.js +16 -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/daemonAgentBuilder.js +4 -1
  14. package/dist/cli/v4/defaultSoul.js +1 -1
  15. package/dist/core/v4/aidenAgent.js +219 -1
  16. package/dist/core/v4/daemon/bootstrap.js +47 -0
  17. package/dist/core/v4/daemon/db/migrations.js +66 -0
  18. package/dist/core/v4/daemon/runStore.js +33 -3
  19. package/dist/core/v4/providerFallback.js +35 -2
  20. package/dist/core/v4/runtimeToggles.js +30 -3
  21. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  22. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  23. package/dist/core/v4/subagent/childBuilder.js +391 -0
  24. package/dist/core/v4/subagent/fanout.js +75 -51
  25. package/dist/core/v4/subagent/spawnPause.js +191 -0
  26. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  27. package/dist/core/v4/toolRegistry.js +19 -3
  28. package/dist/core/version.js +1 -1
  29. package/dist/moat/plannerGuard.js +29 -0
  30. package/dist/providers/v4/anthropicAdapter.js +31 -3
  31. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  32. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  33. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  34. package/dist/tools/v4/index.js +17 -3
  35. package/dist/tools/v4/skills/lookupToolSchema.js +6 -1
  36. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  37. package/dist/tools/v4/subagent/subagentFanout.js +53 -1
  38. package/package.json +7 -3
@@ -0,0 +1,93 @@
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
+ * cli/v4/commands/spawnPause.ts — v4.6 Phase 3A.
10
+ *
11
+ * `/spawn-pause on|off|status [reason...]` — operator kill-switch
12
+ * for sub-agent spawning. Backed by a file marker at
13
+ * `$aidenHome/spawn.paused` (see `core/v4/subagent/spawnPause.ts`)
14
+ * so REPL + daemon + MCP server all coordinate via the same state.
15
+ *
16
+ * /spawn-pause on — pause, no reason
17
+ * /spawn-pause on runaway-fanout — pause, reason="runaway-fanout"
18
+ * /spawn-pause on deploy window — pause, reason="deploy window"
19
+ * /spawn-pause off — resume
20
+ * /spawn-pause status — current state + reason + duration
21
+ *
22
+ * Unlike `/planner-guard`, `/sandbox`, etc., this command does NOT
23
+ * route through `runtimeToggles` — pause state is file-marker-
24
+ * backed (cross-process visibility) with first-class
25
+ * reason/pausedAt/pausedBy metadata that the boolean toggle surface
26
+ * can't carry. Mirrors plannerGuard.ts's command shape; diverges
27
+ * from `_runtimeToggleHelpers` because the storage backend is
28
+ * different.
29
+ *
30
+ * Hard contract: in-flight children are NEVER cancelled by this
31
+ * command. Pause affects only NEW spawns. Operators who want to
32
+ * stop in-flight runs use `aiden runs interrupt <runId>` (the
33
+ * existing per-run cancellation surface from v4.5 Phase 6).
34
+ */
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.spawnPause = void 0;
37
+ const spawnPause_1 = require("../../../core/v4/subagent/spawnPause");
38
+ /** Format a duration in ms as a compact `Xs` / `Xm` / `Xh` string. */
39
+ function formatDuration(ms) {
40
+ if (ms < 1000)
41
+ return `${ms}ms`;
42
+ if (ms < 60000)
43
+ return `${Math.round(ms / 1000)}s`;
44
+ if (ms < 3600000)
45
+ return `${Math.round(ms / 60000)}m`;
46
+ return `${Math.round(ms / 3600000)}h`;
47
+ }
48
+ exports.spawnPause = {
49
+ name: 'spawn-pause',
50
+ description: 'Pause/resume sub-agent spawning (in-flight children continue).',
51
+ category: 'system',
52
+ icon: '⏸',
53
+ handler: async (ctx) => {
54
+ const action = (ctx.args[0] ?? 'status').toLowerCase();
55
+ const reasonArg = ctx.args.slice(1).join(' ').trim() || null;
56
+ let state;
57
+ try {
58
+ state = (0, spawnPause_1.getSpawnPause)();
59
+ }
60
+ catch (e) {
61
+ ctx.display.printError('spawn-pause: not initialized — REPL boot did not wire the singleton.', e instanceof Error ? e.message : String(e));
62
+ return {};
63
+ }
64
+ if (action === 'on' || action === 'enable' || action === 'true' || action === '1') {
65
+ state.pause({ reason: reasonArg, pausedBy: 'repl' });
66
+ const s = state.status();
67
+ const reasonLine = s.reason ? ` reason: ${s.reason}\n` : '';
68
+ ctx.display.write(`spawn-pause: ON\n${reasonLine}`);
69
+ ctx.display.dim(' in-flight children continue. New spawn_sub_agent / subagent_fanout calls will reject.');
70
+ return {};
71
+ }
72
+ if (action === 'off' || action === 'disable' || action === 'false' || action === '0' || action === 'resume') {
73
+ state.resume();
74
+ ctx.display.write('spawn-pause: OFF (resumed)\n');
75
+ return {};
76
+ }
77
+ if (action === 'status' || action === '') {
78
+ const s = state.status();
79
+ if (!s.paused) {
80
+ ctx.display.write('spawn-pause: OFF\n');
81
+ return {};
82
+ }
83
+ const reasonLine = s.reason ? ` reason: ${s.reason}\n` : '';
84
+ const durationLine = s.durationMs !== undefined ? ` duration: ${formatDuration(s.durationMs)}\n` : '';
85
+ const pausedAtLine = s.pausedAt ? ` pausedAt: ${new Date(s.pausedAt).toISOString()}\n` : '';
86
+ const pausedByLine = s.pausedBy ? ` pausedBy: ${s.pausedBy}\n` : '';
87
+ ctx.display.write(`spawn-pause: ON\n${reasonLine}${durationLine}${pausedAtLine}${pausedByLine}`);
88
+ return {};
89
+ }
90
+ ctx.display.printError('Usage: /spawn-pause on [reason...] | off | status');
91
+ return {};
92
+ },
93
+ };
@@ -87,7 +87,10 @@ function buildDaemonAgentBuilder(deps) {
87
87
  };
88
88
  const agent = new aidenAgent_1.AidenAgent({
89
89
  provider: adapter,
90
- tools: deps.toolRegistry.getSchemas(),
90
+ // v4.6 Phase 1 — 'daemon' context filter excludes REPL-only
91
+ // tools (`spawn_sub_agent` per Q6). Tools without an explicit
92
+ // `contexts` field stay visible to both REPL and daemon.
93
+ tools: deps.toolRegistry.getSchemas(undefined, 'daemon'),
91
94
  toolExecutor: deps.toolExecutor,
92
95
  maxTurns,
93
96
  auxiliaryClient: deps.auxiliaryClient,
@@ -30,7 +30,7 @@ exports.PREVIOUS_BUNDLED_SOULS = exports.DEFAULT_SOUL_MD = exports.BUNDLED_SOUL_
30
30
  // <act_dont_ask>. ensureSoulMdSeeded compares this against the user's
31
31
  // on-disk SOUL.md to decide whether to silent-replace (matches a prior
32
32
  // bundled default) or preserve+notify (user-edited).
33
- exports.BUNDLED_SOUL_VERSION = 'v4.5.0';
33
+ exports.BUNDLED_SOUL_VERSION = 'v4.6.0';
34
34
  exports.DEFAULT_SOUL_MD = `You are Aiden — a local-first AI agent built by Shiva Deore at Taracod.
35
35
 
36
36
  Identity:
@@ -40,6 +40,39 @@
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;
45
78
  // v4.1.6 spike — Task Completion Engine (TCE) per-turn loop detector
@@ -60,6 +93,12 @@ const failureClassifier_1 = require("./failureClassifier");
60
93
  // guidance. Implicitly gated by TCE being enabled (surface only
61
94
  // reachable when TurnState is enabled — default ON as of Phase 6).
62
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");
63
102
  // v4.2 Phase 4 — checkpoint / restore. Lets the recovery controller
64
103
  // roll conversation messages + TurnState internals back to before a
65
104
  // looping tool started failing, so the model retries from a clean
@@ -84,6 +123,14 @@ class AidenAgent {
84
123
  constructor(opts) {
85
124
  this.skillMinerTurnIdx = 0;
86
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;
87
134
  /** Cached system prompt — invalidated by setPersonalityOverlay/markMemoryDirty/explicit. */
88
135
  this.cachedSystemPrompt = null;
89
136
  this.compressionEvents = 0;
@@ -263,6 +310,17 @@ class AidenAgent {
263
310
  getEmptyResponseMetrics() {
264
311
  return { ...this.emptyResponseMetrics };
265
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
+ }
266
324
  // ── Main entry: runConversation ──────────────────────────────────────
267
325
  async runConversation(history, options = {}) {
268
326
  // 1. Refresh memory snapshot if the dirty bit was set since last turn.
@@ -512,6 +570,23 @@ class AidenAgent {
512
570
  async narrowTools(userMsg, history) {
513
571
  if (!this.plannerGuard)
514
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
+ }
515
590
  const decision = await this.plannerGuard.decide(userMsg, history);
516
591
  this.onPlannerGuardDecision?.(decision);
517
592
  const allowed = new Set(decision.selectedTools);
@@ -528,8 +603,24 @@ class AidenAgent {
528
603
  * `runConversation` enriches with post-loop scan output.
529
604
  */
530
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;
531
615
  const messages = [...initialMessages];
532
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();
533
624
  // Internal trace mirror that retains tool-call arguments — Honesty's
534
625
  // shape doesn't include args, but SkillTeacher needs them. Both live
535
626
  // off the same entry index.
@@ -564,6 +655,16 @@ class AidenAgent {
564
655
  const failureClassifier = (0, failureClassifier_1.buildDefaultClassifier)();
565
656
  let toolLoopCard = undefined;
566
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
+ }
567
668
  // v4.1.6 spike — decrement cooldown counters once per iteration
568
669
  // so cooled-down tools eventually return to the schemas. No-op
569
670
  // when TCE is disabled.
@@ -604,6 +705,17 @@ class AidenAgent {
604
705
  }
605
706
  catch (err) {
606
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
+ }
607
719
  if (this.fallback && !fallbackActivated) {
608
720
  const next = await this.fallback.activate(error, turnCount);
609
721
  if (next) {
@@ -721,6 +833,16 @@ class AidenAgent {
721
833
  // then continues the outer iteration loop from a clean baseline.
722
834
  let rollbackDecision = null;
723
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
+ }
724
846
  this.onToolCall?.(call, 'before');
725
847
  // v4.2 Phase 4 — mark any active checkpoints as containing a
726
848
  // mutating call BEFORE dispatch. Done pre-dispatch (not post)
@@ -773,6 +895,74 @@ class AidenAgent {
773
895
  // Defensive — a buggy classifier never breaks the loop.
774
896
  classification = null;
775
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
+ }
776
966
  }
777
967
  }
778
968
  toolCallTrace.push({
@@ -852,6 +1042,14 @@ class AidenAgent {
852
1042
  break;
853
1043
  }
854
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
+ }
855
1053
  // v4.2 Phase 4 — apply rollback if the controller asked for it.
856
1054
  // Truncate messages to the captured snapshot length, restore
857
1055
  // TurnState internals, then push a corrective system message
@@ -938,6 +1136,11 @@ class AidenAgent {
938
1136
  messages.push(...turnToolMessages);
939
1137
  // Loop continues — provider gets the tool results next iteration.
940
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;
941
1144
  return {
942
1145
  finalContent,
943
1146
  messages,
@@ -973,7 +1176,9 @@ class AidenAgent {
973
1176
  }
974
1177
  catch { /* defensive */ }
975
1178
  if (!wantStream) {
976
- 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 });
977
1182
  }
978
1183
  let firstDeltaFired = false;
979
1184
  let finalOutput = null;
@@ -981,6 +1186,9 @@ class AidenAgent {
981
1186
  messages,
982
1187
  tools,
983
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,
984
1192
  });
985
1193
  for await (const evt of stream) {
986
1194
  if (evt.type === 'delta') {
@@ -1007,6 +1215,16 @@ class AidenAgent {
1007
1215
  }
1008
1216
  }
1009
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
+ }
1010
1228
  throw new Error('Streaming provider closed without a done event');
1011
1229
  }
1012
1230
  return finalOutput;
@@ -125,9 +125,56 @@ function bootstrapDaemon(opts = {}) {
125
125
  const dbPath = (0, daemonConfig_2.daemonDbPath)(aidenRoot);
126
126
  const lockPath = (0, daemonConfig_2.daemonRuntimeLockPath)(aidenRoot);
127
127
  const markerPath = (0, daemonConfig_2.daemonCleanShutdownMarkerPath)(aidenRoot);
128
+ // v4.6 Phase 3A — wire the spawn-pause singleton against the
129
+ // same `aidenRoot` the REPL uses. Daemon-fired turns that
130
+ // invoke `subagent_fanout` will read the same marker file the
131
+ // REPL writes via /spawn-pause. Cross-process coordination is
132
+ // the whole point of the file-marker design (in-process
133
+ // singletons in three runtimes would each have independent
134
+ // pause flags, which would defeat the operator control).
135
+ // The init is idempotent — if the REPL already ran initSpawnPause
136
+ // in this same process, this call replaces the singleton with
137
+ // an equivalent one pointing at the same path.
138
+ //
139
+ // Defensive try/catch: a pause-init failure must NOT prevent
140
+ // daemon bootstrap. Worst case the singleton stays uninit and
141
+ // tool handlers fall through to their `safeReadPause` path
142
+ // (treat as "not paused"). The daemon's startup probe below
143
+ // is best-effort.
144
+ try {
145
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
146
+ const { initSpawnPause } = require('../subagent/spawnPause');
147
+ const sp = initSpawnPause({ aidenHome: aidenRoot });
148
+ if (sp.isPaused()) {
149
+ const s = sp.status();
150
+ const reasonSuffix = s.reason ? ` (reason: ${s.reason})` : '';
151
+ log('warn', `[daemon] sub-agent spawning is PAUSED${reasonSuffix}. ` +
152
+ 'Daemon-fired subagent_fanout calls will reject until an operator ' +
153
+ 'runs /spawn-pause off in a REPL session.');
154
+ }
155
+ }
156
+ catch (e) {
157
+ log('warn', '[daemon] spawn-pause init failed (non-fatal): ' +
158
+ (e instanceof Error ? e.message : String(e)));
159
+ }
128
160
  const db = (0, connection_1.openDaemonDb)(dbPath);
129
161
  const tracker = (0, instanceTracker_1.createInstanceTracker)({ db, version: version_1.VERSION });
130
162
  tracker.start();
163
+ // v4.6 Phase 3b — self-improvement loop singleton. Daemon-fired
164
+ // turns that classify failures via TCE write through to the
165
+ // shared failure ledger, so operator queries from a REPL see
166
+ // daemon-side failure patterns too. Defensive try/catch — init
167
+ // failure must not block daemon bootstrap; the TCE write-through
168
+ // path silently no-ops when the singleton is missing.
169
+ try {
170
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
171
+ const { initRecoveryStore } = require('../selfimprovement/recoveryStore');
172
+ initRecoveryStore({ db });
173
+ }
174
+ catch (e) {
175
+ log('warn', '[daemon] recovery-store init failed (non-fatal): ' +
176
+ (e instanceof Error ? e.message : String(e)));
177
+ }
131
178
  // Race-safe runtime lock. EEXIST + live PID → DaemonAlreadyRunningError.
132
179
  let runtimeLock;
133
180
  try {
@@ -252,12 +252,78 @@ CREATE INDEX IF NOT EXISTS idx_scheduled_workflows_next_fire
252
252
  CREATE INDEX IF NOT EXISTS idx_scheduled_workflows_enabled
253
253
  ON scheduled_workflows(enabled);
254
254
  `;
255
+ // Embedded v6 schema. Source of truth lives at
256
+ // `core/v4/daemon/db/schema/v6.sql` (matching v1-v4 convention).
257
+ // Kept in sync via the `tests/v4/daemon/db/migrations-v6.test.ts`
258
+ // snapshot check.
259
+ const V6_SQL = `
260
+ ALTER TABLE runs ADD COLUMN spawned_from_run_id INTEGER;
261
+ ALTER TABLE runs ADD COLUMN spawned_from_session_id TEXT;
262
+
263
+ CREATE INDEX IF NOT EXISTS idx_runs_spawned_from
264
+ ON runs(spawned_from_run_id)
265
+ WHERE spawned_from_run_id IS NOT NULL;
266
+ `;
267
+ // Embedded v7 schema. Source of truth at
268
+ // `core/v4/daemon/db/schema/v7.sql` (same convention). Kept in
269
+ // sync via `tests/v4/daemon/db/migrations-v7.test.ts`.
270
+ //
271
+ // v4.6 Phase 3b: self-improvement loop foundation — adds two
272
+ // tables for durable cross-session failure tracking:
273
+ // * `failure_signatures` — one row per (tool, category, args_hash);
274
+ // `occurrences` increments on every observed failure, so the
275
+ // operator can `SELECT … ORDER BY occurrences DESC` to find the
276
+ // most-stubborn failure shapes.
277
+ // * `recovery_reports` — one row per observed failure → success
278
+ // transition; carries the strategy that worked + verification +
279
+ // free-text notes for operator review.
280
+ const V7_SQL = `
281
+ CREATE TABLE IF NOT EXISTS failure_signatures (
282
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
283
+ signature TEXT UNIQUE NOT NULL,
284
+ tool_name TEXT NOT NULL,
285
+ failure_category TEXT NOT NULL,
286
+ args_hash TEXT,
287
+ first_seen_at INTEGER NOT NULL,
288
+ last_seen_at INTEGER NOT NULL,
289
+ occurrences INTEGER NOT NULL DEFAULT 1,
290
+ recovered_count INTEGER NOT NULL DEFAULT 0,
291
+ last_recovery_report_id INTEGER
292
+ );
293
+
294
+ CREATE INDEX IF NOT EXISTS idx_failure_signatures_signature
295
+ ON failure_signatures(signature);
296
+
297
+ CREATE INDEX IF NOT EXISTS idx_failure_signatures_tool
298
+ ON failure_signatures(tool_name);
299
+
300
+ CREATE TABLE IF NOT EXISTS recovery_reports (
301
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
302
+ signature_id INTEGER NOT NULL REFERENCES failure_signatures(id),
303
+ run_id INTEGER REFERENCES runs(id),
304
+ session_id TEXT,
305
+ failed_attempts INTEGER NOT NULL,
306
+ successful_strategy TEXT NOT NULL,
307
+ changed_parameters TEXT,
308
+ verification TEXT,
309
+ created_at INTEGER NOT NULL,
310
+ notes TEXT
311
+ );
312
+
313
+ CREATE INDEX IF NOT EXISTS idx_recovery_reports_signature
314
+ ON recovery_reports(signature_id);
315
+
316
+ CREATE INDEX IF NOT EXISTS idx_recovery_reports_run
317
+ ON recovery_reports(run_id);
318
+ `;
255
319
  const MIGRATIONS = [
256
320
  { version: 1, name: 'phase 1 — daemon foundation', sql: V1_SQL },
257
321
  { version: 2, name: 'phase 2 — file watcher observations', sql: V2_SQL },
258
322
  { version: 3, name: 'phase 3 — webhook deliveries log', sql: V3_SQL },
259
323
  { version: 4, name: 'phase 4a — email seen forensic table', sql: V4_SQL },
260
324
  { version: 5, name: 'phase 5b — scheduled workflows', sql: V5_SQL },
325
+ { version: 6, name: 'v4.6 phase 1 — sub-agent lineage', sql: V6_SQL },
326
+ { version: 7, name: 'v4.6 phase 3b — self-improvement loop', sql: V7_SQL },
261
327
  ];
262
328
  exports.LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
263
329
  function getCurrentVersion(db) {
@@ -35,12 +35,17 @@ function rowToTs(r) {
35
35
  function createRunStore(opts) {
36
36
  const db = opts.db;
37
37
  return {
38
- create({ sessionId, instanceId, triggerEventId, status, startedAt }) {
38
+ create({ sessionId, instanceId, triggerEventId, status, startedAt, spawnedFromRunId, spawnedFromSessionId }) {
39
39
  const now = startedAt ?? Date.now();
40
+ // v4.6 Phase 1 — explicit 8-column INSERT including the two
41
+ // sub-agent lineage columns. Top-level runs pass NULL for both;
42
+ // sub-agent runs pass the parent run_id + session_id. Single
43
+ // insert path keeps the code simple at the cost of two extra
44
+ // bound NULLs on the common (top-level) case.
40
45
  const r = db.prepare(`INSERT INTO runs
41
46
  (trigger_event_id, session_id, instance_id, status, started_at,
42
- resume_pending)
43
- VALUES (?, ?, ?, ?, ?, 0)`).run(triggerEventId ?? null, sessionId, instanceId, status ?? 'queued', now);
47
+ resume_pending, spawned_from_run_id, spawned_from_session_id)
48
+ VALUES (?, ?, ?, ?, ?, 0, ?, ?)`).run(triggerEventId ?? null, sessionId, instanceId, status ?? 'queued', now, spawnedFromRunId ?? null, spawnedFromSessionId ?? null);
44
49
  return Number(r.lastInsertRowid);
45
50
  },
46
51
  setStatus(runId, status, opts2 = {}) {
@@ -95,6 +100,17 @@ function createRunStore(opts) {
95
100
  whereParts.push('r.session_id LIKE ?');
96
101
  params.push(`${opts2.sessionIdPrefix}%`);
97
102
  }
103
+ // v4.6 Phase 2Q-B — default to top-level rows only. Children
104
+ // (rows with non-NULL `spawned_from_run_id`) clutter the list
105
+ // when you really want "what user-triggered runs happened
106
+ // recently". The partial index `idx_runs_spawned_from` makes
107
+ // the negated predicate cheap (children indexed; parents NOT
108
+ // indexed but the predicate is `IS NULL` — table scan, but
109
+ // the planner uses the limit + ORDER BY started_at to cap
110
+ // work). `--include-children` flips the flag for flat view.
111
+ if (opts2.topLevelOnly !== false) {
112
+ whereParts.push('r.spawned_from_run_id IS NULL');
113
+ }
98
114
  const where = whereParts.length > 0 ? `WHERE ${whereParts.join(' AND ')}` : '';
99
115
  const sql = `
100
116
  SELECT r.* FROM runs r
@@ -106,6 +122,20 @@ function createRunStore(opts) {
106
122
  const rows = db.prepare(sql).all(...params);
107
123
  return rows.map(rowToTs);
108
124
  },
125
+ countChildren(parentRunId) {
126
+ // Single round-trip via conditional COUNT — sqlite handles
127
+ // this fine even with a few thousand children per parent,
128
+ // which we'll never see in practice (fanout caps at 5).
129
+ const r = db.prepare(`SELECT
130
+ COUNT(*) AS total,
131
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed
132
+ FROM runs
133
+ WHERE spawned_from_run_id = ?`).get(parentRunId);
134
+ return {
135
+ total: r.total,
136
+ completed: r.completed ?? 0,
137
+ };
138
+ },
109
139
  listEvents(runId, limit = 200) {
110
140
  const rows = db.prepare(`SELECT ts, kind, payload FROM run_events WHERE run_id = ? ORDER BY ts ASC LIMIT ?`).all(runId, Math.max(1, Math.min(limit, 5000)));
111
141
  return rows;