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
package/README.md CHANGED
@@ -10,14 +10,15 @@
10
10
 
11
11
  Autonomous AI Engine — local-first, Windows-native, yours to own
12
12
 
13
- 74 skills · 59 tools · 19 providers · 9 channels · AGPL-3.0
13
+ 74 skills · 60 tools · 19 providers · 9 channels · AGPL-3.0
14
14
 
15
15
  Windows · Linux · WSL · macOS (API Mode)
16
16
  ```
17
17
 
18
18
  <p align="center">
19
- <a href="https://github.com/taracodlabs/aiden/releases/latest"><img src="https://img.shields.io/badge/version-v4.5.0-f97316?style=for-the-badge" alt="v4.5.0" /></a>
19
+ <a href="https://github.com/taracodlabs/aiden/releases/latest"><img src="https://img.shields.io/badge/version-v4.6.0-f97316?style=for-the-badge" alt="v4.6.0" /></a>
20
20
  <a href="https://www.npmjs.com/package/aiden-runtime"><img src="https://img.shields.io/npm/v/aiden-runtime?color=f97316&label=npm&style=for-the-badge" alt="npm" /></a>
21
+ <a href="https://www.npmjs.com/package/aiden-runtime"><img src="https://img.shields.io/npm/dm/aiden-runtime?color=f97316&label=downloads&style=for-the-badge" alt="npm downloads" /></a>
21
22
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-orange?style=for-the-badge" alt="License: AGPL-3.0" /></a>
22
23
  <a href="https://discord.gg/gMZ3hUnQTm"><img src="https://img.shields.io/badge/chat-discord-7289da?logo=discord&logoColor=white&style=for-the-badge" alt="Discord" /></a>
23
24
  <a href="https://github.com/taracodlabs/aiden/stargazers"><img src="https://img.shields.io/github/stars/taracodlabs/aiden?style=for-the-badge&color=f9d71c" alt="Stars" /></a>
@@ -33,6 +34,20 @@ Windows · Linux · WSL · macOS (API Mode)
33
34
 
34
35
  ---
35
36
 
37
+ ## What's new in v4.6
38
+
39
+ Aiden now spawns workers and learns from itself.
40
+
41
+ - **Sub-agents.** `spawn_sub_agent` runs a focused child with an isolated context + intersected toolset; `subagent_fanout` runs N children in parallel (ensemble or partition) with merge strategies (`all` / `vote` / `pick-best` / `combine`) and provider rotation across configured fallback slots.
42
+ - **Operator kill-switch.** `/spawn-pause on|off|status` blocks new sub-agent spawning while in-flight children continue. Marker file at `~/.aiden/spawn.paused` so the state survives restart and is shared across REPL, daemon, and MCP runtimes. Optional reason field captured in the typed `SUBAGENT_SPAWN_PAUSED` error envelope.
43
+ - **Self-improvement loop foundation.** TCE classifications + recoveries persist to two new SQLite tables (`failure_signatures`, `recovery_reports`); `/recovery list|show|clear` surfaces recurring failure patterns across sessions.
44
+ - **REPL parent-run lineage.** Each REPL turn writes its own `runs` row; sub-agent children link back via `spawned_from_run_id`. `aiden runs list` hides children by default and shows a `(N children, M OK)` badge per parent; `--include-children` flips to flat view.
45
+ - **PlannerGuard opt-in.** The keyword-based per-turn tool narrower is OFF by default in v4.6 (modern models pick well from the full catalog). Enable via `/planner-guard on` or `AIDEN_PLANNER_GUARD=1` for smaller local models.
46
+
47
+ Phase 2 also fixed an MCP-mode `subagent_fanout` regression that had silently broken in the v4.5 refactor.
48
+
49
+ ---
50
+
36
51
  ## What's new in v4.5
37
52
 
38
53
  Aiden now wakes up by itself.
@@ -97,6 +97,13 @@ const sessionManager_1 = require("../../core/v4/sessionManager");
97
97
  const toolRegistry_1 = require("../../core/v4/toolRegistry");
98
98
  const skillLoader_1 = require("../../core/v4/skillLoader");
99
99
  const index_1 = require("../../tools/v4/index");
100
+ // v4.6 Phase 1 — spawn_sub_agent: always-on runStore + LLM-callable tool.
101
+ const node_crypto_1 = require("node:crypto");
102
+ const node_os_1 = __importDefault(require("node:os"));
103
+ const daemonConfig_1 = require("../../core/v4/daemon/daemonConfig");
104
+ const connection_1 = require("../../core/v4/daemon/db/connection");
105
+ const runStore_1 = require("../../core/v4/daemon/runStore");
106
+ const spawnSubAgentTool_1 = require("../../tools/v4/subagent/spawnSubAgentTool");
100
107
  const skillCommands_1 = require("../../core/v4/skillCommands");
101
108
  const aidenAgent_1 = require("../../core/v4/aidenAgent");
102
109
  const promptBuilder_1 = require("../../core/v4/promptBuilder");
@@ -502,6 +509,7 @@ async function main(argv, opts = {}) {
502
509
  .option('--source <src>', 'list: filter by trigger source (file/webhook/email/schedule/manual)')
503
510
  .option('--status <s>', 'list: filter by status (queued/running/completed/failed/cancelled/interrupted)')
504
511
  .option('--trigger <prefix>', 'list: sessionId prefix (e.g. "trigger:file:<id>:")')
512
+ .option('--include-children', 'list: include sub-agent children (default: top-level only, with per-parent badge)')
505
513
  .action(async (action, posArgs, cmdObj) => {
506
514
  const { runRunsSubcommand } = await Promise.resolve().then(() => __importStar(require('./commands/runs')));
507
515
  const code = await runRunsSubcommand(action, posArgs ?? [], cmdObj, {
@@ -623,6 +631,60 @@ async function main(argv, opts = {}) {
623
631
  async function buildAgentRuntime(cliOpts, opts) {
624
632
  const paths = opts.pathsOverride ?? (0, paths_1.resolveAidenPaths)();
625
633
  await (0, paths_1.ensureAidenDirsExist)(paths);
634
+ // ── v4.6 Phase 1 — always-on runStore for spawn_sub_agent ──────────────
635
+ //
636
+ // The spawn_sub_agent primitive persists each child run to the runs
637
+ // table via spawned_from_run_id FK. The REPL needs a runStore handle
638
+ // regardless of whether AIDEN_DAEMON=1 or not. SQLite WAL mode
639
+ // (enabled in openDaemonDb) allows REPL + daemon to coexist on the
640
+ // same file without lock contention. The connection.ts module caches
641
+ // per-path, so when daemon foundation has already opened the DB this
642
+ // call returns the same handle. Migration runner is idempotent on
643
+ // already-current schemas.
644
+ const replInstanceId = `repl-${(0, node_crypto_1.randomUUID)().slice(0, 8)}`;
645
+ const replDb = (0, connection_1.openDaemonDb)((0, daemonConfig_1.daemonDbPath)(paths.root));
646
+ // Seed the REPL's daemon_instances row so the FK on runs.instance_id
647
+ // is satisfied. Idempotent under INSERT OR IGNORE — multiple REPL
648
+ // launches reusing the same instance_id (rare with random UUID
649
+ // suffix) silently no-op.
650
+ replDb.prepare(`INSERT OR IGNORE INTO daemon_instances
651
+ (instance_id, pid, hostname, started_at, last_heartbeat, version)
652
+ VALUES (?, ?, ?, ?, ?, ?)`).run(replInstanceId, process.pid, node_os_1.default.hostname(), Date.now(), Date.now(), version_1.VERSION);
653
+ const replRunStore = (0, runStore_1.createRunStore)({ db: replDb });
654
+ // v4.6 Phase 3A — operator kill-switch for sub-agent spawning.
655
+ // Initialised as early as possible so any subsequent tool wiring
656
+ // sees the singleton. The marker file lives at
657
+ // `<paths.root>/spawn.paused` and is shared across REPL + daemon
658
+ // + MCP for cross-process coordination. The startup probe (warn
659
+ // operator that pause is active from a prior session) fires
660
+ // later, once `bootLogger` is available — see the
661
+ // `spawnPauseBootStatus` block below.
662
+ const { initSpawnPause } = await Promise.resolve().then(() => __importStar(require('../../core/v4/subagent/spawnPause')));
663
+ const spawnPauseState = initSpawnPause({ aidenHome: paths.root });
664
+ // v4.6 Phase 3b — self-improvement loop. Initialise the durable
665
+ // failure-ledger / recovery-report store against the same
666
+ // daemon.db handle the runStore uses. WAL coexistence: REPL +
667
+ // daemon + MCP all share the same connection-cached handle, so
668
+ // any writes from one runtime are visible to the others. The
669
+ // TCE write-through path inside the agent loop reads through the
670
+ // module-level singleton; initialising here makes spawnSubAgent
671
+ // and daemon-fired turns observe the same persistence.
672
+ const { initRecoveryStore } = await Promise.resolve().then(() => __importStar(require('../../core/v4/selfimprovement/recoveryStore')));
673
+ initRecoveryStore({ db: replDb });
674
+ const spawnPauseBootStatus = spawnPauseState.isPaused()
675
+ ? spawnPauseState.status()
676
+ : null;
677
+ // v4.6 Phase 2Q-B — mutable holder for the REPL's current parent
678
+ // run id. ChatSession's `runAgentTurn` writes a row before each
679
+ // turn dispatches and stores the id here (and clears it on
680
+ // completion). The spawn / fanout tool factories below read it
681
+ // through `resolveParentRunId` / `resolveParentSessionId`
682
+ // callbacks so any child spawned mid-turn is linked back to the
683
+ // live REPL parent row via `runs.spawned_from_run_id`.
684
+ const replParentRunRef = {
685
+ runId: null,
686
+ sessionId: null,
687
+ };
626
688
  // Phase 16c.2: load `paths.envFile` (the aiden-managed `.env` that
627
689
  // `setupWizard.ts::upsertEnvVar` writes to) into `process.env` BEFORE
628
690
  // any provider resolution. The bug: setup wrote keys to this file but
@@ -1238,7 +1300,11 @@ async function buildAgentRuntime(cliOpts, opts) {
1238
1300
  // ── Build agent with all moat layers attached ────────────────────────
1239
1301
  const agent = new aidenAgent_1.AidenAgent({
1240
1302
  provider: adapter,
1241
- tools: toolRegistry.getSchemas(),
1303
+ // v4.6 Phase 1 — 'repl' context filter excludes tools tagged
1304
+ // daemon-only (none today) and INCLUDES tools tagged repl-only
1305
+ // (e.g. spawn_sub_agent, registered after this line). Tools with
1306
+ // no `contexts` field default to visible in both contexts.
1307
+ tools: toolRegistry.getSchemas(undefined, 'repl'),
1242
1308
  toolExecutor,
1243
1309
  maxTurns: config.getValue('agent.max_turns', 90),
1244
1310
  auxiliaryClient,
@@ -1426,6 +1492,21 @@ async function buildAgentRuntime(cliOpts, opts) {
1426
1492
  // Wire the gateway singleton's logger BEFORE registering its processor
1427
1493
  // so register / unregister channel events are scoped correctly.
1428
1494
  gateway_1.gateway.attachLogger(bootLogger.child('gateway'));
1495
+ // v4.6 Phase 3A — startup probe for the spawn-pause kill-switch.
1496
+ // The state was initialised early (line ~740) before tool wiring.
1497
+ // Now that bootLogger exists, emit a visible warning so an
1498
+ // operator who forgot they paused in a prior session learns
1499
+ // immediately rather than puzzling at silent rejected fanouts.
1500
+ if (spawnPauseBootStatus) {
1501
+ const s = spawnPauseBootStatus;
1502
+ const reasonSuffix = s.reason ? ` (reason: ${s.reason})` : '';
1503
+ bootLogger.warn(`spawn_sub_agent / subagent_fanout are PAUSED${reasonSuffix}. ` +
1504
+ 'Run /spawn-pause off to resume.', {
1505
+ pausedAt: s.pausedAt ?? null,
1506
+ pausedBy: s.pausedBy ?? null,
1507
+ durationMs: s.durationMs ?? null,
1508
+ });
1509
+ }
1429
1510
  // ── Phase v4.1-subagent.1 — replace subagent_fanout stub with wired version
1430
1511
  //
1431
1512
  // tools/v4/index.ts registers a stub at boot so the schema is visible
@@ -1442,10 +1523,45 @@ async function buildAgentRuntime(cliOpts, opts) {
1442
1523
  // shared subsystems (registry, skillLoader, paths, memoryManager,
1443
1524
  // promptBuilder, promptBuilderOptions) are read-only and pass by
1444
1525
  // reference.
1526
+ // v4.6 Phase 2Q-A — `runFanout` routes each child through
1527
+ // `spawnSubAgent`. `spawnDeps` mirrors the deps the
1528
+ // `makeSpawnSubAgentTool` factory accepts (see registration below
1529
+ // this block). The legacy per-call `runChild` closure that lived
1530
+ // here pre-2R has been deleted; the primitive owns child
1531
+ // construction now.
1445
1532
  toolRegistry.register((0, index_1.makeSubagentFanoutTool)({
1446
1533
  logger: bootLogger.child('subagent'),
1447
1534
  resolveActiveModel: () => ({ providerId, modelId }),
1448
1535
  aggregatorAdapter: adapter,
1536
+ spawnDeps: {
1537
+ toolRegistry,
1538
+ parentToolContext: {
1539
+ cwd: process.cwd(),
1540
+ paths,
1541
+ sessions: sessionManager,
1542
+ memory: memoryManager,
1543
+ memoryGuard,
1544
+ ssrfProtection,
1545
+ tirithScanner,
1546
+ skillLoader,
1547
+ },
1548
+ parentProvider: adapter,
1549
+ parentProviderId: providerId,
1550
+ parentModelId: modelId,
1551
+ resolveVerifiedFlag,
1552
+ resolveToolset,
1553
+ resolveMutates,
1554
+ runStore: replRunStore,
1555
+ instanceId: replInstanceId,
1556
+ logger: bootLogger.child('subagent'),
1557
+ },
1558
+ // v4.6 Phase 2Q-B — REPL parent-run wiring. Reads the shared
1559
+ // `replParentRunRef` mutated by `ChatSession.runAgentTurn` so
1560
+ // fanout children get `spawned_from_run_id` populated. Returns
1561
+ // undefined between turns (ref cleared post-turn), matching the
1562
+ // pre-2Q-B behaviour for slash-command-triggered spawns.
1563
+ resolveParentRunId: () => replParentRunRef.runId ?? undefined,
1564
+ resolveParentSessionId: () => replParentRunRef.sessionId ?? undefined,
1449
1565
  resolveProviders: () => {
1450
1566
  // When the parent uses FallbackAdapter, expose every key-present
1451
1567
  // slot's (providerId, modelId) so rotation can spread children
@@ -1466,110 +1582,68 @@ async function buildAgentRuntime(cliOpts, opts) {
1466
1582
  }
1467
1583
  return [{ providerId, modelId }];
1468
1584
  },
1469
- runChild: async (childOpts) => {
1470
- // Per-child context: paths / skillLoader / memoryManager / processes
1471
- // are SAFE to share (read-only or per-call by design). The approval
1472
- // engine is intentionally OMITTED — N children competing for one
1473
- // stdin REPL would deadlock.
1474
- const childCtx = {
1475
- cwd: process.cwd(),
1476
- paths,
1477
- sessions: sessionManager,
1478
- memory: memoryManager,
1479
- skillLoader,
1480
- // approvalEngine, ssrfProtection, tirithScanner, memoryGuard:
1481
- // SSRF + Tirith would be safe to share but adding them now
1482
- // expands the per-child surface; keep lean for v4.1-subagent.1
1483
- // and revisit when fanout actually exercises network or shell
1484
- // tools (gated by ALLOW_DESTRUCTIVE).
1485
- };
1486
- // Filter the tool surface. Default-safe: read-only tools only.
1487
- // AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE=1 mirrors the MCP env from
1488
- // v4.1-mcp — predictable, env-driven.
1489
- const allowDestructive = process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === '1' ||
1490
- process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === 'true';
1491
- const childToolNames = [];
1492
- for (const name of toolRegistry.list()) {
1493
- const h = toolRegistry.get(name);
1494
- if (!h)
1495
- continue;
1496
- if (h.mutates && !allowDestructive)
1497
- continue;
1498
- // Avoid recursive fanout this phase — children cannot spawn
1499
- // their own children. Recursion was capped at depth 1 by
1500
- // default in prior multi-agent systems for the same reason;
1501
- // v3 starved nested spawns.
1502
- if (name === 'subagent_fanout')
1503
- continue;
1504
- childToolNames.push(name);
1505
- }
1506
- const childExecutor = toolRegistry.buildExecutor(childCtx);
1507
- const childTools = childToolNames
1508
- .map((n) => toolRegistry.get(n)?.schema)
1509
- .filter((s) => !!s);
1510
- // Provider isolation: clone the FallbackAdapter so per-child
1511
- // rate-limit state doesn't pollute the parent or siblings.
1512
- // Non-Fallback adapters are stateless by spec (providers/v4/
1513
- // types.ts:190) so direct reuse is safe.
1514
- const childProvider = adapter instanceof providerFallback_1.FallbackAdapter
1515
- ? adapter.clone()
1516
- : adapter;
1517
- // Build per-child AidenAgent. Skip the moat layers (PlannerGuard,
1518
- // HonestyEnforcement, SkillTeacher, SkillEnforcementTracker) —
1519
- // they're parent-loop concerns and add cost without value at the
1520
- // child scale. Skip promptBuilder too: children get a SHORT
1521
- // system prompt (brief identity + role) instead of the parent's
1522
- // full SOUL.md + 72-skills inventory + memory snapshot. The
1523
- // tradeoff is deliberate — children answer the GOAL, not "be
1524
- // Aiden". With the full prompt, trivial queries take 30s+ for
1525
- // children to generate verbose self-introductions; the lean
1526
- // child prompt brings n=2 trivial fanouts under 12s. Parent
1527
- // should pass any context children genuinely need via the
1528
- // `query` / `tasks[].context` argument.
1529
- const child = new aidenAgent_1.AidenAgent({
1530
- provider: childProvider,
1531
- tools: childTools,
1532
- toolExecutor: childExecutor,
1533
- maxTurns: childOpts.maxIterations,
1534
- providerId: childOpts.provider.providerId,
1535
- modelId: childOpts.provider.modelId,
1536
- // No promptBuilder — childSystemPrompt prepended manually below.
1537
- // No fallback strategy — child failures bubble up to the
1538
- // orchestrator, which surfaces them in the result envelope.
1539
- });
1540
- // Honour the abort signal — if the parent aborts mid-call (or the
1541
- // per-child timeout fires), short-circuit before dispatching to
1542
- // the provider. AidenAgent doesn't take an AbortSignal directly;
1543
- // the AbortController plumbing through fetch is the
1544
- // v4.1-subagent.2 / v4.2 hardening pass. Pre-check here for the
1545
- // synchronous path.
1546
- if (childOpts.signal.aborted) {
1547
- throw new Error('aborted before dispatch');
1548
- }
1549
- // Brief, role-aware system prompt — drops 5KB+ of Aiden identity
1550
- // boilerplate that would otherwise inflate every child to 30s+
1551
- // wall-clock for a trivial query. The parent agent retains the
1552
- // full prompt when it's the orchestrator; children answer the
1553
- // goal directly.
1554
- const roleLine = childOpts.role
1555
- ? `Role: ${childOpts.role}. `
1556
- : '';
1557
- const childSystemPrompt = `You are one of ${childOpts.index >= 0 ? 'N' : '?'} parallel subagents. ` +
1558
- `${roleLine}Answer the user's request concisely. Use available tools when ` +
1559
- `the answer requires real-world information you don't have memorized.`;
1560
- const history = [
1561
- { role: 'system', content: childSystemPrompt },
1562
- { role: 'user', content: childOpts.prompt },
1563
- ];
1564
- const result = await child.runConversation(history);
1565
- return result.finalContent;
1566
- },
1567
1585
  }));
1568
1586
  bootLogger.child('subagent').info('subagent_fanout: wired (replaces stub)', {
1569
1587
  providerId,
1570
1588
  modelId,
1571
1589
  fallback: adapter instanceof providerFallback_1.FallbackAdapter ? 'FallbackAdapter' : 'direct',
1572
1590
  });
1591
+ // ── v4.6 Phase 1 — register spawn_sub_agent (REPL only) ────────────────
1592
+ //
1593
+ // The new single-child synchronous primitive. Coexists with
1594
+ // subagent_fanout (Q9 — additive in Phase 1; Phase 2 will refactor
1595
+ // fanout to call this primitive N times).
1596
+ //
1597
+ // Wired here, AFTER `agent` and `toolExecutor` are in scope, because
1598
+ // the child builder needs the parent agent's reference (to read
1599
+ // `getCurrentSignal()` at dispatch time per the agent-instance signal
1600
+ // pattern) and the parent's tool context for ssrf/tirith/memory/etc.
1601
+ //
1602
+ // Deliberately NOT registered in cli/v4/daemonAgentBuilder.ts —
1603
+ // daemon-fired agents don't expose spawn_sub_agent in their tool
1604
+ // catalog (Q6 lock).
1605
+ toolRegistry.register((0, spawnSubAgentTool_1.makeSpawnSubAgentTool)({
1606
+ parentAgent: agent,
1607
+ toolRegistry,
1608
+ parentToolContext: {
1609
+ cwd: process.cwd(),
1610
+ paths,
1611
+ sessions: sessionManager,
1612
+ memory: memoryManager,
1613
+ memoryGuard,
1614
+ // approvalEngine intentionally OMITTED — the child builder
1615
+ // constructs its own auto-deny ApprovalEngine. Listing it here
1616
+ // would be ignored (childBuilder overrides via spread), but
1617
+ // keeping it out makes the intent explicit.
1618
+ ssrfProtection,
1619
+ tirithScanner,
1620
+ skillLoader,
1621
+ },
1622
+ parentProvider: adapter,
1623
+ parentProviderId: providerId,
1624
+ parentModelId: modelId,
1625
+ resolveVerifiedFlag,
1626
+ resolveToolset,
1627
+ resolveMutates,
1628
+ runStore: replRunStore,
1629
+ instanceId: replInstanceId,
1630
+ // v4.6 Phase 2Q-B — REPL parent-run wiring. Reads the same
1631
+ // shared `replParentRunRef` the fanout factory above uses;
1632
+ // ChatSession.runAgentTurn populates it before each turn
1633
+ // dispatches. Returns undefined between turns so spawns from
1634
+ // slash-command handlers stay top-level (consistent with
1635
+ // pre-2Q-B observable behaviour).
1636
+ resolveParentRunId: () => replParentRunRef.runId ?? undefined,
1637
+ resolveParentSessionId: () => replParentRunRef.sessionId ?? undefined,
1638
+ // v4.6 Phase 1 observability — info-level traces for spec at
1639
+ // invocation, child-tools count, completion, and per-tool-call
1640
+ // run_events on the child's runs row.
1641
+ logger: bootLogger.child('subagent'),
1642
+ }));
1643
+ bootLogger.child('subagent').info('spawn_sub_agent: wired (REPL only)', {
1644
+ instanceId: replInstanceId,
1645
+ dbPath: (0, daemonConfig_1.daemonDbPath)(paths.root),
1646
+ });
1573
1647
  // ── Phase v4.1-2.1: gateway message processor ────────────────────
1574
1648
  //
1575
1649
  // Channel adapters call `gateway.routeMessage(...)` for every inbound
@@ -1762,6 +1836,10 @@ async function buildAgentRuntime(cliOpts, opts) {
1762
1836
  exploreMode,
1763
1837
  channelManager,
1764
1838
  daemonAgentBuilder,
1839
+ // v4.6 Phase 2Q-B — REPL parent-run wiring.
1840
+ replRunStore,
1841
+ replInstanceId,
1842
+ replParentRunRef,
1765
1843
  };
1766
1844
  }
1767
1845
  async function runInteractiveChat(cliOpts, opts) {
@@ -1828,6 +1906,14 @@ async function runInteractiveChat(cliOpts, opts) {
1828
1906
  // when /quit fires the auto-summary path.
1829
1907
  memoryManager: runtime.memoryManager,
1830
1908
  memoryGuard: runtime.memoryGuard,
1909
+ // v4.6 Phase 2Q-B — REPL parent-run wiring. ChatSession.runAgentTurn
1910
+ // writes a runs row per turn and stores its id into
1911
+ // `replParentRunRef`; the spawn / fanout factories above read the
1912
+ // ref via their `resolveParentRunId` / `resolveParentSessionId`
1913
+ // callbacks so children get `spawned_from_run_id` populated.
1914
+ replRunStore: runtime.replRunStore,
1915
+ replInstanceId: runtime.replInstanceId,
1916
+ replParentRunRef: runtime.replParentRunRef,
1831
1917
  };
1832
1918
  if (cliOpts.tui) {
1833
1919
  await (0, aidenTUI_1.runTuiMode)({
@@ -922,6 +922,43 @@ class ChatSession {
922
922
  const baseHistory = newHistory.length > 0
923
923
  ? [...this.history, ...newHistory, userMsg]
924
924
  : [...this.history, userMsg];
925
+ // v4.6 Phase 2Q-B — REPL parent-run row (best-effort).
926
+ //
927
+ // Insert a `runs` row tagged with this REPL session BEFORE the
928
+ // agent loop dispatches. Capture the row id into the shared
929
+ // `replParentRunRef` so any `spawn_sub_agent` / `subagent_fanout`
930
+ // child this turn produces can link back via
931
+ // `spawned_from_run_id`. The ref is cleared in the catch /
932
+ // success paths below regardless of outcome.
933
+ //
934
+ // Defensive: a runStore write failure (locked DB, schema drift,
935
+ // etc.) must NOT crash the REPL — every persistence call here is
936
+ // wrapped in try/catch and reduces to a logged warning. The
937
+ // user-facing turn still runs.
938
+ let replRunId = null;
939
+ const replRunStore = this.opts.replRunStore;
940
+ const replInstanceId = this.opts.replInstanceId;
941
+ const replParentRunRef = this.opts.replParentRunRef;
942
+ if (replRunStore && replInstanceId && this.sessionId) {
943
+ try {
944
+ replRunId = replRunStore.create({
945
+ sessionId: this.sessionId,
946
+ instanceId: replInstanceId,
947
+ status: 'running',
948
+ startedAt: turnStartedAt,
949
+ });
950
+ if (replParentRunRef) {
951
+ replParentRunRef.runId = replRunId;
952
+ replParentRunRef.sessionId = this.sessionId;
953
+ }
954
+ }
955
+ catch (err) {
956
+ // Logged once per turn; the user's chat is not interrupted.
957
+ // eslint-disable-next-line no-console
958
+ console.warn('[runs] failed to write REPL parent-run row:', err instanceof Error ? err.message : String(err));
959
+ replRunId = null;
960
+ }
961
+ }
925
962
  // Phase 16c: streaming gated on display.streaming config.
926
963
  // v4.1.4 Part 1.6: PRODUCTION DEFAULT FLIPPED FROM FALSE TO TRUE.
927
964
  // Streaming delivers the activity indicator, tool-row live tick,
@@ -1179,6 +1216,35 @@ class ChatSession {
1179
1216
  this.history = result.messages;
1180
1217
  this.totalUsage.inputTokens += result.totalUsage.inputTokens;
1181
1218
  this.totalUsage.outputTokens += result.totalUsage.outputTokens;
1219
+ // v4.6 Phase 2Q-B — finalize the REPL parent-run row on success.
1220
+ // `finishReason` from the agent loop maps directly into our DB
1221
+ // status: `stop` → completed; `interrupted` / `tool_loop` →
1222
+ // surface as 'interrupted' so it's visible in `runs list`;
1223
+ // `budget_exhausted` / `error` → failed. Wrapped in try/catch
1224
+ // so even a runStore write failure here can't crash the REPL.
1225
+ if (replRunStore && replRunId !== null) {
1226
+ try {
1227
+ const dbStatus = result.finishReason === 'stop' ? 'completed' :
1228
+ result.finishReason === 'interrupted' ? 'interrupted' :
1229
+ result.finishReason === 'tool_loop' ? 'interrupted' :
1230
+ 'failed';
1231
+ replRunStore.setStatus(replRunId, dbStatus, {
1232
+ finishReason: result.finishReason,
1233
+ completedAt: Date.now(),
1234
+ });
1235
+ }
1236
+ catch (err) {
1237
+ // eslint-disable-next-line no-console
1238
+ console.warn('[runs] failed to finalize REPL parent-run row:', err instanceof Error ? err.message : String(err));
1239
+ }
1240
+ }
1241
+ // Clear the shared ref so a subsequent turn (or stray
1242
+ // spawn/fanout dispatched between turns from a slash command
1243
+ // handler) doesn't see a stale parent id.
1244
+ if (replParentRunRef) {
1245
+ replParentRunRef.runId = null;
1246
+ replParentRunRef.sessionId = null;
1247
+ }
1182
1248
  // Phase 16d: surface inline confirmations for verified memory writes.
1183
1249
  // We MUST gate on verified=true (the post-write read flag from
1184
1250
  // MemoryGuard) — HonestyEnforcement uses the same flag to catch
@@ -1289,6 +1355,26 @@ class ChatSession {
1289
1355
  progressBar?.hide();
1290
1356
  if (streamingActive)
1291
1357
  this.opts.display.streamComplete();
1358
+ // v4.6 Phase 2Q-B — finalize REPL parent-run row on error.
1359
+ // Visible in `aiden runs list` as a failed top-level row so
1360
+ // operators can correlate a chat error with whatever children
1361
+ // it had already kicked off this turn.
1362
+ if (replRunStore && replRunId !== null) {
1363
+ try {
1364
+ replRunStore.setStatus(replRunId, 'failed', {
1365
+ finishReason: 'error',
1366
+ completedAt: Date.now(),
1367
+ });
1368
+ }
1369
+ catch (e2) {
1370
+ // eslint-disable-next-line no-console
1371
+ console.warn('[runs] failed to mark REPL parent-run failed:', e2 instanceof Error ? e2.message : String(e2));
1372
+ }
1373
+ }
1374
+ if (replParentRunRef) {
1375
+ replParentRunRef.runId = null;
1376
+ replParentRunRef.sessionId = null;
1377
+ }
1292
1378
  const msg = err?.message ?? String(err);
1293
1379
  // v4.1.3-prebump: classify the error so the suggestion below
1294
1380
  // points at the actual fix instead of the generic "/model or
@@ -1418,6 +1504,27 @@ class ChatSession {
1418
1504
  providerOk: !this.opts.unconfigured,
1419
1505
  version: version_1.VERSION,
1420
1506
  }) + '\n');
1507
+ // v4.6 Phase 3A — operator kill-switch indicator. Lands ABOVE
1508
+ // the blank-line + provider-source annotation so an operator
1509
+ // who paused in a prior session sees the state immediately on
1510
+ // boot, alongside the standard status pills. Single dim
1511
+ // warning line; no special chrome — the message itself is the
1512
+ // visual signal.
1513
+ try {
1514
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1515
+ const { getSpawnPause } = require('../../core/v4/subagent/spawnPause');
1516
+ const s = getSpawnPause().status();
1517
+ if (s.paused) {
1518
+ const reasonSuffix = s.reason ? ` · ${s.reason}` : '';
1519
+ const durationSuffix = typeof s.durationMs === 'number'
1520
+ ? ` · ${formatDuration(s.durationMs)}`
1521
+ : '';
1522
+ display.warn(`spawn-pause: ON${reasonSuffix}${durationSuffix} — use /spawn-pause off to resume`);
1523
+ }
1524
+ }
1525
+ catch {
1526
+ // Singleton not initialised (test stubs, etc.) — silently skip.
1527
+ }
1421
1528
  // v4.5 TUI polish — blank line so the status pills row doesn't
1422
1529
  // crowd the muted source annotation right beneath it.
1423
1530
  display.write('\n');
@@ -31,12 +31,14 @@ const LABEL = {
31
31
  tce: 'TCE',
32
32
  browser_depth: 'Browser depth',
33
33
  suggestions: 'Suggestions',
34
+ planner_guard: 'Planner-Guard',
34
35
  };
35
36
  const CONFIG_DOTTED = {
36
37
  sandbox: 'runtime_toggles.sandbox',
37
38
  tce: 'runtime_toggles.tce',
38
39
  browser_depth: 'runtime_toggles.browser_depth',
39
40
  suggestions: 'runtime_toggles.suggestions',
41
+ planner_guard: 'runtime_toggles.planner_guard',
40
42
  };
41
43
  /**
42
44
  * Apply a toggle change. When `ctx.config` is wired, persists to