aiden-runtime 4.5.0 → 4.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +17 -2
  2. package/dist/cli/v4/aidenCLI.js +207 -100
  3. package/dist/cli/v4/chatSession.js +120 -0
  4. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +2 -0
  5. package/dist/cli/v4/commands/fanout.js +42 -59
  6. package/dist/cli/v4/commands/help.js +8 -0
  7. package/dist/cli/v4/commands/index.js +21 -1
  8. package/dist/cli/v4/commands/mcp.js +80 -54
  9. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  10. package/dist/cli/v4/commands/recovery.js +122 -0
  11. package/dist/cli/v4/commands/runs.js +22 -2
  12. package/dist/cli/v4/commands/spawnPause.js +93 -0
  13. package/dist/cli/v4/commands/walkthrough.js +140 -0
  14. package/dist/cli/v4/daemonAgentBuilder.js +4 -1
  15. package/dist/cli/v4/defaultSoul.js +1 -1
  16. package/dist/cli/v4/onboarding/disclaimer.js +162 -0
  17. package/dist/cli/v4/onboarding/loading.js +208 -0
  18. package/dist/cli/v4/onboarding/providerPicker.js +126 -0
  19. package/dist/cli/v4/onboarding/successScreen.js +68 -0
  20. package/dist/cli/v4/repl/firstRunHint.js +107 -0
  21. package/dist/cli/v4/setupWizard.js +201 -31
  22. package/dist/core/v4/aidenAgent.js +219 -1
  23. package/dist/core/v4/daemon/bootstrap.js +47 -0
  24. package/dist/core/v4/daemon/db/migrations.js +66 -0
  25. package/dist/core/v4/daemon/runStore.js +33 -3
  26. package/dist/core/v4/providerFallback.js +35 -2
  27. package/dist/core/v4/providers/modelFetch.js +179 -0
  28. package/dist/core/v4/providers/probe.js +275 -0
  29. package/dist/core/v4/runtimeToggles.js +30 -3
  30. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  31. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  32. package/dist/core/v4/subagent/childBuilder.js +391 -0
  33. package/dist/core/v4/subagent/fanout.js +75 -51
  34. package/dist/core/v4/subagent/spawnPause.js +191 -0
  35. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  36. package/dist/core/v4/toolRegistry.js +19 -3
  37. package/dist/core/v4/ui/banner.js +133 -0
  38. package/dist/core/v4/ui/theme.js +164 -0
  39. package/dist/core/version.js +1 -1
  40. package/dist/moat/plannerGuard.js +29 -0
  41. package/dist/providers/v4/anthropicAdapter.js +31 -3
  42. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  43. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  44. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  45. package/dist/tools/v4/index.js +17 -3
  46. package/dist/tools/v4/skills/lookupToolSchema.js +6 -1
  47. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  48. package/dist/tools/v4/subagent/subagentFanout.js +53 -1
  49. package/dist/tools/v4/ui/_uiSmokeTool.js +60 -0
  50. package/package.json +7 -3
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/selfimprovement/recoveryStore.ts — v4.6 Phase 3b.
10
+ *
11
+ * Durable cross-session failure ledger + recovery report writer.
12
+ * Backed by the v7 schema's `failure_signatures` + `recovery_reports`
13
+ * tables. The store is the single write path for both halves of the
14
+ * self-improvement loop:
15
+ *
16
+ * 1. `recordFailureOccurrence(...)` — called on every classified
17
+ * failure (TCE write-through at the aidenAgent classify site).
18
+ * Upserts the signature row, increments `occurrences`, updates
19
+ * `last_seen_at`.
20
+ *
21
+ * 2. `recordRecovery(...)` — called when a previously-failed
22
+ * signature is observed succeeding (or when the agent's TCE
23
+ * surfaces a structured recovery report at turn end).
24
+ * Inserts a `recovery_reports` row + bumps the signature's
25
+ * `recovered_count` + sets `last_recovery_report_id`.
26
+ *
27
+ * Reads:
28
+ *
29
+ * * `listTopFailures(limit)` — operator dashboard query.
30
+ * * `getBySignature(signature)` — `/recovery show` detail surface.
31
+ * * `listForSession(sessionId)` — used by future plugin hooks that
32
+ * want per-session summaries (currently unused; the operator
33
+ * command path goes via `listTopFailures` + `getBySignature`).
34
+ * * `listReportsForSignature(signatureId, limit)` — the recovery
35
+ * history for one signature.
36
+ *
37
+ * Singleton pattern mirrors `spawnPause` (Phase 3a): `initRecoveryStore({db})`
38
+ * at boot; `getRecoveryStore()` thereafter. Production wiring opens the
39
+ * daemon DB once at REPL/daemon/MCP boot and re-uses the singleton
40
+ * across REPL turns and daemon-fired turns. Tests reset via
41
+ * `_resetRecoveryStoreForTests()`.
42
+ *
43
+ * Failure mode: NEVER throws. A persistence failure (locked DB,
44
+ * schema drift, etc.) returns 0 / null and logs a warning. The
45
+ * TCE write-through path treats the store as best-effort — losing
46
+ * a signature increment does not break a turn.
47
+ */
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.RecoveryStore = void 0;
50
+ exports.initRecoveryStore = initRecoveryStore;
51
+ exports.getRecoveryStore = getRecoveryStore;
52
+ exports._resetRecoveryStoreForTests = _resetRecoveryStoreForTests;
53
+ // ── Implementation ───────────────────────────────────────────────────────
54
+ /**
55
+ * SQLite-backed store. Constructed with a `better-sqlite3` Database
56
+ * handle that already has the v7 migration applied. The store does
57
+ * NOT run migrations itself — that's the caller's job (typically
58
+ * `openDaemonDb` + `runMigrations`).
59
+ */
60
+ class RecoveryStore {
61
+ constructor(db) {
62
+ this.db = db;
63
+ }
64
+ /**
65
+ * Upsert a failure signature + bump occurrences. Returns the
66
+ * signature row id, or 0 on persistence failure (logged). The
67
+ * caller (TCE write-through path) treats the return as best-effort.
68
+ */
69
+ recordFailureOccurrence(opts) {
70
+ const now = opts.now ?? Date.now;
71
+ const ts = now();
72
+ try {
73
+ // SQLite-native UPSERT — single round trip per failure. The
74
+ // `excluded.x` syntax references the row we tried to insert.
75
+ const r = this.db.prepare(`
76
+ INSERT INTO failure_signatures
77
+ (signature, tool_name, failure_category, args_hash,
78
+ first_seen_at, last_seen_at, occurrences, recovered_count)
79
+ VALUES (?, ?, ?, ?, ?, ?, 1, 0)
80
+ ON CONFLICT(signature) DO UPDATE SET
81
+ last_seen_at = excluded.last_seen_at,
82
+ occurrences = failure_signatures.occurrences + 1
83
+ RETURNING id
84
+ `).get(opts.signature, opts.toolName, opts.category, opts.argsHash ?? null, ts, ts);
85
+ return r?.id ?? 0;
86
+ }
87
+ catch (err) {
88
+ // eslint-disable-next-line no-console
89
+ console.warn('[recoveryStore] recordFailureOccurrence failed:', err instanceof Error ? err.message : String(err));
90
+ return 0;
91
+ }
92
+ }
93
+ /**
94
+ * Record a successful recovery. Inserts a `recovery_reports`
95
+ * row + atomically bumps the signature's `recovered_count` and
96
+ * `last_recovery_report_id`. Returns the new report id, or 0 on
97
+ * failure.
98
+ */
99
+ recordRecovery(opts) {
100
+ const now = opts.now ?? Date.now;
101
+ const ts = now();
102
+ try {
103
+ // Two-statement transaction — insert then update — keeps the
104
+ // signature's `last_recovery_report_id` consistent with the
105
+ // newly-inserted report row.
106
+ const txn = this.db.transaction(() => {
107
+ const ins = this.db.prepare(`
108
+ INSERT INTO recovery_reports
109
+ (signature_id, run_id, session_id, failed_attempts,
110
+ successful_strategy, changed_parameters, verification,
111
+ created_at, notes)
112
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
113
+ `).run(opts.signatureId, opts.runId ?? null, opts.sessionId ?? null, opts.failedAttempts, opts.successfulStrategy, opts.changedParameters ? JSON.stringify(opts.changedParameters) : null, opts.verification ?? null, ts, opts.notes ?? null);
114
+ const reportId = Number(ins.lastInsertRowid);
115
+ this.db.prepare(`
116
+ UPDATE failure_signatures
117
+ SET recovered_count = recovered_count + 1,
118
+ last_recovery_report_id = ?
119
+ WHERE id = ?
120
+ `).run(reportId, opts.signatureId);
121
+ return reportId;
122
+ });
123
+ return txn();
124
+ }
125
+ catch (err) {
126
+ // eslint-disable-next-line no-console
127
+ console.warn('[recoveryStore] recordRecovery failed:', err instanceof Error ? err.message : String(err));
128
+ return 0;
129
+ }
130
+ }
131
+ /**
132
+ * Top N recurring failure signatures, sorted by occurrence count
133
+ * descending. Backs `/recovery list`. Joins the most recent
134
+ * recovery_report so the operator sees what worked last.
135
+ */
136
+ listTopFailures(limit = 10) {
137
+ const cap = Math.max(1, Math.min(limit, 500));
138
+ try {
139
+ const rows = this.db.prepare(`
140
+ SELECT
141
+ s.signature AS signature,
142
+ s.tool_name AS toolName,
143
+ s.failure_category AS failureCategory,
144
+ s.occurrences AS occurrences,
145
+ s.recovered_count AS recoveredCount,
146
+ s.last_seen_at AS lastSeenAt,
147
+ r.successful_strategy AS lastRecoveryStrategy
148
+ FROM failure_signatures s
149
+ LEFT JOIN recovery_reports r
150
+ ON r.id = s.last_recovery_report_id
151
+ ORDER BY s.occurrences DESC, s.last_seen_at DESC
152
+ LIMIT ?
153
+ `).all(cap);
154
+ return rows;
155
+ }
156
+ catch (err) {
157
+ // eslint-disable-next-line no-console
158
+ console.warn('[recoveryStore] listTopFailures failed:', err instanceof Error ? err.message : String(err));
159
+ return [];
160
+ }
161
+ }
162
+ /**
163
+ * Lookup one signature by its canonical string. Backs `/recovery
164
+ * show`. Returns null when no signature row exists yet.
165
+ */
166
+ getBySignature(signature) {
167
+ try {
168
+ const row = this.db.prepare(`
169
+ SELECT
170
+ id AS id,
171
+ signature AS signature,
172
+ tool_name AS toolName,
173
+ failure_category AS failureCategory,
174
+ args_hash AS argsHash,
175
+ first_seen_at AS firstSeenAt,
176
+ last_seen_at AS lastSeenAt,
177
+ occurrences AS occurrences,
178
+ recovered_count AS recoveredCount,
179
+ last_recovery_report_id AS lastRecoveryReportId
180
+ FROM failure_signatures WHERE signature = ?
181
+ `).get(signature);
182
+ return row ?? null;
183
+ }
184
+ catch (err) {
185
+ // eslint-disable-next-line no-console
186
+ console.warn('[recoveryStore] getBySignature failed:', err instanceof Error ? err.message : String(err));
187
+ return null;
188
+ }
189
+ }
190
+ /**
191
+ * Recovery reports linked to one signature, most recent first.
192
+ * Used by `/recovery show` to render the recovery history below
193
+ * the signature header.
194
+ */
195
+ listReportsForSignature(signatureId, limit = 50) {
196
+ const cap = Math.max(1, Math.min(limit, 500));
197
+ try {
198
+ const rows = this.db.prepare(`
199
+ SELECT
200
+ id AS id,
201
+ signature_id AS signatureId,
202
+ run_id AS runId,
203
+ session_id AS sessionId,
204
+ failed_attempts AS failedAttempts,
205
+ successful_strategy AS successfulStrategy,
206
+ changed_parameters AS changedParameters,
207
+ verification AS verification,
208
+ created_at AS createdAt,
209
+ notes AS notes
210
+ FROM recovery_reports
211
+ WHERE signature_id = ?
212
+ ORDER BY created_at DESC
213
+ LIMIT ?
214
+ `).all(signatureId, cap);
215
+ return rows;
216
+ }
217
+ catch (err) {
218
+ // eslint-disable-next-line no-console
219
+ console.warn('[recoveryStore] listReportsForSignature failed:', err instanceof Error ? err.message : String(err));
220
+ return [];
221
+ }
222
+ }
223
+ /**
224
+ * Recovery reports written during one session, used by future
225
+ * plugin hooks + the `/recovery` command's per-session view.
226
+ * Wraps a single SELECT — no aggregation. Empty array when no
227
+ * recoveries happened.
228
+ */
229
+ listForSession(sessionId) {
230
+ try {
231
+ const rows = this.db.prepare(`
232
+ SELECT
233
+ id AS id,
234
+ signature_id AS signatureId,
235
+ run_id AS runId,
236
+ session_id AS sessionId,
237
+ failed_attempts AS failedAttempts,
238
+ successful_strategy AS successfulStrategy,
239
+ changed_parameters AS changedParameters,
240
+ verification AS verification,
241
+ created_at AS createdAt,
242
+ notes AS notes
243
+ FROM recovery_reports
244
+ WHERE session_id = ?
245
+ ORDER BY created_at DESC
246
+ `).all(sessionId);
247
+ return rows;
248
+ }
249
+ catch (err) {
250
+ // eslint-disable-next-line no-console
251
+ console.warn('[recoveryStore] listForSession failed:', err instanceof Error ? err.message : String(err));
252
+ return [];
253
+ }
254
+ }
255
+ /**
256
+ * Operator escape hatch — `/recovery clear <signature>` lets the
257
+ * operator say "this is fixed, stop counting it." Cascades to
258
+ * the linked recovery_reports rows so the signature genuinely
259
+ * disappears. Returns true when a row was deleted.
260
+ */
261
+ clearSignature(signature) {
262
+ try {
263
+ const sig = this.getBySignature(signature);
264
+ if (!sig)
265
+ return false;
266
+ const txn = this.db.transaction(() => {
267
+ this.db.prepare(`DELETE FROM recovery_reports WHERE signature_id = ?`).run(sig.id);
268
+ this.db.prepare(`DELETE FROM failure_signatures WHERE id = ?`).run(sig.id);
269
+ });
270
+ txn();
271
+ return true;
272
+ }
273
+ catch (err) {
274
+ // eslint-disable-next-line no-console
275
+ console.warn('[recoveryStore] clearSignature failed:', err instanceof Error ? err.message : String(err));
276
+ return false;
277
+ }
278
+ }
279
+ }
280
+ exports.RecoveryStore = RecoveryStore;
281
+ // ── Module-level singleton ───────────────────────────────────────────────
282
+ let _singleton = null;
283
+ /**
284
+ * Initialise the process-wide store. Called once at REPL / daemon /
285
+ * MCP boot, after `runMigrations` has applied v7. Re-init replaces
286
+ * the singleton so tests can swap DBs cleanly.
287
+ */
288
+ function initRecoveryStore(opts) {
289
+ _singleton = new RecoveryStore(opts.db);
290
+ return _singleton;
291
+ }
292
+ /**
293
+ * Read the current singleton. Returns null when not initialised so
294
+ * callers on the hot path (TCE write-through) can no-op silently
295
+ * instead of throwing. The slash command path (`/recovery list`)
296
+ * does its own "not initialised" error reporting.
297
+ */
298
+ function getRecoveryStore() {
299
+ return _singleton;
300
+ }
301
+ /**
302
+ * Test-only — drop the singleton so the next `initRecoveryStore`
303
+ * call wires a fresh state.
304
+ */
305
+ function _resetRecoveryStoreForTests() {
306
+ _singleton = null;
307
+ }
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/selfimprovement/signatureBuilder.ts — v4.6 Phase 3b.
10
+ *
11
+ * Builds a stable, deterministic signature string for a failed tool
12
+ * call so equivalent failures collapse into one `failure_signatures`
13
+ * row. The shape is:
14
+ *
15
+ * <tool_name>:<failure_category>[:<args_hash_prefix>]
16
+ *
17
+ * The `args_hash_prefix` field is OPTIONAL. When the caller supplies
18
+ * `args`, this module normalises them (strips volatile fields like
19
+ * timestamps, run IDs, UUIDs, monotonic counters), serialises the
20
+ * result deterministically, and takes the first 6 hex chars of a
21
+ * SHA-256 digest. When `args` is omitted, the signature collapses to
22
+ * `<tool>:<category>` only — same logical failure, broader grouping.
23
+ *
24
+ * Granularity trade-offs:
25
+ *
26
+ * * Too granular ("every failure unique") → no aggregation; the
27
+ * `occurrences` column never increments past 1; operators can't
28
+ * see "this tool fails the same way over and over."
29
+ * * Too coarse ("only tool+category") → "file_read failed with
30
+ * `not_found`" groups EVERY missing file together; the operator
31
+ * can't tell which paths are sore points.
32
+ *
33
+ * The args-hash compromise: same tool + same category + same
34
+ * normalized args → same signature (good); same tool + same category
35
+ * + meaningfully different args → different signatures (also good).
36
+ * Volatile fields are stripped BEFORE hashing so re-hashing on a
37
+ * later turn produces the same signature even when only the
38
+ * timestamp / call id changes.
39
+ *
40
+ * Volatile field list (`VOLATILE_KEYS`) — defensive; covers the
41
+ * fields Aiden's tool layer tends to thread through args. Plugin
42
+ * authors who emit custom volatile keys should pre-normalise before
43
+ * calling this module.
44
+ */
45
+ var __importDefault = (this && this.__importDefault) || function (mod) {
46
+ return (mod && mod.__esModule) ? mod : { "default": mod };
47
+ };
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.buildFailureSignature = buildFailureSignature;
50
+ const node_crypto_1 = __importDefault(require("node:crypto"));
51
+ // ── Implementation ───────────────────────────────────────────────────────
52
+ /**
53
+ * Keys whose values are stripped from the args object before hashing.
54
+ * These are fields that DO change between otherwise-identical
55
+ * failures (turn timestamps, run row ids, etc.) so leaving them in
56
+ * would prevent any signature from ever grouping.
57
+ *
58
+ * The list is intentionally narrow — only fields Aiden's tool layer
59
+ * is known to inject. Plugins emitting custom volatile keys must
60
+ * pre-normalise their args before calling this module.
61
+ */
62
+ const VOLATILE_KEYS = new Set([
63
+ 'timestamp',
64
+ 'ts',
65
+ 'requestId',
66
+ 'request_id',
67
+ 'runId',
68
+ 'run_id',
69
+ 'callId',
70
+ 'call_id',
71
+ 'sessionId',
72
+ 'session_id',
73
+ 'turnId',
74
+ 'turn_id',
75
+ 'eventId',
76
+ 'event_id',
77
+ 'createdAt',
78
+ 'created_at',
79
+ 'updatedAt',
80
+ 'updated_at',
81
+ // Common UUID/idempotency-key names.
82
+ 'uuid',
83
+ 'idempotencyKey',
84
+ 'idempotency_key',
85
+ ]);
86
+ /**
87
+ * Deterministically stringify a value. Sorts object keys so
88
+ * `{a:1, b:2}` and `{b:2, a:1}` produce identical bytes. Strips
89
+ * volatile keys from any nested object before stringifying.
90
+ *
91
+ * Non-JSON-serialisable values (functions, symbols, circular refs)
92
+ * collapse to the literal string `'[unserializable]'` so the hash
93
+ * remains stable. Better-than-throwing is the right trade-off for
94
+ * a write-through hot path.
95
+ */
96
+ function deterministicStringify(value) {
97
+ const seen = new WeakSet();
98
+ const visit = (v) => {
99
+ if (v === null || v === undefined)
100
+ return null;
101
+ const t = typeof v;
102
+ if (t === 'string' || t === 'number' || t === 'boolean')
103
+ return v;
104
+ if (t === 'function' || t === 'symbol')
105
+ return '[unserializable]';
106
+ if (typeof v === 'bigint')
107
+ return v.toString();
108
+ if (Array.isArray(v)) {
109
+ if (seen.has(v))
110
+ return '[circular]';
111
+ seen.add(v);
112
+ return v.map(visit);
113
+ }
114
+ if (t === 'object') {
115
+ const obj = v;
116
+ if (seen.has(obj))
117
+ return '[circular]';
118
+ seen.add(obj);
119
+ const out = {};
120
+ const keys = Object.keys(obj).filter((k) => !VOLATILE_KEYS.has(k));
121
+ keys.sort();
122
+ for (const k of keys)
123
+ out[k] = visit(obj[k]);
124
+ return out;
125
+ }
126
+ return '[unserializable]';
127
+ };
128
+ try {
129
+ return JSON.stringify(visit(value));
130
+ }
131
+ catch {
132
+ return '[unserializable]';
133
+ }
134
+ }
135
+ /**
136
+ * Build a failure signature. Pure function — no I/O, no side
137
+ * effects. Safe to call on the hot path of every classified
138
+ * failure; SHA-256 of a small JSON string is cheap (microseconds).
139
+ */
140
+ function buildFailureSignature(input) {
141
+ const base = `${input.toolName}:${input.category}`;
142
+ if (input.args === undefined) {
143
+ return { signature: base };
144
+ }
145
+ const normalized = deterministicStringify(input.args);
146
+ // Empty / trivially-null args don't deserve a hash suffix —
147
+ // collapse to the base signature so "args: {}" and "no args"
148
+ // group together.
149
+ if (normalized === 'null' || normalized === '{}' || normalized === '[]') {
150
+ return { signature: base };
151
+ }
152
+ const digest = node_crypto_1.default.createHash('sha256').update(normalized).digest('hex');
153
+ const argsHash = digest.slice(0, 6);
154
+ return {
155
+ signature: `${base}:${argsHash}`,
156
+ argsHash,
157
+ };
158
+ }