aiden-runtime 4.8.1 → 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 (93) hide show
  1. package/README.md +88 -1
  2. package/dist/cli/v4/aidenCLI.js +35 -4
  3. package/dist/cli/v4/chatSession.js +34 -9
  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/design/tokens.js +52 -4
  18. package/dist/cli/v4/display.js +39 -26
  19. package/dist/cli/v4/replyRenderer.js +6 -5
  20. package/dist/cli/v4/skinEngine.js +67 -0
  21. package/dist/core/v4/aidenAgent.js +45 -2
  22. package/dist/core/v4/daemon/api/runs.js +131 -0
  23. package/dist/core/v4/daemon/bootstrap.js +368 -13
  24. package/dist/core/v4/daemon/db/migrations.js +169 -0
  25. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
  26. package/dist/core/v4/daemon/incarnationStore.js +47 -0
  27. package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
  28. package/dist/core/v4/daemon/runs/reclaim.js +88 -0
  29. package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
  30. package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
  31. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
  32. package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
  33. package/dist/core/v4/daemon/spans/spanStore.js +113 -0
  34. package/dist/core/v4/daemon/triggerBus.js +50 -19
  35. package/dist/core/v4/hooks/auditQuery.js +67 -0
  36. package/dist/core/v4/hooks/dispatcher.js +286 -0
  37. package/dist/core/v4/hooks/index.js +46 -0
  38. package/dist/core/v4/hooks/lifecycle.js +27 -0
  39. package/dist/core/v4/hooks/manifest.js +142 -0
  40. package/dist/core/v4/hooks/registry.js +149 -0
  41. package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
  42. package/dist/core/v4/hooks/toolHookGate.js +76 -0
  43. package/dist/core/v4/hooks/trust.js +14 -0
  44. package/dist/core/v4/identity/contextManager.js +83 -0
  45. package/dist/core/v4/identity/daemonId.js +85 -0
  46. package/dist/core/v4/identity/enforcement.js +103 -0
  47. package/dist/core/v4/identity/executionContext.js +153 -0
  48. package/dist/core/v4/identity/hookExecution.js +62 -0
  49. package/dist/core/v4/identity/httpContext.js +68 -0
  50. package/dist/core/v4/identity/ids.js +185 -0
  51. package/dist/core/v4/identity/index.js +60 -0
  52. package/dist/core/v4/identity/subprocessContext.js +98 -0
  53. package/dist/core/v4/identity/traceparent.js +114 -0
  54. package/dist/core/v4/logger/index.js +3 -1
  55. package/dist/core/v4/logger/logger.js +28 -1
  56. package/dist/core/v4/logger/redact.js +149 -0
  57. package/dist/core/v4/logger/sinks/fileSink.js +13 -0
  58. package/dist/core/v4/logger/sinks/stdSink.js +19 -1
  59. package/dist/core/v4/mcp/install/backup.js +78 -0
  60. package/dist/core/v4/mcp/install/clientPaths.js +90 -0
  61. package/dist/core/v4/mcp/install/clients.js +203 -0
  62. package/dist/core/v4/mcp/install/healthCheck.js +83 -0
  63. package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
  64. package/dist/core/v4/mcp/install/profiles.js +109 -0
  65. package/dist/core/v4/mcp/install/wslDetect.js +62 -0
  66. package/dist/core/v4/memory/namespaceRegistry.js +117 -0
  67. package/dist/core/v4/memory/projectRoot.js +76 -0
  68. package/dist/core/v4/memory/reviewer/index.js +162 -0
  69. package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
  70. package/dist/core/v4/memory/reviewer/prompt.js +105 -0
  71. package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
  72. package/dist/core/v4/memoryManager.js +57 -10
  73. package/dist/core/v4/paths.js +2 -0
  74. package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
  75. package/dist/core/v4/theme/bundledThemes.js +106 -0
  76. package/dist/core/v4/theme/themeLoader.js +160 -0
  77. package/dist/core/v4/theme/themeRegistry.js +97 -0
  78. package/dist/core/v4/theme/themeWatcher.js +95 -0
  79. package/dist/core/v4/toolRegistry.js +71 -8
  80. package/dist/moat/approvalEngine.js +4 -0
  81. package/dist/moat/memoryGuard.js +8 -1
  82. package/dist/providers/v4/anthropicAdapter.js +10 -4
  83. package/dist/tools/v4/backends/local.js +19 -2
  84. package/dist/tools/v4/sessions/recallSession.js +6 -1
  85. package/package.json +3 -1
  86. package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
  87. package/themes/default.yaml +52 -0
  88. package/themes/dracula.yaml +32 -0
  89. package/themes/light.yaml +32 -0
  90. package/themes/monochrome.yaml +31 -0
  91. package/themes/tokyo-night.yaml +32 -0
  92. package/dist/core/pluginSystem.js +0 -121
  93. package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
@@ -0,0 +1,131 @@
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/api/runs.ts — v4.9.0 Slice 5.
10
+ *
11
+ * `POST /api/runs` — durable run-acceptance ingress. The handler:
12
+ *
13
+ * 1. Validates the body (must contain at least `args` or `prompt`).
14
+ * 2. Computes a fingerprint from a canonical JSON of the body.
15
+ * 3. Honours a caller-supplied `Idempotency-Key` header (Stripe/RFC
16
+ * pattern). If absent, falls back to the body fingerprint itself.
17
+ * 4. Calls `triggerBus.insert({source:'api',...})`, which (with
18
+ * `enableRunIdempotency:true`) atomically writes both the
19
+ * `trigger_events` row AND the `run_idempotency_keys` anchor.
20
+ * 5. Returns `202` with the persisted trigger_event id — the
21
+ * dispatcher picks the row up off the queue and creates the
22
+ * `runs` row downstream. This is the "202 only after durable
23
+ * insert" guarantee.
24
+ *
25
+ * AUTH: the existing bind-safety check covers non-loopback binds; this
26
+ * endpoint inherits the same `AIDEN_API_KEY` requirement when the
27
+ * daemon binds beyond 127.0.0.1. Loopback-only callers (the common
28
+ * case) authenticate by being on-host.
29
+ */
30
+ var __importDefault = (this && this.__importDefault) || function (mod) {
31
+ return (mod && mod.__esModule) ? mod : { "default": mod };
32
+ };
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.mountRunsRoutes = mountRunsRoutes;
35
+ const express_1 = __importDefault(require("express"));
36
+ const runIdempotencyStore_1 = require("../idempotency/runIdempotencyStore");
37
+ // v4.9.0 Slice 7 — inbound trace adoption.
38
+ const identity_1 = require("../../identity");
39
+ const bootstrap_1 = require("../bootstrap");
40
+ function mountRunsRoutes(opts) {
41
+ const PATH = '/api/runs';
42
+ opts.app.post(PATH, express_1.default.json({ limit: '1mb' }), (req, res, _next) => {
43
+ // Optional shared-secret auth.
44
+ if (opts.apiKeyRequired) {
45
+ const expected = process.env.AIDEN_API_KEY ?? '';
46
+ const auth = req.header('authorization') ?? '';
47
+ const tokenMatch = /^Bearer\s+(\S+)/i.exec(auth);
48
+ const provided = tokenMatch ? tokenMatch[1] : '';
49
+ if (!expected || expected !== provided) {
50
+ res.status(401).json({ error: 'unauthorized' });
51
+ return;
52
+ }
53
+ }
54
+ const body = (req.body ?? {});
55
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
56
+ res.status(400).json({ error: 'body must be a JSON object' });
57
+ return;
58
+ }
59
+ if (!body.args && !body.prompt) {
60
+ res.status(400).json({ error: 'body requires `args` or `prompt`' });
61
+ return;
62
+ }
63
+ const fingerprint = (0, runIdempotencyStore_1.fingerprintCanonical)(body);
64
+ const headerKey = (req.header('idempotency-key') ?? '').trim();
65
+ const idempotencyKey = headerKey.length > 0 ? headerKey : fingerprint;
66
+ const sourceKey = (typeof body.client_id === 'string' && body.client_id.length > 0)
67
+ ? body.client_id
68
+ : 'default';
69
+ // v4.9.0 Slice 7 — inbound trace adoption.
70
+ const incomingTp = (0, identity_1.parseTraceparent)(req.header('traceparent'));
71
+ const rawIncomingTp = req.header('traceparent');
72
+ if (rawIncomingTp && !incomingTp) {
73
+ opts.log('warn', `[api/runs] dropped malformed traceparent header (length=${rawIncomingTp.length})`);
74
+ }
75
+ const rawExternalReqId = req.header('x-request-id');
76
+ const externalReqId = (0, identity_1.validateExternalRequestId)(rawExternalReqId);
77
+ if (rawExternalReqId && externalReqId === null) {
78
+ opts.log('warn', `[api/runs] dropped invalid X-Request-Id header (length=${rawExternalReqId.length})`);
79
+ }
80
+ const ctx = {
81
+ daemonId: (0, bootstrap_1.getCurrentDaemonId)() ?? '',
82
+ incarnationId: (0, bootstrap_1.getCurrentIncarnationId)() ?? '',
83
+ runId: (0, identity_1.newRunId)(), // pre-claim run id (dispatcher assigns final numeric id)
84
+ traceId: incomingTp ? `trc_${incomingTp.traceId}` : (0, identity_1.newTraceId)(),
85
+ spanId: (0, identity_1.newSpanId)(),
86
+ parentSpanId: incomingTp?.parentSpanId ?? undefined,
87
+ requestId: (0, identity_1.newRequestId)(),
88
+ externalRequestId: externalReqId ?? undefined,
89
+ source: 'api',
90
+ attempt: 1,
91
+ };
92
+ void (0, identity_1.runWithContext)(ctx, () => {
93
+ try {
94
+ const result = opts.triggerBus.insert({
95
+ source: 'manual',
96
+ sourceKey,
97
+ idempotencyKey,
98
+ payload: {
99
+ body, fingerprint, headerKey,
100
+ external_trace_id: incomingTp?.traceId ?? null,
101
+ external_request_id: externalReqId ?? null,
102
+ },
103
+ });
104
+ // Persist `external_trace_id` on the trigger payload so the
105
+ // dispatcher can copy it onto the `runs` row when it
106
+ // creates one. We can't write to `runs` here (no run row
107
+ // yet), but the payload carries the value.
108
+ opts.log('info', `[api/runs] accepted trigger_event_id=${result.id} ` +
109
+ `${incomingTp ? `external_trace_id=${incomingTp.traceId} ` : ''}` +
110
+ `${externalReqId ? `external_request_id=${externalReqId}` : ''}`);
111
+ res.status(202).json({
112
+ accepted: true,
113
+ duplicate: !result.inserted,
114
+ trigger_event_id: result.id,
115
+ idempotency_key: idempotencyKey,
116
+ run_id: ctx.runId,
117
+ trace_id: ctx.traceId,
118
+ external_trace_id: incomingTp?.traceId ?? null,
119
+ });
120
+ }
121
+ catch (e) {
122
+ opts.log('error', `[api/runs] insert failed: ${e instanceof Error ? e.message : String(e)}`);
123
+ res.status(500).json({ error: 'internal_error' });
124
+ }
125
+ });
126
+ // Quiet unused-warning on the db handle; future Slice 8 uses it
127
+ // to back-fill the `runs.external_trace_id` column on creation.
128
+ void bootstrap_1.getCurrentDaemonDb;
129
+ });
130
+ return { path: PATH };
131
+ }
@@ -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)({