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,651 @@
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/bootstrap.ts — v4.5 Phase 1 follow-up: shared entry
10
+ * for daemon foundation initialization.
11
+ *
12
+ * Phase 1 wired the daemon init into `api/server.ts`. That path is
13
+ * only hit when the user starts the HTTP API server (Electron child,
14
+ * `aiden serve`-style invocation, OpenAI-compatible endpoint
15
+ * mode). The interactive REPL entry (`cli/v4/aidenCLI.ts`) is
16
+ * standalone and never imports api/server.ts, so AIDEN_DAEMON=1
17
+ * silently did nothing for REPL users.
18
+ *
19
+ * This module is the single bootstrap that BOTH entry points call.
20
+ * Idempotent — safe to invoke from multiple sites in the same
21
+ * process (the singleton guard returns the existing handle).
22
+ *
23
+ * When an Express app is supplied (api/server.ts path), health
24
+ * endpoints mount onto it. When omitted (CLI path), a minimal
25
+ * Express server is spun up on the configured port so
26
+ * `/health/live`, `/metrics`, etc. are reachable regardless of
27
+ * how the user invoked Aiden.
28
+ */
29
+ var __importDefault = (this && this.__importDefault) || function (mod) {
30
+ return (mod && mod.__esModule) ? mod : { "default": mod };
31
+ };
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.bootstrapDaemon = bootstrapDaemon;
34
+ exports._resetDaemonBootstrapForTests = _resetDaemonBootstrapForTests;
35
+ exports.getDaemonHandle = getDaemonHandle;
36
+ exports.bootstrapDaemonFoundation = bootstrapDaemonFoundation;
37
+ exports.installDaemonAgentBuilder = installDaemonAgentBuilder;
38
+ const express_1 = __importDefault(require("express"));
39
+ const node_http_1 = __importDefault(require("node:http"));
40
+ const daemonConfig_1 = require("./daemonConfig");
41
+ const daemonConfig_2 = require("./daemonConfig");
42
+ const paths_1 = require("../paths");
43
+ const connection_1 = require("./db/connection");
44
+ const runtimeLock_1 = require("./runtimeLock");
45
+ const instanceTracker_1 = require("./instanceTracker");
46
+ const cleanShutdown_1 = require("./cleanShutdown");
47
+ const triggerBus_1 = require("./triggerBus");
48
+ const idempotencyStore_1 = require("./idempotencyStore");
49
+ const runStore_1 = require("./runStore");
50
+ const restartFailureCounter_1 = require("./restartFailureCounter");
51
+ const resourceRegistry_1 = require("./resourceRegistry");
52
+ const eventLoopLag_1 = require("./eventLoopLag");
53
+ const health_1 = require("./health");
54
+ const signals_1 = require("./signals");
55
+ // v4.5 Phase 2 — file watcher trigger.
56
+ const fileObservationsStore_1 = require("./triggers/fileObservationsStore");
57
+ const fileWatcher_1 = require("./triggers/fileWatcher");
58
+ const reconcile_1 = require("./triggers/reconcile");
59
+ const fileWatcherSpec_1 = require("./triggers/fileWatcherSpec");
60
+ // v4.5 Phase 3 — webhook trigger.
61
+ const webhook_1 = require("./triggers/webhook");
62
+ // v4.5 Phase 4a — email trigger.
63
+ const email_1 = require("./triggers/email");
64
+ const emailSpec_1 = require("./triggers/email/emailSpec");
65
+ const emailSeenStore_1 = require("./triggers/email/emailSeenStore");
66
+ // v4.5 Phase 5a — trigger dispatcher.
67
+ const dispatcher_1 = require("./dispatcher");
68
+ // v4.5 Phase 5b — cron migration to SQLite + daemon-mode emitter.
69
+ const migration_1 = require("./cron/migration");
70
+ const cronEmitter_1 = require("./cron/cronEmitter");
71
+ const playwrightBridge_1 = require("../../playwrightBridge");
72
+ const version_1 = require("../../version");
73
+ const NOOP_HANDLE = Object.freeze({
74
+ active: false,
75
+ instanceId: null,
76
+ ownsHttpServer: false,
77
+ triggerBus: null,
78
+ idempotencyStore: null,
79
+ runStore: null,
80
+ restartFailureCounter: null,
81
+ resourceRegistry: null,
82
+ instanceTracker: null,
83
+ runtimeLock: null,
84
+ dbPath: null,
85
+ markerPath: null,
86
+ httpServer: null,
87
+ fileWatchers: Object.freeze([]),
88
+ webhookRoutes: null,
89
+ emailTriggers: Object.freeze([]),
90
+ dispatcher: null,
91
+ cronMigration: null,
92
+ });
93
+ // Process-wide singleton — the second call returns the same handle.
94
+ let _singleton = null;
95
+ /**
96
+ * Initialize the daemon foundation IF AIDEN_DAEMON=1. Returns a
97
+ * handle describing what was wired (or a NOOP_HANDLE when the
98
+ * daemon is disabled).
99
+ *
100
+ * Idempotent: a second call returns the existing singleton.
101
+ *
102
+ * Failures during init log a loud error but do NOT throw — the
103
+ * agent loop should keep running even if daemon foundation init
104
+ * fails, so the user isn't blocked from chatting because (say)
105
+ * docker is dead. Health endpoints will simply be absent.
106
+ */
107
+ function bootstrapDaemon(opts = {}) {
108
+ if (_singleton)
109
+ return _singleton;
110
+ const cfg = (0, daemonConfig_1.getDaemonConfig)();
111
+ const log = opts.log ?? ((level, msg) => {
112
+ if (level === 'error')
113
+ console.error(msg);
114
+ else if (level === 'warn')
115
+ console.warn(msg);
116
+ else
117
+ console.log(msg);
118
+ });
119
+ if (!cfg.enabled) {
120
+ _singleton = NOOP_HANDLE;
121
+ return NOOP_HANDLE;
122
+ }
123
+ try {
124
+ const aidenRoot = (0, paths_1.resolveAidenRoot)();
125
+ const dbPath = (0, daemonConfig_2.daemonDbPath)(aidenRoot);
126
+ const lockPath = (0, daemonConfig_2.daemonRuntimeLockPath)(aidenRoot);
127
+ const markerPath = (0, daemonConfig_2.daemonCleanShutdownMarkerPath)(aidenRoot);
128
+ // v4.6 Phase 3A — wire the spawn-pause singleton against the
129
+ // same `aidenRoot` the REPL uses. Daemon-fired turns that
130
+ // invoke `subagent_fanout` will read the same marker file the
131
+ // REPL writes via /spawn-pause. Cross-process coordination is
132
+ // the whole point of the file-marker design (in-process
133
+ // singletons in three runtimes would each have independent
134
+ // pause flags, which would defeat the operator control).
135
+ // The init is idempotent — if the REPL already ran initSpawnPause
136
+ // in this same process, this call replaces the singleton with
137
+ // an equivalent one pointing at the same path.
138
+ //
139
+ // Defensive try/catch: a pause-init failure must NOT prevent
140
+ // daemon bootstrap. Worst case the singleton stays uninit and
141
+ // tool handlers fall through to their `safeReadPause` path
142
+ // (treat as "not paused"). The daemon's startup probe below
143
+ // is best-effort.
144
+ try {
145
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
146
+ const { initSpawnPause } = require('../subagent/spawnPause');
147
+ const sp = initSpawnPause({ aidenHome: aidenRoot });
148
+ if (sp.isPaused()) {
149
+ const s = sp.status();
150
+ const reasonSuffix = s.reason ? ` (reason: ${s.reason})` : '';
151
+ log('warn', `[daemon] sub-agent spawning is PAUSED${reasonSuffix}. ` +
152
+ 'Daemon-fired subagent_fanout calls will reject until an operator ' +
153
+ 'runs /spawn-pause off in a REPL session.');
154
+ }
155
+ }
156
+ catch (e) {
157
+ log('warn', '[daemon] spawn-pause init failed (non-fatal): ' +
158
+ (e instanceof Error ? e.message : String(e)));
159
+ }
160
+ const db = (0, connection_1.openDaemonDb)(dbPath);
161
+ const tracker = (0, instanceTracker_1.createInstanceTracker)({ db, version: version_1.VERSION });
162
+ tracker.start();
163
+ // v4.6 Phase 3b — self-improvement loop singleton. Daemon-fired
164
+ // turns that classify failures via TCE write through to the
165
+ // shared failure ledger, so operator queries from a REPL see
166
+ // daemon-side failure patterns too. Defensive try/catch — init
167
+ // failure must not block daemon bootstrap; the TCE write-through
168
+ // path silently no-ops when the singleton is missing.
169
+ try {
170
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
171
+ const { initRecoveryStore } = require('../selfimprovement/recoveryStore');
172
+ initRecoveryStore({ db });
173
+ }
174
+ catch (e) {
175
+ log('warn', '[daemon] recovery-store init failed (non-fatal): ' +
176
+ (e instanceof Error ? e.message : String(e)));
177
+ }
178
+ // Race-safe runtime lock. EEXIST + live PID → DaemonAlreadyRunningError.
179
+ let runtimeLock;
180
+ try {
181
+ runtimeLock = (0, runtimeLock_1.acquireRuntimeLock)({
182
+ lockPath,
183
+ instanceId: tracker.instanceId,
184
+ log,
185
+ });
186
+ }
187
+ catch (e) {
188
+ tracker.stop();
189
+ if (e instanceof runtimeLock_1.DaemonAlreadyRunningError) {
190
+ log('error', '[daemon] ' + e.message);
191
+ // Fail-loud: AIDEN_DAEMON=1 + another daemon already running is
192
+ // an unambiguous error condition that should surface.
193
+ process.exit(1);
194
+ }
195
+ throw e;
196
+ }
197
+ // Boot-state evaluation: detects crashed prior instance, writes
198
+ // crash_reports, marks affected runs interrupted+resume_pending=1.
199
+ const boot = (0, cleanShutdown_1.evaluateBootState)({ db, markerPath, instanceId: tracker.instanceId });
200
+ if (boot.crashDetected) {
201
+ log('warn', '[daemon] crash recovery: prior instance crashed; affected runs marked resume_pending=1');
202
+ }
203
+ // Module singletons.
204
+ const triggerBus = (0, triggerBus_1.createTriggerBus)({ db });
205
+ const idempotencyStore = (0, idempotencyStore_1.createIdempotencyStore)({ db });
206
+ const runStore = (0, runStore_1.createRunStore)({ db });
207
+ const restartFailureCounter = (0, restartFailureCounter_1.createRestartFailureCounter)({ db, threshold: cfg.restartFailureThreshold });
208
+ const resourceRegistry = (0, resourceRegistry_1.getResourceRegistry)();
209
+ try {
210
+ idempotencyStore.reseed();
211
+ }
212
+ catch { /* best-effort */ }
213
+ (0, eventLoopLag_1.startEventLoopLagSampler)();
214
+ // Mount health endpoints. When the caller supplied an existing
215
+ // Express app (api/server.ts path), use it. Otherwise spin up a
216
+ // minimal Express server on the configured port (CLI path).
217
+ let app = opts.app;
218
+ let httpServer = null;
219
+ let ownsHttpServer = false;
220
+ if (!app) {
221
+ app = (0, express_1.default)();
222
+ // NOTE: deliberately DO NOT install express.json() globally.
223
+ // The daemon-only routes mounted below are:
224
+ // - GET /health/{live,ready,degraded} — no body
225
+ // - GET /metrics — no body
226
+ // - GET /api/daemon/{status,resources} — no body
227
+ // - POST /api/triggers/webhook/:id — requires RAW body
228
+ // (express.raw inline)
229
+ // If a global json parser were registered here, it would
230
+ // consume the webhook body BEFORE the route's express.raw
231
+ // could see it, making HMAC verification always fail.
232
+ ownsHttpServer = true;
233
+ }
234
+ (0, health_1.mountHealthEndpoints)(app, {
235
+ db,
236
+ triggerBus,
237
+ resourceRegistry,
238
+ instanceTracker: tracker,
239
+ version: version_1.VERSION,
240
+ });
241
+ // v4.5 Phase 3 — webhook routes mount on the same Express app.
242
+ // Single dispatch endpoint POST /api/triggers/webhook/:id resolves
243
+ // routes at request time, so no per-route Express handler bloat.
244
+ const webhookRoutes = (0, webhook_1.mountWebhookRoutes)({
245
+ app,
246
+ db,
247
+ triggerBus,
248
+ idempotencyStore,
249
+ resourceRegistry,
250
+ log,
251
+ });
252
+ // v4.5 Phase 3 — bind safety check. When AIDEN_DAEMON_BIND opts
253
+ // into a non-loopback interface, require AIDEN_API_KEY + refuse
254
+ // INSECURE_NO_AUTH webhook routes. Runs BEFORE the listener binds.
255
+ const bindHost = process.env.AIDEN_DAEMON_BIND ?? '127.0.0.1';
256
+ try {
257
+ (0, webhook_1.assertSafeBind)({
258
+ bindHost,
259
+ apiKeyConfigured: typeof process.env.AIDEN_API_KEY === 'string' && process.env.AIDEN_API_KEY.length > 0,
260
+ db,
261
+ log,
262
+ });
263
+ }
264
+ catch (e) {
265
+ // Refuse to bring up the HTTP listener but DO keep the foundation
266
+ // running (file watchers, daemon db, instance tracker) so the
267
+ // operator can see status via the daemon-already-running guard.
268
+ log('error', '[daemon] refusing to start HTTP listener due to bind-safety check failure');
269
+ throw e;
270
+ }
271
+ if (ownsHttpServer) {
272
+ httpServer = node_http_1.default.createServer(app);
273
+ httpServer.listen(cfg.port, bindHost, () => {
274
+ log('info', `[daemon] http server listening on http://${bindHost}:${cfg.port}`);
275
+ });
276
+ httpServer.on('error', (err) => {
277
+ if (err.code === 'EADDRINUSE') {
278
+ log('warn', `[daemon] port ${cfg.port} in use — health endpoints unavailable (db / triggers still active)`);
279
+ }
280
+ else {
281
+ log('error', `[daemon] http server error: ${err.message}`);
282
+ }
283
+ });
284
+ }
285
+ // v4.5 Phase 3 — webhook deliveries retention sweep. Runs once on
286
+ // boot then every 24h. Configurable via env (default 7 days).
287
+ const retentionDays = (() => {
288
+ const raw = process.env.AIDEN_DAEMON_WEBHOOK_RETENTION_DAYS;
289
+ const n = raw ? Number.parseInt(raw, 10) : NaN;
290
+ return Number.isFinite(n) && n > 0 ? n : 7;
291
+ })();
292
+ try {
293
+ const swept = webhookRoutes.sweepDeliveries(retentionDays);
294
+ if (swept.deleted > 0) {
295
+ log('info', `[webhook] retention sweep: deleted ${swept.deleted} delivery rows older than ${retentionDays}d`);
296
+ }
297
+ }
298
+ catch { /* best-effort */ }
299
+ const retentionTimer = setInterval(() => {
300
+ try {
301
+ webhookRoutes.sweepDeliveries(retentionDays);
302
+ }
303
+ catch { /* never let sweep crash */ }
304
+ }, 24 * 60 * 60 * 1000);
305
+ if (typeof retentionTimer.unref === 'function')
306
+ retentionTimer.unref();
307
+ // Drain context — same shape api/server.ts wires.
308
+ const getDrainCtx = () => ({
309
+ drainTimeoutMs: cfg.drainTimeoutMs,
310
+ reason: 'sigterm',
311
+ notifySessions: async () => { },
312
+ activeRuns: () => runStore.listActive().map((r) => r.id),
313
+ markResumePending: (runId, reason) => runStore.markResumePending(runId, reason),
314
+ interruptRun: async () => {
315
+ // v4.5 Phase 5a — the dispatcher's runner is the
316
+ // unit-of-interrupt. Phase 5a's runner is synchronous
317
+ // (placeholder); the real AidenAgent-backed runner will
318
+ // receive a per-run abort signal when wired.
319
+ },
320
+ killToolSubprocesses: async () => { },
321
+ closeBrowser: () => (0, playwrightBridge_1.pwClose)(),
322
+ closeCron: () => { },
323
+ closeIdempotency: () => idempotencyStore.close(),
324
+ closeResources: () => resourceRegistry.reapAll(3000),
325
+ touchCleanShutdown: () => (0, cleanShutdown_1.touchCleanShutdownMarker)(markerPath),
326
+ removePid: async () => {
327
+ // v4.5 Phase 5a — drain in-flight dispatcher claims before
328
+ // releasing the runtime lock so SIGTERM-replace doesn't
329
+ // duplicate trigger work on the incoming instance.
330
+ if (_singleton?.dispatcher) {
331
+ try {
332
+ await _singleton.dispatcher.stop(cfg.drainTimeoutMs);
333
+ }
334
+ catch { /* never block shutdown on dispatcher cleanup */ }
335
+ }
336
+ try {
337
+ runtimeLock.release();
338
+ }
339
+ catch { /* noop */ }
340
+ (0, eventLoopLag_1.stopEventLoopLagSampler)();
341
+ tracker.stop();
342
+ if (httpServer) {
343
+ try {
344
+ httpServer.close();
345
+ }
346
+ catch { /* noop */ }
347
+ }
348
+ },
349
+ markShutdown: (reason, exitCode) => tracker.markShutdown(reason, exitCode),
350
+ });
351
+ (0, signals_1.installDaemonSignalHandlers)({ getDrainContext: getDrainCtx });
352
+ log('info', `[daemon] foundation initializing instance_id=${tracker.instanceId} db=${dbPath}`);
353
+ if (boot.crashDetected) {
354
+ log('warn', '[daemon] boot state: crash recovery applied');
355
+ }
356
+ else if (boot.cleanShutdown) {
357
+ log('info', '[daemon] boot state: clean (previous instance exited gracefully)');
358
+ }
359
+ else {
360
+ log('info', '[daemon] boot state: first boot / no prior daemon detected');
361
+ }
362
+ // ── v4.5 Phase 2 — load enabled file-watcher triggers ────────────────
363
+ const fileWatchers = [];
364
+ try {
365
+ const obsStore = (0, fileObservationsStore_1.createFileObservationsStore)({ db });
366
+ const rows = db
367
+ .prepare(`SELECT * FROM triggers WHERE source = 'file' AND enabled = 1 ORDER BY name`)
368
+ .all();
369
+ for (const t of rows) {
370
+ try {
371
+ const spec = (0, fileWatcherSpec_1.parseFileWatcherSpec)(t.spec_json);
372
+ // Boot-time reconciliation BEFORE the watcher starts so
373
+ // the policy decision is deterministic.
374
+ (0, reconcile_1.reconcileFileWatcher)({
375
+ watcherId: t.id, spec, triggerBus, obsStore, log,
376
+ });
377
+ const handle = (0, fileWatcher_1.createFileWatcher)({
378
+ watcherId: t.id, spec, triggerBus, obsStore,
379
+ registry: resourceRegistry, log,
380
+ });
381
+ fileWatchers.push(handle);
382
+ log('info', `[file-watcher] active: ${t.name} (${t.id}) paths=${spec.paths.length}`);
383
+ }
384
+ catch (e) {
385
+ log('error', `[file-watcher] failed to start ${t.name}: ${e instanceof Error ? e.message : String(e)}`);
386
+ }
387
+ }
388
+ if (rows.length === 0) {
389
+ log('info', '[file-watcher] no file triggers registered');
390
+ }
391
+ }
392
+ catch (e) {
393
+ log('error', `[file-watcher] trigger registry scan failed: ${e instanceof Error ? e.message : String(e)}`);
394
+ }
395
+ // ── v4.5 Phase 4a — load enabled email-IMAP triggers ─────────────────
396
+ const emailTriggers = [];
397
+ try {
398
+ const seenStore = (0, emailSeenStore_1.createEmailSeenStore)({ db });
399
+ const rows = db
400
+ .prepare(`SELECT * FROM triggers WHERE source = 'email' AND enabled = 1 ORDER BY name`)
401
+ .all();
402
+ for (const t of rows) {
403
+ try {
404
+ const spec = (0, emailSpec_1.parseEmailSpec)(t.spec_json);
405
+ const handle = (0, email_1.createEmailTrigger)({
406
+ watcherId: t.id,
407
+ spec,
408
+ triggerBus,
409
+ emailSeenStore: seenStore,
410
+ db,
411
+ registry: resourceRegistry,
412
+ log,
413
+ });
414
+ emailTriggers.push(handle);
415
+ log('info', `[email] active: ${t.name} (${t.id}) host=${spec.imap.host} mailbox=${spec.mailbox}`);
416
+ }
417
+ catch (e) {
418
+ log('error', `[email] failed to start ${t.name}: ${e instanceof Error ? e.message : String(e)}`);
419
+ }
420
+ }
421
+ if (rows.length === 0) {
422
+ log('info', '[email] no email triggers registered');
423
+ }
424
+ // Retention sweep on boot (and every 24h via unref'd interval).
425
+ const retentionDays = (() => {
426
+ const raw = process.env.AIDEN_DAEMON_EMAIL_RETENTION_DAYS;
427
+ const n = raw ? Number.parseInt(raw, 10) : NaN;
428
+ return Number.isFinite(n) && n > 0 ? n : 30;
429
+ })();
430
+ try {
431
+ const swept = seenStore.sweep(retentionDays);
432
+ if (swept.deleted > 0) {
433
+ log('info', `[email] retention sweep: deleted ${swept.deleted} email_seen rows older than ${retentionDays}d`);
434
+ }
435
+ }
436
+ catch { /* best-effort */ }
437
+ const emailRetTimer = setInterval(() => {
438
+ try {
439
+ seenStore.sweep(retentionDays);
440
+ }
441
+ catch { /* never let sweep crash */ }
442
+ }, 24 * 60 * 60 * 1000);
443
+ if (typeof emailRetTimer.unref === 'function')
444
+ emailRetTimer.unref();
445
+ }
446
+ catch (e) {
447
+ log('error', `[email] trigger registry scan failed: ${e instanceof Error ? e.message : String(e)}`);
448
+ }
449
+ // ── v4.5 Phase 5b — cron JSON → SQLite migration ─────────────────────
450
+ // Idempotent: only copies rows when scheduled_workflows is empty
451
+ // AND ~/.aiden/cron_jobs.json exists. Subsequent boots return
452
+ // {ran:false, reason:'already_migrated'}. Original JSON file is
453
+ // left in place so AIDEN_DAEMON=0 callers keep working.
454
+ let cronMigrationResult = null;
455
+ try {
456
+ const res = (0, migration_1.runCronMigration)({ db, log });
457
+ cronMigrationResult = {
458
+ ran: res.ran,
459
+ migrated: res.migrated,
460
+ skipped: res.skipped,
461
+ backupPath: res.backupPath,
462
+ reason: res.reason,
463
+ };
464
+ }
465
+ catch (e) {
466
+ log('error', `[cron-migration] unhandled failure: ${e instanceof Error ? e.message : String(e)}`);
467
+ }
468
+ // ── v4.5 Phase 5b — install daemon-mode cron emitter ─────────────────
469
+ // When AIDEN_DAEMON=1, cron fires go through the trigger bus
470
+ // (consumed by the Phase 5a dispatcher) instead of shelling out.
471
+ // We swap the cronManager's RunActionFn here so any cron heartbeat
472
+ // started by the CLI uses the daemon-mode path.
473
+ //
474
+ // Best-effort import — we don't pull cronManager into the daemon
475
+ // hot path unless the user actually runs cron. The import is
476
+ // lazy so non-cron CLIs don't pay the cost.
477
+ try {
478
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
479
+ const cm = require('../cron/cronManager');
480
+ const emitter = (0, cronEmitter_1.createCronEmitter)({
481
+ triggerBus, db, log,
482
+ });
483
+ cm.setRunActionForTests(emitter);
484
+ log('info', `[cron-emitter] daemon-mode runAction installed`);
485
+ }
486
+ catch (e) {
487
+ log('warn', `[cron-emitter] install skipped: ${e instanceof Error ? e.message : String(e)}`);
488
+ }
489
+ // ── v4.5 Phase 5a — start the trigger dispatcher ─────────────────────
490
+ // The dispatcher is the bus consumer. It claims pending
491
+ // trigger_events and routes them through the agent loop (or
492
+ // the deliverOnly stub when spec.deliver_only=1). Phase 5a
493
+ // wires a placeholder runner that returns 'stop' immediately
494
+ // — the real AidenAgent-backed runner is wired by the CLI
495
+ // entry path in a follow-up (it owns provider/toolExecutor
496
+ // construction). The dispatcher infrastructure is fully
497
+ // functional NOW; the runner adapter is the last seam.
498
+ let dispatcher = null;
499
+ try {
500
+ // v4.5 Phase 7 — runner selection: real agent when builder
501
+ // injected, placeholder otherwise. Both paths exercise the
502
+ // full bus / claim / lease / markDone / run_events plumbing;
503
+ // the difference is whether real `AidenAgent.runConversation`
504
+ // fires or just an immediate stop.
505
+ const runnerFactory = opts.agentBuilder
506
+ ? () => (0, dispatcher_1.createRealAgentRunner)({
507
+ db, runStore, resourceRegistry,
508
+ log, agentBuilder: opts.agentBuilder,
509
+ persistedDefault: opts.persistedDefaultModel,
510
+ })
511
+ : () => (0, dispatcher_1.makeRunner)(async (input) => {
512
+ // Phase 5a placeholder runner — used when no AgentBuilder
513
+ // is injected (e.g. user has no provider configured yet).
514
+ // Marks the run completed with finishReason='stop' after
515
+ // creating the run row + emitting a placeholder event.
516
+ // The bus / dispatch / lease / markDone path is fully
517
+ // exercised end-to-end so soak harness + tests still
518
+ // work without a real model wired.
519
+ const runId = runStore.create({
520
+ sessionId: input.sessionId,
521
+ instanceId: input.instanceId,
522
+ triggerEventId: input.triggerEventId,
523
+ status: 'running',
524
+ });
525
+ runStore.emitEvent(runId, 'dispatcher:invoked', {
526
+ source: input.triggerContext.source,
527
+ triggerId: input.triggerContext.triggerId,
528
+ eventId: input.triggerEventId,
529
+ templated: input.triggerContext.promptTemplate !== null,
530
+ messageLen: input.initialMessage.length,
531
+ });
532
+ runStore.setStatus(runId, 'completed', { finishReason: 'stop' });
533
+ const result = { runId, finishReason: 'stop' };
534
+ return result;
535
+ });
536
+ dispatcher = (0, dispatcher_1.createDispatcher)({
537
+ triggerBus,
538
+ runStore,
539
+ db,
540
+ ownerId: tracker.instanceId,
541
+ instanceId: tracker.instanceId,
542
+ workerCount: 1, // Q-P5-1(a)
543
+ runnerFactory,
544
+ log,
545
+ });
546
+ dispatcher.start();
547
+ log('info', `[dispatcher] active workerCount=1 runner=${opts.agentBuilder ? 'real' : 'placeholder'}`);
548
+ }
549
+ catch (e) {
550
+ log('error', `[dispatcher] start failed: ${e instanceof Error ? e.message : String(e)}`);
551
+ dispatcher = null;
552
+ }
553
+ _singleton = {
554
+ active: true,
555
+ instanceId: tracker.instanceId,
556
+ ownsHttpServer,
557
+ triggerBus,
558
+ idempotencyStore,
559
+ runStore,
560
+ restartFailureCounter,
561
+ resourceRegistry,
562
+ instanceTracker: tracker,
563
+ runtimeLock,
564
+ dbPath,
565
+ markerPath,
566
+ httpServer,
567
+ fileWatchers,
568
+ webhookRoutes,
569
+ emailTriggers,
570
+ dispatcher,
571
+ cronMigration: cronMigrationResult,
572
+ };
573
+ return _singleton;
574
+ }
575
+ catch (e) {
576
+ // Fail-loud but non-fatal: the agent should keep running.
577
+ log('error', `[daemon] foundation init failed: ${e instanceof Error ? e.message : String(e)}`);
578
+ _singleton = NOOP_HANDLE;
579
+ return NOOP_HANDLE;
580
+ }
581
+ }
582
+ /** Test-only — clears the singleton so subsequent calls re-bootstrap. */
583
+ function _resetDaemonBootstrapForTests() {
584
+ _singleton = null;
585
+ }
586
+ /** Diagnostic — returns the current handle (or null if not yet bootstrapped). */
587
+ function getDaemonHandle() {
588
+ return _singleton;
589
+ }
590
+ /**
591
+ * v4.5 Phase 7c — boot the daemon foundation without any agent
592
+ * builder. Drop-in replacement for `bootstrapDaemon()` callers that
593
+ * want the rails (file watchers, webhook routes, email triggers,
594
+ * cron emitter, dispatcher with placeholder runner) up immediately,
595
+ * with the real-agent runner installed later via
596
+ * `installDaemonAgentBuilder()`.
597
+ *
598
+ * Use this at the top of REPL boot (before `buildAgentRuntime`)
599
+ * so the daemon foundation comes up regardless of whether the user
600
+ * has a provider configured. Once `buildAgentRuntime` returns, call
601
+ * `installDaemonAgentBuilder(handle, builder, persistedDefaultModel)`
602
+ * to swap in the real runner.
603
+ *
604
+ * Idempotent — second call returns the existing singleton.
605
+ */
606
+ function bootstrapDaemonFoundation(opts = {}) {
607
+ return bootstrapDaemon(opts);
608
+ }
609
+ /**
610
+ * v4.5 Phase 7c — swap the dispatcher's placeholder runner for a
611
+ * real `AidenAgent`-backed one. Safe to call once the REPL's
612
+ * provider, toolRegistry, and prompt builder are constructed.
613
+ *
614
+ * Returns `false` when the foundation isn't active (AIDEN_DAEMON=0
615
+ * or `bootstrapDaemonFoundation` not called) — caller can decide
616
+ * what to do.
617
+ *
618
+ * Returns `true` when the swap succeeded. The dispatcher's next
619
+ * claim uses the new runner; any in-flight claim continues on the
620
+ * placeholder until completion (the placeholder's behavior is
621
+ * instant-stop, so this window is effectively zero).
622
+ */
623
+ function installDaemonAgentBuilder(handle, agentBuilder, persistedDefaultModel, log) {
624
+ if (!handle.active || !handle.dispatcher || !handle.triggerBus || !handle.runStore) {
625
+ return false;
626
+ }
627
+ const logFn = log ?? ((level, msg) => {
628
+ if (level === 'error')
629
+ console.error(msg);
630
+ else if (level === 'warn')
631
+ console.warn(msg);
632
+ else
633
+ console.log(msg);
634
+ });
635
+ try {
636
+ const realRunner = (0, dispatcher_1.createRealAgentRunner)({
637
+ db: (0, connection_1.openDaemonDb)(handle.dbPath),
638
+ runStore: handle.runStore,
639
+ resourceRegistry: handle.resourceRegistry ?? undefined,
640
+ log: logFn,
641
+ agentBuilder,
642
+ persistedDefault: persistedDefaultModel,
643
+ });
644
+ handle.dispatcher.installRunner(realRunner);
645
+ return true;
646
+ }
647
+ catch (e) {
648
+ logFn('error', `[daemon] installDaemonAgentBuilder failed: ${e instanceof Error ? e.message : String(e)}`);
649
+ return false;
650
+ }
651
+ }