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.
- package/README.md +88 -1
- package/dist/cli/v4/aidenCLI.js +37 -6
- package/dist/cli/v4/chatSession.js +53 -13
- package/dist/cli/v4/commands/daemon.js +53 -3
- package/dist/cli/v4/commands/daemonDoctor.js +212 -0
- package/dist/cli/v4/commands/daemonStatus.js +45 -26
- package/dist/cli/v4/commands/help.js +5 -0
- package/dist/cli/v4/commands/hooks.js +466 -0
- package/dist/cli/v4/commands/hooksSlash.js +33 -0
- package/dist/cli/v4/commands/index.js +13 -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 +707 -0
- package/dist/cli/v4/commands/memorySlash.js +38 -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/cli/v4/ui/progressBar.js +179 -0
- package/dist/cli/v4/util/closestAction.js +48 -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/core/v4/update/depWarningFilter.js +76 -0
- package/dist/core/v4/update/executeInstall.js +41 -35
- package/dist/core/v4/update/platformInstructions.js +128 -0
- 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
|
@@ -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)({
|
|
@@ -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) {
|