aiden-runtime 4.1.5 → 4.5.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 (163) hide show
  1. package/README.md +250 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +351 -53
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +138 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +92 -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/help.js +7 -0
  12. package/dist/cli/v4/commands/index.js +20 -1
  13. package/dist/cli/v4/commands/runs.js +203 -0
  14. package/dist/cli/v4/commands/sandbox.js +48 -0
  15. package/dist/cli/v4/commands/suggestions.js +68 -0
  16. package/dist/cli/v4/commands/tce.js +41 -0
  17. package/dist/cli/v4/commands/trigger.js +378 -0
  18. package/dist/cli/v4/commands/update.js +95 -3
  19. package/dist/cli/v4/daemonAgentBuilder.js +142 -0
  20. package/dist/cli/v4/defaultSoul.js +1 -1
  21. package/dist/cli/v4/display/capabilityCard.js +26 -0
  22. package/dist/cli/v4/display.js +18 -8
  23. package/dist/cli/v4/replyRenderer.js +31 -23
  24. package/dist/cli/v4/updateBootPrompt.js +170 -0
  25. package/dist/core/playwrightBridge.js +129 -0
  26. package/dist/core/v4/aidenAgent.js +308 -4
  27. package/dist/core/v4/browserState.js +436 -0
  28. package/dist/core/v4/checkpoint.js +79 -0
  29. package/dist/core/v4/daemon/bootstrap.js +604 -0
  30. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  31. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  32. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  33. package/dist/core/v4/daemon/cron/migration.js +199 -0
  34. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  35. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  36. package/dist/core/v4/daemon/db/connection.js +106 -0
  37. package/dist/core/v4/daemon/db/migrations.js +296 -0
  38. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  39. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  40. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  41. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  42. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  43. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  44. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  45. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  46. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  47. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  48. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  49. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  50. package/dist/core/v4/daemon/drain.js +156 -0
  51. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  52. package/dist/core/v4/daemon/health.js +159 -0
  53. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  54. package/dist/core/v4/daemon/index.js +179 -0
  55. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  56. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  57. package/dist/core/v4/daemon/restartCode.js +32 -0
  58. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  59. package/dist/core/v4/daemon/runStore.js +114 -0
  60. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  61. package/dist/core/v4/daemon/signals.js +50 -0
  62. package/dist/core/v4/daemon/supervisor.js +272 -0
  63. package/dist/core/v4/daemon/triggerBus.js +279 -0
  64. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  65. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  66. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  67. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  68. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  69. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  70. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  71. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  72. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  73. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  74. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  75. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  76. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  77. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  78. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  79. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  80. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  81. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  82. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  83. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  84. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  85. package/dist/core/v4/daemon/types.js +15 -0
  86. package/dist/core/v4/dockerSession.js +461 -0
  87. package/dist/core/v4/dryRun.js +117 -0
  88. package/dist/core/v4/failureClassifier.js +779 -0
  89. package/dist/core/v4/recoveryReport.js +449 -0
  90. package/dist/core/v4/runtimeToggles.js +187 -0
  91. package/dist/core/v4/sandboxConfig.js +285 -0
  92. package/dist/core/v4/sandboxFs.js +316 -0
  93. package/dist/core/v4/suggestionCatalog.js +41 -0
  94. package/dist/core/v4/suggestionEngine.js +210 -0
  95. package/dist/core/v4/toolRegistry.js +18 -0
  96. package/dist/core/v4/turnState.js +587 -0
  97. package/dist/core/v4/update/checkUpdate.js +63 -3
  98. package/dist/core/v4/update/installMethodDetect.js +115 -0
  99. package/dist/core/v4/update/registryClient.js +121 -0
  100. package/dist/core/v4/update/skipState.js +75 -0
  101. package/dist/core/v4/verifier.js +448 -0
  102. package/dist/core/version.js +1 -1
  103. package/dist/tools/v4/browser/_observer.js +224 -0
  104. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  105. package/dist/tools/v4/browser/browserClick.js +18 -1
  106. package/dist/tools/v4/browser/browserClose.js +18 -1
  107. package/dist/tools/v4/browser/browserExtract.js +5 -1
  108. package/dist/tools/v4/browser/browserFill.js +17 -1
  109. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  110. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  111. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  112. package/dist/tools/v4/browser/browserScroll.js +18 -1
  113. package/dist/tools/v4/browser/browserType.js +17 -1
  114. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  115. package/dist/tools/v4/executeCode.js +1 -0
  116. package/dist/tools/v4/files/fileCopy.js +56 -2
  117. package/dist/tools/v4/files/fileDelete.js +38 -1
  118. package/dist/tools/v4/files/fileList.js +12 -1
  119. package/dist/tools/v4/files/fileMove.js +59 -2
  120. package/dist/tools/v4/files/filePatch.js +43 -1
  121. package/dist/tools/v4/files/fileRead.js +12 -1
  122. package/dist/tools/v4/files/fileWrite.js +41 -1
  123. package/dist/tools/v4/index.js +71 -58
  124. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  125. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  126. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  127. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  128. package/dist/tools/v4/process/processKill.js +19 -0
  129. package/dist/tools/v4/process/processList.js +1 -0
  130. package/dist/tools/v4/process/processLogRead.js +1 -0
  131. package/dist/tools/v4/process/processSpawn.js +13 -0
  132. package/dist/tools/v4/process/processWait.js +1 -0
  133. package/dist/tools/v4/sessions/recallSession.js +1 -0
  134. package/dist/tools/v4/sessions/sessionList.js +1 -0
  135. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  136. package/dist/tools/v4/skills/lookupToolSchema.js +2 -0
  137. package/dist/tools/v4/skills/skillManage.js +13 -0
  138. package/dist/tools/v4/skills/skillView.js +1 -0
  139. package/dist/tools/v4/skills/skillsList.js +1 -0
  140. package/dist/tools/v4/subagent/subagentFanout.js +1 -0
  141. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  142. package/dist/tools/v4/system/appClose.js +13 -0
  143. package/dist/tools/v4/system/appInput.js +13 -0
  144. package/dist/tools/v4/system/appLaunch.js +13 -0
  145. package/dist/tools/v4/system/clipboardRead.js +1 -0
  146. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  147. package/dist/tools/v4/system/mediaKey.js +12 -0
  148. package/dist/tools/v4/system/mediaSessions.js +1 -0
  149. package/dist/tools/v4/system/mediaTransport.js +13 -0
  150. package/dist/tools/v4/system/naturalEvents.js +1 -0
  151. package/dist/tools/v4/system/nowPlaying.js +1 -0
  152. package/dist/tools/v4/system/osProcessList.js +1 -0
  153. package/dist/tools/v4/system/screenshot.js +1 -0
  154. package/dist/tools/v4/system/systemInfo.js +1 -0
  155. package/dist/tools/v4/system/volumeSet.js +17 -0
  156. package/dist/tools/v4/terminal/shellExec.js +81 -9
  157. package/dist/tools/v4/web/deepResearch.js +1 -0
  158. package/dist/tools/v4/web/openUrl.js +1 -0
  159. package/dist/tools/v4/web/webFetch.js +1 -0
  160. package/dist/tools/v4/web/webPage.js +1 -0
  161. package/dist/tools/v4/web/webSearch.js +1 -0
  162. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  163. package/package.json +7 -1
@@ -0,0 +1,604 @@
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
+ const db = (0, connection_1.openDaemonDb)(dbPath);
129
+ const tracker = (0, instanceTracker_1.createInstanceTracker)({ db, version: version_1.VERSION });
130
+ tracker.start();
131
+ // Race-safe runtime lock. EEXIST + live PID → DaemonAlreadyRunningError.
132
+ let runtimeLock;
133
+ try {
134
+ runtimeLock = (0, runtimeLock_1.acquireRuntimeLock)({
135
+ lockPath,
136
+ instanceId: tracker.instanceId,
137
+ log,
138
+ });
139
+ }
140
+ catch (e) {
141
+ tracker.stop();
142
+ if (e instanceof runtimeLock_1.DaemonAlreadyRunningError) {
143
+ log('error', '[daemon] ' + e.message);
144
+ // Fail-loud: AIDEN_DAEMON=1 + another daemon already running is
145
+ // an unambiguous error condition that should surface.
146
+ process.exit(1);
147
+ }
148
+ throw e;
149
+ }
150
+ // Boot-state evaluation: detects crashed prior instance, writes
151
+ // crash_reports, marks affected runs interrupted+resume_pending=1.
152
+ const boot = (0, cleanShutdown_1.evaluateBootState)({ db, markerPath, instanceId: tracker.instanceId });
153
+ if (boot.crashDetected) {
154
+ log('warn', '[daemon] crash recovery: prior instance crashed; affected runs marked resume_pending=1');
155
+ }
156
+ // Module singletons.
157
+ const triggerBus = (0, triggerBus_1.createTriggerBus)({ db });
158
+ const idempotencyStore = (0, idempotencyStore_1.createIdempotencyStore)({ db });
159
+ const runStore = (0, runStore_1.createRunStore)({ db });
160
+ const restartFailureCounter = (0, restartFailureCounter_1.createRestartFailureCounter)({ db, threshold: cfg.restartFailureThreshold });
161
+ const resourceRegistry = (0, resourceRegistry_1.getResourceRegistry)();
162
+ try {
163
+ idempotencyStore.reseed();
164
+ }
165
+ catch { /* best-effort */ }
166
+ (0, eventLoopLag_1.startEventLoopLagSampler)();
167
+ // Mount health endpoints. When the caller supplied an existing
168
+ // Express app (api/server.ts path), use it. Otherwise spin up a
169
+ // minimal Express server on the configured port (CLI path).
170
+ let app = opts.app;
171
+ let httpServer = null;
172
+ let ownsHttpServer = false;
173
+ if (!app) {
174
+ app = (0, express_1.default)();
175
+ // NOTE: deliberately DO NOT install express.json() globally.
176
+ // The daemon-only routes mounted below are:
177
+ // - GET /health/{live,ready,degraded} — no body
178
+ // - GET /metrics — no body
179
+ // - GET /api/daemon/{status,resources} — no body
180
+ // - POST /api/triggers/webhook/:id — requires RAW body
181
+ // (express.raw inline)
182
+ // If a global json parser were registered here, it would
183
+ // consume the webhook body BEFORE the route's express.raw
184
+ // could see it, making HMAC verification always fail.
185
+ ownsHttpServer = true;
186
+ }
187
+ (0, health_1.mountHealthEndpoints)(app, {
188
+ db,
189
+ triggerBus,
190
+ resourceRegistry,
191
+ instanceTracker: tracker,
192
+ version: version_1.VERSION,
193
+ });
194
+ // v4.5 Phase 3 — webhook routes mount on the same Express app.
195
+ // Single dispatch endpoint POST /api/triggers/webhook/:id resolves
196
+ // routes at request time, so no per-route Express handler bloat.
197
+ const webhookRoutes = (0, webhook_1.mountWebhookRoutes)({
198
+ app,
199
+ db,
200
+ triggerBus,
201
+ idempotencyStore,
202
+ resourceRegistry,
203
+ log,
204
+ });
205
+ // v4.5 Phase 3 — bind safety check. When AIDEN_DAEMON_BIND opts
206
+ // into a non-loopback interface, require AIDEN_API_KEY + refuse
207
+ // INSECURE_NO_AUTH webhook routes. Runs BEFORE the listener binds.
208
+ const bindHost = process.env.AIDEN_DAEMON_BIND ?? '127.0.0.1';
209
+ try {
210
+ (0, webhook_1.assertSafeBind)({
211
+ bindHost,
212
+ apiKeyConfigured: typeof process.env.AIDEN_API_KEY === 'string' && process.env.AIDEN_API_KEY.length > 0,
213
+ db,
214
+ log,
215
+ });
216
+ }
217
+ catch (e) {
218
+ // Refuse to bring up the HTTP listener but DO keep the foundation
219
+ // running (file watchers, daemon db, instance tracker) so the
220
+ // operator can see status via the daemon-already-running guard.
221
+ log('error', '[daemon] refusing to start HTTP listener due to bind-safety check failure');
222
+ throw e;
223
+ }
224
+ if (ownsHttpServer) {
225
+ httpServer = node_http_1.default.createServer(app);
226
+ httpServer.listen(cfg.port, bindHost, () => {
227
+ log('info', `[daemon] http server listening on http://${bindHost}:${cfg.port}`);
228
+ });
229
+ httpServer.on('error', (err) => {
230
+ if (err.code === 'EADDRINUSE') {
231
+ log('warn', `[daemon] port ${cfg.port} in use — health endpoints unavailable (db / triggers still active)`);
232
+ }
233
+ else {
234
+ log('error', `[daemon] http server error: ${err.message}`);
235
+ }
236
+ });
237
+ }
238
+ // v4.5 Phase 3 — webhook deliveries retention sweep. Runs once on
239
+ // boot then every 24h. Configurable via env (default 7 days).
240
+ const retentionDays = (() => {
241
+ const raw = process.env.AIDEN_DAEMON_WEBHOOK_RETENTION_DAYS;
242
+ const n = raw ? Number.parseInt(raw, 10) : NaN;
243
+ return Number.isFinite(n) && n > 0 ? n : 7;
244
+ })();
245
+ try {
246
+ const swept = webhookRoutes.sweepDeliveries(retentionDays);
247
+ if (swept.deleted > 0) {
248
+ log('info', `[webhook] retention sweep: deleted ${swept.deleted} delivery rows older than ${retentionDays}d`);
249
+ }
250
+ }
251
+ catch { /* best-effort */ }
252
+ const retentionTimer = setInterval(() => {
253
+ try {
254
+ webhookRoutes.sweepDeliveries(retentionDays);
255
+ }
256
+ catch { /* never let sweep crash */ }
257
+ }, 24 * 60 * 60 * 1000);
258
+ if (typeof retentionTimer.unref === 'function')
259
+ retentionTimer.unref();
260
+ // Drain context — same shape api/server.ts wires.
261
+ const getDrainCtx = () => ({
262
+ drainTimeoutMs: cfg.drainTimeoutMs,
263
+ reason: 'sigterm',
264
+ notifySessions: async () => { },
265
+ activeRuns: () => runStore.listActive().map((r) => r.id),
266
+ markResumePending: (runId, reason) => runStore.markResumePending(runId, reason),
267
+ interruptRun: async () => {
268
+ // v4.5 Phase 5a — the dispatcher's runner is the
269
+ // unit-of-interrupt. Phase 5a's runner is synchronous
270
+ // (placeholder); the real AidenAgent-backed runner will
271
+ // receive a per-run abort signal when wired.
272
+ },
273
+ killToolSubprocesses: async () => { },
274
+ closeBrowser: () => (0, playwrightBridge_1.pwClose)(),
275
+ closeCron: () => { },
276
+ closeIdempotency: () => idempotencyStore.close(),
277
+ closeResources: () => resourceRegistry.reapAll(3000),
278
+ touchCleanShutdown: () => (0, cleanShutdown_1.touchCleanShutdownMarker)(markerPath),
279
+ removePid: async () => {
280
+ // v4.5 Phase 5a — drain in-flight dispatcher claims before
281
+ // releasing the runtime lock so SIGTERM-replace doesn't
282
+ // duplicate trigger work on the incoming instance.
283
+ if (_singleton?.dispatcher) {
284
+ try {
285
+ await _singleton.dispatcher.stop(cfg.drainTimeoutMs);
286
+ }
287
+ catch { /* never block shutdown on dispatcher cleanup */ }
288
+ }
289
+ try {
290
+ runtimeLock.release();
291
+ }
292
+ catch { /* noop */ }
293
+ (0, eventLoopLag_1.stopEventLoopLagSampler)();
294
+ tracker.stop();
295
+ if (httpServer) {
296
+ try {
297
+ httpServer.close();
298
+ }
299
+ catch { /* noop */ }
300
+ }
301
+ },
302
+ markShutdown: (reason, exitCode) => tracker.markShutdown(reason, exitCode),
303
+ });
304
+ (0, signals_1.installDaemonSignalHandlers)({ getDrainContext: getDrainCtx });
305
+ log('info', `[daemon] foundation initializing instance_id=${tracker.instanceId} db=${dbPath}`);
306
+ if (boot.crashDetected) {
307
+ log('warn', '[daemon] boot state: crash recovery applied');
308
+ }
309
+ else if (boot.cleanShutdown) {
310
+ log('info', '[daemon] boot state: clean (previous instance exited gracefully)');
311
+ }
312
+ else {
313
+ log('info', '[daemon] boot state: first boot / no prior daemon detected');
314
+ }
315
+ // ── v4.5 Phase 2 — load enabled file-watcher triggers ────────────────
316
+ const fileWatchers = [];
317
+ try {
318
+ const obsStore = (0, fileObservationsStore_1.createFileObservationsStore)({ db });
319
+ const rows = db
320
+ .prepare(`SELECT * FROM triggers WHERE source = 'file' AND enabled = 1 ORDER BY name`)
321
+ .all();
322
+ for (const t of rows) {
323
+ try {
324
+ const spec = (0, fileWatcherSpec_1.parseFileWatcherSpec)(t.spec_json);
325
+ // Boot-time reconciliation BEFORE the watcher starts so
326
+ // the policy decision is deterministic.
327
+ (0, reconcile_1.reconcileFileWatcher)({
328
+ watcherId: t.id, spec, triggerBus, obsStore, log,
329
+ });
330
+ const handle = (0, fileWatcher_1.createFileWatcher)({
331
+ watcherId: t.id, spec, triggerBus, obsStore,
332
+ registry: resourceRegistry, log,
333
+ });
334
+ fileWatchers.push(handle);
335
+ log('info', `[file-watcher] active: ${t.name} (${t.id}) paths=${spec.paths.length}`);
336
+ }
337
+ catch (e) {
338
+ log('error', `[file-watcher] failed to start ${t.name}: ${e instanceof Error ? e.message : String(e)}`);
339
+ }
340
+ }
341
+ if (rows.length === 0) {
342
+ log('info', '[file-watcher] no file triggers registered');
343
+ }
344
+ }
345
+ catch (e) {
346
+ log('error', `[file-watcher] trigger registry scan failed: ${e instanceof Error ? e.message : String(e)}`);
347
+ }
348
+ // ── v4.5 Phase 4a — load enabled email-IMAP triggers ─────────────────
349
+ const emailTriggers = [];
350
+ try {
351
+ const seenStore = (0, emailSeenStore_1.createEmailSeenStore)({ db });
352
+ const rows = db
353
+ .prepare(`SELECT * FROM triggers WHERE source = 'email' AND enabled = 1 ORDER BY name`)
354
+ .all();
355
+ for (const t of rows) {
356
+ try {
357
+ const spec = (0, emailSpec_1.parseEmailSpec)(t.spec_json);
358
+ const handle = (0, email_1.createEmailTrigger)({
359
+ watcherId: t.id,
360
+ spec,
361
+ triggerBus,
362
+ emailSeenStore: seenStore,
363
+ db,
364
+ registry: resourceRegistry,
365
+ log,
366
+ });
367
+ emailTriggers.push(handle);
368
+ log('info', `[email] active: ${t.name} (${t.id}) host=${spec.imap.host} mailbox=${spec.mailbox}`);
369
+ }
370
+ catch (e) {
371
+ log('error', `[email] failed to start ${t.name}: ${e instanceof Error ? e.message : String(e)}`);
372
+ }
373
+ }
374
+ if (rows.length === 0) {
375
+ log('info', '[email] no email triggers registered');
376
+ }
377
+ // Retention sweep on boot (and every 24h via unref'd interval).
378
+ const retentionDays = (() => {
379
+ const raw = process.env.AIDEN_DAEMON_EMAIL_RETENTION_DAYS;
380
+ const n = raw ? Number.parseInt(raw, 10) : NaN;
381
+ return Number.isFinite(n) && n > 0 ? n : 30;
382
+ })();
383
+ try {
384
+ const swept = seenStore.sweep(retentionDays);
385
+ if (swept.deleted > 0) {
386
+ log('info', `[email] retention sweep: deleted ${swept.deleted} email_seen rows older than ${retentionDays}d`);
387
+ }
388
+ }
389
+ catch { /* best-effort */ }
390
+ const emailRetTimer = setInterval(() => {
391
+ try {
392
+ seenStore.sweep(retentionDays);
393
+ }
394
+ catch { /* never let sweep crash */ }
395
+ }, 24 * 60 * 60 * 1000);
396
+ if (typeof emailRetTimer.unref === 'function')
397
+ emailRetTimer.unref();
398
+ }
399
+ catch (e) {
400
+ log('error', `[email] trigger registry scan failed: ${e instanceof Error ? e.message : String(e)}`);
401
+ }
402
+ // ── v4.5 Phase 5b — cron JSON → SQLite migration ─────────────────────
403
+ // Idempotent: only copies rows when scheduled_workflows is empty
404
+ // AND ~/.aiden/cron_jobs.json exists. Subsequent boots return
405
+ // {ran:false, reason:'already_migrated'}. Original JSON file is
406
+ // left in place so AIDEN_DAEMON=0 callers keep working.
407
+ let cronMigrationResult = null;
408
+ try {
409
+ const res = (0, migration_1.runCronMigration)({ db, log });
410
+ cronMigrationResult = {
411
+ ran: res.ran,
412
+ migrated: res.migrated,
413
+ skipped: res.skipped,
414
+ backupPath: res.backupPath,
415
+ reason: res.reason,
416
+ };
417
+ }
418
+ catch (e) {
419
+ log('error', `[cron-migration] unhandled failure: ${e instanceof Error ? e.message : String(e)}`);
420
+ }
421
+ // ── v4.5 Phase 5b — install daemon-mode cron emitter ─────────────────
422
+ // When AIDEN_DAEMON=1, cron fires go through the trigger bus
423
+ // (consumed by the Phase 5a dispatcher) instead of shelling out.
424
+ // We swap the cronManager's RunActionFn here so any cron heartbeat
425
+ // started by the CLI uses the daemon-mode path.
426
+ //
427
+ // Best-effort import — we don't pull cronManager into the daemon
428
+ // hot path unless the user actually runs cron. The import is
429
+ // lazy so non-cron CLIs don't pay the cost.
430
+ try {
431
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
432
+ const cm = require('../cron/cronManager');
433
+ const emitter = (0, cronEmitter_1.createCronEmitter)({
434
+ triggerBus, db, log,
435
+ });
436
+ cm.setRunActionForTests(emitter);
437
+ log('info', `[cron-emitter] daemon-mode runAction installed`);
438
+ }
439
+ catch (e) {
440
+ log('warn', `[cron-emitter] install skipped: ${e instanceof Error ? e.message : String(e)}`);
441
+ }
442
+ // ── v4.5 Phase 5a — start the trigger dispatcher ─────────────────────
443
+ // The dispatcher is the bus consumer. It claims pending
444
+ // trigger_events and routes them through the agent loop (or
445
+ // the deliverOnly stub when spec.deliver_only=1). Phase 5a
446
+ // wires a placeholder runner that returns 'stop' immediately
447
+ // — the real AidenAgent-backed runner is wired by the CLI
448
+ // entry path in a follow-up (it owns provider/toolExecutor
449
+ // construction). The dispatcher infrastructure is fully
450
+ // functional NOW; the runner adapter is the last seam.
451
+ let dispatcher = null;
452
+ try {
453
+ // v4.5 Phase 7 — runner selection: real agent when builder
454
+ // injected, placeholder otherwise. Both paths exercise the
455
+ // full bus / claim / lease / markDone / run_events plumbing;
456
+ // the difference is whether real `AidenAgent.runConversation`
457
+ // fires or just an immediate stop.
458
+ const runnerFactory = opts.agentBuilder
459
+ ? () => (0, dispatcher_1.createRealAgentRunner)({
460
+ db, runStore, resourceRegistry,
461
+ log, agentBuilder: opts.agentBuilder,
462
+ persistedDefault: opts.persistedDefaultModel,
463
+ })
464
+ : () => (0, dispatcher_1.makeRunner)(async (input) => {
465
+ // Phase 5a placeholder runner — used when no AgentBuilder
466
+ // is injected (e.g. user has no provider configured yet).
467
+ // Marks the run completed with finishReason='stop' after
468
+ // creating the run row + emitting a placeholder event.
469
+ // The bus / dispatch / lease / markDone path is fully
470
+ // exercised end-to-end so soak harness + tests still
471
+ // work without a real model wired.
472
+ const runId = runStore.create({
473
+ sessionId: input.sessionId,
474
+ instanceId: input.instanceId,
475
+ triggerEventId: input.triggerEventId,
476
+ status: 'running',
477
+ });
478
+ runStore.emitEvent(runId, 'dispatcher:invoked', {
479
+ source: input.triggerContext.source,
480
+ triggerId: input.triggerContext.triggerId,
481
+ eventId: input.triggerEventId,
482
+ templated: input.triggerContext.promptTemplate !== null,
483
+ messageLen: input.initialMessage.length,
484
+ });
485
+ runStore.setStatus(runId, 'completed', { finishReason: 'stop' });
486
+ const result = { runId, finishReason: 'stop' };
487
+ return result;
488
+ });
489
+ dispatcher = (0, dispatcher_1.createDispatcher)({
490
+ triggerBus,
491
+ runStore,
492
+ db,
493
+ ownerId: tracker.instanceId,
494
+ instanceId: tracker.instanceId,
495
+ workerCount: 1, // Q-P5-1(a)
496
+ runnerFactory,
497
+ log,
498
+ });
499
+ dispatcher.start();
500
+ log('info', `[dispatcher] active workerCount=1 runner=${opts.agentBuilder ? 'real' : 'placeholder'}`);
501
+ }
502
+ catch (e) {
503
+ log('error', `[dispatcher] start failed: ${e instanceof Error ? e.message : String(e)}`);
504
+ dispatcher = null;
505
+ }
506
+ _singleton = {
507
+ active: true,
508
+ instanceId: tracker.instanceId,
509
+ ownsHttpServer,
510
+ triggerBus,
511
+ idempotencyStore,
512
+ runStore,
513
+ restartFailureCounter,
514
+ resourceRegistry,
515
+ instanceTracker: tracker,
516
+ runtimeLock,
517
+ dbPath,
518
+ markerPath,
519
+ httpServer,
520
+ fileWatchers,
521
+ webhookRoutes,
522
+ emailTriggers,
523
+ dispatcher,
524
+ cronMigration: cronMigrationResult,
525
+ };
526
+ return _singleton;
527
+ }
528
+ catch (e) {
529
+ // Fail-loud but non-fatal: the agent should keep running.
530
+ log('error', `[daemon] foundation init failed: ${e instanceof Error ? e.message : String(e)}`);
531
+ _singleton = NOOP_HANDLE;
532
+ return NOOP_HANDLE;
533
+ }
534
+ }
535
+ /** Test-only — clears the singleton so subsequent calls re-bootstrap. */
536
+ function _resetDaemonBootstrapForTests() {
537
+ _singleton = null;
538
+ }
539
+ /** Diagnostic — returns the current handle (or null if not yet bootstrapped). */
540
+ function getDaemonHandle() {
541
+ return _singleton;
542
+ }
543
+ /**
544
+ * v4.5 Phase 7c — boot the daemon foundation without any agent
545
+ * builder. Drop-in replacement for `bootstrapDaemon()` callers that
546
+ * want the rails (file watchers, webhook routes, email triggers,
547
+ * cron emitter, dispatcher with placeholder runner) up immediately,
548
+ * with the real-agent runner installed later via
549
+ * `installDaemonAgentBuilder()`.
550
+ *
551
+ * Use this at the top of REPL boot (before `buildAgentRuntime`)
552
+ * so the daemon foundation comes up regardless of whether the user
553
+ * has a provider configured. Once `buildAgentRuntime` returns, call
554
+ * `installDaemonAgentBuilder(handle, builder, persistedDefaultModel)`
555
+ * to swap in the real runner.
556
+ *
557
+ * Idempotent — second call returns the existing singleton.
558
+ */
559
+ function bootstrapDaemonFoundation(opts = {}) {
560
+ return bootstrapDaemon(opts);
561
+ }
562
+ /**
563
+ * v4.5 Phase 7c — swap the dispatcher's placeholder runner for a
564
+ * real `AidenAgent`-backed one. Safe to call once the REPL's
565
+ * provider, toolRegistry, and prompt builder are constructed.
566
+ *
567
+ * Returns `false` when the foundation isn't active (AIDEN_DAEMON=0
568
+ * or `bootstrapDaemonFoundation` not called) — caller can decide
569
+ * what to do.
570
+ *
571
+ * Returns `true` when the swap succeeded. The dispatcher's next
572
+ * claim uses the new runner; any in-flight claim continues on the
573
+ * placeholder until completion (the placeholder's behavior is
574
+ * instant-stop, so this window is effectively zero).
575
+ */
576
+ function installDaemonAgentBuilder(handle, agentBuilder, persistedDefaultModel, log) {
577
+ if (!handle.active || !handle.dispatcher || !handle.triggerBus || !handle.runStore) {
578
+ return false;
579
+ }
580
+ const logFn = log ?? ((level, msg) => {
581
+ if (level === 'error')
582
+ console.error(msg);
583
+ else if (level === 'warn')
584
+ console.warn(msg);
585
+ else
586
+ console.log(msg);
587
+ });
588
+ try {
589
+ const realRunner = (0, dispatcher_1.createRealAgentRunner)({
590
+ db: (0, connection_1.openDaemonDb)(handle.dbPath),
591
+ runStore: handle.runStore,
592
+ resourceRegistry: handle.resourceRegistry ?? undefined,
593
+ log: logFn,
594
+ agentBuilder,
595
+ persistedDefault: persistedDefaultModel,
596
+ });
597
+ handle.dispatcher.installRunner(realRunner);
598
+ return true;
599
+ }
600
+ catch (e) {
601
+ logFn('error', `[daemon] installDaemonAgentBuilder failed: ${e instanceof Error ? e.message : String(e)}`);
602
+ return false;
603
+ }
604
+ }