aiden-runtime 4.8.0 → 4.9.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 (99) hide show
  1. package/README.md +88 -1
  2. package/dist/cli/v4/aidenCLI.js +35 -4
  3. package/dist/cli/v4/chatSession.js +43 -16
  4. package/dist/cli/v4/commands/daemon.js +47 -2
  5. package/dist/cli/v4/commands/daemonDoctor.js +212 -0
  6. package/dist/cli/v4/commands/daemonStatus.js +1 -1
  7. package/dist/cli/v4/commands/help.js +2 -0
  8. package/dist/cli/v4/commands/hooks.js +428 -0
  9. package/dist/cli/v4/commands/index.js +5 -1
  10. package/dist/cli/v4/commands/mcp.js +89 -1
  11. package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
  12. package/dist/cli/v4/commands/memory.js +702 -0
  13. package/dist/cli/v4/commands/recovery.js +1 -1
  14. package/dist/cli/v4/commands/skin.js +7 -0
  15. package/dist/cli/v4/commands/theme.js +217 -0
  16. package/dist/cli/v4/commands/trigger.js +1 -1
  17. package/dist/cli/v4/commands/update.js +14 -2
  18. package/dist/cli/v4/design/tokens.js +52 -4
  19. package/dist/cli/v4/display.js +102 -46
  20. package/dist/cli/v4/pasteIntercept.js +214 -70
  21. package/dist/cli/v4/replyRenderer.js +145 -5
  22. package/dist/cli/v4/skinEngine.js +67 -0
  23. package/dist/core/v4/aidenAgent.js +45 -2
  24. package/dist/core/v4/daemon/api/runs.js +131 -0
  25. package/dist/core/v4/daemon/bootstrap.js +368 -13
  26. package/dist/core/v4/daemon/db/migrations.js +169 -0
  27. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
  28. package/dist/core/v4/daemon/incarnationStore.js +47 -0
  29. package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
  30. package/dist/core/v4/daemon/runs/reclaim.js +88 -0
  31. package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
  32. package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
  33. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
  34. package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
  35. package/dist/core/v4/daemon/spans/spanStore.js +113 -0
  36. package/dist/core/v4/daemon/triggerBus.js +50 -19
  37. package/dist/core/v4/hooks/auditQuery.js +67 -0
  38. package/dist/core/v4/hooks/dispatcher.js +286 -0
  39. package/dist/core/v4/hooks/index.js +46 -0
  40. package/dist/core/v4/hooks/lifecycle.js +27 -0
  41. package/dist/core/v4/hooks/manifest.js +142 -0
  42. package/dist/core/v4/hooks/registry.js +149 -0
  43. package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
  44. package/dist/core/v4/hooks/toolHookGate.js +76 -0
  45. package/dist/core/v4/hooks/trust.js +14 -0
  46. package/dist/core/v4/identity/contextManager.js +83 -0
  47. package/dist/core/v4/identity/daemonId.js +85 -0
  48. package/dist/core/v4/identity/enforcement.js +103 -0
  49. package/dist/core/v4/identity/executionContext.js +153 -0
  50. package/dist/core/v4/identity/hookExecution.js +62 -0
  51. package/dist/core/v4/identity/httpContext.js +68 -0
  52. package/dist/core/v4/identity/ids.js +185 -0
  53. package/dist/core/v4/identity/index.js +60 -0
  54. package/dist/core/v4/identity/subprocessContext.js +98 -0
  55. package/dist/core/v4/identity/traceparent.js +114 -0
  56. package/dist/core/v4/logger/index.js +3 -1
  57. package/dist/core/v4/logger/logger.js +28 -1
  58. package/dist/core/v4/logger/redact.js +149 -0
  59. package/dist/core/v4/logger/sinks/fileSink.js +13 -0
  60. package/dist/core/v4/logger/sinks/stdSink.js +19 -1
  61. package/dist/core/v4/mcp/install/backup.js +78 -0
  62. package/dist/core/v4/mcp/install/clientPaths.js +90 -0
  63. package/dist/core/v4/mcp/install/clients.js +203 -0
  64. package/dist/core/v4/mcp/install/healthCheck.js +83 -0
  65. package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
  66. package/dist/core/v4/mcp/install/profiles.js +109 -0
  67. package/dist/core/v4/mcp/install/wslDetect.js +62 -0
  68. package/dist/core/v4/memory/namespaceRegistry.js +117 -0
  69. package/dist/core/v4/memory/projectRoot.js +76 -0
  70. package/dist/core/v4/memory/reviewer/index.js +162 -0
  71. package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
  72. package/dist/core/v4/memory/reviewer/prompt.js +105 -0
  73. package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
  74. package/dist/core/v4/memoryManager.js +57 -10
  75. package/dist/core/v4/paths.js +2 -0
  76. package/dist/core/v4/promptBuilder.js +6 -0
  77. package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
  78. package/dist/core/v4/theme/bundledThemes.js +106 -0
  79. package/dist/core/v4/theme/themeLoader.js +160 -0
  80. package/dist/core/v4/theme/themeRegistry.js +97 -0
  81. package/dist/core/v4/theme/themeWatcher.js +95 -0
  82. package/dist/core/v4/toolRegistry.js +71 -8
  83. package/dist/core/v4/update/executeInstall.js +10 -6
  84. package/dist/core/v4/update/installMethodDetect.js +7 -0
  85. package/dist/core/version.js +67 -2
  86. package/dist/moat/approvalEngine.js +4 -0
  87. package/dist/moat/memoryGuard.js +8 -1
  88. package/dist/providers/v4/anthropicAdapter.js +10 -4
  89. package/dist/tools/v4/backends/local.js +19 -2
  90. package/dist/tools/v4/sessions/recallSession.js +6 -1
  91. package/package.json +3 -3
  92. package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
  93. package/themes/default.yaml +52 -0
  94. package/themes/dracula.yaml +32 -0
  95. package/themes/light.yaml +32 -0
  96. package/themes/monochrome.yaml +31 -0
  97. package/themes/tokyo-night.yaml +32 -0
  98. package/dist/core/pluginSystem.js +0 -121
  99. package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
@@ -316,6 +316,170 @@ CREATE INDEX IF NOT EXISTS idx_recovery_reports_signature
316
316
  CREATE INDEX IF NOT EXISTS idx_recovery_reports_run
317
317
  ON recovery_reports(run_id);
318
318
  `;
319
+ // v4.9.0 Slice 4 — daemon_incarnations table. Source of truth lives at
320
+ // `core/v4/daemon/db/schema/v8.sql`; kept in sync via the migrations
321
+ // test snapshot check. Distinct from the v1 `daemon_instances` table
322
+ // (which keeps its random-UUID instance_id intact for existing
323
+ // `evaluateBootState` / `reclaimStuckRuns` consumers); v8 introduces
324
+ // the persistent daemon identity + per-boot incarnation correlation.
325
+ const V8_SQL = `
326
+ CREATE TABLE IF NOT EXISTS daemon_incarnations (
327
+ incarnation_id TEXT PRIMARY KEY,
328
+ daemon_id TEXT NOT NULL,
329
+ pid INTEGER NOT NULL,
330
+ started_at TEXT NOT NULL,
331
+ ended_at TEXT,
332
+ exit_reason TEXT,
333
+ exit_code INTEGER,
334
+ aiden_version TEXT,
335
+ node_version TEXT
336
+ );
337
+ CREATE INDEX IF NOT EXISTS idx_incarnations_daemon
338
+ ON daemon_incarnations(daemon_id, started_at DESC);
339
+ `;
340
+ // v4.9.0 Slice 5 — durable run queue. Source of truth lives at
341
+ // `core/v4/daemon/db/schema/v9.sql`; kept in sync via the migrations
342
+ // test snapshot check.
343
+ const V9_SQL = `
344
+ CREATE TABLE IF NOT EXISTS run_attempts (
345
+ attempt_id TEXT PRIMARY KEY,
346
+ run_id INTEGER NOT NULL,
347
+ attempt_number INTEGER NOT NULL,
348
+ incarnation_id TEXT NOT NULL,
349
+ started_at TEXT NOT NULL,
350
+ ended_at TEXT,
351
+ status TEXT NOT NULL,
352
+ finish_reason TEXT,
353
+ error_class TEXT,
354
+ error_message TEXT,
355
+ FOREIGN KEY (run_id) REFERENCES runs(id) ON DELETE CASCADE
356
+ );
357
+ CREATE INDEX IF NOT EXISTS idx_run_attempts_run
358
+ ON run_attempts(run_id, attempt_number);
359
+ CREATE INDEX IF NOT EXISTS idx_run_attempts_incarnation
360
+ ON run_attempts(incarnation_id);
361
+
362
+ CREATE TABLE IF NOT EXISTS spans (
363
+ span_id TEXT PRIMARY KEY,
364
+ trace_id TEXT NOT NULL,
365
+ parent_span_id TEXT,
366
+ run_id INTEGER,
367
+ attempt_id TEXT,
368
+ incarnation_id TEXT NOT NULL,
369
+ kind TEXT NOT NULL,
370
+ name TEXT NOT NULL,
371
+ started_at TEXT NOT NULL,
372
+ ended_at TEXT,
373
+ status TEXT,
374
+ attrs_json TEXT,
375
+ error_class TEXT,
376
+ error_message TEXT
377
+ );
378
+ CREATE INDEX IF NOT EXISTS idx_spans_trace ON spans(trace_id, started_at);
379
+ CREATE INDEX IF NOT EXISTS idx_spans_run ON spans(run_id, started_at);
380
+ CREATE INDEX IF NOT EXISTS idx_spans_parent ON spans(parent_span_id);
381
+
382
+ CREATE TABLE IF NOT EXISTS run_idempotency_keys (
383
+ namespace TEXT NOT NULL,
384
+ key TEXT NOT NULL,
385
+ fingerprint TEXT NOT NULL,
386
+ run_id INTEGER,
387
+ trigger_event_id INTEGER,
388
+ span_id TEXT,
389
+ status TEXT NOT NULL,
390
+ created_at TEXT NOT NULL,
391
+ expires_at TEXT,
392
+ result_ref TEXT,
393
+ PRIMARY KEY (namespace, key)
394
+ );
395
+ CREATE INDEX IF NOT EXISTS idx_idempotency_expires
396
+ ON run_idempotency_keys(expires_at) WHERE expires_at IS NOT NULL;
397
+ CREATE INDEX IF NOT EXISTS idx_idempotency_run
398
+ ON run_idempotency_keys(run_id) WHERE run_id IS NOT NULL;
399
+ `;
400
+ // v4.9.0 Slice 7 — external trace adoption. Adds `external_trace_id`
401
+ // to `spans` + `runs` for W3C traceparent adoption alongside Aiden's
402
+ // typed `trc_<uuidv7>`. Source of truth: `db/schema/v10.sql`.
403
+ const V10_SQL = `
404
+ ALTER TABLE spans ADD COLUMN external_trace_id TEXT;
405
+ ALTER TABLE runs ADD COLUMN external_trace_id TEXT;
406
+ CREATE INDEX IF NOT EXISTS idx_spans_external_trace
407
+ ON spans(external_trace_id) WHERE external_trace_id IS NOT NULL;
408
+ `;
409
+ // v4.9.0 Slice 12a — Hook system tables. Source of truth lives at
410
+ // `core/v4/daemon/db/schema/v11.sql`.
411
+ const V11_SQL = `
412
+ CREATE TABLE IF NOT EXISTS hooks (
413
+ hook_id TEXT PRIMARY KEY,
414
+ name TEXT NOT NULL,
415
+ version TEXT,
416
+ source TEXT NOT NULL,
417
+ runtime TEXT NOT NULL,
418
+ manifest_path TEXT NOT NULL,
419
+ code_hash TEXT NOT NULL,
420
+ enabled INTEGER NOT NULL DEFAULT 0,
421
+ trust_state TEXT NOT NULL,
422
+ created_at TEXT NOT NULL,
423
+ updated_at TEXT NOT NULL,
424
+ UNIQUE(manifest_path)
425
+ );
426
+ CREATE TABLE IF NOT EXISTS hook_subscriptions (
427
+ subscription_id TEXT PRIMARY KEY,
428
+ hook_id TEXT NOT NULL REFERENCES hooks(hook_id) ON DELETE CASCADE,
429
+ event TEXT NOT NULL,
430
+ matcher_json TEXT,
431
+ authority TEXT NOT NULL,
432
+ mode TEXT NOT NULL,
433
+ priority INTEGER NOT NULL DEFAULT 0,
434
+ timeout_ms INTEGER NOT NULL,
435
+ on_error TEXT NOT NULL,
436
+ on_timeout TEXT NOT NULL,
437
+ enabled INTEGER NOT NULL DEFAULT 1
438
+ );
439
+ CREATE INDEX IF NOT EXISTS idx_hook_subscriptions_event ON hook_subscriptions(event, enabled);
440
+ CREATE TABLE IF NOT EXISTS hook_capability_grants (
441
+ grant_id TEXT PRIMARY KEY,
442
+ hook_id TEXT NOT NULL REFERENCES hooks(hook_id) ON DELETE CASCADE,
443
+ capability TEXT NOT NULL,
444
+ scope_json TEXT NOT NULL,
445
+ granted_by TEXT,
446
+ granted_at TEXT NOT NULL,
447
+ revoked_at TEXT
448
+ );
449
+ CREATE TABLE IF NOT EXISTS hook_executions (
450
+ hook_execution_id TEXT PRIMARY KEY,
451
+ hook_id TEXT NOT NULL REFERENCES hooks(hook_id),
452
+ subscription_id TEXT REFERENCES hook_subscriptions(subscription_id),
453
+ event TEXT NOT NULL,
454
+ run_id TEXT,
455
+ trace_id TEXT,
456
+ span_id TEXT,
457
+ parent_span_id TEXT,
458
+ tool_call_id TEXT,
459
+ status TEXT NOT NULL,
460
+ decision TEXT,
461
+ elapsed_ms INTEGER NOT NULL,
462
+ cpu_ms INTEGER,
463
+ max_rss_kb INTEGER,
464
+ exit_code INTEGER,
465
+ payload_hash TEXT,
466
+ response_hash TEXT,
467
+ stdout_preview TEXT,
468
+ stderr_preview TEXT,
469
+ error_kind TEXT,
470
+ error_message TEXT,
471
+ started_at TEXT NOT NULL,
472
+ finished_at TEXT NOT NULL
473
+ );
474
+ CREATE INDEX IF NOT EXISTS idx_hook_executions_run ON hook_executions(run_id, started_at);
475
+ CREATE INDEX IF NOT EXISTS idx_hook_executions_hook ON hook_executions(hook_id, started_at);
476
+ CREATE INDEX IF NOT EXISTS idx_hook_executions_event ON hook_executions(event, started_at);
477
+ `;
478
+ // v4.9.0 Slice 12b — auto-disable rail. Just an ADD COLUMN; full
479
+ // rationale lives in `core/v4/daemon/db/schema/v12.sql`.
480
+ const V12_SQL = `
481
+ ALTER TABLE hooks ADD COLUMN consecutive_failures INTEGER NOT NULL DEFAULT 0;
482
+ `;
319
483
  const MIGRATIONS = [
320
484
  { version: 1, name: 'phase 1 — daemon foundation', sql: V1_SQL },
321
485
  { version: 2, name: 'phase 2 — file watcher observations', sql: V2_SQL },
@@ -324,6 +488,11 @@ const MIGRATIONS = [
324
488
  { version: 5, name: 'phase 5b — scheduled workflows', sql: V5_SQL },
325
489
  { version: 6, name: 'v4.6 phase 1 — sub-agent lineage', sql: V6_SQL },
326
490
  { version: 7, name: 'v4.6 phase 3b — self-improvement loop', sql: V7_SQL },
491
+ { version: 8, name: 'v4.9 slice 4 — daemon identity + incarnations', sql: V8_SQL },
492
+ { version: 9, name: 'v4.9 slice 5 — durable run queue', sql: V9_SQL },
493
+ { version: 10, name: 'v4.9 slice 7 — external trace adoption', sql: V10_SQL },
494
+ { version: 11, name: 'v4.9 slice 12a — hook system', sql: V11_SQL },
495
+ { version: 12, name: 'v4.9 slice 12b — hook auto-disable counter', sql: V12_SQL },
327
496
  ];
328
497
  exports.LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
329
498
  function getCurrentVersion(db) {
@@ -0,0 +1,128 @@
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/daemon/idempotency/runIdempotencyStore.ts — v4.9.0 Slice 5.
10
+ *
11
+ * Durable ingress / write-side idempotency. Distinct from the v1
12
+ * `idempotency_keys` response-replay cache (which stores HTTP response
13
+ * bodies for exact-byte replay). This store tracks (namespace, key) →
14
+ * acceptance outcome so a duplicate webhook / email / file / API
15
+ * request never creates a second `runs` row.
16
+ *
17
+ * `acquire()` is the central primitive: atomic insert against the
18
+ * (namespace, key) PK. Three outcomes:
19
+ *
20
+ * - 'accepted' — fresh row inserted, caller proceeds with
21
+ * run/trigger creation.
22
+ * - 'duplicate' — row exists with the SAME fingerprint;
23
+ * caller should reuse the linked run/event id.
24
+ * - 'rejected_conflict' — row exists with a DIFFERENT fingerprint;
25
+ * caller is reusing a key for different work
26
+ * and should reject with a loud error.
27
+ *
28
+ * Call `link()` after the run/trigger row exists to back-fill the FKs.
29
+ * `complete()` patches the final result_ref (e.g. span_id of the
30
+ * outcome). `sweepExpired()` is the GC tick for keys with `expires_at`
31
+ * in the past.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.fingerprintCanonical = fingerprintCanonical;
35
+ exports.acquire = acquire;
36
+ exports.link = link;
37
+ exports.complete = complete;
38
+ exports.sweepExpired = sweepExpired;
39
+ exports.getKey = getKey;
40
+ const node_crypto_1 = require("node:crypto");
41
+ /**
42
+ * Canonical fingerprint helper — sort keys, stringify, SHA-256 hex.
43
+ * Drops `undefined` values to keep the hash stable across optional
44
+ * fields. Use this when the caller doesn't already have a domain-
45
+ * specific hash (e.g. webhook deliveries already compute one in
46
+ * `webhookIdempotency.ts`).
47
+ */
48
+ function fingerprintCanonical(payload) {
49
+ const canon = canonicalise(payload);
50
+ return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(canon)).digest('hex');
51
+ }
52
+ function canonicalise(v) {
53
+ if (v === null || v === undefined)
54
+ return null;
55
+ if (Array.isArray(v))
56
+ return v.map(canonicalise);
57
+ if (typeof v === 'object') {
58
+ const out = {};
59
+ for (const k of Object.keys(v).sort()) {
60
+ const val = v[k];
61
+ if (val !== undefined)
62
+ out[k] = canonicalise(val);
63
+ }
64
+ return out;
65
+ }
66
+ return v;
67
+ }
68
+ /**
69
+ * Attempt to claim (namespace, key) for an incoming request. Atomic
70
+ * via INSERT OR IGNORE against the (namespace, key) PK + a follow-up
71
+ * SELECT for the existing row.
72
+ */
73
+ function acquire(db, opts) {
74
+ const nowMs = (opts.now ?? Date.now)();
75
+ const nowIso = new Date(nowMs).toISOString();
76
+ const expires = opts.ttlMs ? new Date(nowMs + opts.ttlMs).toISOString() : null;
77
+ const result = db.prepare(`INSERT OR IGNORE INTO run_idempotency_keys
78
+ (namespace, key, fingerprint, status, created_at, expires_at)
79
+ VALUES (?, ?, ?, 'accepted', ?, ?)`).run(opts.namespace, opts.key, opts.fingerprint, nowIso, expires);
80
+ if (result.changes > 0) {
81
+ const row = readRow(db, opts.namespace, opts.key);
82
+ return { outcome: 'accepted', row };
83
+ }
84
+ // Collision — read existing and compare fingerprint.
85
+ const existing = readRow(db, opts.namespace, opts.key);
86
+ if (!existing) {
87
+ // Defensive: should not happen, the INSERT OR IGNORE only skips
88
+ // when the PK conflicts.
89
+ throw new Error('runIdempotencyStore.acquire: INSERT OR IGNORE skipped but no row found');
90
+ }
91
+ if (existing.fingerprint === opts.fingerprint) {
92
+ return { outcome: 'duplicate', existing };
93
+ }
94
+ return { outcome: 'rejected_conflict', existing };
95
+ }
96
+ /** Back-fill the run/trigger/span FKs once those rows exist. */
97
+ function link(db, opts) {
98
+ db.prepare(`UPDATE run_idempotency_keys
99
+ SET run_id = COALESCE(?, run_id),
100
+ trigger_event_id = COALESCE(?, trigger_event_id),
101
+ span_id = COALESCE(?, span_id)
102
+ WHERE namespace = ? AND key = ?`).run(opts.runId ?? null, opts.triggerEventId ?? null, opts.spanId ?? null, opts.namespace, opts.key);
103
+ }
104
+ /**
105
+ * Patch the terminal status + optional result_ref. Caller picks the
106
+ * status: `'completed'` for success, `'failed'` for terminal failure.
107
+ */
108
+ function complete(db, opts) {
109
+ db.prepare(`UPDATE run_idempotency_keys
110
+ SET status = ?,
111
+ result_ref = COALESCE(?, result_ref)
112
+ WHERE namespace = ? AND key = ?`).run(opts.status, opts.resultRef ?? null, opts.namespace, opts.key);
113
+ }
114
+ /** Delete keys whose `expires_at` is in the past. Returns the count. */
115
+ function sweepExpired(db, now) {
116
+ const nowIso = new Date(now ?? Date.now()).toISOString();
117
+ const r = db.prepare(`DELETE FROM run_idempotency_keys
118
+ WHERE expires_at IS NOT NULL AND expires_at < ?`).run(nowIso);
119
+ return { deleted: Number(r.changes ?? 0) };
120
+ }
121
+ /** Diagnostic — single-key lookup. */
122
+ function getKey(db, namespace, key) {
123
+ return readRow(db, namespace, key);
124
+ }
125
+ function readRow(db, namespace, key) {
126
+ const r = db.prepare(`SELECT * FROM run_idempotency_keys WHERE namespace = ? AND key = ?`).get(namespace, key);
127
+ return r ?? null;
128
+ }
@@ -0,0 +1,47 @@
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/daemon/incarnationStore.ts — v4.9.0 Slice 4.
10
+ *
11
+ * Thin DDL wrapper around the `daemon_incarnations` table (schema v8).
12
+ * Three operations:
13
+ *
14
+ * 1. `insertIncarnation()` — boot. Writes one row with `started_at`
15
+ * = now ISO. Idempotent on PRIMARY KEY collision (safe re-call).
16
+ * 2. `markEnded()` — clean shutdown. Patches `ended_at`, `exit_reason`,
17
+ * `exit_code` for this incarnation. No-op when the row is missing
18
+ * (defensive — e.g. SQLite died before insert).
19
+ * 3. `lastForDaemon()` — diagnostic. Returns the most recent row for
20
+ * a given daemon_id. Used by `aiden doctor`-style surfaces (not
21
+ * wired in this slice).
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.insertIncarnation = insertIncarnation;
25
+ exports.markEnded = markEnded;
26
+ exports.lastForDaemon = lastForDaemon;
27
+ function insertIncarnation(db, opts) {
28
+ const started = opts.startedAt ?? new Date().toISOString();
29
+ db.prepare(`INSERT OR IGNORE INTO daemon_incarnations
30
+ (incarnation_id, daemon_id, pid, started_at, aiden_version, node_version)
31
+ VALUES (?, ?, ?, ?, ?, ?)`).run(opts.incarnationId, opts.daemonId, opts.pid, started, opts.aidenVersion, opts.nodeVersion);
32
+ }
33
+ function markEnded(db, opts) {
34
+ const ended = opts.endedAt ?? new Date().toISOString();
35
+ db.prepare(`UPDATE daemon_incarnations
36
+ SET ended_at = COALESCE(ended_at, ?),
37
+ exit_reason = COALESCE(exit_reason, ?),
38
+ exit_code = COALESCE(exit_code, ?)
39
+ WHERE incarnation_id = ?`).run(ended, opts.exitReason, opts.exitCode, opts.incarnationId);
40
+ }
41
+ function lastForDaemon(db, daemonId) {
42
+ const r = db.prepare(`SELECT * FROM daemon_incarnations
43
+ WHERE daemon_id = ?
44
+ ORDER BY started_at DESC
45
+ LIMIT 1`).get(daemonId);
46
+ return r ?? null;
47
+ }
@@ -0,0 +1,67 @@
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/daemon/runs/attemptStore.ts — v4.9.0 Slice 5.
10
+ *
11
+ * One row per execution attempt of a run. `runs.id` (numeric, autoincrement)
12
+ * stays the canonical run identifier; `run_attempts.attempt_id`
13
+ * (`att_<uuidv7>`) is the per-try identifier. attempt_number is 1-indexed
14
+ * and stable per (run_id) — the store computes it via MAX+1 on insert.
15
+ *
16
+ * Slice 5 lands schema + writers. Retry-policy logic (which attempts
17
+ * get retried, with what cooldown) is deferred to Slice 6+.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.createAttempt = createAttempt;
21
+ exports.completeAttempt = completeAttempt;
22
+ exports.listAttemptsForRun = listAttemptsForRun;
23
+ exports.getAttempt = getAttempt;
24
+ const identity_1 = require("../../identity");
25
+ /**
26
+ * Create a fresh attempt for the given run. `attempt_number` is derived
27
+ * by MAX+1 inside a transaction so concurrent creators don't collide.
28
+ * Returns the new attempt id.
29
+ */
30
+ function createAttempt(db, opts) {
31
+ const attemptId = opts.attemptId ?? (0, identity_1.newAttemptId)();
32
+ const startedAt = opts.startedAt ?? new Date().toISOString();
33
+ const tx = db.transaction(() => {
34
+ const row = db.prepare(`SELECT COALESCE(MAX(attempt_number), 0) AS n FROM run_attempts WHERE run_id = ?`).get(opts.runId);
35
+ const nextNumber = row.n + 1;
36
+ db.prepare(`INSERT INTO run_attempts
37
+ (attempt_id, run_id, attempt_number, incarnation_id, started_at, status)
38
+ VALUES (?, ?, ?, ?, ?, 'running')`).run(attemptId, opts.runId, nextNumber, opts.incarnationId, startedAt);
39
+ });
40
+ tx();
41
+ return attemptId;
42
+ }
43
+ /**
44
+ * Patch an attempt with a terminal status. COALESCE-protected so the
45
+ * first-wins semantics match the Slice 4 incarnation pattern: if the
46
+ * attempt already has an `ended_at`, this call is a no-op for those
47
+ * fields (status update only applies to the in-flight case).
48
+ */
49
+ function completeAttempt(db, opts) {
50
+ const endedAt = opts.endedAt ?? new Date().toISOString();
51
+ db.prepare(`UPDATE run_attempts
52
+ SET status = ?,
53
+ ended_at = COALESCE(ended_at, ?),
54
+ finish_reason = COALESCE(finish_reason, ?),
55
+ error_class = COALESCE(error_class, ?),
56
+ error_message = COALESCE(error_message, ?)
57
+ WHERE attempt_id = ?`).run(opts.status, endedAt, opts.finishReason ?? null, opts.errorClass ?? null, opts.errorMessage ?? null, opts.attemptId);
58
+ }
59
+ /** List all attempts for a run in attempt-number order. */
60
+ function listAttemptsForRun(db, runId) {
61
+ return db.prepare(`SELECT * FROM run_attempts WHERE run_id = ? ORDER BY attempt_number ASC`).all(runId);
62
+ }
63
+ /** Diagnostic — single-attempt lookup. */
64
+ function getAttempt(db, attemptId) {
65
+ const r = db.prepare(`SELECT * FROM run_attempts WHERE attempt_id = ?`).get(attemptId);
66
+ return r ?? null;
67
+ }
@@ -0,0 +1,88 @@
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/daemon/runs/reclaim.ts — v4.9.0 Slice 3.
10
+ *
11
+ * Mark runs orphaned by a crashed daemon as `interrupted` so a follow-up
12
+ * boot (or a human looking at `aiden runs list`) sees an unambiguous
13
+ * post-mortem instead of an eternally-`running` row.
14
+ *
15
+ * Called from two sites:
16
+ * 1. Process-wide crash handlers (`uncaughtException` /
17
+ * `unhandledRejection`) installed in `bootstrap.ts`. Before the
18
+ * handler calls `process.exit(1)`, it reclaims this incarnation's
19
+ * still-running rows. The DB connection is the same handle the
20
+ * dispatcher used; the write is one statement so even a crashed
21
+ * process can finish it.
22
+ * 2. Boot-time, against ANY non-current instance — defence in depth
23
+ * for the case where `evaluateBootState` didn't sweep a row (e.g.
24
+ * the prior crash happened after BootState ran but before the
25
+ * run completed). Idempotent: a no-op when no rows match.
26
+ *
27
+ * Uses existing schema columns only — NO new tables, NO new RunStatus
28
+ * value. Sets `status='interrupted', finish_reason='daemon_crashed',
29
+ * resume_pending=1, resume_reason='daemon_crashed', completed_at=now`.
30
+ * Matches the semantics `evaluateBootState` uses for prior-instance
31
+ * crash recovery, so downstream consumers (`aiden runs list`, restart
32
+ * resume logic) see one consistent shape.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.reclaimStuckRuns = reclaimStuckRuns;
36
+ /**
37
+ * Reclaim stuck `runs.status='running'` rows. Returns the rows touched
38
+ * so the caller can emit `run_events` entries (skipped on the crash
39
+ * path — the inserts could themselves throw, and the UPDATE is the
40
+ * authoritative outcome).
41
+ */
42
+ function reclaimStuckRuns(db, opts) {
43
+ const now = (opts.now ?? Date.now)();
44
+ // Phase 1 — select the candidate rows. We pull ids first so the
45
+ // caller can act on them (logging, event emission) without a second
46
+ // round-trip. Using a single WHERE clause keeps the index hit on
47
+ // idx_runs_active (`status IN ('queued','running')`).
48
+ let candidates;
49
+ if (opts.instanceId !== undefined) {
50
+ candidates = db.prepare(`SELECT id FROM runs WHERE status = 'running' AND instance_id = ?`).all(opts.instanceId);
51
+ }
52
+ else {
53
+ if (!opts.currentInstanceId) {
54
+ throw new Error('reclaimStuckRuns: currentInstanceId required when instanceId omitted');
55
+ }
56
+ candidates = db.prepare(`SELECT id FROM runs WHERE status = 'running' AND instance_id != ?`).all(opts.currentInstanceId);
57
+ }
58
+ if (candidates.length === 0) {
59
+ return { reclaimed: 0, runIds: [] };
60
+ }
61
+ // Phase 2 — single UPDATE for the matching predicate. We re-derive
62
+ // the WHERE from `opts` rather than passing ids inline (would blow
63
+ // past SQLite's bound-parameter cap on a pathologically large run
64
+ // table — unlikely, but cheap to avoid).
65
+ let updateResult;
66
+ if (opts.instanceId !== undefined) {
67
+ updateResult = db.prepare(`UPDATE runs
68
+ SET status = 'interrupted',
69
+ finish_reason = 'daemon_crashed',
70
+ completed_at = ?,
71
+ resume_pending = 1,
72
+ resume_reason = 'daemon_crashed'
73
+ WHERE status = 'running' AND instance_id = ?`).run(now, opts.instanceId);
74
+ }
75
+ else {
76
+ updateResult = db.prepare(`UPDATE runs
77
+ SET status = 'interrupted',
78
+ finish_reason = 'daemon_crashed',
79
+ completed_at = ?,
80
+ resume_pending = 1,
81
+ resume_reason = 'daemon_crashed'
82
+ WHERE status = 'running' AND instance_id != ?`).run(now, opts.currentInstanceId);
83
+ }
84
+ return {
85
+ reclaimed: Number(updateResult.changes ?? candidates.length),
86
+ runIds: candidates.map((c) => c.id),
87
+ };
88
+ }
@@ -0,0 +1,73 @@
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/daemon/runs/retryPolicy.ts — v4.9.0 Slice 8.
10
+ *
11
+ * Pure policy primitives — `shouldRetry` + `computeBackoffMs`. The
12
+ * orchestrator (`runWithRetry`) lives next door; this module is
13
+ * test-friendly with no side effects.
14
+ *
15
+ * Defaults chosen to match the implicit policy across the rest of
16
+ * v4: max 3 attempts, exponential backoff with full jitter capped
17
+ * at 30s. NetworkError / TimeoutError / ResourceExhausted are
18
+ * retryable; auth + permission + validation are terminal.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.DEFAULT_RETRY_POLICY = void 0;
22
+ exports.shouldRetry = shouldRetry;
23
+ exports.computeBackoffMs = computeBackoffMs;
24
+ exports.DEFAULT_RETRY_POLICY = {
25
+ maxAttempts: 3,
26
+ baseDelayMs: 1000,
27
+ maxDelayMs: 30000,
28
+ jitter: 'full',
29
+ retryableErrorClasses: ['NetworkError', 'TimeoutError', 'ResourceExhausted'],
30
+ nonRetryableErrorClasses: ['AuthError', 'PermissionDenied', 'ValidationError'],
31
+ };
32
+ /**
33
+ * Decide whether to retry given the error class + attempt number.
34
+ *
35
+ * Precedence:
36
+ * 1. attemptNumber >= maxAttempts → false (cap reached)
37
+ * 2. nonRetryableErrorClasses hit → false (terminal)
38
+ * 3. retryableErrorClasses hit → true
39
+ * 4. unknown error class → false (fail closed)
40
+ *
41
+ * Treating unknown errors as non-retryable matches Aiden's safety
42
+ * stance: a retry burst on an unknown failure can multiply damage.
43
+ */
44
+ function shouldRetry(errorClass, attemptNumber, policy = exports.DEFAULT_RETRY_POLICY) {
45
+ if (attemptNumber >= policy.maxAttempts)
46
+ return false;
47
+ if (policy.nonRetryableErrorClasses.includes(errorClass))
48
+ return false;
49
+ if (policy.retryableErrorClasses.includes(errorClass))
50
+ return true;
51
+ return false;
52
+ }
53
+ /**
54
+ * Exponential backoff with optional jitter, capped at `maxDelayMs`.
55
+ * attemptNumber=1 → base
56
+ * attemptNumber=2 → base*2
57
+ * attemptNumber=3 → base*4
58
+ * Jitter modes:
59
+ * 'none' — exact value
60
+ * 'equal' — value/2 + random(0..value/2)
61
+ * 'full' — random(0..value)
62
+ */
63
+ function computeBackoffMs(attemptNumber, policy = exports.DEFAULT_RETRY_POLICY, rng = Math.random) {
64
+ if (attemptNumber < 1)
65
+ return 0;
66
+ const exp = policy.baseDelayMs * Math.pow(2, attemptNumber - 1);
67
+ const capped = Math.min(exp, policy.maxDelayMs);
68
+ switch (policy.jitter) {
69
+ case 'none': return capped;
70
+ case 'equal': return Math.floor(capped / 2 + rng() * (capped / 2));
71
+ case 'full': return Math.floor(rng() * capped);
72
+ }
73
+ }
@@ -0,0 +1,80 @@
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/daemon/runs/runWithRetry.ts — v4.9.0 Slice 8.
10
+ *
11
+ * Attempt orchestration on top of Slice 5's `run_attempts` table.
12
+ * Caller supplies an existing run row id + a `RetryPolicy`; this
13
+ * function creates one `run_attempts` row per attempt, runs `fn`
14
+ * inside an incrementing-attempt context, and on retryable error
15
+ * sleeps + tries again. On non-retryable error OR max-attempts cap,
16
+ * returns `'dead_letter'` so the caller can route to a poison queue.
17
+ *
18
+ * The "what's retryable" decision lives in `retryPolicy.ts` so the
19
+ * orchestrator stays small + testable.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.runWithRetry = runWithRetry;
23
+ const attemptStore_1 = require("./attemptStore");
24
+ const retryPolicy_1 = require("./retryPolicy");
25
+ function defaultSleep(ms) {
26
+ return new Promise((r) => setTimeout(r, ms));
27
+ }
28
+ /**
29
+ * Run `fn` with retry policy. Each attempt gets its own `run_attempts`
30
+ * row + an incremented `ctx.attempt` value visible to the callee.
31
+ */
32
+ async function runWithRetry(db, ctx, opts, fn) {
33
+ const sleep = opts.sleep ?? defaultSleep;
34
+ let lastError = new Error('runWithRetry: no attempts made');
35
+ let attemptCount = 0;
36
+ for (let attemptNumber = 1; attemptNumber <= opts.policy.maxAttempts; attemptNumber += 1) {
37
+ attemptCount = attemptNumber;
38
+ const attemptId = (0, attemptStore_1.createAttempt)(db, {
39
+ runId: opts.runId,
40
+ incarnationId: opts.incarnationId,
41
+ });
42
+ const attemptCtx = { ...ctx, attempt: attemptNumber };
43
+ try {
44
+ const value = await fn(attemptCtx, attemptNumber);
45
+ (0, attemptStore_1.completeAttempt)(db, { attemptId, status: 'completed' });
46
+ return { outcome: 'completed', value, attempts: attemptNumber };
47
+ }
48
+ catch (err) {
49
+ const error = err instanceof Error ? err : new Error(String(err));
50
+ lastError = error;
51
+ const errorClass = error.name || 'Error';
52
+ const terminalStatus = 'failed';
53
+ (0, attemptStore_1.completeAttempt)(db, {
54
+ attemptId,
55
+ status: terminalStatus,
56
+ errorClass,
57
+ errorMessage: error.message,
58
+ });
59
+ // Three cases:
60
+ // (a) Class is in nonRetryableErrorClasses → dead_letter (terminal class)
61
+ // (b) Class is in retryableErrorClasses → retry IF cap not reached; else dead_letter (exhausted)
62
+ // (c) Class is unknown → failed (caller introspects; no auto-retry)
63
+ const isNonRetryable = opts.policy.nonRetryableErrorClasses.includes(errorClass);
64
+ const isRetryable = opts.policy.retryableErrorClasses.includes(errorClass);
65
+ if (isNonRetryable) {
66
+ return { outcome: 'dead_letter', attempts: attemptNumber, lastError: error };
67
+ }
68
+ if (!isRetryable) {
69
+ return { outcome: 'failed', attempts: attemptNumber, lastError: error };
70
+ }
71
+ if (attemptNumber >= opts.policy.maxAttempts) {
72
+ return { outcome: 'dead_letter', attempts: attemptNumber, lastError: error };
73
+ }
74
+ // Retryable + cap not reached: back off and try again.
75
+ await sleep((0, retryPolicy_1.computeBackoffMs)(attemptNumber, opts.policy));
76
+ }
77
+ }
78
+ // Defensive fallback — loop exited without explicit return.
79
+ return { outcome: 'dead_letter', attempts: attemptCount, lastError };
80
+ }