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,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/runtimeLock.ts — v4.5 Phase 1: race-safe daemon
|
|
10
|
+
* runtime lock.
|
|
11
|
+
*
|
|
12
|
+
* Replaces the plain `fs.writeFileSync(PID_FILE, pid)` in
|
|
13
|
+
* `core/backgroundService.ts:21` which has a TOCTOU race window
|
|
14
|
+
* (two daemons racing to write the PID file both succeed, then
|
|
15
|
+
* step on each other's adapters).
|
|
16
|
+
*
|
|
17
|
+
* Mechanism: `fs.openSync(lockPath, 'wx')` — the `wx` flag opens
|
|
18
|
+
* with O_CREAT | O_EXCL, throwing EEXIST atomically if the file
|
|
19
|
+
* already exists. We write the instance metadata into the locked
|
|
20
|
+
* file and rely on `atexit`-style cleanup + signal handlers to
|
|
21
|
+
* release.
|
|
22
|
+
*
|
|
23
|
+
* Stale-lock recovery: when EEXIST fires, we read the existing
|
|
24
|
+
* file, parse out the PID, and probe it with `process.kill(pid, 0)`.
|
|
25
|
+
* If the owner is dead, we unlink the stale file and retry once.
|
|
26
|
+
* If alive, we throw `DaemonAlreadyRunningError` carrying the PID
|
|
27
|
+
* so the caller can surface a clear error.
|
|
28
|
+
*
|
|
29
|
+
* Cross-platform: Node's `wx` flag works identically on Windows
|
|
30
|
+
* (where the underlying call is CreateFile with FILE_FLAG_OPEN_NO_RECALL
|
|
31
|
+
* + CREATE_NEW). No fcntl/msvcrt fallbacks needed.
|
|
32
|
+
*/
|
|
33
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
34
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
35
|
+
};
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.DaemonAlreadyRunningError = void 0;
|
|
38
|
+
exports.acquireRuntimeLock = acquireRuntimeLock;
|
|
39
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
40
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
41
|
+
class DaemonAlreadyRunningError extends Error {
|
|
42
|
+
constructor(pid, lockPath) {
|
|
43
|
+
super(`Daemon already running (pid ${pid}). Lock file: ${lockPath}. ` +
|
|
44
|
+
`Use \`aiden daemon stop\` to stop the running instance, or set ` +
|
|
45
|
+
`AIDEN_DAEMON=0 to skip daemon mode.`);
|
|
46
|
+
this.name = 'DaemonAlreadyRunningError';
|
|
47
|
+
this.pid = pid;
|
|
48
|
+
this.lockPath = lockPath;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.DaemonAlreadyRunningError = DaemonAlreadyRunningError;
|
|
52
|
+
function readLockFile(lockPath) {
|
|
53
|
+
try {
|
|
54
|
+
const raw = node_fs_1.default.readFileSync(lockPath, 'utf-8').trim();
|
|
55
|
+
if (!raw)
|
|
56
|
+
return null;
|
|
57
|
+
// Format: 3 lines (instance_id, pid, started_at_ms).
|
|
58
|
+
const lines = raw.split(/\r?\n/);
|
|
59
|
+
if (lines.length < 2)
|
|
60
|
+
return null;
|
|
61
|
+
const pid = Number.parseInt(lines[1], 10);
|
|
62
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
instanceId: lines[0],
|
|
66
|
+
pid,
|
|
67
|
+
startedAt: Number.parseInt(lines[2] ?? '0', 10) || 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function isPidAlive(pid) {
|
|
75
|
+
try {
|
|
76
|
+
process.kill(pid, 0);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
// ESRCH = no such process → dead. EPERM = exists but not ours → treat as alive.
|
|
81
|
+
const code = err.code;
|
|
82
|
+
if (code === 'EPERM')
|
|
83
|
+
return true;
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function writeLockFile(fd, contents) {
|
|
88
|
+
const body = `${contents.instanceId}\n${contents.pid}\n${contents.startedAt}\n`;
|
|
89
|
+
node_fs_1.default.writeSync(fd, body, 0, 'utf-8');
|
|
90
|
+
node_fs_1.default.fsyncSync(fd);
|
|
91
|
+
node_fs_1.default.closeSync(fd);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Atomically acquire the daemon's runtime lock at `lockPath`.
|
|
95
|
+
* Throws `DaemonAlreadyRunningError` when another live daemon
|
|
96
|
+
* already holds it. Recovers automatically from stale locks left
|
|
97
|
+
* by crashed daemons.
|
|
98
|
+
*/
|
|
99
|
+
function acquireRuntimeLock(opts) {
|
|
100
|
+
const lockPath = opts.lockPath;
|
|
101
|
+
const pid = opts.pid ?? process.pid;
|
|
102
|
+
const startedAt = opts.startedAt ?? Date.now();
|
|
103
|
+
const contents = {
|
|
104
|
+
instanceId: opts.instanceId,
|
|
105
|
+
pid,
|
|
106
|
+
startedAt,
|
|
107
|
+
};
|
|
108
|
+
// Ensure parent dir exists.
|
|
109
|
+
try {
|
|
110
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(lockPath), { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
catch { /* will surface on open */ }
|
|
113
|
+
const attempt = () => {
|
|
114
|
+
try {
|
|
115
|
+
const fd = node_fs_1.default.openSync(lockPath, 'wx');
|
|
116
|
+
return { ok: true, fd };
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
const err = e;
|
|
120
|
+
if (err.code === 'EEXIST')
|
|
121
|
+
return { ok: false, reason: 'eexist', err };
|
|
122
|
+
return { ok: false, reason: 'other', err };
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
let r = attempt();
|
|
126
|
+
if (r.ok === false && r.reason === 'eexist') {
|
|
127
|
+
// Stale-lock recovery — read existing contents, probe the PID.
|
|
128
|
+
const existing = readLockFile(lockPath);
|
|
129
|
+
if (existing && isPidAlive(existing.pid)) {
|
|
130
|
+
throw new DaemonAlreadyRunningError(existing.pid, lockPath);
|
|
131
|
+
}
|
|
132
|
+
opts.log?.('warn', `Daemon: removing stale runtime lock (pid ${existing?.pid ?? '?'} dead)`);
|
|
133
|
+
try {
|
|
134
|
+
node_fs_1.default.unlinkSync(lockPath);
|
|
135
|
+
}
|
|
136
|
+
catch { /* race-safe: ignore */ }
|
|
137
|
+
r = attempt();
|
|
138
|
+
}
|
|
139
|
+
if (r.ok === false) {
|
|
140
|
+
throw new Error(`Failed to acquire daemon runtime lock at ${lockPath}: ${r.err.message}`);
|
|
141
|
+
}
|
|
142
|
+
writeLockFile(r.fd, contents);
|
|
143
|
+
let released = false;
|
|
144
|
+
const release = () => {
|
|
145
|
+
if (released)
|
|
146
|
+
return;
|
|
147
|
+
released = true;
|
|
148
|
+
try {
|
|
149
|
+
node_fs_1.default.unlinkSync(lockPath);
|
|
150
|
+
}
|
|
151
|
+
catch { /* best-effort */ }
|
|
152
|
+
};
|
|
153
|
+
// Defense-in-depth: also clean up on process exit even if the
|
|
154
|
+
// caller forgets. Drain handler calls release() explicitly.
|
|
155
|
+
const onExit = () => { release(); };
|
|
156
|
+
process.once('exit', onExit);
|
|
157
|
+
return {
|
|
158
|
+
release: () => {
|
|
159
|
+
release();
|
|
160
|
+
try {
|
|
161
|
+
process.removeListener('exit', onExit);
|
|
162
|
+
}
|
|
163
|
+
catch { /* noop */ }
|
|
164
|
+
},
|
|
165
|
+
lockPath,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/signals.ts — v4.5 Phase 1: signal handler installation.
|
|
10
|
+
*
|
|
11
|
+
* Installs SIGUSR1 → graceful restart (exit code 75) and
|
|
12
|
+
* SIGTERM/SIGINT → graceful shutdown (exit code 0). All three
|
|
13
|
+
* routes go through the same `performDrain` so cleanup ordering
|
|
14
|
+
* stays uniform.
|
|
15
|
+
*
|
|
16
|
+
* SIGUSR1 is unavailable on Windows; the install is a no-op
|
|
17
|
+
* there. The CLI's `aiden daemon restart` falls back to stop+start
|
|
18
|
+
* sequentially on Windows.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.installDaemonSignalHandlers = installDaemonSignalHandlers;
|
|
22
|
+
exports._resetDaemonSignalHandlersForTests = _resetDaemonSignalHandlersForTests;
|
|
23
|
+
const drain_1 = require("./drain");
|
|
24
|
+
const restartCode_1 = require("./restartCode");
|
|
25
|
+
let _installed = false;
|
|
26
|
+
function installDaemonSignalHandlers(opts) {
|
|
27
|
+
if (_installed)
|
|
28
|
+
return;
|
|
29
|
+
_installed = true;
|
|
30
|
+
const supportsSIGUSR1 = opts.installRestartSignal ?? (process.platform !== 'win32');
|
|
31
|
+
process.once('SIGTERM', () => {
|
|
32
|
+
void (0, drain_1.performDrain)({ ...opts.getDrainContext(), reason: 'sigterm', exitCode: 0 });
|
|
33
|
+
});
|
|
34
|
+
process.once('SIGINT', () => {
|
|
35
|
+
void (0, drain_1.performDrain)({ ...opts.getDrainContext(), reason: 'sigint', exitCode: 0 });
|
|
36
|
+
});
|
|
37
|
+
if (supportsSIGUSR1) {
|
|
38
|
+
process.once('SIGUSR1', () => {
|
|
39
|
+
void (0, drain_1.performDrain)({
|
|
40
|
+
...opts.getDrainContext(),
|
|
41
|
+
reason: 'sigusr1_restart',
|
|
42
|
+
exitCode: restartCode_1.DAEMON_RESTART_EXIT_CODE,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Test helper: rearm the installer guard. */
|
|
48
|
+
function _resetDaemonSignalHandlersForTests() {
|
|
49
|
+
_installed = false;
|
|
50
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/supervisor.ts — v4.5 Phase 1: internal supervisor +
|
|
10
|
+
* OS service template generators.
|
|
11
|
+
*
|
|
12
|
+
* Two-tier supervision strategy:
|
|
13
|
+
*
|
|
14
|
+
* 1. **OS service is the primary supervisor** wherever available.
|
|
15
|
+
* systemd (Linux), launchd (macOS), and third-party tools on
|
|
16
|
+
* Windows do this better than any in-process supervisor — they
|
|
17
|
+
* survive logout, integrate with reboots, and surface in OS
|
|
18
|
+
* tooling. `aiden daemon install` writes the appropriate unit
|
|
19
|
+
* via the template generators in this module.
|
|
20
|
+
*
|
|
21
|
+
* 2. **Internal supervisor is the fallback** for environments
|
|
22
|
+
* that lack an OS service manager OR where the user prefers to
|
|
23
|
+
* keep things simple. Parent process spawns the daemon child,
|
|
24
|
+
* watches for exits, and respawns with exponential backoff.
|
|
25
|
+
* Graceful-restart exit code (75) triggers immediate respawn
|
|
26
|
+
* without backoff.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.startSupervisor = startSupervisor;
|
|
30
|
+
exports.generateSystemdUnit = generateSystemdUnit;
|
|
31
|
+
exports.generateLaunchdPlist = generateLaunchdPlist;
|
|
32
|
+
exports.windowsServiceGuidance = windowsServiceGuidance;
|
|
33
|
+
const node_child_process_1 = require("node:child_process");
|
|
34
|
+
const restartCode_1 = require("./restartCode");
|
|
35
|
+
function startSupervisor(opts) {
|
|
36
|
+
const initialMs = opts.backoff?.initialMs ?? 1000;
|
|
37
|
+
const maxMs = opts.backoff?.maxMs ?? 60000;
|
|
38
|
+
const multiplier = opts.backoff?.multiplier ?? 2;
|
|
39
|
+
const maxFailures = opts.backoff?.maxConsecutiveFailures ?? 5;
|
|
40
|
+
const graceful = new Set(opts.gracefulExitCodes ?? [restartCode_1.DAEMON_RESTART_EXIT_CODE]);
|
|
41
|
+
const drainGrace = opts.drainTimeoutMs ?? 30000;
|
|
42
|
+
let child = null;
|
|
43
|
+
let consecutiveFailures = 0;
|
|
44
|
+
let respawnTimer = null;
|
|
45
|
+
let stopping = false;
|
|
46
|
+
let stopResolve = null;
|
|
47
|
+
const stopPromise = new Promise((res) => { stopResolve = res; });
|
|
48
|
+
const respawn = (delayMs) => {
|
|
49
|
+
if (stopping)
|
|
50
|
+
return;
|
|
51
|
+
if (respawnTimer)
|
|
52
|
+
return;
|
|
53
|
+
opts.onRespawn?.(consecutiveFailures, delayMs);
|
|
54
|
+
respawnTimer = setTimeout(() => {
|
|
55
|
+
respawnTimer = null;
|
|
56
|
+
launch();
|
|
57
|
+
}, delayMs);
|
|
58
|
+
if (typeof respawnTimer.unref === 'function')
|
|
59
|
+
respawnTimer.unref();
|
|
60
|
+
};
|
|
61
|
+
const launch = () => {
|
|
62
|
+
if (stopping)
|
|
63
|
+
return;
|
|
64
|
+
const cmd = opts.childCmd[0];
|
|
65
|
+
const args = opts.childCmd.slice(1);
|
|
66
|
+
child = (0, node_child_process_1.spawn)(cmd, args, {
|
|
67
|
+
cwd: opts.cwd,
|
|
68
|
+
env: { ...process.env, ...(opts.env ?? {}) },
|
|
69
|
+
stdio: 'inherit',
|
|
70
|
+
});
|
|
71
|
+
child.on('exit', (code, signal) => {
|
|
72
|
+
opts.onChildExit?.(code, signal);
|
|
73
|
+
const wasGraceful = code != null && graceful.has(code);
|
|
74
|
+
if (stopping) {
|
|
75
|
+
// We requested the stop; resolve the promise.
|
|
76
|
+
stopResolve?.();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (wasGraceful) {
|
|
80
|
+
// Graceful restart — respawn immediately, no backoff.
|
|
81
|
+
consecutiveFailures = 0;
|
|
82
|
+
respawn(0);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
consecutiveFailures += 1;
|
|
86
|
+
if (consecutiveFailures >= maxFailures) {
|
|
87
|
+
opts.onGiveUp?.(`Child exited ${consecutiveFailures} consecutive times (last: ` +
|
|
88
|
+
`code=${code} signal=${signal}). Giving up.`);
|
|
89
|
+
stopping = true;
|
|
90
|
+
stopResolve?.();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const delay = Math.min(maxMs, initialMs * Math.pow(multiplier, consecutiveFailures - 1));
|
|
94
|
+
respawn(delay);
|
|
95
|
+
});
|
|
96
|
+
child.on('error', () => {
|
|
97
|
+
// 'error' fires before 'exit' in spawn-failure cases. Let
|
|
98
|
+
// 'exit' handle the backoff; just keep the supervisor alive.
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
launch();
|
|
102
|
+
return {
|
|
103
|
+
async stop() {
|
|
104
|
+
if (stopping)
|
|
105
|
+
return stopPromise;
|
|
106
|
+
stopping = true;
|
|
107
|
+
if (respawnTimer) {
|
|
108
|
+
clearTimeout(respawnTimer);
|
|
109
|
+
respawnTimer = null;
|
|
110
|
+
}
|
|
111
|
+
if (!child) {
|
|
112
|
+
stopResolve?.();
|
|
113
|
+
return stopPromise;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
child.kill('SIGTERM');
|
|
117
|
+
}
|
|
118
|
+
catch { /* noop */ }
|
|
119
|
+
// Hard cap so a stuck child doesn't hang the supervisor.
|
|
120
|
+
const killer = setTimeout(() => {
|
|
121
|
+
try {
|
|
122
|
+
child?.kill('SIGKILL');
|
|
123
|
+
}
|
|
124
|
+
catch { /* noop */ }
|
|
125
|
+
}, drainGrace + 5000);
|
|
126
|
+
if (typeof killer.unref === 'function')
|
|
127
|
+
killer.unref();
|
|
128
|
+
return stopPromise;
|
|
129
|
+
},
|
|
130
|
+
childPid() {
|
|
131
|
+
return child?.pid ?? null;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Render a systemd user-unit suitable for
|
|
137
|
+
* `~/.config/systemd/user/aiden.service`.
|
|
138
|
+
*
|
|
139
|
+
* Key invariants:
|
|
140
|
+
* - `RestartForceExitStatus=75` triggers an immediate respawn on
|
|
141
|
+
* the daemon's graceful-restart exit code.
|
|
142
|
+
* - `TimeoutStopSec = max(60, ceil(drainTimeoutMs/1000)) + 30` so
|
|
143
|
+
* post-interrupt cleanup has headroom beyond the drain. Without
|
|
144
|
+
* this, the cgroup SIGKILLs in-flight tool subprocesses and
|
|
145
|
+
* attribution is lost.
|
|
146
|
+
* - `ExecReload=/bin/kill -USR1 $MAINPID` lets `aiden daemon
|
|
147
|
+
* restart` (or `systemctl --user reload aiden`) trigger a
|
|
148
|
+
* drain-aware graceful restart.
|
|
149
|
+
*/
|
|
150
|
+
function generateSystemdUnit(ctx) {
|
|
151
|
+
const drainSec = Math.max(60, Math.ceil(ctx.drainTimeoutMs / 1000));
|
|
152
|
+
const stopSec = drainSec + 30;
|
|
153
|
+
const envLines = Object.entries({
|
|
154
|
+
AIDEN_DAEMON: '1',
|
|
155
|
+
AIDEN_PORT: String(ctx.port),
|
|
156
|
+
AIDEN_DAEMON_AUTO_RESTART: '0', // OS-service primary; disable internal supervisor
|
|
157
|
+
...(ctx.env ?? {}),
|
|
158
|
+
}).map(([k, v]) => `Environment="${k}=${v}"`).join('\n');
|
|
159
|
+
return `[Unit]
|
|
160
|
+
Description=Aiden local-first AI agent (daemon mode)
|
|
161
|
+
After=network.target
|
|
162
|
+
|
|
163
|
+
[Service]
|
|
164
|
+
Type=simple
|
|
165
|
+
ExecStart=${ctx.nodeBin} ${ctx.bundlePath}
|
|
166
|
+
WorkingDirectory=${ctx.workingDir}
|
|
167
|
+
${envLines}
|
|
168
|
+
Restart=always
|
|
169
|
+
RestartSec=60
|
|
170
|
+
RestartMaxDelaySec=300
|
|
171
|
+
RestartSteps=5
|
|
172
|
+
RestartForceExitStatus=${restartCode_1.DAEMON_RESTART_EXIT_CODE}
|
|
173
|
+
KillMode=mixed
|
|
174
|
+
KillSignal=SIGTERM
|
|
175
|
+
TimeoutStopSec=${stopSec}
|
|
176
|
+
ExecReload=/bin/kill -USR1 $MAINPID
|
|
177
|
+
|
|
178
|
+
[Install]
|
|
179
|
+
WantedBy=default.target
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Render a launchd plist for
|
|
184
|
+
* `~/Library/LaunchAgents/com.aiden.daemon.plist`.
|
|
185
|
+
*
|
|
186
|
+
* `KeepAlive.SuccessfulExit=false` is the launchd analog of
|
|
187
|
+
* systemd's `RestartForceExitStatus=75`: respawn on ANY non-zero
|
|
188
|
+
* exit; do not respawn on exit 0. The graceful-restart path
|
|
189
|
+
* always exits with 75, which is non-zero, so the daemon respawns
|
|
190
|
+
* automatically.
|
|
191
|
+
*
|
|
192
|
+
* `userPath` should be the captured login-shell PATH so Homebrew /
|
|
193
|
+
* nvm / cargo / etc. are reachable.
|
|
194
|
+
*/
|
|
195
|
+
function generateLaunchdPlist(ctx) {
|
|
196
|
+
const envEntries = Object.entries({
|
|
197
|
+
AIDEN_DAEMON: '1',
|
|
198
|
+
AIDEN_PORT: String(ctx.port),
|
|
199
|
+
AIDEN_DAEMON_AUTO_RESTART: '0',
|
|
200
|
+
...(ctx.userPath ? { PATH: ctx.userPath } : {}),
|
|
201
|
+
...(ctx.env ?? {}),
|
|
202
|
+
});
|
|
203
|
+
const envXml = envEntries
|
|
204
|
+
.map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
|
|
205
|
+
.join('\n');
|
|
206
|
+
const stdoutXml = ctx.stdoutPath
|
|
207
|
+
? ` <key>StandardOutPath</key>\n <string>${escapeXml(ctx.stdoutPath)}</string>\n`
|
|
208
|
+
: '';
|
|
209
|
+
const stderrXml = ctx.stderrPath
|
|
210
|
+
? ` <key>StandardErrorPath</key>\n <string>${escapeXml(ctx.stderrPath)}</string>\n`
|
|
211
|
+
: '';
|
|
212
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
213
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
214
|
+
<plist version="1.0">
|
|
215
|
+
<dict>
|
|
216
|
+
<key>Label</key>
|
|
217
|
+
<string>com.aiden.daemon</string>
|
|
218
|
+
<key>ProgramArguments</key>
|
|
219
|
+
<array>
|
|
220
|
+
<string>${escapeXml(ctx.nodeBin)}</string>
|
|
221
|
+
<string>${escapeXml(ctx.bundlePath)}</string>
|
|
222
|
+
</array>
|
|
223
|
+
<key>EnvironmentVariables</key>
|
|
224
|
+
<dict>
|
|
225
|
+
${envXml}
|
|
226
|
+
</dict>
|
|
227
|
+
<key>RunAtLoad</key>
|
|
228
|
+
<true/>
|
|
229
|
+
<key>KeepAlive</key>
|
|
230
|
+
<dict>
|
|
231
|
+
<key>SuccessfulExit</key>
|
|
232
|
+
<false/>
|
|
233
|
+
</dict>
|
|
234
|
+
<key>WorkingDirectory</key>
|
|
235
|
+
<string>${escapeXml(ctx.workingDir)}</string>
|
|
236
|
+
${stdoutXml}${stderrXml}</dict>
|
|
237
|
+
</plist>
|
|
238
|
+
`;
|
|
239
|
+
}
|
|
240
|
+
function escapeXml(s) {
|
|
241
|
+
return s
|
|
242
|
+
.replace(/&/g, '&')
|
|
243
|
+
.replace(/</g, '<')
|
|
244
|
+
.replace(/>/g, '>')
|
|
245
|
+
.replace(/"/g, '"')
|
|
246
|
+
.replace(/'/g, ''');
|
|
247
|
+
}
|
|
248
|
+
// ── Windows guidance (docs-only) ───────────────────────────────────────────
|
|
249
|
+
/**
|
|
250
|
+
* Phase 1 deliberately does NOT auto-generate Scheduled Task /
|
|
251
|
+
* Windows Service entries. NSSM/SCM variance + admin requirements
|
|
252
|
+
* make an automatic installer too risky for the v4.5 ship. The CLI
|
|
253
|
+
* `aiden daemon install` on Windows prints this guidance and exits
|
|
254
|
+
* with code 0 without writing anything.
|
|
255
|
+
*/
|
|
256
|
+
function windowsServiceGuidance() {
|
|
257
|
+
return [
|
|
258
|
+
'Aiden v4.5 does not auto-install a Windows service in Phase 1.',
|
|
259
|
+
'',
|
|
260
|
+
'Recommended approaches:',
|
|
261
|
+
' - Foreground: aiden daemon start',
|
|
262
|
+
' (the internal supervisor keeps the daemon running',
|
|
263
|
+
' until you close the terminal)',
|
|
264
|
+
' - Background: Use a third-party supervisor like `pm2` or `nssm`.',
|
|
265
|
+
' Example with pm2:',
|
|
266
|
+
' npm install -g pm2',
|
|
267
|
+
' pm2 start aiden -- daemon start',
|
|
268
|
+
' pm2 save && pm2 startup',
|
|
269
|
+
'',
|
|
270
|
+
'See docs/v4.5/daemon-windows.md for details.',
|
|
271
|
+
].join('\n');
|
|
272
|
+
}
|