aiden-runtime 4.1.5 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +265 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +536 -152
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +245 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
  7. package/dist/cli/v4/commands/browserDepth.js +45 -0
  8. package/dist/cli/v4/commands/cron.js +264 -0
  9. package/dist/cli/v4/commands/daemon.js +541 -0
  10. package/dist/cli/v4/commands/daemonStatus.js +253 -0
  11. package/dist/cli/v4/commands/fanout.js +42 -59
  12. package/dist/cli/v4/commands/help.js +13 -0
  13. package/dist/cli/v4/commands/index.js +35 -1
  14. package/dist/cli/v4/commands/mcp.js +80 -54
  15. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  16. package/dist/cli/v4/commands/recovery.js +122 -0
  17. package/dist/cli/v4/commands/runs.js +223 -0
  18. package/dist/cli/v4/commands/sandbox.js +48 -0
  19. package/dist/cli/v4/commands/spawnPause.js +93 -0
  20. package/dist/cli/v4/commands/suggestions.js +68 -0
  21. package/dist/cli/v4/commands/tce.js +41 -0
  22. package/dist/cli/v4/commands/trigger.js +378 -0
  23. package/dist/cli/v4/commands/update.js +95 -3
  24. package/dist/cli/v4/daemonAgentBuilder.js +145 -0
  25. package/dist/cli/v4/defaultSoul.js +1 -1
  26. package/dist/cli/v4/display/capabilityCard.js +26 -0
  27. package/dist/cli/v4/display.js +18 -8
  28. package/dist/cli/v4/replyRenderer.js +31 -23
  29. package/dist/cli/v4/updateBootPrompt.js +170 -0
  30. package/dist/core/playwrightBridge.js +129 -0
  31. package/dist/core/v4/aidenAgent.js +527 -5
  32. package/dist/core/v4/browserState.js +436 -0
  33. package/dist/core/v4/checkpoint.js +79 -0
  34. package/dist/core/v4/daemon/bootstrap.js +651 -0
  35. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  36. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  37. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  38. package/dist/core/v4/daemon/cron/migration.js +199 -0
  39. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  40. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  41. package/dist/core/v4/daemon/db/connection.js +106 -0
  42. package/dist/core/v4/daemon/db/migrations.js +362 -0
  43. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  44. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  45. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  46. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  47. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  48. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  49. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  50. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  51. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  52. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  53. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  54. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  55. package/dist/core/v4/daemon/drain.js +156 -0
  56. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  57. package/dist/core/v4/daemon/health.js +159 -0
  58. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  59. package/dist/core/v4/daemon/index.js +179 -0
  60. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  61. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  62. package/dist/core/v4/daemon/restartCode.js +32 -0
  63. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  64. package/dist/core/v4/daemon/runStore.js +144 -0
  65. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  66. package/dist/core/v4/daemon/signals.js +50 -0
  67. package/dist/core/v4/daemon/supervisor.js +272 -0
  68. package/dist/core/v4/daemon/triggerBus.js +279 -0
  69. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  70. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  71. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  72. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  73. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  74. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  75. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  76. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  77. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  78. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  79. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  80. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  81. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  82. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  83. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  84. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  85. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  86. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  87. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  88. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  89. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  90. package/dist/core/v4/daemon/types.js +15 -0
  91. package/dist/core/v4/dockerSession.js +461 -0
  92. package/dist/core/v4/dryRun.js +117 -0
  93. package/dist/core/v4/failureClassifier.js +779 -0
  94. package/dist/core/v4/providerFallback.js +35 -2
  95. package/dist/core/v4/recoveryReport.js +449 -0
  96. package/dist/core/v4/runtimeToggles.js +214 -0
  97. package/dist/core/v4/sandboxConfig.js +285 -0
  98. package/dist/core/v4/sandboxFs.js +316 -0
  99. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  100. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  101. package/dist/core/v4/subagent/childBuilder.js +391 -0
  102. package/dist/core/v4/subagent/fanout.js +75 -51
  103. package/dist/core/v4/subagent/spawnPause.js +191 -0
  104. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  105. package/dist/core/v4/suggestionCatalog.js +41 -0
  106. package/dist/core/v4/suggestionEngine.js +210 -0
  107. package/dist/core/v4/toolRegistry.js +37 -3
  108. package/dist/core/v4/turnState.js +587 -0
  109. package/dist/core/v4/update/checkUpdate.js +63 -3
  110. package/dist/core/v4/update/installMethodDetect.js +115 -0
  111. package/dist/core/v4/update/registryClient.js +121 -0
  112. package/dist/core/v4/update/skipState.js +75 -0
  113. package/dist/core/v4/verifier.js +448 -0
  114. package/dist/core/version.js +1 -1
  115. package/dist/moat/plannerGuard.js +29 -0
  116. package/dist/providers/v4/anthropicAdapter.js +31 -3
  117. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  118. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  119. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  120. package/dist/tools/v4/browser/_observer.js +224 -0
  121. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  122. package/dist/tools/v4/browser/browserClick.js +18 -1
  123. package/dist/tools/v4/browser/browserClose.js +18 -1
  124. package/dist/tools/v4/browser/browserExtract.js +5 -1
  125. package/dist/tools/v4/browser/browserFill.js +17 -1
  126. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  127. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  128. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  129. package/dist/tools/v4/browser/browserScroll.js +18 -1
  130. package/dist/tools/v4/browser/browserType.js +17 -1
  131. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  132. package/dist/tools/v4/executeCode.js +1 -0
  133. package/dist/tools/v4/files/fileCopy.js +56 -2
  134. package/dist/tools/v4/files/fileDelete.js +38 -1
  135. package/dist/tools/v4/files/fileList.js +12 -1
  136. package/dist/tools/v4/files/fileMove.js +59 -2
  137. package/dist/tools/v4/files/filePatch.js +43 -1
  138. package/dist/tools/v4/files/fileRead.js +12 -1
  139. package/dist/tools/v4/files/fileWrite.js +41 -1
  140. package/dist/tools/v4/index.js +88 -61
  141. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  142. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  143. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  144. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  145. package/dist/tools/v4/process/processKill.js +19 -0
  146. package/dist/tools/v4/process/processList.js +1 -0
  147. package/dist/tools/v4/process/processLogRead.js +1 -0
  148. package/dist/tools/v4/process/processSpawn.js +13 -0
  149. package/dist/tools/v4/process/processWait.js +1 -0
  150. package/dist/tools/v4/sessions/recallSession.js +1 -0
  151. package/dist/tools/v4/sessions/sessionList.js +1 -0
  152. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  153. package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
  154. package/dist/tools/v4/skills/skillManage.js +13 -0
  155. package/dist/tools/v4/skills/skillView.js +1 -0
  156. package/dist/tools/v4/skills/skillsList.js +1 -0
  157. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  158. package/dist/tools/v4/subagent/subagentFanout.js +54 -1
  159. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  160. package/dist/tools/v4/system/appClose.js +13 -0
  161. package/dist/tools/v4/system/appInput.js +13 -0
  162. package/dist/tools/v4/system/appLaunch.js +13 -0
  163. package/dist/tools/v4/system/clipboardRead.js +1 -0
  164. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  165. package/dist/tools/v4/system/mediaKey.js +12 -0
  166. package/dist/tools/v4/system/mediaSessions.js +1 -0
  167. package/dist/tools/v4/system/mediaTransport.js +13 -0
  168. package/dist/tools/v4/system/naturalEvents.js +1 -0
  169. package/dist/tools/v4/system/nowPlaying.js +1 -0
  170. package/dist/tools/v4/system/osProcessList.js +1 -0
  171. package/dist/tools/v4/system/screenshot.js +1 -0
  172. package/dist/tools/v4/system/systemInfo.js +1 -0
  173. package/dist/tools/v4/system/volumeSet.js +17 -0
  174. package/dist/tools/v4/terminal/shellExec.js +81 -9
  175. package/dist/tools/v4/web/deepResearch.js +1 -0
  176. package/dist/tools/v4/web/openUrl.js +1 -0
  177. package/dist/tools/v4/web/webFetch.js +1 -0
  178. package/dist/tools/v4/web/webPage.js +1 -0
  179. package/dist/tools/v4/web/webSearch.js +1 -0
  180. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  181. package/package.json +13 -3
@@ -0,0 +1,144 @@
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/runStore.ts — v4.5 Phase 1: runs + run_events writers.
10
+ *
11
+ * Daemon-fired runs are persisted in `runs` (one row per turn) with
12
+ * a stream of `run_events` rows for per-event detail (tool calls,
13
+ * verifications, classifications, recovery actions, log lines).
14
+ *
15
+ * CLI-fired turns continue using in-memory trace structures — zero
16
+ * overhead for interactive use. The daemon path opts in by creating
17
+ * a run row + emitting events.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.createRunStore = createRunStore;
21
+ function rowToTs(r) {
22
+ return {
23
+ id: r.id,
24
+ triggerEventId: r.trigger_event_id,
25
+ sessionId: r.session_id,
26
+ instanceId: r.instance_id,
27
+ status: r.status,
28
+ finishReason: r.finish_reason,
29
+ startedAt: r.started_at,
30
+ completedAt: r.completed_at,
31
+ resumePending: r.resume_pending === 1,
32
+ resumeReason: r.resume_reason,
33
+ };
34
+ }
35
+ function createRunStore(opts) {
36
+ const db = opts.db;
37
+ return {
38
+ create({ sessionId, instanceId, triggerEventId, status, startedAt, spawnedFromRunId, spawnedFromSessionId }) {
39
+ const now = startedAt ?? Date.now();
40
+ // v4.6 Phase 1 — explicit 8-column INSERT including the two
41
+ // sub-agent lineage columns. Top-level runs pass NULL for both;
42
+ // sub-agent runs pass the parent run_id + session_id. Single
43
+ // insert path keeps the code simple at the cost of two extra
44
+ // bound NULLs on the common (top-level) case.
45
+ const r = db.prepare(`INSERT INTO runs
46
+ (trigger_event_id, session_id, instance_id, status, started_at,
47
+ resume_pending, spawned_from_run_id, spawned_from_session_id)
48
+ VALUES (?, ?, ?, ?, ?, 0, ?, ?)`).run(triggerEventId ?? null, sessionId, instanceId, status ?? 'queued', now, spawnedFromRunId ?? null, spawnedFromSessionId ?? null);
49
+ return Number(r.lastInsertRowid);
50
+ },
51
+ setStatus(runId, status, opts2 = {}) {
52
+ const completedAt = opts2.completedAt
53
+ ?? (status === 'completed' || status === 'failed' || status === 'cancelled' || status === 'interrupted'
54
+ ? Date.now()
55
+ : null);
56
+ db.prepare(`UPDATE runs
57
+ SET status = ?,
58
+ finish_reason = COALESCE(?, finish_reason),
59
+ completed_at = COALESCE(?, completed_at)
60
+ WHERE id = ?`).run(status, opts2.finishReason ?? null, completedAt, runId);
61
+ },
62
+ markResumePending(runId, reason) {
63
+ db.prepare(`UPDATE runs SET resume_pending = 1, resume_reason = ? WHERE id = ?`).run(reason, runId);
64
+ },
65
+ emitEvent(runId, kind, payload) {
66
+ const json = JSON.stringify(payload).slice(0, 4096);
67
+ db.prepare(`INSERT INTO run_events (run_id, ts, kind, payload) VALUES (?, ?, ?, ?)`).run(runId, Date.now(), kind, json);
68
+ },
69
+ listActive() {
70
+ const rows = db
71
+ .prepare(`SELECT * FROM runs WHERE status IN ('queued','running')`)
72
+ .all();
73
+ return rows.map(rowToTs);
74
+ },
75
+ get(runId) {
76
+ const r = db
77
+ .prepare('SELECT * FROM runs WHERE id = ?')
78
+ .get(runId);
79
+ return r ? rowToTs(r) : null;
80
+ },
81
+ countEvents(runId) {
82
+ const r = db
83
+ .prepare('SELECT COUNT(*) AS c FROM run_events WHERE run_id = ?')
84
+ .get(runId);
85
+ return r.c;
86
+ },
87
+ listRecent(opts2 = {}) {
88
+ const limit = Math.max(1, Math.min(opts2.limit ?? 50, 1000));
89
+ const whereParts = [];
90
+ const params = [];
91
+ if (opts2.status) {
92
+ whereParts.push('r.status = ?');
93
+ params.push(opts2.status);
94
+ }
95
+ if (opts2.source) {
96
+ whereParts.push('te.source = ?');
97
+ params.push(opts2.source);
98
+ }
99
+ if (opts2.sessionIdPrefix) {
100
+ whereParts.push('r.session_id LIKE ?');
101
+ params.push(`${opts2.sessionIdPrefix}%`);
102
+ }
103
+ // v4.6 Phase 2Q-B — default to top-level rows only. Children
104
+ // (rows with non-NULL `spawned_from_run_id`) clutter the list
105
+ // when you really want "what user-triggered runs happened
106
+ // recently". The partial index `idx_runs_spawned_from` makes
107
+ // the negated predicate cheap (children indexed; parents NOT
108
+ // indexed but the predicate is `IS NULL` — table scan, but
109
+ // the planner uses the limit + ORDER BY started_at to cap
110
+ // work). `--include-children` flips the flag for flat view.
111
+ if (opts2.topLevelOnly !== false) {
112
+ whereParts.push('r.spawned_from_run_id IS NULL');
113
+ }
114
+ const where = whereParts.length > 0 ? `WHERE ${whereParts.join(' AND ')}` : '';
115
+ const sql = `
116
+ SELECT r.* FROM runs r
117
+ LEFT JOIN trigger_events te ON r.trigger_event_id = te.id
118
+ ${where}
119
+ ORDER BY r.started_at DESC
120
+ LIMIT ?`;
121
+ params.push(limit);
122
+ const rows = db.prepare(sql).all(...params);
123
+ return rows.map(rowToTs);
124
+ },
125
+ countChildren(parentRunId) {
126
+ // Single round-trip via conditional COUNT — sqlite handles
127
+ // this fine even with a few thousand children per parent,
128
+ // which we'll never see in practice (fanout caps at 5).
129
+ const r = db.prepare(`SELECT
130
+ COUNT(*) AS total,
131
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed
132
+ FROM runs
133
+ WHERE spawned_from_run_id = ?`).get(parentRunId);
134
+ return {
135
+ total: r.total,
136
+ completed: r.completed ?? 0,
137
+ };
138
+ },
139
+ listEvents(runId, limit = 200) {
140
+ const rows = db.prepare(`SELECT ts, kind, payload FROM run_events WHERE run_id = ? ORDER BY ts ASC LIMIT ?`).all(runId, Math.max(1, Math.min(limit, 5000)));
141
+ return rows;
142
+ },
143
+ };
144
+ }
@@ -0,0 +1,167 @@
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/runtimeLock.ts — v4.5 Phase 1: race-safe daemon
10
+ * runtime lock.
11
+ *
12
+ * Replaces the plain `fs.writeFileSync(PID_FILE, pid)` in
13
+ * `core/backgroundService.ts:21` which has a TOCTOU race window
14
+ * (two daemons racing to write the PID file both succeed, then
15
+ * step on each other's adapters).
16
+ *
17
+ * Mechanism: `fs.openSync(lockPath, 'wx')` — the `wx` flag opens
18
+ * with O_CREAT | O_EXCL, throwing EEXIST atomically if the file
19
+ * already exists. We write the instance metadata into the locked
20
+ * file and rely on `atexit`-style cleanup + signal handlers to
21
+ * release.
22
+ *
23
+ * Stale-lock recovery: when EEXIST fires, we read the existing
24
+ * file, parse out the PID, and probe it with `process.kill(pid, 0)`.
25
+ * If the owner is dead, we unlink the stale file and retry once.
26
+ * If alive, we throw `DaemonAlreadyRunningError` carrying the PID
27
+ * so the caller can surface a clear error.
28
+ *
29
+ * Cross-platform: Node's `wx` flag works identically on Windows
30
+ * (where the underlying call is CreateFile with FILE_FLAG_OPEN_NO_RECALL
31
+ * + CREATE_NEW). No fcntl/msvcrt fallbacks needed.
32
+ */
33
+ var __importDefault = (this && this.__importDefault) || function (mod) {
34
+ return (mod && mod.__esModule) ? mod : { "default": mod };
35
+ };
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.DaemonAlreadyRunningError = void 0;
38
+ exports.acquireRuntimeLock = acquireRuntimeLock;
39
+ const node_fs_1 = __importDefault(require("node:fs"));
40
+ const node_path_1 = __importDefault(require("node:path"));
41
+ class DaemonAlreadyRunningError extends Error {
42
+ constructor(pid, lockPath) {
43
+ super(`Daemon already running (pid ${pid}). Lock file: ${lockPath}. ` +
44
+ `Use \`aiden daemon stop\` to stop the running instance, or set ` +
45
+ `AIDEN_DAEMON=0 to skip daemon mode.`);
46
+ this.name = 'DaemonAlreadyRunningError';
47
+ this.pid = pid;
48
+ this.lockPath = lockPath;
49
+ }
50
+ }
51
+ exports.DaemonAlreadyRunningError = DaemonAlreadyRunningError;
52
+ function readLockFile(lockPath) {
53
+ try {
54
+ const raw = node_fs_1.default.readFileSync(lockPath, 'utf-8').trim();
55
+ if (!raw)
56
+ return null;
57
+ // Format: 3 lines (instance_id, pid, started_at_ms).
58
+ const lines = raw.split(/\r?\n/);
59
+ if (lines.length < 2)
60
+ return null;
61
+ const pid = Number.parseInt(lines[1], 10);
62
+ if (!Number.isFinite(pid) || pid <= 0)
63
+ return null;
64
+ return {
65
+ instanceId: lines[0],
66
+ pid,
67
+ startedAt: Number.parseInt(lines[2] ?? '0', 10) || 0,
68
+ };
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ function isPidAlive(pid) {
75
+ try {
76
+ process.kill(pid, 0);
77
+ return true;
78
+ }
79
+ catch (err) {
80
+ // ESRCH = no such process → dead. EPERM = exists but not ours → treat as alive.
81
+ const code = err.code;
82
+ if (code === 'EPERM')
83
+ return true;
84
+ return false;
85
+ }
86
+ }
87
+ function writeLockFile(fd, contents) {
88
+ const body = `${contents.instanceId}\n${contents.pid}\n${contents.startedAt}\n`;
89
+ node_fs_1.default.writeSync(fd, body, 0, 'utf-8');
90
+ node_fs_1.default.fsyncSync(fd);
91
+ node_fs_1.default.closeSync(fd);
92
+ }
93
+ /**
94
+ * Atomically acquire the daemon's runtime lock at `lockPath`.
95
+ * Throws `DaemonAlreadyRunningError` when another live daemon
96
+ * already holds it. Recovers automatically from stale locks left
97
+ * by crashed daemons.
98
+ */
99
+ function acquireRuntimeLock(opts) {
100
+ const lockPath = opts.lockPath;
101
+ const pid = opts.pid ?? process.pid;
102
+ const startedAt = opts.startedAt ?? Date.now();
103
+ const contents = {
104
+ instanceId: opts.instanceId,
105
+ pid,
106
+ startedAt,
107
+ };
108
+ // Ensure parent dir exists.
109
+ try {
110
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(lockPath), { recursive: true });
111
+ }
112
+ catch { /* will surface on open */ }
113
+ const attempt = () => {
114
+ try {
115
+ const fd = node_fs_1.default.openSync(lockPath, 'wx');
116
+ return { ok: true, fd };
117
+ }
118
+ catch (e) {
119
+ const err = e;
120
+ if (err.code === 'EEXIST')
121
+ return { ok: false, reason: 'eexist', err };
122
+ return { ok: false, reason: 'other', err };
123
+ }
124
+ };
125
+ let r = attempt();
126
+ if (r.ok === false && r.reason === 'eexist') {
127
+ // Stale-lock recovery — read existing contents, probe the PID.
128
+ const existing = readLockFile(lockPath);
129
+ if (existing && isPidAlive(existing.pid)) {
130
+ throw new DaemonAlreadyRunningError(existing.pid, lockPath);
131
+ }
132
+ opts.log?.('warn', `Daemon: removing stale runtime lock (pid ${existing?.pid ?? '?'} dead)`);
133
+ try {
134
+ node_fs_1.default.unlinkSync(lockPath);
135
+ }
136
+ catch { /* race-safe: ignore */ }
137
+ r = attempt();
138
+ }
139
+ if (r.ok === false) {
140
+ throw new Error(`Failed to acquire daemon runtime lock at ${lockPath}: ${r.err.message}`);
141
+ }
142
+ writeLockFile(r.fd, contents);
143
+ let released = false;
144
+ const release = () => {
145
+ if (released)
146
+ return;
147
+ released = true;
148
+ try {
149
+ node_fs_1.default.unlinkSync(lockPath);
150
+ }
151
+ catch { /* best-effort */ }
152
+ };
153
+ // Defense-in-depth: also clean up on process exit even if the
154
+ // caller forgets. Drain handler calls release() explicitly.
155
+ const onExit = () => { release(); };
156
+ process.once('exit', onExit);
157
+ return {
158
+ release: () => {
159
+ release();
160
+ try {
161
+ process.removeListener('exit', onExit);
162
+ }
163
+ catch { /* noop */ }
164
+ },
165
+ lockPath,
166
+ };
167
+ }
@@ -0,0 +1,50 @@
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/signals.ts — v4.5 Phase 1: signal handler installation.
10
+ *
11
+ * Installs SIGUSR1 → graceful restart (exit code 75) and
12
+ * SIGTERM/SIGINT → graceful shutdown (exit code 0). All three
13
+ * routes go through the same `performDrain` so cleanup ordering
14
+ * stays uniform.
15
+ *
16
+ * SIGUSR1 is unavailable on Windows; the install is a no-op
17
+ * there. The CLI's `aiden daemon restart` falls back to stop+start
18
+ * sequentially on Windows.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.installDaemonSignalHandlers = installDaemonSignalHandlers;
22
+ exports._resetDaemonSignalHandlersForTests = _resetDaemonSignalHandlersForTests;
23
+ const drain_1 = require("./drain");
24
+ const restartCode_1 = require("./restartCode");
25
+ let _installed = false;
26
+ function installDaemonSignalHandlers(opts) {
27
+ if (_installed)
28
+ return;
29
+ _installed = true;
30
+ const supportsSIGUSR1 = opts.installRestartSignal ?? (process.platform !== 'win32');
31
+ process.once('SIGTERM', () => {
32
+ void (0, drain_1.performDrain)({ ...opts.getDrainContext(), reason: 'sigterm', exitCode: 0 });
33
+ });
34
+ process.once('SIGINT', () => {
35
+ void (0, drain_1.performDrain)({ ...opts.getDrainContext(), reason: 'sigint', exitCode: 0 });
36
+ });
37
+ if (supportsSIGUSR1) {
38
+ process.once('SIGUSR1', () => {
39
+ void (0, drain_1.performDrain)({
40
+ ...opts.getDrainContext(),
41
+ reason: 'sigusr1_restart',
42
+ exitCode: restartCode_1.DAEMON_RESTART_EXIT_CODE,
43
+ });
44
+ });
45
+ }
46
+ }
47
+ /** Test helper: rearm the installer guard. */
48
+ function _resetDaemonSignalHandlersForTests() {
49
+ _installed = false;
50
+ }
@@ -0,0 +1,272 @@
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/supervisor.ts — v4.5 Phase 1: internal supervisor +
10
+ * OS service template generators.
11
+ *
12
+ * Two-tier supervision strategy:
13
+ *
14
+ * 1. **OS service is the primary supervisor** wherever available.
15
+ * systemd (Linux), launchd (macOS), and third-party tools on
16
+ * Windows do this better than any in-process supervisor — they
17
+ * survive logout, integrate with reboots, and surface in OS
18
+ * tooling. `aiden daemon install` writes the appropriate unit
19
+ * via the template generators in this module.
20
+ *
21
+ * 2. **Internal supervisor is the fallback** for environments
22
+ * that lack an OS service manager OR where the user prefers to
23
+ * keep things simple. Parent process spawns the daemon child,
24
+ * watches for exits, and respawns with exponential backoff.
25
+ * Graceful-restart exit code (75) triggers immediate respawn
26
+ * without backoff.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.startSupervisor = startSupervisor;
30
+ exports.generateSystemdUnit = generateSystemdUnit;
31
+ exports.generateLaunchdPlist = generateLaunchdPlist;
32
+ exports.windowsServiceGuidance = windowsServiceGuidance;
33
+ const node_child_process_1 = require("node:child_process");
34
+ const restartCode_1 = require("./restartCode");
35
+ function startSupervisor(opts) {
36
+ const initialMs = opts.backoff?.initialMs ?? 1000;
37
+ const maxMs = opts.backoff?.maxMs ?? 60000;
38
+ const multiplier = opts.backoff?.multiplier ?? 2;
39
+ const maxFailures = opts.backoff?.maxConsecutiveFailures ?? 5;
40
+ const graceful = new Set(opts.gracefulExitCodes ?? [restartCode_1.DAEMON_RESTART_EXIT_CODE]);
41
+ const drainGrace = opts.drainTimeoutMs ?? 30000;
42
+ let child = null;
43
+ let consecutiveFailures = 0;
44
+ let respawnTimer = null;
45
+ let stopping = false;
46
+ let stopResolve = null;
47
+ const stopPromise = new Promise((res) => { stopResolve = res; });
48
+ const respawn = (delayMs) => {
49
+ if (stopping)
50
+ return;
51
+ if (respawnTimer)
52
+ return;
53
+ opts.onRespawn?.(consecutiveFailures, delayMs);
54
+ respawnTimer = setTimeout(() => {
55
+ respawnTimer = null;
56
+ launch();
57
+ }, delayMs);
58
+ if (typeof respawnTimer.unref === 'function')
59
+ respawnTimer.unref();
60
+ };
61
+ const launch = () => {
62
+ if (stopping)
63
+ return;
64
+ const cmd = opts.childCmd[0];
65
+ const args = opts.childCmd.slice(1);
66
+ child = (0, node_child_process_1.spawn)(cmd, args, {
67
+ cwd: opts.cwd,
68
+ env: { ...process.env, ...(opts.env ?? {}) },
69
+ stdio: 'inherit',
70
+ });
71
+ child.on('exit', (code, signal) => {
72
+ opts.onChildExit?.(code, signal);
73
+ const wasGraceful = code != null && graceful.has(code);
74
+ if (stopping) {
75
+ // We requested the stop; resolve the promise.
76
+ stopResolve?.();
77
+ return;
78
+ }
79
+ if (wasGraceful) {
80
+ // Graceful restart — respawn immediately, no backoff.
81
+ consecutiveFailures = 0;
82
+ respawn(0);
83
+ return;
84
+ }
85
+ consecutiveFailures += 1;
86
+ if (consecutiveFailures >= maxFailures) {
87
+ opts.onGiveUp?.(`Child exited ${consecutiveFailures} consecutive times (last: ` +
88
+ `code=${code} signal=${signal}). Giving up.`);
89
+ stopping = true;
90
+ stopResolve?.();
91
+ return;
92
+ }
93
+ const delay = Math.min(maxMs, initialMs * Math.pow(multiplier, consecutiveFailures - 1));
94
+ respawn(delay);
95
+ });
96
+ child.on('error', () => {
97
+ // 'error' fires before 'exit' in spawn-failure cases. Let
98
+ // 'exit' handle the backoff; just keep the supervisor alive.
99
+ });
100
+ };
101
+ launch();
102
+ return {
103
+ async stop() {
104
+ if (stopping)
105
+ return stopPromise;
106
+ stopping = true;
107
+ if (respawnTimer) {
108
+ clearTimeout(respawnTimer);
109
+ respawnTimer = null;
110
+ }
111
+ if (!child) {
112
+ stopResolve?.();
113
+ return stopPromise;
114
+ }
115
+ try {
116
+ child.kill('SIGTERM');
117
+ }
118
+ catch { /* noop */ }
119
+ // Hard cap so a stuck child doesn't hang the supervisor.
120
+ const killer = setTimeout(() => {
121
+ try {
122
+ child?.kill('SIGKILL');
123
+ }
124
+ catch { /* noop */ }
125
+ }, drainGrace + 5000);
126
+ if (typeof killer.unref === 'function')
127
+ killer.unref();
128
+ return stopPromise;
129
+ },
130
+ childPid() {
131
+ return child?.pid ?? null;
132
+ },
133
+ };
134
+ }
135
+ /**
136
+ * Render a systemd user-unit suitable for
137
+ * `~/.config/systemd/user/aiden.service`.
138
+ *
139
+ * Key invariants:
140
+ * - `RestartForceExitStatus=75` triggers an immediate respawn on
141
+ * the daemon's graceful-restart exit code.
142
+ * - `TimeoutStopSec = max(60, ceil(drainTimeoutMs/1000)) + 30` so
143
+ * post-interrupt cleanup has headroom beyond the drain. Without
144
+ * this, the cgroup SIGKILLs in-flight tool subprocesses and
145
+ * attribution is lost.
146
+ * - `ExecReload=/bin/kill -USR1 $MAINPID` lets `aiden daemon
147
+ * restart` (or `systemctl --user reload aiden`) trigger a
148
+ * drain-aware graceful restart.
149
+ */
150
+ function generateSystemdUnit(ctx) {
151
+ const drainSec = Math.max(60, Math.ceil(ctx.drainTimeoutMs / 1000));
152
+ const stopSec = drainSec + 30;
153
+ const envLines = Object.entries({
154
+ AIDEN_DAEMON: '1',
155
+ AIDEN_PORT: String(ctx.port),
156
+ AIDEN_DAEMON_AUTO_RESTART: '0', // OS-service primary; disable internal supervisor
157
+ ...(ctx.env ?? {}),
158
+ }).map(([k, v]) => `Environment="${k}=${v}"`).join('\n');
159
+ return `[Unit]
160
+ Description=Aiden local-first AI agent (daemon mode)
161
+ After=network.target
162
+
163
+ [Service]
164
+ Type=simple
165
+ ExecStart=${ctx.nodeBin} ${ctx.bundlePath}
166
+ WorkingDirectory=${ctx.workingDir}
167
+ ${envLines}
168
+ Restart=always
169
+ RestartSec=60
170
+ RestartMaxDelaySec=300
171
+ RestartSteps=5
172
+ RestartForceExitStatus=${restartCode_1.DAEMON_RESTART_EXIT_CODE}
173
+ KillMode=mixed
174
+ KillSignal=SIGTERM
175
+ TimeoutStopSec=${stopSec}
176
+ ExecReload=/bin/kill -USR1 $MAINPID
177
+
178
+ [Install]
179
+ WantedBy=default.target
180
+ `;
181
+ }
182
+ /**
183
+ * Render a launchd plist for
184
+ * `~/Library/LaunchAgents/com.aiden.daemon.plist`.
185
+ *
186
+ * `KeepAlive.SuccessfulExit=false` is the launchd analog of
187
+ * systemd's `RestartForceExitStatus=75`: respawn on ANY non-zero
188
+ * exit; do not respawn on exit 0. The graceful-restart path
189
+ * always exits with 75, which is non-zero, so the daemon respawns
190
+ * automatically.
191
+ *
192
+ * `userPath` should be the captured login-shell PATH so Homebrew /
193
+ * nvm / cargo / etc. are reachable.
194
+ */
195
+ function generateLaunchdPlist(ctx) {
196
+ const envEntries = Object.entries({
197
+ AIDEN_DAEMON: '1',
198
+ AIDEN_PORT: String(ctx.port),
199
+ AIDEN_DAEMON_AUTO_RESTART: '0',
200
+ ...(ctx.userPath ? { PATH: ctx.userPath } : {}),
201
+ ...(ctx.env ?? {}),
202
+ });
203
+ const envXml = envEntries
204
+ .map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
205
+ .join('\n');
206
+ const stdoutXml = ctx.stdoutPath
207
+ ? ` <key>StandardOutPath</key>\n <string>${escapeXml(ctx.stdoutPath)}</string>\n`
208
+ : '';
209
+ const stderrXml = ctx.stderrPath
210
+ ? ` <key>StandardErrorPath</key>\n <string>${escapeXml(ctx.stderrPath)}</string>\n`
211
+ : '';
212
+ return `<?xml version="1.0" encoding="UTF-8"?>
213
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
214
+ <plist version="1.0">
215
+ <dict>
216
+ <key>Label</key>
217
+ <string>com.aiden.daemon</string>
218
+ <key>ProgramArguments</key>
219
+ <array>
220
+ <string>${escapeXml(ctx.nodeBin)}</string>
221
+ <string>${escapeXml(ctx.bundlePath)}</string>
222
+ </array>
223
+ <key>EnvironmentVariables</key>
224
+ <dict>
225
+ ${envXml}
226
+ </dict>
227
+ <key>RunAtLoad</key>
228
+ <true/>
229
+ <key>KeepAlive</key>
230
+ <dict>
231
+ <key>SuccessfulExit</key>
232
+ <false/>
233
+ </dict>
234
+ <key>WorkingDirectory</key>
235
+ <string>${escapeXml(ctx.workingDir)}</string>
236
+ ${stdoutXml}${stderrXml}</dict>
237
+ </plist>
238
+ `;
239
+ }
240
+ function escapeXml(s) {
241
+ return s
242
+ .replace(/&/g, '&amp;')
243
+ .replace(/</g, '&lt;')
244
+ .replace(/>/g, '&gt;')
245
+ .replace(/"/g, '&quot;')
246
+ .replace(/'/g, '&apos;');
247
+ }
248
+ // ── Windows guidance (docs-only) ───────────────────────────────────────────
249
+ /**
250
+ * Phase 1 deliberately does NOT auto-generate Scheduled Task /
251
+ * Windows Service entries. NSSM/SCM variance + admin requirements
252
+ * make an automatic installer too risky for the v4.5 ship. The CLI
253
+ * `aiden daemon install` on Windows prints this guidance and exits
254
+ * with code 0 without writing anything.
255
+ */
256
+ function windowsServiceGuidance() {
257
+ return [
258
+ 'Aiden v4.5 does not auto-install a Windows service in Phase 1.',
259
+ '',
260
+ 'Recommended approaches:',
261
+ ' - Foreground: aiden daemon start',
262
+ ' (the internal supervisor keeps the daemon running',
263
+ ' until you close the terminal)',
264
+ ' - Background: Use a third-party supervisor like `pm2` or `nssm`.',
265
+ ' Example with pm2:',
266
+ ' npm install -g pm2',
267
+ ' pm2 start aiden -- daemon start',
268
+ ' pm2 save && pm2 startup',
269
+ '',
270
+ 'See docs/v4.5/daemon-windows.md for details.',
271
+ ].join('\n');
272
+ }