aiden-runtime 4.1.5 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +265 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +536 -152
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +245 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
  7. package/dist/cli/v4/commands/browserDepth.js +45 -0
  8. package/dist/cli/v4/commands/cron.js +264 -0
  9. package/dist/cli/v4/commands/daemon.js +541 -0
  10. package/dist/cli/v4/commands/daemonStatus.js +253 -0
  11. package/dist/cli/v4/commands/fanout.js +42 -59
  12. package/dist/cli/v4/commands/help.js +13 -0
  13. package/dist/cli/v4/commands/index.js +35 -1
  14. package/dist/cli/v4/commands/mcp.js +80 -54
  15. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  16. package/dist/cli/v4/commands/recovery.js +122 -0
  17. package/dist/cli/v4/commands/runs.js +223 -0
  18. package/dist/cli/v4/commands/sandbox.js +48 -0
  19. package/dist/cli/v4/commands/spawnPause.js +93 -0
  20. package/dist/cli/v4/commands/suggestions.js +68 -0
  21. package/dist/cli/v4/commands/tce.js +41 -0
  22. package/dist/cli/v4/commands/trigger.js +378 -0
  23. package/dist/cli/v4/commands/update.js +95 -3
  24. package/dist/cli/v4/daemonAgentBuilder.js +145 -0
  25. package/dist/cli/v4/defaultSoul.js +1 -1
  26. package/dist/cli/v4/display/capabilityCard.js +26 -0
  27. package/dist/cli/v4/display.js +18 -8
  28. package/dist/cli/v4/replyRenderer.js +31 -23
  29. package/dist/cli/v4/updateBootPrompt.js +170 -0
  30. package/dist/core/playwrightBridge.js +129 -0
  31. package/dist/core/v4/aidenAgent.js +527 -5
  32. package/dist/core/v4/browserState.js +436 -0
  33. package/dist/core/v4/checkpoint.js +79 -0
  34. package/dist/core/v4/daemon/bootstrap.js +651 -0
  35. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  36. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  37. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  38. package/dist/core/v4/daemon/cron/migration.js +199 -0
  39. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  40. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  41. package/dist/core/v4/daemon/db/connection.js +106 -0
  42. package/dist/core/v4/daemon/db/migrations.js +362 -0
  43. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  44. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  45. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  46. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  47. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  48. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  49. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  50. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  51. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  52. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  53. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  54. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  55. package/dist/core/v4/daemon/drain.js +156 -0
  56. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  57. package/dist/core/v4/daemon/health.js +159 -0
  58. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  59. package/dist/core/v4/daemon/index.js +179 -0
  60. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  61. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  62. package/dist/core/v4/daemon/restartCode.js +32 -0
  63. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  64. package/dist/core/v4/daemon/runStore.js +144 -0
  65. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  66. package/dist/core/v4/daemon/signals.js +50 -0
  67. package/dist/core/v4/daemon/supervisor.js +272 -0
  68. package/dist/core/v4/daemon/triggerBus.js +279 -0
  69. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  70. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  71. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  72. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  73. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  74. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  75. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  76. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  77. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  78. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  79. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  80. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  81. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  82. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  83. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  84. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  85. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  86. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  87. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  88. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  89. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  90. package/dist/core/v4/daemon/types.js +15 -0
  91. package/dist/core/v4/dockerSession.js +461 -0
  92. package/dist/core/v4/dryRun.js +117 -0
  93. package/dist/core/v4/failureClassifier.js +779 -0
  94. package/dist/core/v4/providerFallback.js +35 -2
  95. package/dist/core/v4/recoveryReport.js +449 -0
  96. package/dist/core/v4/runtimeToggles.js +214 -0
  97. package/dist/core/v4/sandboxConfig.js +285 -0
  98. package/dist/core/v4/sandboxFs.js +316 -0
  99. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  100. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  101. package/dist/core/v4/subagent/childBuilder.js +391 -0
  102. package/dist/core/v4/subagent/fanout.js +75 -51
  103. package/dist/core/v4/subagent/spawnPause.js +191 -0
  104. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  105. package/dist/core/v4/suggestionCatalog.js +41 -0
  106. package/dist/core/v4/suggestionEngine.js +210 -0
  107. package/dist/core/v4/toolRegistry.js +37 -3
  108. package/dist/core/v4/turnState.js +587 -0
  109. package/dist/core/v4/update/checkUpdate.js +63 -3
  110. package/dist/core/v4/update/installMethodDetect.js +115 -0
  111. package/dist/core/v4/update/registryClient.js +121 -0
  112. package/dist/core/v4/update/skipState.js +75 -0
  113. package/dist/core/v4/verifier.js +448 -0
  114. package/dist/core/version.js +1 -1
  115. package/dist/moat/plannerGuard.js +29 -0
  116. package/dist/providers/v4/anthropicAdapter.js +31 -3
  117. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  118. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  119. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  120. package/dist/tools/v4/browser/_observer.js +224 -0
  121. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  122. package/dist/tools/v4/browser/browserClick.js +18 -1
  123. package/dist/tools/v4/browser/browserClose.js +18 -1
  124. package/dist/tools/v4/browser/browserExtract.js +5 -1
  125. package/dist/tools/v4/browser/browserFill.js +17 -1
  126. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  127. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  128. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  129. package/dist/tools/v4/browser/browserScroll.js +18 -1
  130. package/dist/tools/v4/browser/browserType.js +17 -1
  131. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  132. package/dist/tools/v4/executeCode.js +1 -0
  133. package/dist/tools/v4/files/fileCopy.js +56 -2
  134. package/dist/tools/v4/files/fileDelete.js +38 -1
  135. package/dist/tools/v4/files/fileList.js +12 -1
  136. package/dist/tools/v4/files/fileMove.js +59 -2
  137. package/dist/tools/v4/files/filePatch.js +43 -1
  138. package/dist/tools/v4/files/fileRead.js +12 -1
  139. package/dist/tools/v4/files/fileWrite.js +41 -1
  140. package/dist/tools/v4/index.js +88 -61
  141. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  142. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  143. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  144. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  145. package/dist/tools/v4/process/processKill.js +19 -0
  146. package/dist/tools/v4/process/processList.js +1 -0
  147. package/dist/tools/v4/process/processLogRead.js +1 -0
  148. package/dist/tools/v4/process/processSpawn.js +13 -0
  149. package/dist/tools/v4/process/processWait.js +1 -0
  150. package/dist/tools/v4/sessions/recallSession.js +1 -0
  151. package/dist/tools/v4/sessions/sessionList.js +1 -0
  152. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  153. package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
  154. package/dist/tools/v4/skills/skillManage.js +13 -0
  155. package/dist/tools/v4/skills/skillView.js +1 -0
  156. package/dist/tools/v4/skills/skillsList.js +1 -0
  157. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  158. package/dist/tools/v4/subagent/subagentFanout.js +54 -1
  159. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  160. package/dist/tools/v4/system/appClose.js +13 -0
  161. package/dist/tools/v4/system/appInput.js +13 -0
  162. package/dist/tools/v4/system/appLaunch.js +13 -0
  163. package/dist/tools/v4/system/clipboardRead.js +1 -0
  164. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  165. package/dist/tools/v4/system/mediaKey.js +12 -0
  166. package/dist/tools/v4/system/mediaSessions.js +1 -0
  167. package/dist/tools/v4/system/mediaTransport.js +13 -0
  168. package/dist/tools/v4/system/naturalEvents.js +1 -0
  169. package/dist/tools/v4/system/nowPlaying.js +1 -0
  170. package/dist/tools/v4/system/osProcessList.js +1 -0
  171. package/dist/tools/v4/system/screenshot.js +1 -0
  172. package/dist/tools/v4/system/systemInfo.js +1 -0
  173. package/dist/tools/v4/system/volumeSet.js +17 -0
  174. package/dist/tools/v4/terminal/shellExec.js +81 -9
  175. package/dist/tools/v4/web/deepResearch.js +1 -0
  176. package/dist/tools/v4/web/openUrl.js +1 -0
  177. package/dist/tools/v4/web/webFetch.js +1 -0
  178. package/dist/tools/v4/web/webPage.js +1 -0
  179. package/dist/tools/v4/web/webSearch.js +1 -0
  180. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  181. package/package.json +13 -3
@@ -0,0 +1,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
+ }