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.
- package/README.md +88 -1
- package/dist/cli/v4/aidenCLI.js +35 -4
- package/dist/cli/v4/chatSession.js +34 -9
- package/dist/cli/v4/commands/daemon.js +47 -2
- package/dist/cli/v4/commands/daemonDoctor.js +212 -0
- package/dist/cli/v4/commands/daemonStatus.js +1 -1
- package/dist/cli/v4/commands/help.js +2 -0
- package/dist/cli/v4/commands/hooks.js +428 -0
- package/dist/cli/v4/commands/index.js +5 -1
- package/dist/cli/v4/commands/mcp.js +89 -1
- package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
- package/dist/cli/v4/commands/memory.js +702 -0
- package/dist/cli/v4/commands/recovery.js +1 -1
- package/dist/cli/v4/commands/skin.js +7 -0
- package/dist/cli/v4/commands/theme.js +217 -0
- package/dist/cli/v4/commands/trigger.js +1 -1
- package/dist/cli/v4/design/tokens.js +52 -4
- package/dist/cli/v4/display.js +39 -26
- package/dist/cli/v4/replyRenderer.js +6 -5
- package/dist/cli/v4/skinEngine.js +67 -0
- package/dist/core/v4/aidenAgent.js +45 -2
- package/dist/core/v4/daemon/api/runs.js +131 -0
- package/dist/core/v4/daemon/bootstrap.js +368 -13
- package/dist/core/v4/daemon/db/migrations.js +169 -0
- package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
- package/dist/core/v4/daemon/incarnationStore.js +47 -0
- package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
- package/dist/core/v4/daemon/runs/reclaim.js +88 -0
- package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
- package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
- package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
- package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
- package/dist/core/v4/daemon/spans/spanStore.js +113 -0
- package/dist/core/v4/daemon/triggerBus.js +50 -19
- package/dist/core/v4/hooks/auditQuery.js +67 -0
- package/dist/core/v4/hooks/dispatcher.js +286 -0
- package/dist/core/v4/hooks/index.js +46 -0
- package/dist/core/v4/hooks/lifecycle.js +27 -0
- package/dist/core/v4/hooks/manifest.js +142 -0
- package/dist/core/v4/hooks/registry.js +149 -0
- package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
- package/dist/core/v4/hooks/toolHookGate.js +76 -0
- package/dist/core/v4/hooks/trust.js +14 -0
- package/dist/core/v4/identity/contextManager.js +83 -0
- package/dist/core/v4/identity/daemonId.js +85 -0
- package/dist/core/v4/identity/enforcement.js +103 -0
- package/dist/core/v4/identity/executionContext.js +153 -0
- package/dist/core/v4/identity/hookExecution.js +62 -0
- package/dist/core/v4/identity/httpContext.js +68 -0
- package/dist/core/v4/identity/ids.js +185 -0
- package/dist/core/v4/identity/index.js +60 -0
- package/dist/core/v4/identity/subprocessContext.js +98 -0
- package/dist/core/v4/identity/traceparent.js +114 -0
- package/dist/core/v4/logger/index.js +3 -1
- package/dist/core/v4/logger/logger.js +28 -1
- package/dist/core/v4/logger/redact.js +149 -0
- package/dist/core/v4/logger/sinks/fileSink.js +13 -0
- package/dist/core/v4/logger/sinks/stdSink.js +19 -1
- package/dist/core/v4/mcp/install/backup.js +78 -0
- package/dist/core/v4/mcp/install/clientPaths.js +90 -0
- package/dist/core/v4/mcp/install/clients.js +203 -0
- package/dist/core/v4/mcp/install/healthCheck.js +83 -0
- package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
- package/dist/core/v4/mcp/install/profiles.js +109 -0
- package/dist/core/v4/mcp/install/wslDetect.js +62 -0
- package/dist/core/v4/memory/namespaceRegistry.js +117 -0
- package/dist/core/v4/memory/projectRoot.js +76 -0
- package/dist/core/v4/memory/reviewer/index.js +162 -0
- package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
- package/dist/core/v4/memory/reviewer/prompt.js +105 -0
- package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
- package/dist/core/v4/memoryManager.js +57 -10
- package/dist/core/v4/paths.js +2 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
- package/dist/core/v4/theme/bundledThemes.js +106 -0
- package/dist/core/v4/theme/themeLoader.js +160 -0
- package/dist/core/v4/theme/themeRegistry.js +97 -0
- package/dist/core/v4/theme/themeWatcher.js +95 -0
- package/dist/core/v4/toolRegistry.js +71 -8
- package/dist/moat/approvalEngine.js +4 -0
- package/dist/moat/memoryGuard.js +8 -1
- package/dist/providers/v4/anthropicAdapter.js +10 -4
- package/dist/tools/v4/backends/local.js +19 -2
- package/dist/tools/v4/sessions/recallSession.js +6 -1
- package/package.json +3 -1
- package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
- package/themes/default.yaml +52 -0
- package/themes/dracula.yaml +32 -0
- package/themes/light.yaml +32 -0
- package/themes/monochrome.yaml +31 -0
- package/themes/tokyo-night.yaml +32 -0
- package/dist/core/pluginSystem.js +0 -121
- 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
|
-
|
|
237
|
+
daemonLogger.error(msg);
|
|
114
238
|
else if (level === 'warn')
|
|
115
|
-
|
|
239
|
+
daemonLogger.warn(msg);
|
|
116
240
|
else
|
|
117
|
-
|
|
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 =
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
984
|
+
fallbackLogger.error(msg);
|
|
630
985
|
else if (level === 'warn')
|
|
631
|
-
|
|
986
|
+
fallbackLogger.warn(msg);
|
|
632
987
|
else
|
|
633
|
-
|
|
988
|
+
fallbackLogger.info(msg);
|
|
634
989
|
});
|
|
635
990
|
try {
|
|
636
991
|
const realRunner = (0, dispatcher_1.createRealAgentRunner)({
|