aiden-runtime 4.9.1 → 4.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +47 -1
  2. package/dist/cli/v4/aidenPrompt.js +12 -0
  3. package/dist/cli/v4/chatSession.js +41 -13
  4. package/dist/cli/v4/commands/channel.js +4 -6
  5. package/dist/cli/v4/commands/cron.js +6 -1
  6. package/dist/cli/v4/commands/daemonDoctor.js +5 -5
  7. package/dist/cli/v4/commands/daemonStatus.js +1 -1
  8. package/dist/cli/v4/commands/greeter.js +86 -0
  9. package/dist/cli/v4/commands/help.js +2 -0
  10. package/dist/cli/v4/commands/index.js +4 -0
  11. package/dist/cli/v4/commands/mcp.js +2 -2
  12. package/dist/cli/v4/commands/plugins.js +4 -6
  13. package/dist/cli/v4/commands/trigger.js +18 -18
  14. package/dist/cli/v4/confirmPrompt.js +67 -0
  15. package/dist/cli/v4/greeter/history.js +134 -0
  16. package/dist/cli/v4/greeter/index.js +147 -0
  17. package/dist/cli/v4/greeter/scan.js +140 -0
  18. package/dist/cli/v4/greeter/selectOffer.js +118 -0
  19. package/dist/cli/v4/greeter/templates.js +51 -0
  20. package/dist/cli/v4/greeter/types.js +23 -0
  21. package/dist/core/v4/daemon/db/migrations.js +398 -398
  22. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +10 -10
  23. package/dist/core/v4/daemon/incarnationStore.js +9 -9
  24. package/dist/core/v4/daemon/runs/attemptStore.js +8 -8
  25. package/dist/core/v4/daemon/runs/reclaim.js +12 -12
  26. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +19 -19
  27. package/dist/core/v4/daemon/spans/spanStore.js +14 -14
  28. package/dist/core/v4/daemon/triggerBus.js +61 -61
  29. package/dist/core/v4/hooks/auditQuery.js +11 -11
  30. package/dist/core/v4/hooks/dispatcher.js +13 -13
  31. package/dist/core/v4/hooks/registry.js +8 -8
  32. package/dist/core/v4/mcp/transport.js +9 -9
  33. package/dist/core/v4/update/executeInstall.js +29 -18
  34. package/dist/core/v4/update/recoveryScript.js +70 -0
  35. package/dist/core/v4/util/spawnCommand.js +151 -0
  36. package/package.json +1 -1
  37. package/themes/default.yaml +52 -52
  38. package/themes/dracula.yaml +32 -32
  39. package/themes/light.yaml +32 -32
  40. package/themes/monochrome.yaml +31 -31
  41. 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
- const spawner = opts.spawnFn ?? node_child_process_1.spawn;
40
- // Windows .cmd/.bat shims (npx.cmd) can't be spawned with shell:false
41
- // (Node 18.20+ refuses with EINVAL), and shell:true mangles arguments
42
- // containing path separators. Callers should resolve the underlying
43
- // executable themselves e.g., spawn `node <npx-cli.js>` instead of
44
- // `npx.cmd`. Phase 11 integration tests demonstrate this pattern in
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
- shell: false,
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));