aiden-runtime 4.9.0 → 4.9.2
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 +1 -1
- package/dist/cli/v4/aidenCLI.js +2 -2
- package/dist/cli/v4/aidenPrompt.js +12 -0
- package/dist/cli/v4/chatSession.js +43 -17
- package/dist/cli/v4/commands/channel.js +4 -6
- package/dist/cli/v4/commands/cron.js +6 -1
- package/dist/cli/v4/commands/daemon.js +6 -1
- package/dist/cli/v4/commands/daemonDoctor.js +6 -6
- package/dist/cli/v4/commands/daemonStatus.js +46 -27
- package/dist/cli/v4/commands/help.js +3 -0
- package/dist/cli/v4/commands/hooks.js +39 -1
- package/dist/cli/v4/commands/hooksSlash.js +33 -0
- package/dist/cli/v4/commands/index.js +9 -1
- package/dist/cli/v4/commands/mcp.js +2 -2
- package/dist/cli/v4/commands/memory.js +6 -1
- package/dist/cli/v4/commands/memorySlash.js +38 -0
- package/dist/cli/v4/commands/plugins.js +4 -6
- package/dist/cli/v4/commands/trigger.js +18 -18
- package/dist/cli/v4/confirmPrompt.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/daemon/db/migrations.js +398 -398
- package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +10 -10
- package/dist/core/v4/daemon/incarnationStore.js +9 -9
- package/dist/core/v4/daemon/runs/attemptStore.js +8 -8
- package/dist/core/v4/daemon/runs/reclaim.js +12 -12
- package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +19 -19
- package/dist/core/v4/daemon/spans/spanStore.js +14 -14
- package/dist/core/v4/daemon/triggerBus.js +61 -61
- package/dist/core/v4/hooks/auditQuery.js +11 -11
- package/dist/core/v4/hooks/dispatcher.js +13 -13
- package/dist/core/v4/hooks/registry.js +8 -8
- package/dist/core/v4/mcp/transport.js +9 -9
- package/dist/core/v4/update/depWarningFilter.js +76 -0
- package/dist/core/v4/update/executeInstall.js +70 -53
- package/dist/core/v4/update/platformInstructions.js +128 -0
- package/dist/core/v4/update/recoveryScript.js +70 -0
- package/dist/core/v4/util/spawnCommand.js +151 -0
- package/package.json +1 -1
- package/themes/default.yaml +52 -52
- package/themes/dracula.yaml +32 -32
- package/themes/light.yaml +32 -32
- package/themes/monochrome.yaml +31 -31
- package/themes/tokyo-night.yaml +32 -32
|
@@ -74,8 +74,8 @@ function acquire(db, opts) {
|
|
|
74
74
|
const nowMs = (opts.now ?? Date.now)();
|
|
75
75
|
const nowIso = new Date(nowMs).toISOString();
|
|
76
76
|
const expires = opts.ttlMs ? new Date(nowMs + opts.ttlMs).toISOString() : null;
|
|
77
|
-
const result = db.prepare(`INSERT OR IGNORE INTO run_idempotency_keys
|
|
78
|
-
(namespace, key, fingerprint, status, created_at, expires_at)
|
|
77
|
+
const result = db.prepare(`INSERT OR IGNORE INTO run_idempotency_keys
|
|
78
|
+
(namespace, key, fingerprint, status, created_at, expires_at)
|
|
79
79
|
VALUES (?, ?, ?, 'accepted', ?, ?)`).run(opts.namespace, opts.key, opts.fingerprint, nowIso, expires);
|
|
80
80
|
if (result.changes > 0) {
|
|
81
81
|
const row = readRow(db, opts.namespace, opts.key);
|
|
@@ -95,10 +95,10 @@ function acquire(db, opts) {
|
|
|
95
95
|
}
|
|
96
96
|
/** Back-fill the run/trigger/span FKs once those rows exist. */
|
|
97
97
|
function link(db, opts) {
|
|
98
|
-
db.prepare(`UPDATE run_idempotency_keys
|
|
99
|
-
SET run_id = COALESCE(?, run_id),
|
|
100
|
-
trigger_event_id = COALESCE(?, trigger_event_id),
|
|
101
|
-
span_id = COALESCE(?, span_id)
|
|
98
|
+
db.prepare(`UPDATE run_idempotency_keys
|
|
99
|
+
SET run_id = COALESCE(?, run_id),
|
|
100
|
+
trigger_event_id = COALESCE(?, trigger_event_id),
|
|
101
|
+
span_id = COALESCE(?, span_id)
|
|
102
102
|
WHERE namespace = ? AND key = ?`).run(opts.runId ?? null, opts.triggerEventId ?? null, opts.spanId ?? null, opts.namespace, opts.key);
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
@@ -106,15 +106,15 @@ function link(db, opts) {
|
|
|
106
106
|
* status: `'completed'` for success, `'failed'` for terminal failure.
|
|
107
107
|
*/
|
|
108
108
|
function complete(db, opts) {
|
|
109
|
-
db.prepare(`UPDATE run_idempotency_keys
|
|
110
|
-
SET status = ?,
|
|
111
|
-
result_ref = COALESCE(?, result_ref)
|
|
109
|
+
db.prepare(`UPDATE run_idempotency_keys
|
|
110
|
+
SET status = ?,
|
|
111
|
+
result_ref = COALESCE(?, result_ref)
|
|
112
112
|
WHERE namespace = ? AND key = ?`).run(opts.status, opts.resultRef ?? null, opts.namespace, opts.key);
|
|
113
113
|
}
|
|
114
114
|
/** Delete keys whose `expires_at` is in the past. Returns the count. */
|
|
115
115
|
function sweepExpired(db, now) {
|
|
116
116
|
const nowIso = new Date(now ?? Date.now()).toISOString();
|
|
117
|
-
const r = db.prepare(`DELETE FROM run_idempotency_keys
|
|
117
|
+
const r = db.prepare(`DELETE FROM run_idempotency_keys
|
|
118
118
|
WHERE expires_at IS NOT NULL AND expires_at < ?`).run(nowIso);
|
|
119
119
|
return { deleted: Number(r.changes ?? 0) };
|
|
120
120
|
}
|
|
@@ -26,22 +26,22 @@ exports.markEnded = markEnded;
|
|
|
26
26
|
exports.lastForDaemon = lastForDaemon;
|
|
27
27
|
function insertIncarnation(db, opts) {
|
|
28
28
|
const started = opts.startedAt ?? new Date().toISOString();
|
|
29
|
-
db.prepare(`INSERT OR IGNORE INTO daemon_incarnations
|
|
30
|
-
(incarnation_id, daemon_id, pid, started_at, aiden_version, node_version)
|
|
29
|
+
db.prepare(`INSERT OR IGNORE INTO daemon_incarnations
|
|
30
|
+
(incarnation_id, daemon_id, pid, started_at, aiden_version, node_version)
|
|
31
31
|
VALUES (?, ?, ?, ?, ?, ?)`).run(opts.incarnationId, opts.daemonId, opts.pid, started, opts.aidenVersion, opts.nodeVersion);
|
|
32
32
|
}
|
|
33
33
|
function markEnded(db, opts) {
|
|
34
34
|
const ended = opts.endedAt ?? new Date().toISOString();
|
|
35
|
-
db.prepare(`UPDATE daemon_incarnations
|
|
36
|
-
SET ended_at = COALESCE(ended_at, ?),
|
|
37
|
-
exit_reason = COALESCE(exit_reason, ?),
|
|
38
|
-
exit_code = COALESCE(exit_code, ?)
|
|
35
|
+
db.prepare(`UPDATE daemon_incarnations
|
|
36
|
+
SET ended_at = COALESCE(ended_at, ?),
|
|
37
|
+
exit_reason = COALESCE(exit_reason, ?),
|
|
38
|
+
exit_code = COALESCE(exit_code, ?)
|
|
39
39
|
WHERE incarnation_id = ?`).run(ended, opts.exitReason, opts.exitCode, opts.incarnationId);
|
|
40
40
|
}
|
|
41
41
|
function lastForDaemon(db, daemonId) {
|
|
42
|
-
const r = db.prepare(`SELECT * FROM daemon_incarnations
|
|
43
|
-
WHERE daemon_id = ?
|
|
44
|
-
ORDER BY started_at DESC
|
|
42
|
+
const r = db.prepare(`SELECT * FROM daemon_incarnations
|
|
43
|
+
WHERE daemon_id = ?
|
|
44
|
+
ORDER BY started_at DESC
|
|
45
45
|
LIMIT 1`).get(daemonId);
|
|
46
46
|
return r ?? null;
|
|
47
47
|
}
|
|
@@ -33,8 +33,8 @@ function createAttempt(db, opts) {
|
|
|
33
33
|
const tx = db.transaction(() => {
|
|
34
34
|
const row = db.prepare(`SELECT COALESCE(MAX(attempt_number), 0) AS n FROM run_attempts WHERE run_id = ?`).get(opts.runId);
|
|
35
35
|
const nextNumber = row.n + 1;
|
|
36
|
-
db.prepare(`INSERT INTO run_attempts
|
|
37
|
-
(attempt_id, run_id, attempt_number, incarnation_id, started_at, status)
|
|
36
|
+
db.prepare(`INSERT INTO run_attempts
|
|
37
|
+
(attempt_id, run_id, attempt_number, incarnation_id, started_at, status)
|
|
38
38
|
VALUES (?, ?, ?, ?, ?, 'running')`).run(attemptId, opts.runId, nextNumber, opts.incarnationId, startedAt);
|
|
39
39
|
});
|
|
40
40
|
tx();
|
|
@@ -48,12 +48,12 @@ function createAttempt(db, opts) {
|
|
|
48
48
|
*/
|
|
49
49
|
function completeAttempt(db, opts) {
|
|
50
50
|
const endedAt = opts.endedAt ?? new Date().toISOString();
|
|
51
|
-
db.prepare(`UPDATE run_attempts
|
|
52
|
-
SET status = ?,
|
|
53
|
-
ended_at = COALESCE(ended_at, ?),
|
|
54
|
-
finish_reason = COALESCE(finish_reason, ?),
|
|
55
|
-
error_class = COALESCE(error_class, ?),
|
|
56
|
-
error_message = COALESCE(error_message, ?)
|
|
51
|
+
db.prepare(`UPDATE run_attempts
|
|
52
|
+
SET status = ?,
|
|
53
|
+
ended_at = COALESCE(ended_at, ?),
|
|
54
|
+
finish_reason = COALESCE(finish_reason, ?),
|
|
55
|
+
error_class = COALESCE(error_class, ?),
|
|
56
|
+
error_message = COALESCE(error_message, ?)
|
|
57
57
|
WHERE attempt_id = ?`).run(opts.status, endedAt, opts.finishReason ?? null, opts.errorClass ?? null, opts.errorMessage ?? null, opts.attemptId);
|
|
58
58
|
}
|
|
59
59
|
/** List all attempts for a run in attempt-number order. */
|
|
@@ -64,21 +64,21 @@ function reclaimStuckRuns(db, opts) {
|
|
|
64
64
|
// table — unlikely, but cheap to avoid).
|
|
65
65
|
let updateResult;
|
|
66
66
|
if (opts.instanceId !== undefined) {
|
|
67
|
-
updateResult = db.prepare(`UPDATE runs
|
|
68
|
-
SET status = 'interrupted',
|
|
69
|
-
finish_reason = 'daemon_crashed',
|
|
70
|
-
completed_at = ?,
|
|
71
|
-
resume_pending = 1,
|
|
72
|
-
resume_reason = 'daemon_crashed'
|
|
67
|
+
updateResult = db.prepare(`UPDATE runs
|
|
68
|
+
SET status = 'interrupted',
|
|
69
|
+
finish_reason = 'daemon_crashed',
|
|
70
|
+
completed_at = ?,
|
|
71
|
+
resume_pending = 1,
|
|
72
|
+
resume_reason = 'daemon_crashed'
|
|
73
73
|
WHERE status = 'running' AND instance_id = ?`).run(now, opts.instanceId);
|
|
74
74
|
}
|
|
75
75
|
else {
|
|
76
|
-
updateResult = db.prepare(`UPDATE runs
|
|
77
|
-
SET status = 'interrupted',
|
|
78
|
-
finish_reason = 'daemon_crashed',
|
|
79
|
-
completed_at = ?,
|
|
80
|
-
resume_pending = 1,
|
|
81
|
-
resume_reason = 'daemon_crashed'
|
|
76
|
+
updateResult = db.prepare(`UPDATE runs
|
|
77
|
+
SET status = 'interrupted',
|
|
78
|
+
finish_reason = 'daemon_crashed',
|
|
79
|
+
completed_at = ?,
|
|
80
|
+
resume_pending = 1,
|
|
81
|
+
resume_reason = 'daemon_crashed'
|
|
82
82
|
WHERE status = 'running' AND instance_id != ?`).run(now, opts.currentInstanceId);
|
|
83
83
|
}
|
|
84
84
|
return {
|
|
@@ -30,37 +30,37 @@ function sweepStuckAttempts(db, opts) {
|
|
|
30
30
|
const cutoffIso = new Date(now - thresholdMs).toISOString();
|
|
31
31
|
const endedAtIso = new Date(now).toISOString();
|
|
32
32
|
// ── attempts ────────────────────────────────────────────────────────────
|
|
33
|
-
const attemptRows = db.prepare(`SELECT attempt_id FROM run_attempts
|
|
34
|
-
WHERE status = 'running'
|
|
35
|
-
AND incarnation_id != ?
|
|
33
|
+
const attemptRows = db.prepare(`SELECT attempt_id FROM run_attempts
|
|
34
|
+
WHERE status = 'running'
|
|
35
|
+
AND incarnation_id != ?
|
|
36
36
|
AND started_at < ?`).all(opts.currentIncarnationId, cutoffIso);
|
|
37
37
|
const attemptIds = attemptRows.map((r) => r.attempt_id);
|
|
38
38
|
if (attemptIds.length > 0) {
|
|
39
|
-
db.prepare(`UPDATE run_attempts
|
|
40
|
-
SET status = 'crashed',
|
|
41
|
-
ended_at = COALESCE(ended_at, ?),
|
|
42
|
-
finish_reason = COALESCE(finish_reason, 'stuck_attempt_swept')
|
|
43
|
-
WHERE status = 'running'
|
|
44
|
-
AND incarnation_id != ?
|
|
39
|
+
db.prepare(`UPDATE run_attempts
|
|
40
|
+
SET status = 'crashed',
|
|
41
|
+
ended_at = COALESCE(ended_at, ?),
|
|
42
|
+
finish_reason = COALESCE(finish_reason, 'stuck_attempt_swept')
|
|
43
|
+
WHERE status = 'running'
|
|
44
|
+
AND incarnation_id != ?
|
|
45
45
|
AND started_at < ?`).run(endedAtIso, opts.currentIncarnationId, cutoffIso);
|
|
46
46
|
}
|
|
47
47
|
// ── spans ──────────────────────────────────────────────────────────────
|
|
48
48
|
// Open spans (status NULL = in-flight) from a non-current incarnation.
|
|
49
49
|
// We don't apply the threshold here — any open span owned by a dead
|
|
50
50
|
// incarnation is by definition stuck; the parent process is gone.
|
|
51
|
-
const spanRows = db.prepare(`SELECT span_id FROM spans
|
|
52
|
-
WHERE status IS NULL
|
|
53
|
-
AND ended_at IS NULL
|
|
51
|
+
const spanRows = db.prepare(`SELECT span_id FROM spans
|
|
52
|
+
WHERE status IS NULL
|
|
53
|
+
AND ended_at IS NULL
|
|
54
54
|
AND incarnation_id != ?`).all(opts.currentIncarnationId);
|
|
55
55
|
const spanIds = spanRows.map((r) => r.span_id);
|
|
56
56
|
if (spanIds.length > 0) {
|
|
57
|
-
db.prepare(`UPDATE spans
|
|
58
|
-
SET status = 'cancelled',
|
|
59
|
-
ended_at = COALESCE(ended_at, ?),
|
|
60
|
-
error_class = COALESCE(error_class, 'OrphanedSpan'),
|
|
61
|
-
error_message = COALESCE(error_message, 'incarnation died with span open')
|
|
62
|
-
WHERE status IS NULL
|
|
63
|
-
AND ended_at IS NULL
|
|
57
|
+
db.prepare(`UPDATE spans
|
|
58
|
+
SET status = 'cancelled',
|
|
59
|
+
ended_at = COALESCE(ended_at, ?),
|
|
60
|
+
error_class = COALESCE(error_class, 'OrphanedSpan'),
|
|
61
|
+
error_message = COALESCE(error_message, 'incarnation died with span open')
|
|
62
|
+
WHERE status IS NULL
|
|
63
|
+
AND ended_at IS NULL
|
|
64
64
|
AND incarnation_id != ?`).run(endedAtIso, opts.currentIncarnationId);
|
|
65
65
|
}
|
|
66
66
|
return {
|
|
@@ -48,9 +48,9 @@ function safeParse(s) {
|
|
|
48
48
|
*/
|
|
49
49
|
function openSpan(db, opts) {
|
|
50
50
|
const startedAt = opts.startedAt ?? new Date().toISOString();
|
|
51
|
-
db.prepare(`INSERT INTO spans
|
|
52
|
-
(span_id, trace_id, parent_span_id, run_id, attempt_id,
|
|
53
|
-
incarnation_id, kind, name, started_at, attrs_json)
|
|
51
|
+
db.prepare(`INSERT INTO spans
|
|
52
|
+
(span_id, trace_id, parent_span_id, run_id, attempt_id,
|
|
53
|
+
incarnation_id, kind, name, started_at, attrs_json)
|
|
54
54
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(opts.ctx.spanId, opts.ctx.traceId, opts.ctx.parentSpanId ?? null, opts.runId ?? null, opts.attemptId ?? null, opts.ctx.incarnationId, opts.kind, opts.name, startedAt, safeStringify(opts.attrs));
|
|
55
55
|
return opts.ctx.spanId;
|
|
56
56
|
}
|
|
@@ -66,20 +66,20 @@ function closeSpan(db, opts) {
|
|
|
66
66
|
const cur = db.prepare(`SELECT attrs_json FROM spans WHERE span_id = ?`)
|
|
67
67
|
.get(opts.spanId);
|
|
68
68
|
const merged = { ...safeParse(cur?.attrs_json ?? null), ...opts.attrsPatch };
|
|
69
|
-
db.prepare(`UPDATE spans
|
|
70
|
-
SET status = COALESCE(status, ?),
|
|
71
|
-
ended_at = COALESCE(ended_at, ?),
|
|
72
|
-
error_class = COALESCE(error_class, ?),
|
|
73
|
-
error_message = COALESCE(error_message, ?),
|
|
74
|
-
attrs_json = ?
|
|
69
|
+
db.prepare(`UPDATE spans
|
|
70
|
+
SET status = COALESCE(status, ?),
|
|
71
|
+
ended_at = COALESCE(ended_at, ?),
|
|
72
|
+
error_class = COALESCE(error_class, ?),
|
|
73
|
+
error_message = COALESCE(error_message, ?),
|
|
74
|
+
attrs_json = ?
|
|
75
75
|
WHERE span_id = ?`).run(opts.status, endedAt, opts.errorClass ?? null, opts.errorMessage ?? null, safeStringify(merged), opts.spanId);
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
-
db.prepare(`UPDATE spans
|
|
79
|
-
SET status = COALESCE(status, ?),
|
|
80
|
-
ended_at = COALESCE(ended_at, ?),
|
|
81
|
-
error_class = COALESCE(error_class, ?),
|
|
82
|
-
error_message = COALESCE(error_message, ?)
|
|
78
|
+
db.prepare(`UPDATE spans
|
|
79
|
+
SET status = COALESCE(status, ?),
|
|
80
|
+
ended_at = COALESCE(ended_at, ?),
|
|
81
|
+
error_class = COALESCE(error_class, ?),
|
|
82
|
+
error_message = COALESCE(error_message, ?)
|
|
83
83
|
WHERE span_id = ?`).run(opts.status, endedAt, opts.errorClass ?? null, opts.errorMessage ?? null, opts.spanId);
|
|
84
84
|
}
|
|
85
85
|
/** Single-span lookup. */
|
|
@@ -79,9 +79,9 @@ function createTriggerBus(opts) {
|
|
|
79
79
|
let outcome = null;
|
|
80
80
|
const tx = db.transaction(() => {
|
|
81
81
|
const result = db
|
|
82
|
-
.prepare(`INSERT OR IGNORE INTO trigger_events
|
|
83
|
-
(source, source_key, idempotency_key, payload_json,
|
|
84
|
-
status, attempts, created_at, updated_at)
|
|
82
|
+
.prepare(`INSERT OR IGNORE INTO trigger_events
|
|
83
|
+
(source, source_key, idempotency_key, payload_json,
|
|
84
|
+
status, attempts, created_at, updated_at)
|
|
85
85
|
VALUES (?, ?, ?, ?, 'pending', 0, ?, ?)`)
|
|
86
86
|
.run(ev.source, ev.sourceKey, ev.idempotencyKey ?? null, payloadJson, now, now);
|
|
87
87
|
if (result.changes > 0) {
|
|
@@ -93,9 +93,9 @@ function createTriggerBus(opts) {
|
|
|
93
93
|
const ns = `trigger:${ev.source}`;
|
|
94
94
|
const key = `${ev.sourceKey}::${ev.idempotencyKey}`;
|
|
95
95
|
const fp = ev.idempotencyKey;
|
|
96
|
-
db.prepare(`INSERT OR IGNORE INTO run_idempotency_keys
|
|
97
|
-
(namespace, key, fingerprint, trigger_event_id,
|
|
98
|
-
status, created_at)
|
|
96
|
+
db.prepare(`INSERT OR IGNORE INTO run_idempotency_keys
|
|
97
|
+
(namespace, key, fingerprint, trigger_event_id,
|
|
98
|
+
status, created_at)
|
|
99
99
|
VALUES (?, ?, ?, ?, 'accepted', ?)`).run(ns, key, fp, newId, new Date(now).toISOString());
|
|
100
100
|
}
|
|
101
101
|
outcome = { id: newId, inserted: true };
|
|
@@ -135,13 +135,13 @@ function createTriggerBus(opts) {
|
|
|
135
135
|
// claim_expires_at is still in the future (re-set by
|
|
136
136
|
// markFailed with cooldownMs to delay re-claim).
|
|
137
137
|
const sql = opts2.source
|
|
138
|
-
? `SELECT id FROM trigger_events
|
|
139
|
-
WHERE status = 'pending' AND source = ?
|
|
140
|
-
AND (claim_expires_at IS NULL OR claim_expires_at <= ?)
|
|
138
|
+
? `SELECT id FROM trigger_events
|
|
139
|
+
WHERE status = 'pending' AND source = ?
|
|
140
|
+
AND (claim_expires_at IS NULL OR claim_expires_at <= ?)
|
|
141
141
|
ORDER BY created_at LIMIT 1`
|
|
142
|
-
: `SELECT id FROM trigger_events
|
|
143
|
-
WHERE status = 'pending'
|
|
144
|
-
AND (claim_expires_at IS NULL OR claim_expires_at <= ?)
|
|
142
|
+
: `SELECT id FROM trigger_events
|
|
143
|
+
WHERE status = 'pending'
|
|
144
|
+
AND (claim_expires_at IS NULL OR claim_expires_at <= ?)
|
|
145
145
|
ORDER BY created_at LIMIT 1`;
|
|
146
146
|
const candidate = (opts2.source
|
|
147
147
|
? db.prepare(sql).get(opts2.source, now)
|
|
@@ -149,12 +149,12 @@ function createTriggerBus(opts) {
|
|
|
149
149
|
if (!candidate)
|
|
150
150
|
return null;
|
|
151
151
|
const upd = db
|
|
152
|
-
.prepare(`UPDATE trigger_events
|
|
153
|
-
SET status = 'claimed',
|
|
154
|
-
claim_owner = ?,
|
|
155
|
-
claim_expires_at = ?,
|
|
156
|
-
updated_at = ?,
|
|
157
|
-
attempts = attempts + 1
|
|
152
|
+
.prepare(`UPDATE trigger_events
|
|
153
|
+
SET status = 'claimed',
|
|
154
|
+
claim_owner = ?,
|
|
155
|
+
claim_expires_at = ?,
|
|
156
|
+
updated_at = ?,
|
|
157
|
+
attempts = attempts + 1
|
|
158
158
|
WHERE id = ? AND status = 'pending'`)
|
|
159
159
|
.run(opts2.ownerId, expires, now, candidate.id);
|
|
160
160
|
if (upd.changes === 0)
|
|
@@ -175,9 +175,9 @@ function createTriggerBus(opts) {
|
|
|
175
175
|
return false;
|
|
176
176
|
const now = Date.now();
|
|
177
177
|
const upd = db
|
|
178
|
-
.prepare(`UPDATE trigger_events
|
|
179
|
-
SET claim_expires_at = ?,
|
|
180
|
-
updated_at = ?
|
|
178
|
+
.prepare(`UPDATE trigger_events
|
|
179
|
+
SET claim_expires_at = ?,
|
|
180
|
+
updated_at = ?
|
|
181
181
|
WHERE id = ? AND status = 'claimed'`)
|
|
182
182
|
.run(now + extendMs, now, eventId);
|
|
183
183
|
return upd.changes > 0;
|
|
@@ -186,11 +186,11 @@ function createTriggerBus(opts) {
|
|
|
186
186
|
if (activeClaims.get(eventId) !== claimToken)
|
|
187
187
|
return;
|
|
188
188
|
const now = Date.now();
|
|
189
|
-
db.prepare(`UPDATE trigger_events
|
|
190
|
-
SET status = 'pending',
|
|
191
|
-
claim_owner = NULL,
|
|
192
|
-
claim_expires_at = NULL,
|
|
193
|
-
updated_at = ?
|
|
189
|
+
db.prepare(`UPDATE trigger_events
|
|
190
|
+
SET status = 'pending',
|
|
191
|
+
claim_owner = NULL,
|
|
192
|
+
claim_expires_at = NULL,
|
|
193
|
+
updated_at = ?
|
|
194
194
|
WHERE id = ? AND status = 'claimed'`).run(now, eventId);
|
|
195
195
|
activeClaims.delete(eventId);
|
|
196
196
|
},
|
|
@@ -198,13 +198,13 @@ function createTriggerBus(opts) {
|
|
|
198
198
|
if (activeClaims.get(eventId) !== claimToken)
|
|
199
199
|
return;
|
|
200
200
|
const now = Date.now();
|
|
201
|
-
db.prepare(`UPDATE trigger_events
|
|
202
|
-
SET status = 'done',
|
|
203
|
-
claim_owner = NULL,
|
|
204
|
-
claim_expires_at = NULL,
|
|
205
|
-
updated_at = ?,
|
|
206
|
-
completed_at = ?,
|
|
207
|
-
run_id = COALESCE(?, run_id)
|
|
201
|
+
db.prepare(`UPDATE trigger_events
|
|
202
|
+
SET status = 'done',
|
|
203
|
+
claim_owner = NULL,
|
|
204
|
+
claim_expires_at = NULL,
|
|
205
|
+
updated_at = ?,
|
|
206
|
+
completed_at = ?,
|
|
207
|
+
run_id = COALESCE(?, run_id)
|
|
208
208
|
WHERE id = ? AND status = 'claimed'`).run(now, now, runId ?? null, eventId);
|
|
209
209
|
activeClaims.delete(eventId);
|
|
210
210
|
},
|
|
@@ -232,22 +232,22 @@ function createTriggerBus(opts) {
|
|
|
232
232
|
// attempts was already incremented at claim time. Move to
|
|
233
233
|
// dead_letter when the count hits max, else return to pending.
|
|
234
234
|
if (row.attempts >= max) {
|
|
235
|
-
db.prepare(`UPDATE trigger_events
|
|
236
|
-
SET status = 'dead_letter',
|
|
237
|
-
claim_owner = NULL,
|
|
238
|
-
claim_expires_at = NULL,
|
|
239
|
-
last_error = ?,
|
|
240
|
-
updated_at = ?,
|
|
241
|
-
completed_at = ?
|
|
235
|
+
db.prepare(`UPDATE trigger_events
|
|
236
|
+
SET status = 'dead_letter',
|
|
237
|
+
claim_owner = NULL,
|
|
238
|
+
claim_expires_at = NULL,
|
|
239
|
+
last_error = ?,
|
|
240
|
+
updated_at = ?,
|
|
241
|
+
completed_at = ?
|
|
242
242
|
WHERE id = ?`).run(truncated, now, now, eventId);
|
|
243
243
|
}
|
|
244
244
|
else {
|
|
245
|
-
db.prepare(`UPDATE trigger_events
|
|
246
|
-
SET status = 'pending',
|
|
247
|
-
claim_owner = NULL,
|
|
248
|
-
claim_expires_at = ?,
|
|
249
|
-
last_error = ?,
|
|
250
|
-
updated_at = ?
|
|
245
|
+
db.prepare(`UPDATE trigger_events
|
|
246
|
+
SET status = 'pending',
|
|
247
|
+
claim_owner = NULL,
|
|
248
|
+
claim_expires_at = ?,
|
|
249
|
+
last_error = ?,
|
|
250
|
+
updated_at = ?
|
|
251
251
|
WHERE id = ?`).run(cooldownUntil, truncated, now, eventId);
|
|
252
252
|
}
|
|
253
253
|
});
|
|
@@ -257,27 +257,27 @@ function createTriggerBus(opts) {
|
|
|
257
257
|
reclaimExpired(now) {
|
|
258
258
|
const cutoff = now ?? Date.now();
|
|
259
259
|
const upd = db
|
|
260
|
-
.prepare(`UPDATE trigger_events
|
|
261
|
-
SET status = 'pending',
|
|
262
|
-
claim_owner = NULL,
|
|
263
|
-
claim_expires_at = NULL,
|
|
264
|
-
last_error = COALESCE(last_error, 'claim lease expired'),
|
|
265
|
-
updated_at = ?
|
|
266
|
-
WHERE status = 'claimed'
|
|
267
|
-
AND claim_expires_at IS NOT NULL
|
|
260
|
+
.prepare(`UPDATE trigger_events
|
|
261
|
+
SET status = 'pending',
|
|
262
|
+
claim_owner = NULL,
|
|
263
|
+
claim_expires_at = NULL,
|
|
264
|
+
last_error = COALESCE(last_error, 'claim lease expired'),
|
|
265
|
+
updated_at = ?
|
|
266
|
+
WHERE status = 'claimed'
|
|
267
|
+
AND claim_expires_at IS NOT NULL
|
|
268
268
|
AND claim_expires_at < ?`)
|
|
269
269
|
.run(cutoff, cutoff);
|
|
270
270
|
return { reclaimed: upd.changes };
|
|
271
271
|
},
|
|
272
272
|
deadLetter(eventId, reason) {
|
|
273
273
|
const now = Date.now();
|
|
274
|
-
db.prepare(`UPDATE trigger_events
|
|
275
|
-
SET status = 'dead_letter',
|
|
276
|
-
claim_owner = NULL,
|
|
277
|
-
claim_expires_at = NULL,
|
|
278
|
-
last_error = ?,
|
|
279
|
-
updated_at = ?,
|
|
280
|
-
completed_at = ?
|
|
274
|
+
db.prepare(`UPDATE trigger_events
|
|
275
|
+
SET status = 'dead_letter',
|
|
276
|
+
claim_owner = NULL,
|
|
277
|
+
claim_expires_at = NULL,
|
|
278
|
+
last_error = ?,
|
|
279
|
+
updated_at = ?,
|
|
280
|
+
completed_at = ?
|
|
281
281
|
WHERE id = ?`).run(reason.length > 1024 ? reason.slice(0, 1024) + '…' : reason, now, now, eventId);
|
|
282
282
|
activeClaims.delete(eventId);
|
|
283
283
|
},
|
|
@@ -23,15 +23,15 @@ function queryHookExecutions(db, q = {}) {
|
|
|
23
23
|
params.push(q.since);
|
|
24
24
|
}
|
|
25
25
|
const limit = Math.min(Math.max(q.limit ?? 50, 1), 1000);
|
|
26
|
-
const sql = `
|
|
27
|
-
SELECT e.hook_execution_id, e.hook_id, h.name AS hook_name,
|
|
28
|
-
e.subscription_id, e.event, e.status, e.decision,
|
|
29
|
-
e.elapsed_ms, e.exit_code, e.error_kind, e.error_message,
|
|
30
|
-
e.started_at, e.finished_at, e.run_id, e.trace_id
|
|
31
|
-
FROM hook_executions e
|
|
32
|
-
LEFT JOIN hooks h ON h.hook_id = e.hook_id
|
|
33
|
-
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
34
|
-
ORDER BY e.started_at DESC
|
|
26
|
+
const sql = `
|
|
27
|
+
SELECT e.hook_execution_id, e.hook_id, h.name AS hook_name,
|
|
28
|
+
e.subscription_id, e.event, e.status, e.decision,
|
|
29
|
+
e.elapsed_ms, e.exit_code, e.error_kind, e.error_message,
|
|
30
|
+
e.started_at, e.finished_at, e.run_id, e.trace_id
|
|
31
|
+
FROM hook_executions e
|
|
32
|
+
LEFT JOIN hooks h ON h.hook_id = e.hook_id
|
|
33
|
+
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
34
|
+
ORDER BY e.started_at DESC
|
|
35
35
|
LIMIT ?`;
|
|
36
36
|
params.push(limit);
|
|
37
37
|
return db.prepare(sql).all(...params);
|
|
@@ -41,7 +41,7 @@ function failureRates(db, lookbackN = 100) {
|
|
|
41
41
|
const ids = db.prepare(`SELECT hook_id, name FROM hooks`).all();
|
|
42
42
|
const rows = [];
|
|
43
43
|
for (const h of ids) {
|
|
44
|
-
const recent = db.prepare(`SELECT status FROM hook_executions WHERE hook_id = ?
|
|
44
|
+
const recent = db.prepare(`SELECT status FROM hook_executions WHERE hook_id = ?
|
|
45
45
|
ORDER BY started_at DESC LIMIT ?`).all(h.hook_id, lookbackN);
|
|
46
46
|
if (recent.length === 0)
|
|
47
47
|
continue;
|
|
@@ -58,7 +58,7 @@ function failureRates(db, lookbackN = 100) {
|
|
|
58
58
|
}
|
|
59
59
|
/** Count rows matching a status set over the recent window. */
|
|
60
60
|
function countByStatus(db, sinceIso) {
|
|
61
|
-
const rows = db.prepare(`SELECT status, COUNT(*) AS n FROM hook_executions
|
|
61
|
+
const rows = db.prepare(`SELECT status, COUNT(*) AS n FROM hook_executions
|
|
62
62
|
WHERE started_at >= ? GROUP BY status`).all(sinceIso);
|
|
63
63
|
const out = {};
|
|
64
64
|
for (const r of rows)
|
|
@@ -84,7 +84,7 @@ function setAutoDisableLogger(fn) { _autoDisableLogger = fn; }
|
|
|
84
84
|
* `hook.auto_disabled` log line can cross-reference the failures.
|
|
85
85
|
*/
|
|
86
86
|
function recentExecutionIds(db, hookId, n) {
|
|
87
|
-
const rows = db.prepare(`SELECT hook_execution_id FROM hook_executions WHERE hook_id = ?
|
|
87
|
+
const rows = db.prepare(`SELECT hook_execution_id FROM hook_executions WHERE hook_id = ?
|
|
88
88
|
ORDER BY started_at DESC LIMIT ?`).all(hookId, n);
|
|
89
89
|
return rows.map((r) => r.hook_execution_id);
|
|
90
90
|
}
|
|
@@ -108,7 +108,7 @@ function applyAutoDisablePolicy(db, hookId, subId, status, policy, testMode) {
|
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
// Increment counter for any non-ok outcome.
|
|
111
|
-
db.prepare(`UPDATE hooks SET consecutive_failures = consecutive_failures + 1,
|
|
111
|
+
db.prepare(`UPDATE hooks SET consecutive_failures = consecutive_failures + 1,
|
|
112
112
|
updated_at=? WHERE hook_id=?`)
|
|
113
113
|
.run(new Date().toISOString(), hookId);
|
|
114
114
|
// Immediate revoke when the subscription explicitly opts in.
|
|
@@ -170,12 +170,12 @@ async function readEntrypoint(manifestPath) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
function writeAudit(db, row) {
|
|
173
|
-
db.prepare(`INSERT INTO hook_executions
|
|
174
|
-
(hook_execution_id, hook_id, subscription_id, event,
|
|
175
|
-
run_id, trace_id, span_id, parent_span_id, tool_call_id,
|
|
176
|
-
status, decision, elapsed_ms, exit_code,
|
|
177
|
-
payload_hash, response_hash, stdout_preview, stderr_preview,
|
|
178
|
-
error_kind, error_message, started_at, finished_at)
|
|
173
|
+
db.prepare(`INSERT INTO hook_executions
|
|
174
|
+
(hook_execution_id, hook_id, subscription_id, event,
|
|
175
|
+
run_id, trace_id, span_id, parent_span_id, tool_call_id,
|
|
176
|
+
status, decision, elapsed_ms, exit_code,
|
|
177
|
+
payload_hash, response_hash, stdout_preview, stderr_preview,
|
|
178
|
+
error_kind, error_message, started_at, finished_at)
|
|
179
179
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.hookExecId, row.hookId, row.subscriptionId, row.event, row.ctx.runId ?? null, row.ctx.traceId ?? null, row.ctx.spanId ?? null, row.ctx.parentSpanId ?? null, row.ctx.toolCallId ?? null, row.status, row.decision, row.outcome?.elapsedMs ?? 0, row.outcome?.exitCode ?? null, row.outcome?.payloadHash ?? null, row.outcome?.responseHash ?? null, row.outcome?.stdoutPreview ?? null, row.outcome?.stderrPreview ?? null, row.outcome?.errorKind ?? null, row.outcome?.errorMessage ?? null, row.startedAt, row.finishedAt);
|
|
180
180
|
}
|
|
181
181
|
/**
|
|
@@ -184,11 +184,11 @@ function writeAudit(db, row) {
|
|
|
184
184
|
* `mandatory_policy` and fail-open for everything else.
|
|
185
185
|
*/
|
|
186
186
|
async function dispatchHook(db, event, payload, ctx) {
|
|
187
|
-
const subs = db.prepare(`SELECT s.*, h.manifest_path, h.trust_state, h.enabled AS hook_enabled, h.name AS name
|
|
188
|
-
FROM hook_subscriptions s
|
|
189
|
-
JOIN hooks h ON h.hook_id = s.hook_id
|
|
190
|
-
WHERE s.event = ? AND s.enabled = 1
|
|
191
|
-
AND h.enabled = 1 AND h.trust_state = 'trusted'
|
|
187
|
+
const subs = db.prepare(`SELECT s.*, h.manifest_path, h.trust_state, h.enabled AS hook_enabled, h.name AS name
|
|
188
|
+
FROM hook_subscriptions s
|
|
189
|
+
JOIN hooks h ON h.hook_id = s.hook_id
|
|
190
|
+
WHERE s.event = ? AND s.enabled = 1
|
|
191
|
+
AND h.enabled = 1 AND h.trust_state = 'trusted'
|
|
192
192
|
ORDER BY s.priority DESC, s.subscription_id ASC`).all(event);
|
|
193
193
|
const fired = [];
|
|
194
194
|
let workingPayload = { ...payload };
|
|
@@ -111,8 +111,8 @@ function upsertHook(db, m, codeHash, source) {
|
|
|
111
111
|
hookId = existing.hook_id;
|
|
112
112
|
if (existing.code_hash !== codeHash) {
|
|
113
113
|
drifted = true;
|
|
114
|
-
db.prepare(`UPDATE hooks SET name=?, version=?, source=?, runtime=?, code_hash=?,
|
|
115
|
-
trust_state='drifted', enabled=0, updated_at=?
|
|
114
|
+
db.prepare(`UPDATE hooks SET name=?, version=?, source=?, runtime=?, code_hash=?,
|
|
115
|
+
trust_state='drifted', enabled=0, updated_at=?
|
|
116
116
|
WHERE hook_id = ?`).run(m.name, m.version ?? null, source, m.runtime, codeHash, now, hookId);
|
|
117
117
|
}
|
|
118
118
|
else {
|
|
@@ -121,23 +121,23 @@ function upsertHook(db, m, codeHash, source) {
|
|
|
121
121
|
}
|
|
122
122
|
else {
|
|
123
123
|
hookId = (0, identity_1.newHookId)();
|
|
124
|
-
db.prepare(`INSERT INTO hooks
|
|
125
|
-
(hook_id, name, version, source, runtime, manifest_path, code_hash, enabled, trust_state, created_at, updated_at)
|
|
124
|
+
db.prepare(`INSERT INTO hooks
|
|
125
|
+
(hook_id, name, version, source, runtime, manifest_path, code_hash, enabled, trust_state, created_at, updated_at)
|
|
126
126
|
VALUES (?, ?, ?, ?, ?, ?, ?, 0, 'untrusted', ?, ?)`).run(hookId, m.name, m.version ?? null, source, m.runtime, m.manifestPath, codeHash, now, now);
|
|
127
127
|
}
|
|
128
128
|
// Replace subscriptions wholesale — simpler than diffing.
|
|
129
129
|
db.prepare(`DELETE FROM hook_subscriptions WHERE hook_id = ?`).run(hookId);
|
|
130
130
|
for (const s of m.subscriptions) {
|
|
131
|
-
db.prepare(`INSERT INTO hook_subscriptions
|
|
132
|
-
(subscription_id, hook_id, event, matcher_json, authority, mode, priority, timeout_ms, on_error, on_timeout, enabled)
|
|
131
|
+
db.prepare(`INSERT INTO hook_subscriptions
|
|
132
|
+
(subscription_id, hook_id, event, matcher_json, authority, mode, priority, timeout_ms, on_error, on_timeout, enabled)
|
|
133
133
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`).run((0, identity_1.newHookSubId)(), hookId, s.event, s.matcher ? JSON.stringify(s.matcher) : null, s.authority, s.mode, s.priority ?? 0, s.timeout_ms, s.on_error, s.on_timeout);
|
|
134
134
|
}
|
|
135
135
|
// Capability grants are warn-only — store but don't enforce.
|
|
136
136
|
db.prepare(`DELETE FROM hook_capability_grants WHERE hook_id = ?`).run(hookId);
|
|
137
137
|
if (m.capabilities) {
|
|
138
138
|
for (const [cap, scope] of Object.entries(m.capabilities)) {
|
|
139
|
-
db.prepare(`INSERT INTO hook_capability_grants
|
|
140
|
-
(grant_id, hook_id, capability, scope_json, granted_by, granted_at)
|
|
139
|
+
db.prepare(`INSERT INTO hook_capability_grants
|
|
140
|
+
(grant_id, hook_id, capability, scope_json, granted_by, granted_at)
|
|
141
141
|
VALUES (?, ?, ?, ?, ?, ?)`).run((0, identity_1.newHookId)(), hookId, cap, JSON.stringify(scope), null, now);
|
|
142
142
|
}
|
|
143
143
|
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.HttpTransport = exports.StdioTransport = void 0;
|
|
25
25
|
const node_child_process_1 = require("node:child_process");
|
|
26
|
+
const spawnCommand_1 = require("../util/spawnCommand");
|
|
26
27
|
const DEFAULT_TIMEOUT_MS = 30000;
|
|
27
28
|
const SIGTERM_GRACE_MS = 5000;
|
|
28
29
|
class StdioTransport {
|
|
@@ -36,19 +37,18 @@ class StdioTransport {
|
|
|
36
37
|
this.label = `stdio:${opts.command}`;
|
|
37
38
|
this.defaultTimeout = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
38
39
|
this.log = opts.log;
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
// tests/v4/integration/mcpClient.real.test.ts::resolveNpxLaunch.
|
|
46
|
-
this.proc = spawner(opts.command, opts.args, {
|
|
40
|
+
// v4.9.2 — cross-platform spawn via shared helper. Windows .cmd/.bat
|
|
41
|
+
// shims (npx.cmd is the canonical MCP server case) are wrapped
|
|
42
|
+
// through `cmd.exe /d /s /c` with escaped args; Unix and .exe paths
|
|
43
|
+
// go direct. No shell:true anywhere — argument injection against
|
|
44
|
+
// user-supplied MCP server configs is prevented at the helper layer.
|
|
45
|
+
const { child } = (0, spawnCommand_1.spawnCommand)(opts.command, opts.args, {
|
|
47
46
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
47
|
env: opts.env,
|
|
49
48
|
cwd: opts.cwd,
|
|
50
|
-
|
|
49
|
+
spawnImpl: opts.spawnFn ?? node_child_process_1.spawn,
|
|
51
50
|
});
|
|
51
|
+
this.proc = child;
|
|
52
52
|
this.proc.stdout?.setEncoding('utf8');
|
|
53
53
|
this.proc.stderr?.setEncoding('utf8');
|
|
54
54
|
this.proc.stdout?.on('data', (chunk) => this.onStdout(chunk));
|