aiden-runtime 4.1.5 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +265 -847
- package/dist/api/server.js +32 -5
- package/dist/cli/v4/aidenCLI.js +536 -152
- package/dist/cli/v4/callbacks.js +170 -0
- package/dist/cli/v4/chatSession.js +245 -3
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -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/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +13 -0
- package/dist/cli/v4/commands/index.js +35 -1
- package/dist/cli/v4/commands/mcp.js +80 -54
- package/dist/cli/v4/commands/plannerGuard.js +53 -0
- package/dist/cli/v4/commands/recovery.js +122 -0
- package/dist/cli/v4/commands/runs.js +223 -0
- package/dist/cli/v4/commands/sandbox.js +48 -0
- package/dist/cli/v4/commands/spawnPause.js +93 -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 +145 -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 +527 -5
- package/dist/core/v4/browserState.js +436 -0
- package/dist/core/v4/checkpoint.js +79 -0
- package/dist/core/v4/daemon/bootstrap.js +651 -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 +362 -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 +144 -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/providerFallback.js +35 -2
- package/dist/core/v4/recoveryReport.js +449 -0
- package/dist/core/v4/runtimeToggles.js +214 -0
- package/dist/core/v4/sandboxConfig.js +285 -0
- package/dist/core/v4/sandboxFs.js +316 -0
- package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
- package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
- package/dist/core/v4/subagent/childBuilder.js +391 -0
- package/dist/core/v4/subagent/fanout.js +75 -51
- package/dist/core/v4/subagent/spawnPause.js +191 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
- package/dist/core/v4/suggestionCatalog.js +41 -0
- package/dist/core/v4/suggestionEngine.js +210 -0
- package/dist/core/v4/toolRegistry.js +37 -3
- 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/moat/plannerGuard.js +29 -0
- package/dist/providers/v4/anthropicAdapter.js +31 -3
- package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
- package/dist/providers/v4/codexResponsesAdapter.js +25 -2
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
- 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 +88 -61
- 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 +7 -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/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +54 -1
- 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 +13 -3
|
@@ -0,0 +1,156 @@
|
|
|
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/drain.ts — v4.5 Phase 1: 5-step ordered shutdown drain.
|
|
10
|
+
*
|
|
11
|
+
* The shutdown sequence is order-sensitive. Getting it wrong wastes
|
|
12
|
+
* the drain budget on the wrong thing OR loses attribution on
|
|
13
|
+
* subprocess cleanup. Sequence:
|
|
14
|
+
*
|
|
15
|
+
* Step 0: markShuttingDown(reason) — record intent in DB
|
|
16
|
+
* Step 1: notifySessions() — let sessions emit "shutting
|
|
17
|
+
* down" while adapters are up
|
|
18
|
+
* Step 2: drain active runs (timeout)
|
|
19
|
+
* - on timeout: mark each still-active run with
|
|
20
|
+
* resume_pending + interrupt + wait 5s for cooperation
|
|
21
|
+
* Step 3: kill tool subprocesses — BEFORE adapter teardown,
|
|
22
|
+
* so they don't get reaped
|
|
23
|
+
* by the cgroup and lose
|
|
24
|
+
* attribution
|
|
25
|
+
* Step 4: close resources — parallel: browser, docker,
|
|
26
|
+
* cron, idempotency, sqlite
|
|
27
|
+
* (resourceRegistry.reapAll)
|
|
28
|
+
* Step 5: mark daemon_instances shutdown — final DB write
|
|
29
|
+
* release runtime lock
|
|
30
|
+
* touch .clean_shutdown marker
|
|
31
|
+
* process.exit(exitCode)
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.isDraining = isDraining;
|
|
35
|
+
exports._resetDrainStateForTests = _resetDrainStateForTests;
|
|
36
|
+
exports.performDrain = performDrain;
|
|
37
|
+
exports.signalToReason = signalToReason;
|
|
38
|
+
const POST_INTERRUPT_GRACE_MS = 5000;
|
|
39
|
+
/** True once a drain has started — used by signal handlers to de-bounce. */
|
|
40
|
+
let _draining = false;
|
|
41
|
+
function isDraining() { return _draining; }
|
|
42
|
+
function _resetDrainStateForTests() { _draining = false; }
|
|
43
|
+
async function performDrain(opts) {
|
|
44
|
+
if (_draining) {
|
|
45
|
+
// Idempotent — a second signal during drain is a no-op.
|
|
46
|
+
return { durationMs: 0, drainTimedOut: false, resumePendingIds: [], reapedResources: null };
|
|
47
|
+
}
|
|
48
|
+
_draining = true;
|
|
49
|
+
const startedAt = Date.now();
|
|
50
|
+
// ── Step 0: mark shutting down ────────────────────────────────────────
|
|
51
|
+
try {
|
|
52
|
+
opts.markShutdown?.(opts.reason, opts.exitCode ?? 0);
|
|
53
|
+
}
|
|
54
|
+
catch { /* noop */ }
|
|
55
|
+
// (Note: markShutdown is called twice — once here to set
|
|
56
|
+
// shutdown_reason early so observers see the daemon is exiting,
|
|
57
|
+
// again in Step 5 with the final exit_code.)
|
|
58
|
+
// ── Step 1: notify sessions ───────────────────────────────────────────
|
|
59
|
+
try {
|
|
60
|
+
await Promise.resolve(opts.notifySessions?.());
|
|
61
|
+
}
|
|
62
|
+
catch { /* never block on notify */ }
|
|
63
|
+
// ── Step 2: drain active runs with timeout ────────────────────────────
|
|
64
|
+
const activeIds = await Promise.resolve(opts.activeRuns?.() ?? []);
|
|
65
|
+
const resumePendingIds = [];
|
|
66
|
+
let drainTimedOut = false;
|
|
67
|
+
if (activeIds.length > 0 && opts.drainTimeoutMs > 0) {
|
|
68
|
+
// Implementation note: the daemon's run-completion path is
|
|
69
|
+
// event-driven, but Phase 1 doesn't yet have a "wait for runs
|
|
70
|
+
// to finish" primitive (Phase 5 will, when runs are wired to
|
|
71
|
+
// the agent loop). Phase 1's behaviour: simply wait
|
|
72
|
+
// drainTimeoutMs, then check if any runs are still active and
|
|
73
|
+
// mark them resume_pending. This is correct for Phase 1's
|
|
74
|
+
// shape (no triggers wired = no active daemon-runs to drain).
|
|
75
|
+
await sleep(opts.drainTimeoutMs);
|
|
76
|
+
const stillActive = await Promise.resolve(opts.activeRuns?.() ?? []);
|
|
77
|
+
if (stillActive.length > 0) {
|
|
78
|
+
drainTimedOut = true;
|
|
79
|
+
for (const runId of stillActive) {
|
|
80
|
+
try {
|
|
81
|
+
await Promise.resolve(opts.markResumePending?.(runId, 'drain_timeout'));
|
|
82
|
+
}
|
|
83
|
+
catch { /* best-effort */ }
|
|
84
|
+
try {
|
|
85
|
+
await Promise.resolve(opts.interruptRun?.(runId, 'shutdown'));
|
|
86
|
+
}
|
|
87
|
+
catch { /* best-effort */ }
|
|
88
|
+
resumePendingIds.push(runId);
|
|
89
|
+
}
|
|
90
|
+
await sleep(opts.postInterruptGraceMs ?? POST_INTERRUPT_GRACE_MS);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ── Step 3: kill tool subprocesses BEFORE adapter teardown ────────────
|
|
94
|
+
try {
|
|
95
|
+
await Promise.resolve(opts.killToolSubprocesses?.('post-interrupt'));
|
|
96
|
+
}
|
|
97
|
+
catch { /* best-effort */ }
|
|
98
|
+
// ── Step 4: close resources (parallel) ────────────────────────────────
|
|
99
|
+
let reapedResources = null;
|
|
100
|
+
try {
|
|
101
|
+
const tasks = [];
|
|
102
|
+
if (opts.closeBrowser)
|
|
103
|
+
tasks.push(Promise.resolve(opts.closeBrowser()).catch(() => undefined));
|
|
104
|
+
if (opts.closeCron)
|
|
105
|
+
tasks.push(Promise.resolve(opts.closeCron()).catch(() => undefined));
|
|
106
|
+
if (opts.closeDocker)
|
|
107
|
+
tasks.push(Promise.resolve(opts.closeDocker()).catch(() => undefined));
|
|
108
|
+
if (opts.closeIdempotency)
|
|
109
|
+
tasks.push(Promise.resolve(opts.closeIdempotency()).catch(() => undefined));
|
|
110
|
+
if (opts.closeResources)
|
|
111
|
+
tasks.push(Promise.resolve(opts.closeResources())
|
|
112
|
+
.then((r) => {
|
|
113
|
+
if (r && typeof r === 'object' && 'reaped' in r && 'failed' in r) {
|
|
114
|
+
reapedResources = r;
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
.catch(() => undefined));
|
|
118
|
+
if (tasks.length > 0)
|
|
119
|
+
await Promise.all(tasks);
|
|
120
|
+
}
|
|
121
|
+
catch { /* never block shutdown on resource close */ }
|
|
122
|
+
// ── Step 5: final markers + exit ──────────────────────────────────────
|
|
123
|
+
try {
|
|
124
|
+
opts.markShutdown?.(opts.reason, opts.exitCode ?? 0);
|
|
125
|
+
}
|
|
126
|
+
catch { /* noop */ }
|
|
127
|
+
try {
|
|
128
|
+
opts.touchCleanShutdown?.();
|
|
129
|
+
}
|
|
130
|
+
catch { /* noop */ }
|
|
131
|
+
try {
|
|
132
|
+
opts.removePid?.();
|
|
133
|
+
}
|
|
134
|
+
catch { /* noop */ }
|
|
135
|
+
const durationMs = Date.now() - startedAt;
|
|
136
|
+
if (opts.callProcessExit !== false) {
|
|
137
|
+
// Default — exit the process. Tests pass `callProcessExit: false`.
|
|
138
|
+
process.exit(opts.exitCode ?? 0);
|
|
139
|
+
}
|
|
140
|
+
return { durationMs, drainTimedOut, resumePendingIds, reapedResources };
|
|
141
|
+
}
|
|
142
|
+
function sleep(ms) {
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
const t = setTimeout(resolve, ms);
|
|
145
|
+
if (typeof t.unref === 'function')
|
|
146
|
+
t.unref();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** Convenience for callers that want a shutdown reason from a signal name. */
|
|
150
|
+
function signalToReason(signal) {
|
|
151
|
+
switch (signal) {
|
|
152
|
+
case 'SIGINT': return 'sigint';
|
|
153
|
+
case 'SIGTERM': return 'sigterm';
|
|
154
|
+
case 'SIGUSR1': return 'sigusr1_restart';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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/eventLoopLag.ts — v4.5 Phase 1: event-loop responsiveness.
|
|
10
|
+
*
|
|
11
|
+
* A ticking timer measures how long it takes the event loop to fire
|
|
12
|
+
* a 100ms `setInterval`. Lag = `(actual - expected)`. A healthy
|
|
13
|
+
* loop reports lag ≤ a few ms; a saturated loop blows out into
|
|
14
|
+
* hundreds of ms.
|
|
15
|
+
*
|
|
16
|
+
* Consumed by:
|
|
17
|
+
* - /health/live — endpoint returns 500 when lag > 5s for > 5s
|
|
18
|
+
* - /metrics — `aiden_daemon_event_loop_lag_ms` gauge
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.startEventLoopLagSampler = startEventLoopLagSampler;
|
|
22
|
+
exports.stopEventLoopLagSampler = stopEventLoopLagSampler;
|
|
23
|
+
exports.getEventLoopLagMs = getEventLoopLagMs;
|
|
24
|
+
exports.getLastTickAt = getLastTickAt;
|
|
25
|
+
exports.isEventLoopResponsive = isEventLoopResponsive;
|
|
26
|
+
const SAMPLE_INTERVAL_MS = 100;
|
|
27
|
+
let _timer = null;
|
|
28
|
+
let _lastTickAt = 0;
|
|
29
|
+
let _lastLagMs = 0;
|
|
30
|
+
function tick() {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
if (_lastTickAt !== 0) {
|
|
33
|
+
_lastLagMs = Math.max(0, now - _lastTickAt - SAMPLE_INTERVAL_MS);
|
|
34
|
+
}
|
|
35
|
+
_lastTickAt = now;
|
|
36
|
+
}
|
|
37
|
+
/** Start the sampler. Idempotent. */
|
|
38
|
+
function startEventLoopLagSampler() {
|
|
39
|
+
if (_timer)
|
|
40
|
+
return;
|
|
41
|
+
_lastTickAt = Date.now();
|
|
42
|
+
_lastLagMs = 0;
|
|
43
|
+
_timer = setInterval(tick, SAMPLE_INTERVAL_MS);
|
|
44
|
+
if (typeof _timer.unref === 'function')
|
|
45
|
+
_timer.unref();
|
|
46
|
+
}
|
|
47
|
+
/** Stop the sampler. Idempotent. */
|
|
48
|
+
function stopEventLoopLagSampler() {
|
|
49
|
+
if (!_timer)
|
|
50
|
+
return;
|
|
51
|
+
clearInterval(_timer);
|
|
52
|
+
_timer = null;
|
|
53
|
+
_lastTickAt = 0;
|
|
54
|
+
_lastLagMs = 0;
|
|
55
|
+
}
|
|
56
|
+
/** Most-recent sampled lag in ms. Zero when sampler hasn't run yet. */
|
|
57
|
+
function getEventLoopLagMs() {
|
|
58
|
+
return _lastLagMs;
|
|
59
|
+
}
|
|
60
|
+
/** Wall-clock time of the last successful tick (0 when never). */
|
|
61
|
+
function getLastTickAt() {
|
|
62
|
+
return _lastTickAt;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Liveness verdict: true when the event loop has ticked within the
|
|
66
|
+
* tolerance window. The `tolerance` defaults to 5s, matching the
|
|
67
|
+
* /health/live endpoint's threshold.
|
|
68
|
+
*/
|
|
69
|
+
function isEventLoopResponsive(toleranceMs = 5000) {
|
|
70
|
+
if (_lastTickAt === 0)
|
|
71
|
+
return false;
|
|
72
|
+
return Date.now() - _lastTickAt <= toleranceMs;
|
|
73
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
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/health.ts — v4.5 Phase 1: health + metrics endpoints.
|
|
10
|
+
*
|
|
11
|
+
* Three layered health endpoints + a Prometheus-style /metrics:
|
|
12
|
+
*
|
|
13
|
+
* /health/live — 200 if the event loop is responsive
|
|
14
|
+
* (event-loop-lag sampler ticked within 5s).
|
|
15
|
+
* Returns 500 otherwise. Used by external
|
|
16
|
+
* watchdogs (Kubernetes liveness, simple
|
|
17
|
+
* curl checks).
|
|
18
|
+
*
|
|
19
|
+
* /health/ready — 200 if the daemon can accept new triggers
|
|
20
|
+
* AND the SQLite DB is writable. 503 otherwise.
|
|
21
|
+
*
|
|
22
|
+
* /health/degraded — 200 with { degraded: boolean, reasons }.
|
|
23
|
+
* Reasons surface resource-budget overruns,
|
|
24
|
+
* non-zero dead_letter count, stale cron
|
|
25
|
+
* heartbeat. Designed for dashboards, not
|
|
26
|
+
* load balancers.
|
|
27
|
+
*
|
|
28
|
+
* /metrics — text/plain Prometheus exposition format.
|
|
29
|
+
*
|
|
30
|
+
* /api/daemon/status — JSON: instance + uptime + version + counts
|
|
31
|
+
* /api/daemon/resources — JSON: registry list + budgetByKind
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.evaluateDegraded = evaluateDegraded;
|
|
35
|
+
exports.mountHealthEndpoints = mountHealthEndpoints;
|
|
36
|
+
const eventLoopLag_1 = require("./eventLoopLag");
|
|
37
|
+
function evaluateDegraded(deps) {
|
|
38
|
+
const reasons = [];
|
|
39
|
+
// Trigger bus health.
|
|
40
|
+
try {
|
|
41
|
+
const stats = deps.triggerBus.stats();
|
|
42
|
+
if (stats.deadLetter > 0) {
|
|
43
|
+
reasons.push({
|
|
44
|
+
code: 'dead_letter_nonzero',
|
|
45
|
+
message: `${stats.deadLetter} trigger event(s) in dead_letter`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (stats.oldestPendingMs != null && stats.oldestPendingMs > 60 * 60 * 1000) {
|
|
49
|
+
reasons.push({
|
|
50
|
+
code: 'pending_stale',
|
|
51
|
+
message: `oldest pending trigger is ${Math.round(stats.oldestPendingMs / 1000)}s old`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
reasons.push({
|
|
57
|
+
code: 'trigger_bus_error',
|
|
58
|
+
message: e instanceof Error ? e.message : String(e),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Event-loop lag.
|
|
62
|
+
const lag = (0, eventLoopLag_1.getEventLoopLagMs)();
|
|
63
|
+
if (lag > 1000) {
|
|
64
|
+
reasons.push({ code: 'event_loop_lag', message: `${lag}ms` });
|
|
65
|
+
}
|
|
66
|
+
return reasons;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Mount the v4.5 daemon health endpoints onto an Express router.
|
|
70
|
+
* Idempotent at the endpoint level (Express dedup is the caller's
|
|
71
|
+
* responsibility — call once during server boot).
|
|
72
|
+
*/
|
|
73
|
+
function mountHealthEndpoints(router, deps) {
|
|
74
|
+
router.get('/health/live', (_req, res) => {
|
|
75
|
+
const ok = (0, eventLoopLag_1.isEventLoopResponsive)(5000);
|
|
76
|
+
res
|
|
77
|
+
.status(ok ? 200 : 500)
|
|
78
|
+
.json({ ok, lagMs: (0, eventLoopLag_1.getEventLoopLagMs)() });
|
|
79
|
+
});
|
|
80
|
+
router.get('/health/ready', (_req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const row = deps.db.prepare('SELECT 1 AS v').get();
|
|
83
|
+
const dbOk = row?.v === 1;
|
|
84
|
+
res.status(dbOk ? 200 : 503).json({ ok: dbOk, db: dbOk ? 'ready' : 'unreachable' });
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
res.status(503).json({
|
|
88
|
+
ok: false,
|
|
89
|
+
db: 'error',
|
|
90
|
+
error: e instanceof Error ? e.message : String(e),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
router.get('/health/degraded', (_req, res) => {
|
|
95
|
+
const reasons = evaluateDegraded(deps);
|
|
96
|
+
res.status(200).json({ degraded: reasons.length > 0, reasons });
|
|
97
|
+
});
|
|
98
|
+
router.get('/metrics', (_req, res) => {
|
|
99
|
+
const mu = process.memoryUsage();
|
|
100
|
+
const ts = deps.triggerBus.stats();
|
|
101
|
+
const budgets = deps.resourceRegistry.budgetByKind();
|
|
102
|
+
const inst = deps.instanceTracker.current();
|
|
103
|
+
const uptimeSec = inst ? Math.floor((Date.now() - inst.startedAt) / 1000) : 0;
|
|
104
|
+
const lines = [];
|
|
105
|
+
const m = (name, value, help, type = 'gauge') => {
|
|
106
|
+
if (help)
|
|
107
|
+
lines.push(`# HELP ${name} ${help}`);
|
|
108
|
+
lines.push(`# TYPE ${name} ${type}`);
|
|
109
|
+
lines.push(`${name} ${value}`);
|
|
110
|
+
};
|
|
111
|
+
m('aiden_daemon_rss_bytes', mu.rss, 'Resident set size in bytes');
|
|
112
|
+
m('aiden_daemon_heap_used_bytes', mu.heapUsed, 'V8 heap used in bytes');
|
|
113
|
+
m('aiden_daemon_event_loop_lag_ms', (0, eventLoopLag_1.getEventLoopLagMs)(), 'Event loop lag in ms');
|
|
114
|
+
m('aiden_daemon_uptime_seconds', uptimeSec, 'Daemon uptime in seconds');
|
|
115
|
+
m('aiden_daemon_trigger_pending', ts.pending, 'Pending trigger events');
|
|
116
|
+
m('aiden_daemon_trigger_claimed', ts.claimed, 'Claimed trigger events');
|
|
117
|
+
m('aiden_daemon_trigger_running', ts.running, 'Running trigger events');
|
|
118
|
+
m('aiden_daemon_trigger_deadletter', ts.deadLetter, 'Dead-letter trigger events');
|
|
119
|
+
for (const [kind, b] of Object.entries(budgets)) {
|
|
120
|
+
lines.push(`# HELP aiden_daemon_resource_count Count of registered resources by kind`);
|
|
121
|
+
lines.push(`# TYPE aiden_daemon_resource_count gauge`);
|
|
122
|
+
lines.push(`aiden_daemon_resource_count{kind="${kind}"} ${b.count}`);
|
|
123
|
+
if (b.budgetUnits > 0) {
|
|
124
|
+
lines.push(`# HELP aiden_daemon_resource_budget Sum of budget units by kind`);
|
|
125
|
+
lines.push(`# TYPE aiden_daemon_resource_budget gauge`);
|
|
126
|
+
lines.push(`aiden_daemon_resource_budget{kind="${kind}"} ${b.budgetUnits}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
res.type('text/plain; version=0.0.4').send(lines.join('\n') + '\n');
|
|
130
|
+
});
|
|
131
|
+
router.get('/api/daemon/status', (_req, res) => {
|
|
132
|
+
const inst = deps.instanceTracker.current();
|
|
133
|
+
const stats = deps.triggerBus.stats();
|
|
134
|
+
res.status(200).json({
|
|
135
|
+
instance: inst,
|
|
136
|
+
version: deps.version,
|
|
137
|
+
uptimeMs: inst ? Date.now() - inst.startedAt : 0,
|
|
138
|
+
triggers: stats,
|
|
139
|
+
eventLoopLagMs: (0, eventLoopLag_1.getEventLoopLagMs)(),
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
router.get('/api/daemon/resources', (_req, res) => {
|
|
143
|
+
const list = deps.resourceRegistry.list().map((r) => ({
|
|
144
|
+
id: r.id,
|
|
145
|
+
kind: r.kind,
|
|
146
|
+
owner: r.owner,
|
|
147
|
+
createdAt: r.createdAt,
|
|
148
|
+
lastUsedAt: r.lastUsedAt,
|
|
149
|
+
ttlMs: r.ttlMs,
|
|
150
|
+
budgetUnits: r.budgetUnits,
|
|
151
|
+
metadata: r.metadata,
|
|
152
|
+
}));
|
|
153
|
+
res.status(200).json({
|
|
154
|
+
total: list.length,
|
|
155
|
+
budgetByKind: deps.resourceRegistry.budgetByKind(),
|
|
156
|
+
resources: list,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
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/idempotencyStore.ts — v4.5 Phase 1: L1/L2 cache.
|
|
10
|
+
*
|
|
11
|
+
* Two-tier cache for "exactly once" semantics on webhook deliveries
|
|
12
|
+
* (Phase 3) and authenticated API runs (Phase 1+).
|
|
13
|
+
*
|
|
14
|
+
* L1 — in-memory `Map<scopeKey, IdempotencyEntry>` for hot path.
|
|
15
|
+
* L2 — SQLite `idempotency_keys` for durability across restarts.
|
|
16
|
+
*
|
|
17
|
+
* Workflow:
|
|
18
|
+
* - getOrSet(scope, key, fingerprint, compute):
|
|
19
|
+
* 1. Look up L1; if hit and unexpired → return cached.
|
|
20
|
+
* 2. Look up L2; if hit and unexpired → reseed L1 → return cached.
|
|
21
|
+
* 3. Miss: invoke compute(), persist to L2 + L1, return.
|
|
22
|
+
* - Daemon boot calls `reseed()` once to load unexpired L2 rows
|
|
23
|
+
* into L1 (warm-start the in-memory cache).
|
|
24
|
+
* - A background sweep deletes L2 rows whose `expires_at < now`.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.MAX_L1_ENTRIES = exports.SWEEP_INTERVAL_MS = exports.DEFAULT_TTL_MS = void 0;
|
|
28
|
+
exports.createIdempotencyStore = createIdempotencyStore;
|
|
29
|
+
exports.DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
30
|
+
exports.SWEEP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
31
|
+
exports.MAX_L1_ENTRIES = 4096;
|
|
32
|
+
function scopeKey(scope, key) {
|
|
33
|
+
return `${scope}::${key}`;
|
|
34
|
+
}
|
|
35
|
+
function createIdempotencyStore(opts) {
|
|
36
|
+
const db = opts.db;
|
|
37
|
+
const defaultTtl = opts.defaultTtlMs ?? exports.DEFAULT_TTL_MS;
|
|
38
|
+
const sweepInterval = opts.sweepIntervalMs ?? exports.SWEEP_INTERVAL_MS;
|
|
39
|
+
const l1 = new Map();
|
|
40
|
+
// ── L1 eviction (FIFO past cap) ──
|
|
41
|
+
function l1Insert(entry) {
|
|
42
|
+
const k = scopeKey(entry.scope, entry.key);
|
|
43
|
+
l1.set(k, entry);
|
|
44
|
+
if (l1.size > exports.MAX_L1_ENTRIES) {
|
|
45
|
+
const first = l1.keys().next().value;
|
|
46
|
+
if (first !== undefined)
|
|
47
|
+
l1.delete(first);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function readFromL2(scope, key, now) {
|
|
51
|
+
const row = db
|
|
52
|
+
.prepare(`SELECT * FROM idempotency_keys WHERE scope = ? AND key = ?`)
|
|
53
|
+
.get(scope, key);
|
|
54
|
+
if (!row)
|
|
55
|
+
return null;
|
|
56
|
+
if (row.expires_at <= now)
|
|
57
|
+
return null;
|
|
58
|
+
return {
|
|
59
|
+
scope: row.scope,
|
|
60
|
+
key: row.key,
|
|
61
|
+
fingerprint: row.fingerprint,
|
|
62
|
+
responseJson: row.response_json,
|
|
63
|
+
statusCode: row.status_code,
|
|
64
|
+
createdAt: row.created_at,
|
|
65
|
+
expiresAt: row.expires_at,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function writeToL2(entry) {
|
|
69
|
+
db.prepare(`INSERT OR REPLACE INTO idempotency_keys
|
|
70
|
+
(scope, key, fingerprint, response_json, status_code, created_at, expires_at)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(entry.scope, entry.key, entry.fingerprint, entry.responseJson, entry.statusCode, entry.createdAt, entry.expiresAt);
|
|
72
|
+
}
|
|
73
|
+
// Initial reseed from L2 (cheap — bounded by `MAX_L1_ENTRIES`).
|
|
74
|
+
function reseedFromL2() {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const rows = db
|
|
77
|
+
.prepare(`SELECT * FROM idempotency_keys
|
|
78
|
+
WHERE expires_at > ?
|
|
79
|
+
ORDER BY created_at DESC
|
|
80
|
+
LIMIT ?`)
|
|
81
|
+
.all(now, exports.MAX_L1_ENTRIES);
|
|
82
|
+
let loaded = 0;
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
l1Insert({
|
|
85
|
+
scope: row.scope,
|
|
86
|
+
key: row.key,
|
|
87
|
+
fingerprint: row.fingerprint,
|
|
88
|
+
responseJson: row.response_json,
|
|
89
|
+
statusCode: row.status_code,
|
|
90
|
+
createdAt: row.created_at,
|
|
91
|
+
expiresAt: row.expires_at,
|
|
92
|
+
});
|
|
93
|
+
loaded += 1;
|
|
94
|
+
}
|
|
95
|
+
return { loaded };
|
|
96
|
+
}
|
|
97
|
+
function sweep(now) {
|
|
98
|
+
const cutoff = now ?? Date.now();
|
|
99
|
+
const r = db
|
|
100
|
+
.prepare(`DELETE FROM idempotency_keys WHERE expires_at < ?`)
|
|
101
|
+
.run(cutoff);
|
|
102
|
+
// Also evict expired L1 entries.
|
|
103
|
+
for (const [k, v] of l1) {
|
|
104
|
+
if (v.expiresAt < cutoff)
|
|
105
|
+
l1.delete(k);
|
|
106
|
+
}
|
|
107
|
+
return { deleted: r.changes };
|
|
108
|
+
}
|
|
109
|
+
// Background sweep timer.
|
|
110
|
+
let sweepTimer = null;
|
|
111
|
+
if (sweepInterval > 0) {
|
|
112
|
+
sweepTimer = setInterval(() => {
|
|
113
|
+
try {
|
|
114
|
+
sweep();
|
|
115
|
+
}
|
|
116
|
+
catch { /* never let sweep crash */ }
|
|
117
|
+
}, sweepInterval);
|
|
118
|
+
if (typeof sweepTimer.unref === 'function')
|
|
119
|
+
sweepTimer.unref();
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
async getOrSet(scope, key, fingerprint, compute, ttlMs) {
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const sk = scopeKey(scope, key);
|
|
125
|
+
let entry = l1.get(sk);
|
|
126
|
+
if (!entry || entry.expiresAt <= now) {
|
|
127
|
+
const fromL2 = readFromL2(scope, key, now);
|
|
128
|
+
if (fromL2) {
|
|
129
|
+
l1Insert(fromL2);
|
|
130
|
+
entry = fromL2;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (entry) {
|
|
134
|
+
if (fingerprint != null && entry.fingerprint != null && entry.fingerprint !== fingerprint) {
|
|
135
|
+
// Fingerprint mismatch — treat as a new request. Compute fresh.
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
return { responseJson: entry.responseJson, statusCode: entry.statusCode };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const computed = await compute();
|
|
142
|
+
const ttl = ttlMs ?? defaultTtl;
|
|
143
|
+
const newEntry = {
|
|
144
|
+
scope,
|
|
145
|
+
key,
|
|
146
|
+
fingerprint,
|
|
147
|
+
responseJson: computed.responseJson,
|
|
148
|
+
statusCode: computed.statusCode,
|
|
149
|
+
createdAt: now,
|
|
150
|
+
expiresAt: now + ttl,
|
|
151
|
+
};
|
|
152
|
+
writeToL2(newEntry);
|
|
153
|
+
l1Insert(newEntry);
|
|
154
|
+
return computed;
|
|
155
|
+
},
|
|
156
|
+
get(scope, key) {
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const sk = scopeKey(scope, key);
|
|
159
|
+
let entry = l1.get(sk);
|
|
160
|
+
if (!entry || entry.expiresAt <= now) {
|
|
161
|
+
const fromL2 = readFromL2(scope, key, now);
|
|
162
|
+
if (!fromL2)
|
|
163
|
+
return null;
|
|
164
|
+
l1Insert(fromL2);
|
|
165
|
+
entry = fromL2;
|
|
166
|
+
}
|
|
167
|
+
return { responseJson: entry.responseJson, statusCode: entry.statusCode };
|
|
168
|
+
},
|
|
169
|
+
set(scope, key, fingerprint, response, ttlMs) {
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
const ttl = ttlMs ?? defaultTtl;
|
|
172
|
+
const entry = {
|
|
173
|
+
scope,
|
|
174
|
+
key,
|
|
175
|
+
fingerprint,
|
|
176
|
+
responseJson: response.responseJson,
|
|
177
|
+
statusCode: response.statusCode,
|
|
178
|
+
createdAt: now,
|
|
179
|
+
expiresAt: now + ttl,
|
|
180
|
+
};
|
|
181
|
+
writeToL2(entry);
|
|
182
|
+
l1Insert(entry);
|
|
183
|
+
},
|
|
184
|
+
sweepExpired(now) {
|
|
185
|
+
return sweep(now);
|
|
186
|
+
},
|
|
187
|
+
reseed() {
|
|
188
|
+
return reseedFromL2();
|
|
189
|
+
},
|
|
190
|
+
stats() {
|
|
191
|
+
const row = db
|
|
192
|
+
.prepare(`SELECT COUNT(*) AS c FROM idempotency_keys`)
|
|
193
|
+
.get();
|
|
194
|
+
return { l1: l1.size, l2: row.c };
|
|
195
|
+
},
|
|
196
|
+
close() {
|
|
197
|
+
if (sweepTimer) {
|
|
198
|
+
clearInterval(sweepTimer);
|
|
199
|
+
sweepTimer = null;
|
|
200
|
+
}
|
|
201
|
+
l1.clear();
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|