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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/idempotency/runIdempotencyStore.ts — v4.9.0 Slice 5.
|
|
10
|
+
*
|
|
11
|
+
* Durable ingress / write-side idempotency. Distinct from the v1
|
|
12
|
+
* `idempotency_keys` response-replay cache (which stores HTTP response
|
|
13
|
+
* bodies for exact-byte replay). This store tracks (namespace, key) →
|
|
14
|
+
* acceptance outcome so a duplicate webhook / email / file / API
|
|
15
|
+
* request never creates a second `runs` row.
|
|
16
|
+
*
|
|
17
|
+
* `acquire()` is the central primitive: atomic insert against the
|
|
18
|
+
* (namespace, key) PK. Three outcomes:
|
|
19
|
+
*
|
|
20
|
+
* - 'accepted' — fresh row inserted, caller proceeds with
|
|
21
|
+
* run/trigger creation.
|
|
22
|
+
* - 'duplicate' — row exists with the SAME fingerprint;
|
|
23
|
+
* caller should reuse the linked run/event id.
|
|
24
|
+
* - 'rejected_conflict' — row exists with a DIFFERENT fingerprint;
|
|
25
|
+
* caller is reusing a key for different work
|
|
26
|
+
* and should reject with a loud error.
|
|
27
|
+
*
|
|
28
|
+
* Call `link()` after the run/trigger row exists to back-fill the FKs.
|
|
29
|
+
* `complete()` patches the final result_ref (e.g. span_id of the
|
|
30
|
+
* outcome). `sweepExpired()` is the GC tick for keys with `expires_at`
|
|
31
|
+
* in the past.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.fingerprintCanonical = fingerprintCanonical;
|
|
35
|
+
exports.acquire = acquire;
|
|
36
|
+
exports.link = link;
|
|
37
|
+
exports.complete = complete;
|
|
38
|
+
exports.sweepExpired = sweepExpired;
|
|
39
|
+
exports.getKey = getKey;
|
|
40
|
+
const node_crypto_1 = require("node:crypto");
|
|
41
|
+
/**
|
|
42
|
+
* Canonical fingerprint helper — sort keys, stringify, SHA-256 hex.
|
|
43
|
+
* Drops `undefined` values to keep the hash stable across optional
|
|
44
|
+
* fields. Use this when the caller doesn't already have a domain-
|
|
45
|
+
* specific hash (e.g. webhook deliveries already compute one in
|
|
46
|
+
* `webhookIdempotency.ts`).
|
|
47
|
+
*/
|
|
48
|
+
function fingerprintCanonical(payload) {
|
|
49
|
+
const canon = canonicalise(payload);
|
|
50
|
+
return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(canon)).digest('hex');
|
|
51
|
+
}
|
|
52
|
+
function canonicalise(v) {
|
|
53
|
+
if (v === null || v === undefined)
|
|
54
|
+
return null;
|
|
55
|
+
if (Array.isArray(v))
|
|
56
|
+
return v.map(canonicalise);
|
|
57
|
+
if (typeof v === 'object') {
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const k of Object.keys(v).sort()) {
|
|
60
|
+
const val = v[k];
|
|
61
|
+
if (val !== undefined)
|
|
62
|
+
out[k] = canonicalise(val);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
return v;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Attempt to claim (namespace, key) for an incoming request. Atomic
|
|
70
|
+
* via INSERT OR IGNORE against the (namespace, key) PK + a follow-up
|
|
71
|
+
* SELECT for the existing row.
|
|
72
|
+
*/
|
|
73
|
+
function acquire(db, opts) {
|
|
74
|
+
const nowMs = (opts.now ?? Date.now)();
|
|
75
|
+
const nowIso = new Date(nowMs).toISOString();
|
|
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)
|
|
79
|
+
VALUES (?, ?, ?, 'accepted', ?, ?)`).run(opts.namespace, opts.key, opts.fingerprint, nowIso, expires);
|
|
80
|
+
if (result.changes > 0) {
|
|
81
|
+
const row = readRow(db, opts.namespace, opts.key);
|
|
82
|
+
return { outcome: 'accepted', row };
|
|
83
|
+
}
|
|
84
|
+
// Collision — read existing and compare fingerprint.
|
|
85
|
+
const existing = readRow(db, opts.namespace, opts.key);
|
|
86
|
+
if (!existing) {
|
|
87
|
+
// Defensive: should not happen, the INSERT OR IGNORE only skips
|
|
88
|
+
// when the PK conflicts.
|
|
89
|
+
throw new Error('runIdempotencyStore.acquire: INSERT OR IGNORE skipped but no row found');
|
|
90
|
+
}
|
|
91
|
+
if (existing.fingerprint === opts.fingerprint) {
|
|
92
|
+
return { outcome: 'duplicate', existing };
|
|
93
|
+
}
|
|
94
|
+
return { outcome: 'rejected_conflict', existing };
|
|
95
|
+
}
|
|
96
|
+
/** Back-fill the run/trigger/span FKs once those rows exist. */
|
|
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)
|
|
102
|
+
WHERE namespace = ? AND key = ?`).run(opts.runId ?? null, opts.triggerEventId ?? null, opts.spanId ?? null, opts.namespace, opts.key);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Patch the terminal status + optional result_ref. Caller picks the
|
|
106
|
+
* status: `'completed'` for success, `'failed'` for terminal failure.
|
|
107
|
+
*/
|
|
108
|
+
function complete(db, opts) {
|
|
109
|
+
db.prepare(`UPDATE run_idempotency_keys
|
|
110
|
+
SET status = ?,
|
|
111
|
+
result_ref = COALESCE(?, result_ref)
|
|
112
|
+
WHERE namespace = ? AND key = ?`).run(opts.status, opts.resultRef ?? null, opts.namespace, opts.key);
|
|
113
|
+
}
|
|
114
|
+
/** Delete keys whose `expires_at` is in the past. Returns the count. */
|
|
115
|
+
function sweepExpired(db, now) {
|
|
116
|
+
const nowIso = new Date(now ?? Date.now()).toISOString();
|
|
117
|
+
const r = db.prepare(`DELETE FROM run_idempotency_keys
|
|
118
|
+
WHERE expires_at IS NOT NULL AND expires_at < ?`).run(nowIso);
|
|
119
|
+
return { deleted: Number(r.changes ?? 0) };
|
|
120
|
+
}
|
|
121
|
+
/** Diagnostic — single-key lookup. */
|
|
122
|
+
function getKey(db, namespace, key) {
|
|
123
|
+
return readRow(db, namespace, key);
|
|
124
|
+
}
|
|
125
|
+
function readRow(db, namespace, key) {
|
|
126
|
+
const r = db.prepare(`SELECT * FROM run_idempotency_keys WHERE namespace = ? AND key = ?`).get(namespace, key);
|
|
127
|
+
return r ?? null;
|
|
128
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/incarnationStore.ts — v4.9.0 Slice 4.
|
|
10
|
+
*
|
|
11
|
+
* Thin DDL wrapper around the `daemon_incarnations` table (schema v8).
|
|
12
|
+
* Three operations:
|
|
13
|
+
*
|
|
14
|
+
* 1. `insertIncarnation()` — boot. Writes one row with `started_at`
|
|
15
|
+
* = now ISO. Idempotent on PRIMARY KEY collision (safe re-call).
|
|
16
|
+
* 2. `markEnded()` — clean shutdown. Patches `ended_at`, `exit_reason`,
|
|
17
|
+
* `exit_code` for this incarnation. No-op when the row is missing
|
|
18
|
+
* (defensive — e.g. SQLite died before insert).
|
|
19
|
+
* 3. `lastForDaemon()` — diagnostic. Returns the most recent row for
|
|
20
|
+
* a given daemon_id. Used by `aiden doctor`-style surfaces (not
|
|
21
|
+
* wired in this slice).
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.insertIncarnation = insertIncarnation;
|
|
25
|
+
exports.markEnded = markEnded;
|
|
26
|
+
exports.lastForDaemon = lastForDaemon;
|
|
27
|
+
function insertIncarnation(db, opts) {
|
|
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)
|
|
31
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(opts.incarnationId, opts.daemonId, opts.pid, started, opts.aidenVersion, opts.nodeVersion);
|
|
32
|
+
}
|
|
33
|
+
function markEnded(db, opts) {
|
|
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, ?)
|
|
39
|
+
WHERE incarnation_id = ?`).run(ended, opts.exitReason, opts.exitCode, opts.incarnationId);
|
|
40
|
+
}
|
|
41
|
+
function lastForDaemon(db, daemonId) {
|
|
42
|
+
const r = db.prepare(`SELECT * FROM daemon_incarnations
|
|
43
|
+
WHERE daemon_id = ?
|
|
44
|
+
ORDER BY started_at DESC
|
|
45
|
+
LIMIT 1`).get(daemonId);
|
|
46
|
+
return r ?? null;
|
|
47
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/runs/attemptStore.ts — v4.9.0 Slice 5.
|
|
10
|
+
*
|
|
11
|
+
* One row per execution attempt of a run. `runs.id` (numeric, autoincrement)
|
|
12
|
+
* stays the canonical run identifier; `run_attempts.attempt_id`
|
|
13
|
+
* (`att_<uuidv7>`) is the per-try identifier. attempt_number is 1-indexed
|
|
14
|
+
* and stable per (run_id) — the store computes it via MAX+1 on insert.
|
|
15
|
+
*
|
|
16
|
+
* Slice 5 lands schema + writers. Retry-policy logic (which attempts
|
|
17
|
+
* get retried, with what cooldown) is deferred to Slice 6+.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.createAttempt = createAttempt;
|
|
21
|
+
exports.completeAttempt = completeAttempt;
|
|
22
|
+
exports.listAttemptsForRun = listAttemptsForRun;
|
|
23
|
+
exports.getAttempt = getAttempt;
|
|
24
|
+
const identity_1 = require("../../identity");
|
|
25
|
+
/**
|
|
26
|
+
* Create a fresh attempt for the given run. `attempt_number` is derived
|
|
27
|
+
* by MAX+1 inside a transaction so concurrent creators don't collide.
|
|
28
|
+
* Returns the new attempt id.
|
|
29
|
+
*/
|
|
30
|
+
function createAttempt(db, opts) {
|
|
31
|
+
const attemptId = opts.attemptId ?? (0, identity_1.newAttemptId)();
|
|
32
|
+
const startedAt = opts.startedAt ?? new Date().toISOString();
|
|
33
|
+
const tx = db.transaction(() => {
|
|
34
|
+
const row = db.prepare(`SELECT COALESCE(MAX(attempt_number), 0) AS n FROM run_attempts WHERE run_id = ?`).get(opts.runId);
|
|
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)
|
|
38
|
+
VALUES (?, ?, ?, ?, ?, 'running')`).run(attemptId, opts.runId, nextNumber, opts.incarnationId, startedAt);
|
|
39
|
+
});
|
|
40
|
+
tx();
|
|
41
|
+
return attemptId;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Patch an attempt with a terminal status. COALESCE-protected so the
|
|
45
|
+
* first-wins semantics match the Slice 4 incarnation pattern: if the
|
|
46
|
+
* attempt already has an `ended_at`, this call is a no-op for those
|
|
47
|
+
* fields (status update only applies to the in-flight case).
|
|
48
|
+
*/
|
|
49
|
+
function completeAttempt(db, opts) {
|
|
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, ?)
|
|
57
|
+
WHERE attempt_id = ?`).run(opts.status, endedAt, opts.finishReason ?? null, opts.errorClass ?? null, opts.errorMessage ?? null, opts.attemptId);
|
|
58
|
+
}
|
|
59
|
+
/** List all attempts for a run in attempt-number order. */
|
|
60
|
+
function listAttemptsForRun(db, runId) {
|
|
61
|
+
return db.prepare(`SELECT * FROM run_attempts WHERE run_id = ? ORDER BY attempt_number ASC`).all(runId);
|
|
62
|
+
}
|
|
63
|
+
/** Diagnostic — single-attempt lookup. */
|
|
64
|
+
function getAttempt(db, attemptId) {
|
|
65
|
+
const r = db.prepare(`SELECT * FROM run_attempts WHERE attempt_id = ?`).get(attemptId);
|
|
66
|
+
return r ?? null;
|
|
67
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/runs/reclaim.ts — v4.9.0 Slice 3.
|
|
10
|
+
*
|
|
11
|
+
* Mark runs orphaned by a crashed daemon as `interrupted` so a follow-up
|
|
12
|
+
* boot (or a human looking at `aiden runs list`) sees an unambiguous
|
|
13
|
+
* post-mortem instead of an eternally-`running` row.
|
|
14
|
+
*
|
|
15
|
+
* Called from two sites:
|
|
16
|
+
* 1. Process-wide crash handlers (`uncaughtException` /
|
|
17
|
+
* `unhandledRejection`) installed in `bootstrap.ts`. Before the
|
|
18
|
+
* handler calls `process.exit(1)`, it reclaims this incarnation's
|
|
19
|
+
* still-running rows. The DB connection is the same handle the
|
|
20
|
+
* dispatcher used; the write is one statement so even a crashed
|
|
21
|
+
* process can finish it.
|
|
22
|
+
* 2. Boot-time, against ANY non-current instance — defence in depth
|
|
23
|
+
* for the case where `evaluateBootState` didn't sweep a row (e.g.
|
|
24
|
+
* the prior crash happened after BootState ran but before the
|
|
25
|
+
* run completed). Idempotent: a no-op when no rows match.
|
|
26
|
+
*
|
|
27
|
+
* Uses existing schema columns only — NO new tables, NO new RunStatus
|
|
28
|
+
* value. Sets `status='interrupted', finish_reason='daemon_crashed',
|
|
29
|
+
* resume_pending=1, resume_reason='daemon_crashed', completed_at=now`.
|
|
30
|
+
* Matches the semantics `evaluateBootState` uses for prior-instance
|
|
31
|
+
* crash recovery, so downstream consumers (`aiden runs list`, restart
|
|
32
|
+
* resume logic) see one consistent shape.
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.reclaimStuckRuns = reclaimStuckRuns;
|
|
36
|
+
/**
|
|
37
|
+
* Reclaim stuck `runs.status='running'` rows. Returns the rows touched
|
|
38
|
+
* so the caller can emit `run_events` entries (skipped on the crash
|
|
39
|
+
* path — the inserts could themselves throw, and the UPDATE is the
|
|
40
|
+
* authoritative outcome).
|
|
41
|
+
*/
|
|
42
|
+
function reclaimStuckRuns(db, opts) {
|
|
43
|
+
const now = (opts.now ?? Date.now)();
|
|
44
|
+
// Phase 1 — select the candidate rows. We pull ids first so the
|
|
45
|
+
// caller can act on them (logging, event emission) without a second
|
|
46
|
+
// round-trip. Using a single WHERE clause keeps the index hit on
|
|
47
|
+
// idx_runs_active (`status IN ('queued','running')`).
|
|
48
|
+
let candidates;
|
|
49
|
+
if (opts.instanceId !== undefined) {
|
|
50
|
+
candidates = db.prepare(`SELECT id FROM runs WHERE status = 'running' AND instance_id = ?`).all(opts.instanceId);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
if (!opts.currentInstanceId) {
|
|
54
|
+
throw new Error('reclaimStuckRuns: currentInstanceId required when instanceId omitted');
|
|
55
|
+
}
|
|
56
|
+
candidates = db.prepare(`SELECT id FROM runs WHERE status = 'running' AND instance_id != ?`).all(opts.currentInstanceId);
|
|
57
|
+
}
|
|
58
|
+
if (candidates.length === 0) {
|
|
59
|
+
return { reclaimed: 0, runIds: [] };
|
|
60
|
+
}
|
|
61
|
+
// Phase 2 — single UPDATE for the matching predicate. We re-derive
|
|
62
|
+
// the WHERE from `opts` rather than passing ids inline (would blow
|
|
63
|
+
// past SQLite's bound-parameter cap on a pathologically large run
|
|
64
|
+
// table — unlikely, but cheap to avoid).
|
|
65
|
+
let updateResult;
|
|
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'
|
|
73
|
+
WHERE status = 'running' AND instance_id = ?`).run(now, opts.instanceId);
|
|
74
|
+
}
|
|
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'
|
|
82
|
+
WHERE status = 'running' AND instance_id != ?`).run(now, opts.currentInstanceId);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
reclaimed: Number(updateResult.changes ?? candidates.length),
|
|
86
|
+
runIds: candidates.map((c) => c.id),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/runs/retryPolicy.ts — v4.9.0 Slice 8.
|
|
10
|
+
*
|
|
11
|
+
* Pure policy primitives — `shouldRetry` + `computeBackoffMs`. The
|
|
12
|
+
* orchestrator (`runWithRetry`) lives next door; this module is
|
|
13
|
+
* test-friendly with no side effects.
|
|
14
|
+
*
|
|
15
|
+
* Defaults chosen to match the implicit policy across the rest of
|
|
16
|
+
* v4: max 3 attempts, exponential backoff with full jitter capped
|
|
17
|
+
* at 30s. NetworkError / TimeoutError / ResourceExhausted are
|
|
18
|
+
* retryable; auth + permission + validation are terminal.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.DEFAULT_RETRY_POLICY = void 0;
|
|
22
|
+
exports.shouldRetry = shouldRetry;
|
|
23
|
+
exports.computeBackoffMs = computeBackoffMs;
|
|
24
|
+
exports.DEFAULT_RETRY_POLICY = {
|
|
25
|
+
maxAttempts: 3,
|
|
26
|
+
baseDelayMs: 1000,
|
|
27
|
+
maxDelayMs: 30000,
|
|
28
|
+
jitter: 'full',
|
|
29
|
+
retryableErrorClasses: ['NetworkError', 'TimeoutError', 'ResourceExhausted'],
|
|
30
|
+
nonRetryableErrorClasses: ['AuthError', 'PermissionDenied', 'ValidationError'],
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Decide whether to retry given the error class + attempt number.
|
|
34
|
+
*
|
|
35
|
+
* Precedence:
|
|
36
|
+
* 1. attemptNumber >= maxAttempts → false (cap reached)
|
|
37
|
+
* 2. nonRetryableErrorClasses hit → false (terminal)
|
|
38
|
+
* 3. retryableErrorClasses hit → true
|
|
39
|
+
* 4. unknown error class → false (fail closed)
|
|
40
|
+
*
|
|
41
|
+
* Treating unknown errors as non-retryable matches Aiden's safety
|
|
42
|
+
* stance: a retry burst on an unknown failure can multiply damage.
|
|
43
|
+
*/
|
|
44
|
+
function shouldRetry(errorClass, attemptNumber, policy = exports.DEFAULT_RETRY_POLICY) {
|
|
45
|
+
if (attemptNumber >= policy.maxAttempts)
|
|
46
|
+
return false;
|
|
47
|
+
if (policy.nonRetryableErrorClasses.includes(errorClass))
|
|
48
|
+
return false;
|
|
49
|
+
if (policy.retryableErrorClasses.includes(errorClass))
|
|
50
|
+
return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Exponential backoff with optional jitter, capped at `maxDelayMs`.
|
|
55
|
+
* attemptNumber=1 → base
|
|
56
|
+
* attemptNumber=2 → base*2
|
|
57
|
+
* attemptNumber=3 → base*4
|
|
58
|
+
* Jitter modes:
|
|
59
|
+
* 'none' — exact value
|
|
60
|
+
* 'equal' — value/2 + random(0..value/2)
|
|
61
|
+
* 'full' — random(0..value)
|
|
62
|
+
*/
|
|
63
|
+
function computeBackoffMs(attemptNumber, policy = exports.DEFAULT_RETRY_POLICY, rng = Math.random) {
|
|
64
|
+
if (attemptNumber < 1)
|
|
65
|
+
return 0;
|
|
66
|
+
const exp = policy.baseDelayMs * Math.pow(2, attemptNumber - 1);
|
|
67
|
+
const capped = Math.min(exp, policy.maxDelayMs);
|
|
68
|
+
switch (policy.jitter) {
|
|
69
|
+
case 'none': return capped;
|
|
70
|
+
case 'equal': return Math.floor(capped / 2 + rng() * (capped / 2));
|
|
71
|
+
case 'full': return Math.floor(rng() * capped);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/runs/runWithRetry.ts — v4.9.0 Slice 8.
|
|
10
|
+
*
|
|
11
|
+
* Attempt orchestration on top of Slice 5's `run_attempts` table.
|
|
12
|
+
* Caller supplies an existing run row id + a `RetryPolicy`; this
|
|
13
|
+
* function creates one `run_attempts` row per attempt, runs `fn`
|
|
14
|
+
* inside an incrementing-attempt context, and on retryable error
|
|
15
|
+
* sleeps + tries again. On non-retryable error OR max-attempts cap,
|
|
16
|
+
* returns `'dead_letter'` so the caller can route to a poison queue.
|
|
17
|
+
*
|
|
18
|
+
* The "what's retryable" decision lives in `retryPolicy.ts` so the
|
|
19
|
+
* orchestrator stays small + testable.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.runWithRetry = runWithRetry;
|
|
23
|
+
const attemptStore_1 = require("./attemptStore");
|
|
24
|
+
const retryPolicy_1 = require("./retryPolicy");
|
|
25
|
+
function defaultSleep(ms) {
|
|
26
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Run `fn` with retry policy. Each attempt gets its own `run_attempts`
|
|
30
|
+
* row + an incremented `ctx.attempt` value visible to the callee.
|
|
31
|
+
*/
|
|
32
|
+
async function runWithRetry(db, ctx, opts, fn) {
|
|
33
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
34
|
+
let lastError = new Error('runWithRetry: no attempts made');
|
|
35
|
+
let attemptCount = 0;
|
|
36
|
+
for (let attemptNumber = 1; attemptNumber <= opts.policy.maxAttempts; attemptNumber += 1) {
|
|
37
|
+
attemptCount = attemptNumber;
|
|
38
|
+
const attemptId = (0, attemptStore_1.createAttempt)(db, {
|
|
39
|
+
runId: opts.runId,
|
|
40
|
+
incarnationId: opts.incarnationId,
|
|
41
|
+
});
|
|
42
|
+
const attemptCtx = { ...ctx, attempt: attemptNumber };
|
|
43
|
+
try {
|
|
44
|
+
const value = await fn(attemptCtx, attemptNumber);
|
|
45
|
+
(0, attemptStore_1.completeAttempt)(db, { attemptId, status: 'completed' });
|
|
46
|
+
return { outcome: 'completed', value, attempts: attemptNumber };
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
50
|
+
lastError = error;
|
|
51
|
+
const errorClass = error.name || 'Error';
|
|
52
|
+
const terminalStatus = 'failed';
|
|
53
|
+
(0, attemptStore_1.completeAttempt)(db, {
|
|
54
|
+
attemptId,
|
|
55
|
+
status: terminalStatus,
|
|
56
|
+
errorClass,
|
|
57
|
+
errorMessage: error.message,
|
|
58
|
+
});
|
|
59
|
+
// Three cases:
|
|
60
|
+
// (a) Class is in nonRetryableErrorClasses → dead_letter (terminal class)
|
|
61
|
+
// (b) Class is in retryableErrorClasses → retry IF cap not reached; else dead_letter (exhausted)
|
|
62
|
+
// (c) Class is unknown → failed (caller introspects; no auto-retry)
|
|
63
|
+
const isNonRetryable = opts.policy.nonRetryableErrorClasses.includes(errorClass);
|
|
64
|
+
const isRetryable = opts.policy.retryableErrorClasses.includes(errorClass);
|
|
65
|
+
if (isNonRetryable) {
|
|
66
|
+
return { outcome: 'dead_letter', attempts: attemptNumber, lastError: error };
|
|
67
|
+
}
|
|
68
|
+
if (!isRetryable) {
|
|
69
|
+
return { outcome: 'failed', attempts: attemptNumber, lastError: error };
|
|
70
|
+
}
|
|
71
|
+
if (attemptNumber >= opts.policy.maxAttempts) {
|
|
72
|
+
return { outcome: 'dead_letter', attempts: attemptNumber, lastError: error };
|
|
73
|
+
}
|
|
74
|
+
// Retryable + cap not reached: back off and try again.
|
|
75
|
+
await sleep((0, retryPolicy_1.computeBackoffMs)(attemptNumber, opts.policy));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Defensive fallback — loop exited without explicit return.
|
|
79
|
+
return { outcome: 'dead_letter', attempts: attemptCount, lastError };
|
|
80
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/runs/stuckAttemptWatchdog.ts — v4.9.0 Slice 8.
|
|
10
|
+
*
|
|
11
|
+
* Sweeps `run_attempts` (and `spans`) that are stuck `running` from
|
|
12
|
+
* a previous incarnation past `STUCK_THRESHOLD_MS`. Marks them
|
|
13
|
+
* `crashed` so post-mortem queries get one consistent shape rather
|
|
14
|
+
* than mixing "in-flight" with "abandoned".
|
|
15
|
+
*
|
|
16
|
+
* Wired as a `setInterval` ticker from bootstrap. Cadence default 5
|
|
17
|
+
* min, configurable via `AIDEN_STUCK_ATTEMPT_CHECK_MS`. Threshold 30
|
|
18
|
+
* min default, configurable via `AIDEN_STUCK_ATTEMPT_THRESHOLD_MS`.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.sweepStuckAttempts = sweepStuckAttempts;
|
|
22
|
+
const DEFAULT_THRESHOLD_MS = 30 * 60 * 1000;
|
|
23
|
+
/**
|
|
24
|
+
* Run a single sweep pass. Returns the count + ids of rows touched.
|
|
25
|
+
* Idempotent — calling twice in a row sweeps zero on the second call.
|
|
26
|
+
*/
|
|
27
|
+
function sweepStuckAttempts(db, opts) {
|
|
28
|
+
const now = (opts.now ?? Date.now)();
|
|
29
|
+
const thresholdMs = opts.thresholdMs ?? DEFAULT_THRESHOLD_MS;
|
|
30
|
+
const cutoffIso = new Date(now - thresholdMs).toISOString();
|
|
31
|
+
const endedAtIso = new Date(now).toISOString();
|
|
32
|
+
// ── attempts ────────────────────────────────────────────────────────────
|
|
33
|
+
const attemptRows = db.prepare(`SELECT attempt_id FROM run_attempts
|
|
34
|
+
WHERE status = 'running'
|
|
35
|
+
AND incarnation_id != ?
|
|
36
|
+
AND started_at < ?`).all(opts.currentIncarnationId, cutoffIso);
|
|
37
|
+
const attemptIds = attemptRows.map((r) => r.attempt_id);
|
|
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 != ?
|
|
45
|
+
AND started_at < ?`).run(endedAtIso, opts.currentIncarnationId, cutoffIso);
|
|
46
|
+
}
|
|
47
|
+
// ── spans ──────────────────────────────────────────────────────────────
|
|
48
|
+
// Open spans (status NULL = in-flight) from a non-current incarnation.
|
|
49
|
+
// We don't apply the threshold here — any open span owned by a dead
|
|
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
|
|
54
|
+
AND incarnation_id != ?`).all(opts.currentIncarnationId);
|
|
55
|
+
const spanIds = spanRows.map((r) => r.span_id);
|
|
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
|
|
64
|
+
AND incarnation_id != ?`).run(endedAtIso, opts.currentIncarnationId);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
reclaimedAttempts: attemptIds.length,
|
|
68
|
+
reclaimedSpans: spanIds.length,
|
|
69
|
+
attemptIds,
|
|
70
|
+
spanIds,
|
|
71
|
+
};
|
|
72
|
+
}
|