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,389 @@
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/dispatcher/dispatcher.ts — v4.5 Phase 5a.
10
+ *
11
+ * The bus consumer. Bridges the durable trigger queue
12
+ * (`triggerBus`) to the agent loop (`DaemonAgentRunner`).
13
+ *
14
+ * Loop body (workerCount-bounded, default 1 per Q-P5-1):
15
+ *
16
+ * 1. `triggerBus.claim({ownerId, leaseMs})` — atomic claim.
17
+ * 2. Read trigger spec row (`triggers` table) for fire_rate_limit /
18
+ * prompt_template / deliver_only. Missing row → defaults
19
+ * (no template, not deliver-only, no fire-rate cap).
20
+ * 3. Build sessionId via `buildTriggerSessionId`.
21
+ * 4. Render prompt template (if any) with payload vars. Missing
22
+ * vars + non-empty template → markFailed with
23
+ * `trigger_misconfigured` classification hint.
24
+ * 5. Start lease-renew timer (default 60s cadence, extends by
25
+ * `leaseMs`). Runs for the lifetime of the invocation.
26
+ * 6. Invoke runner. Catch any throw.
27
+ * 7. On clean return → `markDone(eventId, claimToken, runId)`.
28
+ * On throw or error finish-reason → `markFailed` which auto-
29
+ * transitions to `dead_letter` after `maxAttempts`.
30
+ * 8. Renew timer cleared.
31
+ *
32
+ * Concurrency: a semaphore (in-flight count vs workerCount)
33
+ * guards the claim loop. Each claim spawns an async worker that
34
+ * decrements the semaphore on exit. The poll interval is short
35
+ * (250ms) when no event was claimed; it ratchets up via the
36
+ * adaptive backoff inside `_pollOnce` when the bus is empty.
37
+ *
38
+ * Shutdown: `stop(timeoutMs)` flips an `_stopping` flag, then
39
+ * awaits the in-flight worker promises (race against
40
+ * `timeoutMs`). Workers that exceed the deadline are NOT killed
41
+ * (the runner contract is cooperative-stop; runs eventually
42
+ * land in `markFailed` via the claim-lease-expired path on the
43
+ * next instance).
44
+ */
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.createDispatcher = createDispatcher;
47
+ exports._dispatcherOwnerId = _dispatcherOwnerId;
48
+ const node_crypto_1 = require("node:crypto");
49
+ const sessionId_1 = require("./sessionId");
50
+ const promptTemplate_1 = require("./promptTemplate");
51
+ const agentRunner_1 = require("./agentRunner");
52
+ const realAgentRunner_1 = require("./realAgentRunner");
53
+ // ── Defaults ───────────────────────────────────────────────────────────────
54
+ const DEFAULT_WORKER_COUNT = 1;
55
+ const DEFAULT_POLL_IDLE_MS = 250;
56
+ const DEFAULT_POLL_BUSY_MS = 0; // immediate re-poll while events drain
57
+ const DEFAULT_LEASE_MS = 5 * 60000;
58
+ const DEFAULT_RENEW_MS = 60000;
59
+ const DEFAULT_MAX_ATTEMPTS = 3;
60
+ const DEFAULT_STOP_TIMEOUT_MS = 30000;
61
+ // ── Implementation ─────────────────────────────────────────────────────────
62
+ function createDispatcher(opts) {
63
+ const workerCount = opts.workerCount ?? DEFAULT_WORKER_COUNT;
64
+ const leaseMs = opts.leaseMs ?? DEFAULT_LEASE_MS;
65
+ const renewMs = opts.renewMs ?? DEFAULT_RENEW_MS;
66
+ const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
67
+ const pollIdleMs = opts.pollIdleMs ?? DEFAULT_POLL_IDLE_MS;
68
+ const log = opts.log ?? (() => { });
69
+ let runner = null;
70
+ // v4.5 Phase 7c — `runnerKind` lets diagnostics + tests distinguish
71
+ // the placeholder-runner phase from the real-agent-runner phase.
72
+ // The factory-bound initial runner is tagged 'placeholder' by
73
+ // default; `installRunner` flips to 'real'.
74
+ let _runnerKind = 'none';
75
+ let _started = false;
76
+ let _stopping = false;
77
+ let _pollTimer = null;
78
+ const _inflight = new Map();
79
+ const _workerPromises = new Set();
80
+ const _stats = {
81
+ claimed: 0,
82
+ succeeded: 0,
83
+ failed: 0,
84
+ deadLetter: 0,
85
+ deliverOnly: 0,
86
+ misconfigured: 0,
87
+ };
88
+ // ── Trigger spec lookup ─────────────────────────────────────────────────
89
+ function readTriggerSpec(sourceKey) {
90
+ try {
91
+ const row = opts.db
92
+ .prepare('SELECT * FROM triggers WHERE id = ?')
93
+ .get(sourceKey);
94
+ return row ?? null;
95
+ }
96
+ catch (e) {
97
+ log('warn', `[dispatcher] failed to read trigger spec ${sourceKey}: ${e instanceof Error ? e.message : String(e)}`);
98
+ return null;
99
+ }
100
+ }
101
+ // ── Build TriggerInvocationContext ──────────────────────────────────────
102
+ function buildContext(event, spec) {
103
+ return {
104
+ triggerId: event.sourceKey,
105
+ source: event.source,
106
+ sourceKey: event.sourceKey,
107
+ fireReason: typeof event.payload.fireReason === 'string'
108
+ ? event.payload.fireReason
109
+ : 'trigger_fired',
110
+ eventId: event.id,
111
+ attempt: event.attempts,
112
+ maxAttempts,
113
+ promptTemplate: spec?.prompt_template ?? null,
114
+ };
115
+ }
116
+ // ── Render initial message ──────────────────────────────────────────────
117
+ /**
118
+ * Returns the rendered message + missing-vars list. When no
119
+ * template is set, falls back to a structured JSON dump of the
120
+ * payload — the agent gets a readable initial prompt even
121
+ * without operator-supplied templates.
122
+ */
123
+ function renderInitialMessage(event, template) {
124
+ if (template && template.length > 0) {
125
+ const vars = (0, promptTemplate_1.flattenPayloadToVars)(event.payload);
126
+ // Inject a couple of dispatcher-known fields so templates can
127
+ // reference {{eventId}} / {{source}} / {{attempt}} / etc.
128
+ vars.eventId = event.id;
129
+ vars.source = event.source;
130
+ vars.sourceKey = event.sourceKey;
131
+ vars.attempt = event.attempts;
132
+ const { rendered, missing } = (0, promptTemplate_1.renderPromptTemplate)(template, vars);
133
+ return { message: rendered, missing };
134
+ }
135
+ // Fallback: structured payload header so the model knows the
136
+ // source + can reason over the JSON body. Keeps the initial
137
+ // message useful when the operator skipped the template.
138
+ return {
139
+ message: defaultInitialMessage(event),
140
+ missing: [],
141
+ };
142
+ }
143
+ function defaultInitialMessage(event) {
144
+ const header = `Trigger fired: ${event.source} (id=${event.sourceKey})`;
145
+ let body;
146
+ try {
147
+ body = JSON.stringify(event.payload, null, 2);
148
+ }
149
+ catch {
150
+ body = '<payload not serialisable>';
151
+ }
152
+ return `${header}\n\nPayload:\n\`\`\`json\n${body}\n\`\`\``;
153
+ }
154
+ // ── Lease renewal ───────────────────────────────────────────────────────
155
+ function startRenewTimer(eventId, claimToken) {
156
+ const t = setInterval(() => {
157
+ try {
158
+ const ok = opts.triggerBus.renewClaim(eventId, claimToken, leaseMs);
159
+ if (!ok) {
160
+ log('warn', `[dispatcher] renew failed eventId=${eventId} — claim invalidated`);
161
+ clearInterval(t);
162
+ }
163
+ }
164
+ catch (e) {
165
+ log('warn', `[dispatcher] renew threw eventId=${eventId}: ${e instanceof Error ? e.message : String(e)}`);
166
+ }
167
+ }, renewMs);
168
+ if (typeof t.unref === 'function')
169
+ t.unref();
170
+ return t;
171
+ }
172
+ // ── Process one claim ───────────────────────────────────────────────────
173
+ async function processClaim(event) {
174
+ const spec = readTriggerSpec(event.sourceKey);
175
+ const context = buildContext(event, spec);
176
+ const sessionId = (0, sessionId_1.buildTriggerSessionId)({
177
+ source: event.source,
178
+ sourceKey: event.sourceKey,
179
+ idempotencyKey: event.idempotencyKey,
180
+ });
181
+ // Record in-flight.
182
+ const inflightRow = {
183
+ eventId: event.id,
184
+ sessionId,
185
+ source: event.source,
186
+ startedAt: Date.now(),
187
+ };
188
+ _inflight.set(event.id, inflightRow);
189
+ // Start renew timer; clean up on every exit path.
190
+ const renewTimer = startRenewTimer(event.id, event.claimToken);
191
+ try {
192
+ // Render initial message + check missing vars.
193
+ const { message, missing } = renderInitialMessage(event, spec?.prompt_template ?? null);
194
+ if (missing.length > 0 && spec?.prompt_template) {
195
+ const reason = `trigger_misconfigured: template references undefined vars: ${missing.join(', ')}`;
196
+ log('warn', `[dispatcher] ${reason} (eventId=${event.id} trigger=${event.sourceKey})`);
197
+ // misconfigured = permanent failure; no cooldown needed since
198
+ // retry won't help, but we still call markFailed (which will
199
+ // dead-letter after maxAttempts).
200
+ opts.triggerBus.markFailed(event.id, event.claimToken, reason, { maxAttempts });
201
+ _stats.misconfigured += 1;
202
+ _stats.failed += 1;
203
+ // markFailed may transition to dead_letter when attempts >= maxAttempts.
204
+ if (event.attempts >= maxAttempts)
205
+ _stats.deadLetter += 1;
206
+ return;
207
+ }
208
+ // Build the input + branch on deliverOnly.
209
+ const deliverOnly = spec?.deliver_only === 1;
210
+ const input = {
211
+ sessionId,
212
+ instanceId: opts.instanceId,
213
+ triggerEventId: event.id,
214
+ triggerContext: context,
215
+ initialMessage: message,
216
+ deliverOnly,
217
+ };
218
+ let result;
219
+ if (deliverOnly) {
220
+ result = (0, agentRunner_1.deliverOnlyStub)(input, opts.runStore);
221
+ _stats.deliverOnly += 1;
222
+ }
223
+ else {
224
+ if (!runner) {
225
+ // Safety net — start() guarantees runner is set, but be
226
+ // defensive in case of test misuse.
227
+ throw new Error('dispatcher: runnerFactory has not been invoked');
228
+ }
229
+ // Discard return — we just need it to not be unused.
230
+ void agentRunner_1.buildInitialHistory;
231
+ result = await runner.invoke(input);
232
+ }
233
+ // Map finishReason to bus action.
234
+ if (result.finishReason === 'error') {
235
+ const errMsg = result.error ?? 'agent reported error finish';
236
+ opts.triggerBus.markFailed(event.id, event.claimToken, errMsg, {
237
+ maxAttempts,
238
+ cooldownMs: (0, realAgentRunner_1.computeRetryCooldownMs)(event.attempts),
239
+ });
240
+ _stats.failed += 1;
241
+ if (event.attempts >= maxAttempts)
242
+ _stats.deadLetter += 1;
243
+ return;
244
+ }
245
+ // Success — markDone with the runId.
246
+ opts.triggerBus.markDone(event.id, event.claimToken, result.runId);
247
+ _stats.succeeded += 1;
248
+ }
249
+ catch (e) {
250
+ const msg = e instanceof Error ? (e.stack ?? e.message) : String(e);
251
+ log('error', `[dispatcher] worker threw eventId=${event.id}: ${msg}`);
252
+ try {
253
+ opts.triggerBus.markFailed(event.id, event.claimToken, msg.slice(0, 500), {
254
+ maxAttempts,
255
+ cooldownMs: (0, realAgentRunner_1.computeRetryCooldownMs)(event.attempts),
256
+ });
257
+ }
258
+ catch { /* bus may be in a weird state; swallow */ }
259
+ _stats.failed += 1;
260
+ if (event.attempts >= maxAttempts)
261
+ _stats.deadLetter += 1;
262
+ }
263
+ finally {
264
+ clearInterval(renewTimer);
265
+ _inflight.delete(event.id);
266
+ }
267
+ }
268
+ // ── Worker semaphore + claim loop ───────────────────────────────────────
269
+ async function _pollOnce() {
270
+ if (_stopping)
271
+ return null;
272
+ if (_inflight.size >= workerCount)
273
+ return null;
274
+ const event = opts.triggerBus.claim({ ownerId: opts.ownerId, leaseMs });
275
+ if (!event)
276
+ return null;
277
+ _stats.claimed += 1;
278
+ log('info', `[dispatcher] claimed eventId=${event.id} source=${event.source} attempt=${event.attempts}/${maxAttempts}`);
279
+ const p = processClaim(event).finally(() => {
280
+ _workerPromises.delete(p);
281
+ });
282
+ _workerPromises.add(p);
283
+ return event.id;
284
+ }
285
+ function _schedulePoll() {
286
+ if (_stopping)
287
+ return;
288
+ if (_pollTimer)
289
+ return;
290
+ _pollTimer = setTimeout(async () => {
291
+ _pollTimer = null;
292
+ try {
293
+ const claimedId = await _pollOnce();
294
+ // Adaptive: if we just claimed something, re-poll immediately
295
+ // (drain bus); otherwise wait `pollIdleMs`.
296
+ const next = claimedId !== null ? DEFAULT_POLL_BUSY_MS : pollIdleMs;
297
+ if (!_stopping) {
298
+ _pollTimer = setTimeout(_schedulePollFromTimer, next);
299
+ if (_pollTimer && typeof _pollTimer.unref === 'function')
300
+ _pollTimer.unref();
301
+ }
302
+ }
303
+ catch (e) {
304
+ log('error', `[dispatcher] poll threw: ${e instanceof Error ? e.message : String(e)}`);
305
+ if (!_stopping) {
306
+ _pollTimer = setTimeout(_schedulePollFromTimer, pollIdleMs);
307
+ if (_pollTimer && typeof _pollTimer.unref === 'function')
308
+ _pollTimer.unref();
309
+ }
310
+ }
311
+ }, pollIdleMs);
312
+ if (typeof _pollTimer.unref === 'function')
313
+ _pollTimer.unref();
314
+ }
315
+ function _schedulePollFromTimer() {
316
+ _pollTimer = null;
317
+ _schedulePoll();
318
+ }
319
+ return {
320
+ start() {
321
+ if (_started)
322
+ return;
323
+ _started = true;
324
+ runner = opts.runnerFactory();
325
+ // Initial runner from the factory is the placeholder unless the
326
+ // factory itself returned a real runner. The CLI's two-phase
327
+ // bootstrap (Phase 7c) starts with a placeholder factory and
328
+ // calls `installRunner` later with the real one.
329
+ _runnerKind = 'placeholder';
330
+ log('info', `[dispatcher] starting workerCount=${workerCount} leaseMs=${leaseMs} runner=placeholder`);
331
+ _schedulePoll();
332
+ },
333
+ installRunner(next) {
334
+ // Atomic swap — JS single-threaded execution means the read in
335
+ // _pumpOnce / processClaim can never see a half-installed runner.
336
+ runner = next;
337
+ _runnerKind = 'real';
338
+ log('info', `[dispatcher] runner swapped → real (next claim uses new runner)`);
339
+ },
340
+ runnerKind() {
341
+ return _runnerKind;
342
+ },
343
+ async stop(timeoutMs = DEFAULT_STOP_TIMEOUT_MS) {
344
+ _stopping = true;
345
+ if (_pollTimer) {
346
+ clearTimeout(_pollTimer);
347
+ _pollTimer = null;
348
+ }
349
+ // Race in-flight drain against the timeout.
350
+ const drain = Promise.allSettled([..._workerPromises]);
351
+ const deadline = new Promise((resolve) => {
352
+ const t = setTimeout(() => resolve(), timeoutMs);
353
+ if (typeof t.unref === 'function')
354
+ t.unref();
355
+ });
356
+ await Promise.race([drain, deadline]);
357
+ log('info', `[dispatcher] stopped — inflight=${_inflight.size} processed=${_stats.claimed}`);
358
+ },
359
+ inflight() {
360
+ return [..._inflight.values()];
361
+ },
362
+ stats() {
363
+ return { ..._stats };
364
+ },
365
+ async _pumpOnce() {
366
+ if (!runner)
367
+ runner = opts.runnerFactory();
368
+ const event = opts.triggerBus.claim({ ownerId: opts.ownerId, leaseMs });
369
+ if (!event)
370
+ return null;
371
+ _stats.claimed += 1;
372
+ // Track the worker promise so stop() can wait on it.
373
+ const p = processClaim(event);
374
+ _workerPromises.add(p);
375
+ try {
376
+ await p;
377
+ }
378
+ finally {
379
+ _workerPromises.delete(p);
380
+ }
381
+ return event.id;
382
+ },
383
+ };
384
+ }
385
+ // Unique id helper (kept here so dispatchers in tests can use the
386
+ // same util their producers do when fabricating event ids).
387
+ function _dispatcherOwnerId(prefix = 'disp') {
388
+ return `${prefix}-${(0, node_crypto_1.randomUUID)()}`;
389
+ }
@@ -0,0 +1,113 @@
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/dispatcher/fireRateLimiter.ts — v4.5 Phase 5a.
10
+ *
11
+ * Per-trigger sliding-window fire-rate cap.
12
+ *
13
+ * Q-P5-5(c): unlimited by default. Each trigger spec carries an
14
+ * optional `fireRateLimit` (rows-per-window). When set, this
15
+ * module enforces a per-triggerId sliding window of size
16
+ * `windowMs` (default 1 hour). The Nth fire within the window is
17
+ * the LAST one allowed; the (N+1)th and beyond are blocked.
18
+ *
19
+ * Behaviour when blocked:
20
+ * - `check()` returns `false` + populates `reason` field
21
+ * - producer chooses what to do (typical: insert event + immediately
22
+ * dead-letter so the operator has forensic visibility via
23
+ * `/api/daemon/triggers/<id>/stats`)
24
+ *
25
+ * Anti-thrash motivation (from the prior-systems learning batch): a
26
+ * misconfigured webhook upstream can fire 60k times per minute. A
27
+ * busted file watcher on a temp directory can fire 1000 times per
28
+ * second. The producer-side cap stops the bus from inflating and
29
+ * keeps the operator's ability to query "what blew up" intact.
30
+ *
31
+ * Storage: in-memory `Map<triggerId, number[]>` of fire timestamps
32
+ * within the active window. Pruned lazily on each `check()` —
33
+ * old timestamps drop off the front when they fall outside `now -
34
+ * windowMs`. Bounded by max-fires-per-window (no unbounded growth
35
+ * even if the producer keeps hammering — once over the cap,
36
+ * incoming attempts are rejected immediately without recording).
37
+ *
38
+ * Idempotent reset via `__resetForTests()`.
39
+ */
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.createFireRateLimiter = createFireRateLimiter;
42
+ exports.getFireRateLimiter = getFireRateLimiter;
43
+ exports.__resetFireRateLimiterSingletonForTests = __resetFireRateLimiterSingletonForTests;
44
+ const DEFAULT_WINDOW_MS = 60 * 60 * 1000; // 1 hour
45
+ function createFireRateLimiter(opts = {}) {
46
+ const windowMs = opts.windowMs ?? DEFAULT_WINDOW_MS;
47
+ const windows = new Map();
48
+ function prune(triggerId, now) {
49
+ const cutoff = now - windowMs;
50
+ const list = windows.get(triggerId);
51
+ if (!list)
52
+ return [];
53
+ // Find the first timestamp >= cutoff and slice. Most-common case:
54
+ // few or zero entries to drop, so iterate from the front.
55
+ let i = 0;
56
+ while (i < list.length && list[i] < cutoff)
57
+ i++;
58
+ if (i > 0) {
59
+ const kept = list.slice(i);
60
+ if (kept.length === 0)
61
+ windows.delete(triggerId);
62
+ else
63
+ windows.set(triggerId, kept);
64
+ return kept;
65
+ }
66
+ return list;
67
+ }
68
+ return {
69
+ check(triggerId, limit, now = Date.now()) {
70
+ // Unlimited path — bypass entirely. No window recording.
71
+ if (limit === null || limit === undefined || limit <= 0) {
72
+ return { allowed: true, windowCount: 0, limit };
73
+ }
74
+ const list = prune(triggerId, now);
75
+ if (list.length >= limit) {
76
+ return {
77
+ allowed: false,
78
+ windowCount: list.length,
79
+ limit,
80
+ reason: `fire-rate cap exceeded: ${list.length}/${limit} per ${Math.round(windowMs / 1000)}s window`,
81
+ };
82
+ }
83
+ // Allowed — record the fire.
84
+ const next = [...list, now];
85
+ windows.set(triggerId, next);
86
+ return { allowed: true, windowCount: next.length, limit };
87
+ },
88
+ peek(triggerId, now = Date.now()) {
89
+ return prune(triggerId, now).length;
90
+ },
91
+ reset(triggerId) {
92
+ windows.delete(triggerId);
93
+ },
94
+ __resetForTests() {
95
+ windows.clear();
96
+ },
97
+ };
98
+ }
99
+ // Process-wide singleton — most call sites (producer-side fire
100
+ // gates) want a shared limiter. Tests instantiate their own.
101
+ let _singleton = null;
102
+ /** Return the process-wide limiter, creating it on first call. */
103
+ function getFireRateLimiter() {
104
+ if (!_singleton)
105
+ _singleton = createFireRateLimiter();
106
+ return _singleton;
107
+ }
108
+ /** Test-only — reset the singleton. */
109
+ function __resetFireRateLimiterSingletonForTests() {
110
+ if (_singleton)
111
+ _singleton.__resetForTests();
112
+ _singleton = null;
113
+ }
@@ -0,0 +1,53 @@
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/dispatcher/index.ts — v4.5 Phase 5a barrel.
10
+ *
11
+ * Single import point for the trigger dispatcher subsystem.
12
+ * Callers should import from here (`../dispatcher`) rather than
13
+ * the individual module files so internal refactors stay
14
+ * encapsulated.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.createPerTurnBudgetWatcher = exports.consumePostTurn = exports.evaluatePreTurn = exports.utcDateKey = exports.createDailyBudgetTracker = exports.DEFAULT_DAEMON_APPROVAL_POLICY = exports.isDaemonApprovalPolicy = exports.decideForPolicy = exports.buildDaemonApprovalCallbacks = exports.resolveDaemonModel = exports.RETRY_DECISION = exports.computeRetryCooldownMs = exports.createRealAgentRunner = exports.makeRunner = exports.deliverOnlyStub = exports.buildInitialHistory = exports.__resetFireRateLimiterSingletonForTests = exports.getFireRateLimiter = exports.createFireRateLimiter = exports.flattenPayloadToVars = exports.renderPromptTemplate = exports.parseTriggerSessionId = exports.buildTriggerSessionId = exports._dispatcherOwnerId = exports.createDispatcher = void 0;
18
+ var dispatcher_1 = require("./dispatcher");
19
+ Object.defineProperty(exports, "createDispatcher", { enumerable: true, get: function () { return dispatcher_1.createDispatcher; } });
20
+ Object.defineProperty(exports, "_dispatcherOwnerId", { enumerable: true, get: function () { return dispatcher_1._dispatcherOwnerId; } });
21
+ var sessionId_1 = require("./sessionId");
22
+ Object.defineProperty(exports, "buildTriggerSessionId", { enumerable: true, get: function () { return sessionId_1.buildTriggerSessionId; } });
23
+ Object.defineProperty(exports, "parseTriggerSessionId", { enumerable: true, get: function () { return sessionId_1.parseTriggerSessionId; } });
24
+ var promptTemplate_1 = require("./promptTemplate");
25
+ Object.defineProperty(exports, "renderPromptTemplate", { enumerable: true, get: function () { return promptTemplate_1.renderPromptTemplate; } });
26
+ Object.defineProperty(exports, "flattenPayloadToVars", { enumerable: true, get: function () { return promptTemplate_1.flattenPayloadToVars; } });
27
+ var fireRateLimiter_1 = require("./fireRateLimiter");
28
+ Object.defineProperty(exports, "createFireRateLimiter", { enumerable: true, get: function () { return fireRateLimiter_1.createFireRateLimiter; } });
29
+ Object.defineProperty(exports, "getFireRateLimiter", { enumerable: true, get: function () { return fireRateLimiter_1.getFireRateLimiter; } });
30
+ Object.defineProperty(exports, "__resetFireRateLimiterSingletonForTests", { enumerable: true, get: function () { return fireRateLimiter_1.__resetFireRateLimiterSingletonForTests; } });
31
+ var agentRunner_1 = require("./agentRunner");
32
+ Object.defineProperty(exports, "buildInitialHistory", { enumerable: true, get: function () { return agentRunner_1.buildInitialHistory; } });
33
+ Object.defineProperty(exports, "deliverOnlyStub", { enumerable: true, get: function () { return agentRunner_1.deliverOnlyStub; } });
34
+ Object.defineProperty(exports, "makeRunner", { enumerable: true, get: function () { return agentRunner_1.makeRunner; } });
35
+ // v4.5 Phase 7 — real agent runner + dependencies.
36
+ var realAgentRunner_1 = require("./realAgentRunner");
37
+ Object.defineProperty(exports, "createRealAgentRunner", { enumerable: true, get: function () { return realAgentRunner_1.createRealAgentRunner; } });
38
+ Object.defineProperty(exports, "computeRetryCooldownMs", { enumerable: true, get: function () { return realAgentRunner_1.computeRetryCooldownMs; } });
39
+ Object.defineProperty(exports, "RETRY_DECISION", { enumerable: true, get: function () { return realAgentRunner_1.RETRY_DECISION; } });
40
+ var resolveModel_1 = require("./resolveModel");
41
+ Object.defineProperty(exports, "resolveDaemonModel", { enumerable: true, get: function () { return resolveModel_1.resolveDaemonModel; } });
42
+ var daemonApproval_1 = require("./daemonApproval");
43
+ Object.defineProperty(exports, "buildDaemonApprovalCallbacks", { enumerable: true, get: function () { return daemonApproval_1.buildDaemonApprovalCallbacks; } });
44
+ Object.defineProperty(exports, "decideForPolicy", { enumerable: true, get: function () { return daemonApproval_1.decideForPolicy; } });
45
+ Object.defineProperty(exports, "isDaemonApprovalPolicy", { enumerable: true, get: function () { return daemonApproval_1.isDaemonApprovalPolicy; } });
46
+ Object.defineProperty(exports, "DEFAULT_DAEMON_APPROVAL_POLICY", { enumerable: true, get: function () { return daemonApproval_1.DEFAULT_DAEMON_APPROVAL_POLICY; } });
47
+ var dailyBudgetTracker_1 = require("./dailyBudgetTracker");
48
+ Object.defineProperty(exports, "createDailyBudgetTracker", { enumerable: true, get: function () { return dailyBudgetTracker_1.createDailyBudgetTracker; } });
49
+ Object.defineProperty(exports, "utcDateKey", { enumerable: true, get: function () { return dailyBudgetTracker_1.utcDateKey; } });
50
+ var budgetGate_1 = require("./budgetGate");
51
+ Object.defineProperty(exports, "evaluatePreTurn", { enumerable: true, get: function () { return budgetGate_1.evaluatePreTurn; } });
52
+ Object.defineProperty(exports, "consumePostTurn", { enumerable: true, get: function () { return budgetGate_1.consumePostTurn; } });
53
+ Object.defineProperty(exports, "createPerTurnBudgetWatcher", { enumerable: true, get: function () { return budgetGate_1.createPerTurnBudgetWatcher; } });
@@ -0,0 +1,95 @@
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/dispatcher/promptTemplate.ts — v4.5 Phase 5a.
10
+ *
11
+ * Minimal `{{var}}` interpolation for trigger prompt templates.
12
+ *
13
+ * Per Q-P5-2(a): NO conditionals, NO loops, NO escapes. A trigger's
14
+ * spec.promptTemplate is a one-line-or-multi-line string with
15
+ * `{{path}}`, `{{event}}`, `{{from}}`, `{{subject}}` placeholders.
16
+ * The dispatcher renders it with payload-derived variables when
17
+ * deliverOnly is true OR the agent's initial message comes from a
18
+ * template.
19
+ *
20
+ * Missing-variable behaviour: the placeholder is LEFT IN PLACE and
21
+ * the name is collected in `missing`. The dispatcher decides what
22
+ * to do — currently it classifies a non-empty `missing` array as
23
+ * `trigger_misconfigured` when the template was non-empty.
24
+ *
25
+ * Whitespace inside braces tolerated: `{{ path }}` ≡ `{{path}}`.
26
+ * Unknown shapes (nested braces, `{{ }}` empty name) fail soft:
27
+ * left in place, not collected.
28
+ *
29
+ * Pure module — no I/O, no side effects, fully synchronous.
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.renderPromptTemplate = renderPromptTemplate;
33
+ exports.flattenPayloadToVars = flattenPayloadToVars;
34
+ const PLACEHOLDER = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
35
+ /**
36
+ * Render a template with `{{var}}` placeholders.
37
+ *
38
+ * - Variables are looked up by exact name (after trimming whitespace).
39
+ * - Numeric / boolean values are stringified via `String(v)`.
40
+ * - `null` / `undefined` are treated as missing → placeholder left
41
+ * in the output AND the variable name pushed onto `missing`.
42
+ * - Empty / whitespace-only template → returns `{ rendered: '', missing: [] }`.
43
+ *
44
+ * @param template Raw template string (may be empty).
45
+ * @param vars Variable map. Excess keys are ignored.
46
+ */
47
+ function renderPromptTemplate(template, vars) {
48
+ if (typeof template !== 'string' || template.length === 0) {
49
+ return { rendered: '', missing: [] };
50
+ }
51
+ const missingSet = new Set();
52
+ const rendered = template.replace(PLACEHOLDER, (match, name) => {
53
+ const v = vars[name];
54
+ if (v === undefined || v === null) {
55
+ missingSet.add(name);
56
+ return match; // leave placeholder in place
57
+ }
58
+ return String(v);
59
+ });
60
+ return { rendered, missing: [...missingSet] };
61
+ }
62
+ /**
63
+ * Convenience helper for the common case where the dispatcher
64
+ * already has a TriggerEventRow payload + a few fixed fields
65
+ * and wants to render the template for that event. Caller can
66
+ * also call `renderPromptTemplate` directly with any var map.
67
+ *
68
+ * Variable shape varies by source — this helper just flattens
69
+ * the payload into top-level keys (e.g. `payload.from` →
70
+ * `{{from}}`). String/number/boolean values pass through;
71
+ * objects/arrays are JSON-stringified so they're at least
72
+ * substitutable (though usually undesirable in a user-facing
73
+ * prompt — Phase 5b cron may extend this with `{{json:foo}}`
74
+ * if needed).
75
+ */
76
+ function flattenPayloadToVars(payload) {
77
+ const out = {};
78
+ for (const [k, v] of Object.entries(payload)) {
79
+ if (v === null || v === undefined) {
80
+ out[k] = null;
81
+ }
82
+ else if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
83
+ out[k] = v;
84
+ }
85
+ else {
86
+ try {
87
+ out[k] = JSON.stringify(v);
88
+ }
89
+ catch {
90
+ out[k] = String(v);
91
+ }
92
+ }
93
+ }
94
+ return out;
95
+ }