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.
- package/README.md +17 -2
- package/dist/cli/v4/aidenCLI.js +185 -99
- package/dist/cli/v4/chatSession.js +107 -0
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +2 -0
- package/dist/cli/v4/commands/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +6 -0
- package/dist/cli/v4/commands/index.js +16 -1
- package/dist/cli/v4/commands/mcp.js +80 -54
- package/dist/cli/v4/commands/plannerGuard.js +53 -0
- package/dist/cli/v4/commands/recovery.js +122 -0
- package/dist/cli/v4/commands/runs.js +22 -2
- package/dist/cli/v4/commands/spawnPause.js +93 -0
- package/dist/cli/v4/daemonAgentBuilder.js +4 -1
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/core/v4/aidenAgent.js +219 -1
- package/dist/core/v4/daemon/bootstrap.js +47 -0
- package/dist/core/v4/daemon/db/migrations.js +66 -0
- package/dist/core/v4/daemon/runStore.js +33 -3
- package/dist/core/v4/providerFallback.js +35 -2
- package/dist/core/v4/runtimeToggles.js +30 -3
- package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
- package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
- package/dist/core/v4/subagent/childBuilder.js +391 -0
- package/dist/core/v4/subagent/fanout.js +75 -51
- package/dist/core/v4/subagent/spawnPause.js +191 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
- package/dist/core/v4/toolRegistry.js +19 -3
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +29 -0
- package/dist/providers/v4/anthropicAdapter.js +31 -3
- package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
- package/dist/providers/v4/codexResponsesAdapter.js +25 -2
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
- package/dist/tools/v4/index.js +17 -3
- package/dist/tools/v4/skills/lookupToolSchema.js +6 -1
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +53 -1
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
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;
|