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.
- package/README.md +250 -847
- package/dist/api/server.js +32 -5
- package/dist/cli/v4/aidenCLI.js +351 -53
- package/dist/cli/v4/callbacks.js +170 -0
- package/dist/cli/v4/chatSession.js +138 -3
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +92 -0
- package/dist/cli/v4/commands/browserDepth.js +45 -0
- package/dist/cli/v4/commands/cron.js +264 -0
- package/dist/cli/v4/commands/daemon.js +541 -0
- package/dist/cli/v4/commands/daemonStatus.js +253 -0
- package/dist/cli/v4/commands/help.js +7 -0
- package/dist/cli/v4/commands/index.js +20 -1
- package/dist/cli/v4/commands/runs.js +203 -0
- package/dist/cli/v4/commands/sandbox.js +48 -0
- package/dist/cli/v4/commands/suggestions.js +68 -0
- package/dist/cli/v4/commands/tce.js +41 -0
- package/dist/cli/v4/commands/trigger.js +378 -0
- package/dist/cli/v4/commands/update.js +95 -3
- package/dist/cli/v4/daemonAgentBuilder.js +142 -0
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/cli/v4/display/capabilityCard.js +26 -0
- package/dist/cli/v4/display.js +18 -8
- package/dist/cli/v4/replyRenderer.js +31 -23
- package/dist/cli/v4/updateBootPrompt.js +170 -0
- package/dist/core/playwrightBridge.js +129 -0
- package/dist/core/v4/aidenAgent.js +308 -4
- package/dist/core/v4/browserState.js +436 -0
- package/dist/core/v4/checkpoint.js +79 -0
- package/dist/core/v4/daemon/bootstrap.js +604 -0
- package/dist/core/v4/daemon/cleanShutdown.js +154 -0
- package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
- package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
- package/dist/core/v4/daemon/cron/migration.js +199 -0
- package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
- package/dist/core/v4/daemon/daemonConfig.js +90 -0
- package/dist/core/v4/daemon/db/connection.js +106 -0
- package/dist/core/v4/daemon/db/migrations.js +296 -0
- package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
- package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
- package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
- package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
- package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
- package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
- package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
- package/dist/core/v4/daemon/dispatcher/index.js +53 -0
- package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
- package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
- package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
- package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
- package/dist/core/v4/daemon/drain.js +156 -0
- package/dist/core/v4/daemon/eventLoopLag.js +73 -0
- package/dist/core/v4/daemon/health.js +159 -0
- package/dist/core/v4/daemon/idempotencyStore.js +204 -0
- package/dist/core/v4/daemon/index.js +179 -0
- package/dist/core/v4/daemon/instanceTracker.js +99 -0
- package/dist/core/v4/daemon/resourceRegistry.js +150 -0
- package/dist/core/v4/daemon/restartCode.js +32 -0
- package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
- package/dist/core/v4/daemon/runStore.js +114 -0
- package/dist/core/v4/daemon/runtimeLock.js +167 -0
- package/dist/core/v4/daemon/signals.js +50 -0
- package/dist/core/v4/daemon/supervisor.js +272 -0
- package/dist/core/v4/daemon/triggerBus.js +279 -0
- package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
- package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
- package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
- package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
- package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
- package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
- package/dist/core/v4/daemon/triggers/email/index.js +332 -0
- package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
- package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
- package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
- package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
- package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
- package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
- package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
- package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
- package/dist/core/v4/daemon/triggers/webhook.js +376 -0
- package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
- package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
- package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
- package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
- package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
- package/dist/core/v4/daemon/types.js +15 -0
- package/dist/core/v4/dockerSession.js +461 -0
- package/dist/core/v4/dryRun.js +117 -0
- package/dist/core/v4/failureClassifier.js +779 -0
- package/dist/core/v4/recoveryReport.js +449 -0
- package/dist/core/v4/runtimeToggles.js +187 -0
- package/dist/core/v4/sandboxConfig.js +285 -0
- package/dist/core/v4/sandboxFs.js +316 -0
- package/dist/core/v4/suggestionCatalog.js +41 -0
- package/dist/core/v4/suggestionEngine.js +210 -0
- package/dist/core/v4/toolRegistry.js +18 -0
- package/dist/core/v4/turnState.js +587 -0
- package/dist/core/v4/update/checkUpdate.js +63 -3
- package/dist/core/v4/update/installMethodDetect.js +115 -0
- package/dist/core/v4/update/registryClient.js +121 -0
- package/dist/core/v4/update/skipState.js +75 -0
- package/dist/core/v4/verifier.js +448 -0
- package/dist/core/version.js +1 -1
- package/dist/tools/v4/browser/_observer.js +224 -0
- package/dist/tools/v4/browser/browserBlocker.js +396 -0
- package/dist/tools/v4/browser/browserClick.js +18 -1
- package/dist/tools/v4/browser/browserClose.js +18 -1
- package/dist/tools/v4/browser/browserExtract.js +5 -1
- package/dist/tools/v4/browser/browserFill.js +17 -1
- package/dist/tools/v4/browser/browserGetUrl.js +5 -1
- package/dist/tools/v4/browser/browserNavigate.js +16 -1
- package/dist/tools/v4/browser/browserScreenshot.js +5 -1
- package/dist/tools/v4/browser/browserScroll.js +18 -1
- package/dist/tools/v4/browser/browserType.js +17 -1
- package/dist/tools/v4/browser/captchaCheck.js +5 -1
- package/dist/tools/v4/executeCode.js +1 -0
- package/dist/tools/v4/files/fileCopy.js +56 -2
- package/dist/tools/v4/files/fileDelete.js +38 -1
- package/dist/tools/v4/files/fileList.js +12 -1
- package/dist/tools/v4/files/fileMove.js +59 -2
- package/dist/tools/v4/files/filePatch.js +43 -1
- package/dist/tools/v4/files/fileRead.js +12 -1
- package/dist/tools/v4/files/fileWrite.js +41 -1
- package/dist/tools/v4/index.js +71 -58
- package/dist/tools/v4/memory/memoryAdd.js +14 -0
- package/dist/tools/v4/memory/memoryRemove.js +14 -0
- package/dist/tools/v4/memory/memoryReplace.js +15 -0
- package/dist/tools/v4/memory/sessionSummary.js +12 -0
- package/dist/tools/v4/process/processKill.js +19 -0
- package/dist/tools/v4/process/processList.js +1 -0
- package/dist/tools/v4/process/processLogRead.js +1 -0
- package/dist/tools/v4/process/processSpawn.js +13 -0
- package/dist/tools/v4/process/processWait.js +1 -0
- package/dist/tools/v4/sessions/recallSession.js +1 -0
- package/dist/tools/v4/sessions/sessionList.js +1 -0
- package/dist/tools/v4/sessions/sessionSearch.js +1 -0
- package/dist/tools/v4/skills/lookupToolSchema.js +2 -0
- package/dist/tools/v4/skills/skillManage.js +13 -0
- package/dist/tools/v4/skills/skillView.js +1 -0
- package/dist/tools/v4/skills/skillsList.js +1 -0
- package/dist/tools/v4/subagent/subagentFanout.js +1 -0
- package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
- package/dist/tools/v4/system/appClose.js +13 -0
- package/dist/tools/v4/system/appInput.js +13 -0
- package/dist/tools/v4/system/appLaunch.js +13 -0
- package/dist/tools/v4/system/clipboardRead.js +1 -0
- package/dist/tools/v4/system/clipboardWrite.js +14 -0
- package/dist/tools/v4/system/mediaKey.js +12 -0
- package/dist/tools/v4/system/mediaSessions.js +1 -0
- package/dist/tools/v4/system/mediaTransport.js +13 -0
- package/dist/tools/v4/system/naturalEvents.js +1 -0
- package/dist/tools/v4/system/nowPlaying.js +1 -0
- package/dist/tools/v4/system/osProcessList.js +1 -0
- package/dist/tools/v4/system/screenshot.js +1 -0
- package/dist/tools/v4/system/systemInfo.js +1 -0
- package/dist/tools/v4/system/volumeSet.js +17 -0
- package/dist/tools/v4/terminal/shellExec.js +81 -9
- package/dist/tools/v4/web/deepResearch.js +1 -0
- package/dist/tools/v4/web/openUrl.js +1 -0
- package/dist/tools/v4/web/webFetch.js +1 -0
- package/dist/tools/v4/web/webPage.js +1 -0
- package/dist/tools/v4/web/webSearch.js +1 -0
- package/dist/tools/v4/web/youtubeSearch.js +1 -0
- 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
|
+
}
|