aiden-runtime 4.8.1 → 4.9.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 (100) hide show
  1. package/README.md +88 -1
  2. package/dist/cli/v4/aidenCLI.js +37 -6
  3. package/dist/cli/v4/chatSession.js +53 -13
  4. package/dist/cli/v4/commands/daemon.js +53 -3
  5. package/dist/cli/v4/commands/daemonDoctor.js +212 -0
  6. package/dist/cli/v4/commands/daemonStatus.js +45 -26
  7. package/dist/cli/v4/commands/help.js +5 -0
  8. package/dist/cli/v4/commands/hooks.js +466 -0
  9. package/dist/cli/v4/commands/hooksSlash.js +33 -0
  10. package/dist/cli/v4/commands/index.js +13 -1
  11. package/dist/cli/v4/commands/mcp.js +89 -1
  12. package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
  13. package/dist/cli/v4/commands/memory.js +707 -0
  14. package/dist/cli/v4/commands/memorySlash.js +38 -0
  15. package/dist/cli/v4/commands/recovery.js +1 -1
  16. package/dist/cli/v4/commands/skin.js +7 -0
  17. package/dist/cli/v4/commands/theme.js +217 -0
  18. package/dist/cli/v4/commands/trigger.js +1 -1
  19. package/dist/cli/v4/design/tokens.js +52 -4
  20. package/dist/cli/v4/display.js +39 -26
  21. package/dist/cli/v4/replyRenderer.js +6 -5
  22. package/dist/cli/v4/skinEngine.js +67 -0
  23. package/dist/cli/v4/ui/progressBar.js +179 -0
  24. package/dist/cli/v4/util/closestAction.js +48 -0
  25. package/dist/core/v4/aidenAgent.js +45 -2
  26. package/dist/core/v4/daemon/api/runs.js +131 -0
  27. package/dist/core/v4/daemon/bootstrap.js +368 -13
  28. package/dist/core/v4/daemon/db/migrations.js +169 -0
  29. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
  30. package/dist/core/v4/daemon/incarnationStore.js +47 -0
  31. package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
  32. package/dist/core/v4/daemon/runs/reclaim.js +88 -0
  33. package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
  34. package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
  35. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
  36. package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
  37. package/dist/core/v4/daemon/spans/spanStore.js +113 -0
  38. package/dist/core/v4/daemon/triggerBus.js +50 -19
  39. package/dist/core/v4/hooks/auditQuery.js +67 -0
  40. package/dist/core/v4/hooks/dispatcher.js +286 -0
  41. package/dist/core/v4/hooks/index.js +46 -0
  42. package/dist/core/v4/hooks/lifecycle.js +27 -0
  43. package/dist/core/v4/hooks/manifest.js +142 -0
  44. package/dist/core/v4/hooks/registry.js +149 -0
  45. package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
  46. package/dist/core/v4/hooks/toolHookGate.js +76 -0
  47. package/dist/core/v4/hooks/trust.js +14 -0
  48. package/dist/core/v4/identity/contextManager.js +83 -0
  49. package/dist/core/v4/identity/daemonId.js +85 -0
  50. package/dist/core/v4/identity/enforcement.js +103 -0
  51. package/dist/core/v4/identity/executionContext.js +153 -0
  52. package/dist/core/v4/identity/hookExecution.js +62 -0
  53. package/dist/core/v4/identity/httpContext.js +68 -0
  54. package/dist/core/v4/identity/ids.js +185 -0
  55. package/dist/core/v4/identity/index.js +60 -0
  56. package/dist/core/v4/identity/subprocessContext.js +98 -0
  57. package/dist/core/v4/identity/traceparent.js +114 -0
  58. package/dist/core/v4/logger/index.js +3 -1
  59. package/dist/core/v4/logger/logger.js +28 -1
  60. package/dist/core/v4/logger/redact.js +149 -0
  61. package/dist/core/v4/logger/sinks/fileSink.js +13 -0
  62. package/dist/core/v4/logger/sinks/stdSink.js +19 -1
  63. package/dist/core/v4/mcp/install/backup.js +78 -0
  64. package/dist/core/v4/mcp/install/clientPaths.js +90 -0
  65. package/dist/core/v4/mcp/install/clients.js +203 -0
  66. package/dist/core/v4/mcp/install/healthCheck.js +83 -0
  67. package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
  68. package/dist/core/v4/mcp/install/profiles.js +109 -0
  69. package/dist/core/v4/mcp/install/wslDetect.js +62 -0
  70. package/dist/core/v4/memory/namespaceRegistry.js +117 -0
  71. package/dist/core/v4/memory/projectRoot.js +76 -0
  72. package/dist/core/v4/memory/reviewer/index.js +162 -0
  73. package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
  74. package/dist/core/v4/memory/reviewer/prompt.js +105 -0
  75. package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
  76. package/dist/core/v4/memoryManager.js +57 -10
  77. package/dist/core/v4/paths.js +2 -0
  78. package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
  79. package/dist/core/v4/theme/bundledThemes.js +106 -0
  80. package/dist/core/v4/theme/themeLoader.js +160 -0
  81. package/dist/core/v4/theme/themeRegistry.js +97 -0
  82. package/dist/core/v4/theme/themeWatcher.js +95 -0
  83. package/dist/core/v4/toolRegistry.js +71 -8
  84. package/dist/core/v4/update/depWarningFilter.js +76 -0
  85. package/dist/core/v4/update/executeInstall.js +41 -35
  86. package/dist/core/v4/update/platformInstructions.js +128 -0
  87. package/dist/moat/approvalEngine.js +4 -0
  88. package/dist/moat/memoryGuard.js +8 -1
  89. package/dist/providers/v4/anthropicAdapter.js +10 -4
  90. package/dist/tools/v4/backends/local.js +19 -2
  91. package/dist/tools/v4/sessions/recallSession.js +6 -1
  92. package/package.json +3 -1
  93. package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
  94. package/themes/default.yaml +52 -0
  95. package/themes/dracula.yaml +32 -0
  96. package/themes/light.yaml +32 -0
  97. package/themes/monochrome.yaml +31 -0
  98. package/themes/tokyo-night.yaml +32 -0
  99. package/dist/core/pluginSystem.js +0 -121
  100. package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
@@ -30,6 +30,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
30
30
  return (mod && mod.__esModule) ? mod : { "default": mod };
31
31
  };
32
32
  Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.getCurrentDaemonLogger = getCurrentDaemonLogger;
34
+ exports.getCurrentDaemonId = getCurrentDaemonId;
35
+ exports.getCurrentIncarnationId = getCurrentIncarnationId;
36
+ exports.getCurrentDaemonDb = getCurrentDaemonDb;
33
37
  exports.bootstrapDaemon = bootstrapDaemon;
34
38
  exports._resetDaemonBootstrapForTests = _resetDaemonBootstrapForTests;
35
39
  exports.getDaemonHandle = getDaemonHandle;
@@ -70,6 +74,19 @@ const migration_1 = require("./cron/migration");
70
74
  const cronEmitter_1 = require("./cron/cronEmitter");
71
75
  const playwrightBridge_1 = require("../../playwrightBridge");
72
76
  const version_1 = require("../../version");
77
+ // v4.9.0 Slice 3 — structured logger + crash recovery.
78
+ const node_path_1 = __importDefault(require("node:path"));
79
+ const logger_1 = require("../logger");
80
+ const reclaim_1 = require("./runs/reclaim");
81
+ // v4.9.0 Slice 8 — stuck-attempt watchdog ticker.
82
+ const stuckAttemptWatchdog_1 = require("./runs/stuckAttemptWatchdog");
83
+ // v4.9.0 Slice 4 — identity substrate + incarnations table.
84
+ const identity_1 = require("../identity");
85
+ const incarnationStore_1 = require("./incarnationStore");
86
+ // v4.9.0 Slice 7 — static import. The lazy `require()` form here
87
+ // silently failed under vite-node (CJS `.ts` resolution) and the
88
+ // `/api/runs` route never mounted in tests.
89
+ const runs_1 = require("./api/runs");
73
90
  const NOOP_HANDLE = Object.freeze({
74
91
  active: false,
75
92
  instanceId: null,
@@ -92,6 +109,100 @@ const NOOP_HANDLE = Object.freeze({
92
109
  });
93
110
  // Process-wide singleton — the second call returns the same handle.
94
111
  let _singleton = null;
112
+ /**
113
+ * v4.9.0 Slice 4 — module-level holders for the persistent daemon
114
+ * identity (`dmn_...`) and the per-process incarnation (`inc_...`).
115
+ * Populated by `bootstrapDaemon()`. Readable by other modules (e.g.
116
+ * the logger sink) via `getCurrentDaemonId()` / `getCurrentIncarnationId()`
117
+ * so they can stamp every record with the identity pair without
118
+ * requiring an ambient ExecutionContext.
119
+ */
120
+ let _currentDaemonId = null;
121
+ let _currentIncarnationId = null;
122
+ // v4.9.0 Slice 6 — module-level reference to the active daemon DB
123
+ // handle. Cached after `openDaemonDb(dbPath)` returns so tool/LLM span
124
+ // wrappers can pull it via `getCurrentDaemonDb()` without re-opening.
125
+ let _currentDaemonDb = null;
126
+ // v4.9.0 Slice 6 — and a reference to the daemon logger, for the
127
+ // same reason: spans + log enrichments need a logger handle without
128
+ // each cross-cutting site plumbing its own.
129
+ let _currentDaemonLogger = null;
130
+ /** v4.9.0 Slice 6 — read the daemon's structured Logger (or null). */
131
+ function getCurrentDaemonLogger() { return _currentDaemonLogger; }
132
+ /** v4.9.0 Slice 4 — read the persistent daemon id (`dmn_...`) or null. */
133
+ function getCurrentDaemonId() { return _currentDaemonId; }
134
+ /** v4.9.0 Slice 4 — read this boot's incarnation id (`inc_...`) or null. */
135
+ function getCurrentIncarnationId() { return _currentIncarnationId; }
136
+ /**
137
+ * v4.9.0 Slice 6 — read the active daemon DB handle, or null when
138
+ * AIDEN_DAEMON=0 / bootstrap hasn't run / bootstrap failed. Lets
139
+ * cross-cutting modules (tool dispatcher, agent loop) opt into
140
+ * span instrumentation without requiring CLI-level wiring. NOOP-safe:
141
+ * returns null silently when the daemon foundation isn't up.
142
+ */
143
+ function getCurrentDaemonDb() {
144
+ return _currentDaemonDb;
145
+ }
146
+ /**
147
+ * v4.9.0 Slice 3 — track whether the process-wide crash handlers
148
+ * already wrapped this process. Reset by the test helper. The guard
149
+ * prevents duplicate handler chains when bootstrap runs twice (which
150
+ * the singleton normally prevents anyway, but tests sometimes punch
151
+ * through `_resetDaemonBootstrapForTests`).
152
+ */
153
+ let _crashHandlersInstalled = false;
154
+ /**
155
+ * v4.9.0 Slice 3 — build a structured daemon logger composed of:
156
+ * stderr (human, warn+) — visible to systemd / journalctl
157
+ * file (NDJSON) — `<aidenRoot>/logs/daemon.log`, rotated at 5 MB
158
+ * Both sinks are wrapped in `RedactingSink` so secret-shaped tokens
159
+ * never reach disk or stderr. Level is `info` by default; the
160
+ * `AIDEN_DAEMON_LOG_LEVEL` env var promotes to `debug` for diagnostics.
161
+ */
162
+ function buildDaemonLogger(logsDir) {
163
+ const envLvl = (process.env.AIDEN_DAEMON_LOG_LEVEL ?? '').toLowerCase();
164
+ const level = envLvl === 'debug' || envLvl === 'info' || envLvl === 'warn' || envLvl === 'error'
165
+ ? envLvl
166
+ : 'info';
167
+ return new logger_1.CoreLogger({
168
+ level,
169
+ sinks: [
170
+ // v4.9.0 Slice 6 — pretty stderr (short timestamp + dim runId
171
+ // last-8 suffix when ambient context is present). File sink
172
+ // stays NDJSON with full IDs for log aggregators.
173
+ new logger_1.RedactingSink(new logger_1.StderrSink({ minLevel: 'warn', pretty: true })),
174
+ new logger_1.RedactingSink(new logger_1.FileSink({ dir: logsDir, name: 'daemon', format: 'ndjson' })),
175
+ ],
176
+ // v4.9.0 Slice 4 — every daemon log line gets stamped with the
177
+ // identity pair (daemonId, incarnationId) plus any ambient
178
+ // ExecutionContext fields (runId, traceId, spanId, ...). Caller
179
+ // ctx wins on key collision. Provider is guarded against init-time
180
+ // calls (before the identity holders are populated) — returning
181
+ // `undefined` then is fine; the merge step skips it.
182
+ getContext: () => {
183
+ const out = {};
184
+ if (_currentDaemonId)
185
+ out.daemonId = _currentDaemonId;
186
+ if (_currentIncarnationId)
187
+ out.incarnationId = _currentIncarnationId;
188
+ const ctx = (0, identity_1.currentContext)();
189
+ if (ctx) {
190
+ out.runId = ctx.runId;
191
+ out.traceId = ctx.traceId;
192
+ out.spanId = ctx.spanId;
193
+ if (ctx.parentSpanId)
194
+ out.parentSpanId = ctx.parentSpanId;
195
+ if (ctx.sessionId)
196
+ out.sessionId = ctx.sessionId;
197
+ if (ctx.triggerId)
198
+ out.triggerId = ctx.triggerId;
199
+ out.source = ctx.source;
200
+ out.attempt = ctx.attempt;
201
+ }
202
+ return Object.keys(out).length > 0 ? out : undefined;
203
+ },
204
+ });
205
+ }
95
206
  /**
96
207
  * Initialize the daemon foundation IF AIDEN_DAEMON=1. Returns a
97
208
  * handle describing what was wired (or a NOOP_HANDLE when the
@@ -108,20 +219,29 @@ function bootstrapDaemon(opts = {}) {
108
219
  if (_singleton)
109
220
  return _singleton;
110
221
  const cfg = (0, daemonConfig_1.getDaemonConfig)();
222
+ if (!cfg.enabled) {
223
+ _singleton = NOOP_HANDLE;
224
+ return NOOP_HANDLE;
225
+ }
226
+ // v4.9.0 Slice 3 — promote startup logging to the structured pipeline
227
+ // BEFORE the first emit. Sub-modules receive the `log(level, msg)`
228
+ // shape they were built against; the adapter forwards to the
229
+ // CoreLogger which redacts before fan-out to stderr + file.
230
+ const aidenRootForLog = (0, paths_1.resolveAidenRoot)();
231
+ const daemonLogger = buildDaemonLogger(node_path_1.default.join(aidenRootForLog, 'logs'));
232
+ // v4.9.0 Slice 6 — stash for cross-cutting consumers (tool dispatcher
233
+ // span wrap, LLM call span wrap). NOOP-safe via `getCurrentDaemonLogger()`.
234
+ _currentDaemonLogger = daemonLogger;
111
235
  const log = opts.log ?? ((level, msg) => {
112
236
  if (level === 'error')
113
- console.error(msg);
237
+ daemonLogger.error(msg);
114
238
  else if (level === 'warn')
115
- console.warn(msg);
239
+ daemonLogger.warn(msg);
116
240
  else
117
- console.log(msg);
241
+ daemonLogger.info(msg);
118
242
  });
119
- if (!cfg.enabled) {
120
- _singleton = NOOP_HANDLE;
121
- return NOOP_HANDLE;
122
- }
123
243
  try {
124
- const aidenRoot = (0, paths_1.resolveAidenRoot)();
244
+ const aidenRoot = aidenRootForLog;
125
245
  const dbPath = (0, daemonConfig_2.daemonDbPath)(aidenRoot);
126
246
  const lockPath = (0, daemonConfig_2.daemonRuntimeLockPath)(aidenRoot);
127
247
  const markerPath = (0, daemonConfig_2.daemonCleanShutdownMarkerPath)(aidenRoot);
@@ -158,6 +278,8 @@ function bootstrapDaemon(opts = {}) {
158
278
  (e instanceof Error ? e.message : String(e)));
159
279
  }
160
280
  const db = (0, connection_1.openDaemonDb)(dbPath);
281
+ // v4.9.0 Slice 6 — cache for cross-cutting consumers.
282
+ _currentDaemonDb = db;
161
283
  const tracker = (0, instanceTracker_1.createInstanceTracker)({ db, version: version_1.VERSION });
162
284
  tracker.start();
163
285
  // v4.6 Phase 3b — self-improvement loop singleton. Daemon-fired
@@ -200,8 +322,118 @@ function bootstrapDaemon(opts = {}) {
200
322
  if (boot.crashDetected) {
201
323
  log('warn', '[daemon] crash recovery: prior instance crashed; affected runs marked resume_pending=1');
202
324
  }
325
+ // v4.9.0 Slice 4 — establish persistent daemon identity + per-process
326
+ // incarnation. The daemon_id file is created on first boot at
327
+ // <aidenRoot>/daemon/daemon_id; subsequent boots load it. The
328
+ // incarnation row in daemon_incarnations gives every process a
329
+ // first-class lineage row so `aiden doctor`-style tooling can show
330
+ // "this daemon has booted N times across M crashes".
331
+ //
332
+ // Distinct from `tracker.instanceId`: the latter is a random UUID
333
+ // for the v1-era `daemon_instances` table that Slice 3's crash
334
+ // recovery still uses. We keep both alive; Slice 4 ADDS identity,
335
+ // doesn't replace.
336
+ try {
337
+ _currentDaemonId = (0, identity_1.loadOrCreateDaemonId)(aidenRoot);
338
+ _currentIncarnationId = (0, identity_1.newIncarnationId)();
339
+ (0, incarnationStore_1.insertIncarnation)(db, {
340
+ incarnationId: _currentIncarnationId,
341
+ daemonId: _currentDaemonId,
342
+ pid: process.pid,
343
+ aidenVersion: version_1.VERSION,
344
+ nodeVersion: process.version,
345
+ });
346
+ log('info', `[daemon] identity established daemon_id=${_currentDaemonId} ` +
347
+ `incarnation_id=${_currentIncarnationId}`);
348
+ }
349
+ catch (e) {
350
+ log('warn', `[daemon] identity init failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
351
+ _currentDaemonId = null;
352
+ _currentIncarnationId = null;
353
+ }
354
+ // v4.9.0 Slice 3 — defence-in-depth: sweep any `running` rows owned
355
+ // by a non-current instance. `evaluateBootState` already covers the
356
+ // common case, but a row racing the crash window could slip past it.
357
+ // Idempotent: no-op when no rows match.
358
+ try {
359
+ const swept = (0, reclaim_1.reclaimStuckRuns)(db, { currentInstanceId: tracker.instanceId });
360
+ if (swept.reclaimed > 0) {
361
+ log('warn', `[daemon] startup reclaim swept ${swept.reclaimed} orphaned run(s) ` +
362
+ `from prior incarnations: ids=[${swept.runIds.join(',')}]`);
363
+ }
364
+ }
365
+ catch (e) {
366
+ log('warn', `[daemon] startup reclaim failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
367
+ }
368
+ // v4.9.0 Slice 3 — process-wide crash handlers. Either signal
369
+ // means "this daemon is about to die unexpectedly". Reclaim our
370
+ // still-`running` rows BEFORE the exit so a follow-up boot sees
371
+ // an unambiguous `interrupted` shape, then exit 1 so service
372
+ // managers (systemd / launchd) know to restart us.
373
+ //
374
+ // We install `process.on(...)` (not `.once`) so the handler covers
375
+ // the rare double-crash. The exit is guarded by a 100ms timeout so
376
+ // a flush of the file sink has a chance to land.
377
+ if (!_crashHandlersInstalled) {
378
+ _crashHandlersInstalled = true;
379
+ const crashedInstanceId = tracker.instanceId;
380
+ const reclaimAndExit = (eventName, reason) => {
381
+ try {
382
+ const err = reason instanceof Error
383
+ ? { type: reason.name, message: reason.message, stack: reason.stack }
384
+ : { type: 'NonError', message: String(reason) };
385
+ daemonLogger.error(`[daemon] ${eventName} — terminating after run reclaim`, {
386
+ event: 'daemon.crashed',
387
+ component: 'daemon.bootstrap',
388
+ incarnationId: crashedInstanceId,
389
+ error: err,
390
+ });
391
+ }
392
+ catch { /* logging must not block the reclaim path */ }
393
+ try {
394
+ const swept = (0, reclaim_1.reclaimStuckRuns)(db, { instanceId: crashedInstanceId });
395
+ if (swept.reclaimed > 0) {
396
+ try {
397
+ daemonLogger.warn(`[daemon] crash-handler reclaim marked ${swept.reclaimed} run(s) interrupted`, { event: 'daemon.crash_reclaim', runIds: swept.runIds });
398
+ }
399
+ catch { /* noop */ }
400
+ }
401
+ }
402
+ catch { /* never block exit on reclaim failure */ }
403
+ // v4.9.0 Slice 4 — flag the incarnation row as crashed before
404
+ // we exit. Best-effort; never blocks the exit path.
405
+ try {
406
+ if (_currentIncarnationId) {
407
+ (0, incarnationStore_1.markEnded)(db, {
408
+ incarnationId: _currentIncarnationId,
409
+ exitReason: 'crash',
410
+ exitCode: 1,
411
+ });
412
+ }
413
+ }
414
+ catch { /* noop */ }
415
+ // 100ms grace for the file sink to flush before we tear down.
416
+ setTimeout(() => { try {
417
+ process.exit(1);
418
+ }
419
+ catch { /* noop */ } }, 100);
420
+ };
421
+ process.on('uncaughtException', (err) => reclaimAndExit('uncaughtException', err));
422
+ process.on('unhandledRejection', (reason) => reclaimAndExit('unhandledRejection', reason));
423
+ }
203
424
  // Module singletons.
204
- const triggerBus = (0, triggerBus_1.createTriggerBus)({ db });
425
+ // v4.9.0 Slice 5 opt the trigger bus into the durable
426
+ // run-idempotency anchor. Every accepted trigger_event with a
427
+ // non-null idempotency_key now also writes a row to
428
+ // `run_idempotency_keys` in the same transaction.
429
+ const triggerBus = (0, triggerBus_1.createTriggerBus)({
430
+ db,
431
+ enableRunIdempotency: true,
432
+ onIdempotencyConflict: (info) => {
433
+ log('warn', `[trigger-bus] duplicate ingress namespace=trigger:${info.source} ` +
434
+ `key=${info.key} — reusing existing run anchor`);
435
+ },
436
+ });
205
437
  const idempotencyStore = (0, idempotencyStore_1.createIdempotencyStore)({ db });
206
438
  const runStore = (0, runStore_1.createRunStore)({ db });
207
439
  const restartFailureCounter = (0, restartFailureCounter_1.createRestartFailureCounter)({ db, threshold: cfg.restartFailureThreshold });
@@ -249,6 +481,22 @@ function bootstrapDaemon(opts = {}) {
249
481
  resourceRegistry,
250
482
  log,
251
483
  });
484
+ // v4.9.0 Slice 5 — POST /api/runs durable ingress. Returns 202
485
+ // only after the trigger_event + run_idempotency_keys rows commit.
486
+ // Slice 7 also adopts inbound `traceparent` here.
487
+ try {
488
+ const ingressBindHost = process.env.AIDEN_DAEMON_BIND ?? '127.0.0.1';
489
+ (0, runs_1.mountRunsRoutes)({
490
+ app,
491
+ triggerBus,
492
+ log,
493
+ apiKeyRequired: ingressBindHost !== '127.0.0.1' && ingressBindHost !== 'localhost',
494
+ });
495
+ log('info', '[api/runs] POST /api/runs durable ingress mounted');
496
+ }
497
+ catch (e) {
498
+ log('error', `[api/runs] mount failed: ${e instanceof Error ? e.message : String(e)}`);
499
+ }
252
500
  // v4.5 Phase 3 — bind safety check. When AIDEN_DAEMON_BIND opts
253
501
  // into a non-loopback interface, require AIDEN_API_KEY + refuse
254
502
  // INSECURE_NO_AUTH webhook routes. Runs BEFORE the listener binds.
@@ -304,6 +552,69 @@ function bootstrapDaemon(opts = {}) {
304
552
  }, 24 * 60 * 60 * 1000);
305
553
  if (typeof retentionTimer.unref === 'function')
306
554
  retentionTimer.unref();
555
+ // v4.9.0 Slice 4 — wire the missing `triggerBus.reclaimExpired()`
556
+ // ticker. The function existed since v4.5 Phase 5a but had no call
557
+ // site; crashed-mid-claim trigger events were stuck in `'claimed'`
558
+ // because the claim picker only matches `status='pending'`. 30s
559
+ // cadence matches what the original `triggerBus.ts` header
560
+ // comment promised. Boot-time call covers the gap where a daemon
561
+ // crashed *after* a claim and a new daemon starts before any lease
562
+ // expires naturally.
563
+ try {
564
+ const swept = triggerBus.reclaimExpired();
565
+ if (swept.reclaimed > 0) {
566
+ log('warn', `[trigger-bus] boot reclaim returned ${swept.reclaimed} expired claim(s) to 'pending'`);
567
+ }
568
+ }
569
+ catch (e) {
570
+ log('warn', `[trigger-bus] boot reclaim failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
571
+ }
572
+ const reclaimTimer = setInterval(() => {
573
+ try {
574
+ triggerBus.reclaimExpired();
575
+ }
576
+ catch { /* never let the sweep crash the daemon */ }
577
+ }, 30000);
578
+ if (typeof reclaimTimer.unref === 'function')
579
+ reclaimTimer.unref();
580
+ // v4.9.0 Slice 8 — stuck-attempt + orphan-span watchdog.
581
+ // Sweeps run_attempts (status='running', older than threshold,
582
+ // owned by non-current incarnation) and spans (open + non-current
583
+ // incarnation). Default cadence 5min; threshold 30min. Both
584
+ // configurable via env. unref'd so the ticker doesn't block exit.
585
+ const watchdogIntervalMs = (() => {
586
+ const raw = process.env.AIDEN_STUCK_ATTEMPT_CHECK_MS;
587
+ const n = raw ? Number.parseInt(raw, 10) : NaN;
588
+ return Number.isFinite(n) && n > 0 ? n : 5 * 60 * 1000;
589
+ })();
590
+ const watchdogThresholdMs = (() => {
591
+ const raw = process.env.AIDEN_STUCK_ATTEMPT_THRESHOLD_MS;
592
+ const n = raw ? Number.parseInt(raw, 10) : NaN;
593
+ return Number.isFinite(n) && n > 0 ? n : 30 * 60 * 1000;
594
+ })();
595
+ const runWatchdogSweep = () => {
596
+ try {
597
+ if (!_currentIncarnationId)
598
+ return;
599
+ const r = (0, stuckAttemptWatchdog_1.sweepStuckAttempts)(db, {
600
+ currentIncarnationId: _currentIncarnationId,
601
+ thresholdMs: watchdogThresholdMs,
602
+ });
603
+ if (r.reclaimedAttempts > 0 || r.reclaimedSpans > 0) {
604
+ log('warn', `[watchdog] swept stuck attempts=${r.reclaimedAttempts} ` +
605
+ `orphan_spans=${r.reclaimedSpans}`);
606
+ }
607
+ }
608
+ catch (e) {
609
+ log('warn', `[watchdog] sweep failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
610
+ }
611
+ };
612
+ // Boot-time sweep covers anything left over from a prior crash
613
+ // that evaluateBootState / reclaimStuckRuns didn't catch.
614
+ runWatchdogSweep();
615
+ const watchdogTimer = setInterval(runWatchdogSweep, watchdogIntervalMs);
616
+ if (typeof watchdogTimer.unref === 'function')
617
+ watchdogTimer.unref();
307
618
  // Drain context — same shape api/server.ts wires.
308
619
  const getDrainCtx = () => ({
309
620
  drainTimeoutMs: cfg.drainTimeoutMs,
@@ -346,7 +657,27 @@ function bootstrapDaemon(opts = {}) {
346
657
  catch { /* noop */ }
347
658
  }
348
659
  },
349
- markShutdown: (reason, exitCode) => tracker.markShutdown(reason, exitCode),
660
+ markShutdown: (reason, exitCode) => {
661
+ tracker.markShutdown(reason, exitCode);
662
+ // v4.9.0 Slice 4 — patch the incarnation row alongside the
663
+ // legacy daemon_instances row. Map sigusr1_restart/replaced
664
+ // → 'clean' (graceful drain), keep sigterm/sigint/crash
665
+ // verbatim. Best-effort; never throws into the drain path.
666
+ try {
667
+ if (_currentIncarnationId) {
668
+ const incReason = reason === 'sigterm' ? 'sigterm' :
669
+ reason === 'sigint' ? 'sigint' :
670
+ reason === 'crash' ? 'crash' :
671
+ 'clean';
672
+ (0, incarnationStore_1.markEnded)(db, {
673
+ incarnationId: _currentIncarnationId,
674
+ exitReason: incReason,
675
+ exitCode,
676
+ });
677
+ }
678
+ }
679
+ catch { /* drain path must never throw */ }
680
+ },
350
681
  });
351
682
  (0, signals_1.installDaemonSignalHandlers)({ getDrainContext: getDrainCtx });
352
683
  log('info', `[daemon] foundation initializing instance_id=${tracker.instanceId} db=${dbPath}`);
@@ -582,6 +913,16 @@ function bootstrapDaemon(opts = {}) {
582
913
  /** Test-only — clears the singleton so subsequent calls re-bootstrap. */
583
914
  function _resetDaemonBootstrapForTests() {
584
915
  _singleton = null;
916
+ // v4.9.0 Slice 3 — also rearm the crash-handler installer guard
917
+ // so a fresh bootstrap in the same test process can install its
918
+ // own handlers without tripping the once-per-process check.
919
+ _crashHandlersInstalled = false;
920
+ // v4.9.0 Slice 4 — clear identity holders too.
921
+ _currentDaemonId = null;
922
+ _currentIncarnationId = null;
923
+ // v4.9.0 Slice 6 — clear cross-cutting refs too.
924
+ _currentDaemonDb = null;
925
+ _currentDaemonLogger = null;
585
926
  }
586
927
  /** Diagnostic — returns the current handle (or null if not yet bootstrapped). */
587
928
  function getDaemonHandle() {
@@ -624,13 +965,27 @@ function installDaemonAgentBuilder(handle, agentBuilder, persistedDefaultModel,
624
965
  if (!handle.active || !handle.dispatcher || !handle.triggerBus || !handle.runStore) {
625
966
  return false;
626
967
  }
968
+ // v4.9.0 Slice 3 — fall back to the daemon's own structured logger
969
+ // (built lazily here in case the caller skipped passing one). When
970
+ // bootstrap already constructed a logger, the singleton's startup
971
+ // log path used the same composition.
972
+ const fallbackLogger = (() => {
973
+ try {
974
+ return buildDaemonLogger(node_path_1.default.join((0, paths_1.resolveAidenRoot)(), 'logs'));
975
+ }
976
+ catch {
977
+ return null;
978
+ }
979
+ })();
627
980
  const logFn = log ?? ((level, msg) => {
981
+ if (!fallbackLogger)
982
+ return;
628
983
  if (level === 'error')
629
- console.error(msg);
984
+ fallbackLogger.error(msg);
630
985
  else if (level === 'warn')
631
- console.warn(msg);
986
+ fallbackLogger.warn(msg);
632
987
  else
633
- console.log(msg);
988
+ fallbackLogger.info(msg);
634
989
  });
635
990
  try {
636
991
  const realRunner = (0, dispatcher_1.createRealAgentRunner)({
@@ -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) {