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,356 @@
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/realAgentRunner.ts — v4.5 Phase 7.
10
+ *
11
+ * Replaces the Phase 5a placeholder runner with a real
12
+ * `AidenAgent.runConversation` invocation.
13
+ *
14
+ * Why this is a *factory* + *injectable agent builder* rather than
15
+ * a direct `new AidenAgent(...)` call: the daemon module sits
16
+ * BELOW the CLI in the import graph — pulling provider /
17
+ * toolExecutor / plannerGuard / honesty / skillTeacher / ...
18
+ * construction into `core/v4/daemon` would invert the dependency
19
+ * direction. Instead, the CLI (which already owns agent
20
+ * construction for the REPL) injects an `AgentBuilder` function
21
+ * the runner calls per turn. Tests pass stubs.
22
+ *
23
+ * Bootstrap wiring (in `bootstrap.ts`):
24
+ *
25
+ * - When AIDEN_DAEMON=1 AND an `agentBuilder` is provided →
26
+ * `createRealAgentRunner({ ..., agentBuilder })` is the
27
+ * dispatcher's runner factory.
28
+ * - When no builder is provided → falls back to the Phase 5a
29
+ * placeholder (still useful for rails-only integration tests
30
+ * and for environments where the user has no provider
31
+ * configured yet).
32
+ *
33
+ * Lifecycle per claim:
34
+ *
35
+ * 1. evaluatePreTurn — global daily budget check; reject with
36
+ * `trigger_quota` tag when exhausted.
37
+ * 2. resolveDaemonModel — trigger spec → env → persisted chain.
38
+ * 3. buildDaemonApprovalCallbacks — non-interactive auto-decide
39
+ * per Q-P7-1a policy.
40
+ * 4. createPerTurnBudgetWatcher — per-trigger soft cap; AbortSignal
41
+ * threads into the agent invocation.
42
+ * 5. runStore.create() → runId; emit `dispatcher:invoked` with
43
+ * sessionId, model, modelSource, policy, dailySnapshot.
44
+ * 6. Build initial history via buildInitialHistory(input).
45
+ * 7. agentBuilder({...}) → AidenAgent (caller-injected).
46
+ * 8. agent.runConversation(history) — major events emitted via
47
+ * onToolCall + onBudgetWarning hooks → run_events.
48
+ * 9. Post-turn: consumePostTurn updates daily tracker; emit
49
+ * `dispatcher:completed` with finishReason + totalTokens +
50
+ * classification + retry decision.
51
+ * 10. Map AidenAgentResult → DaemonAgentResult.
52
+ *
53
+ * Failure handling: any throw or `finishReason: 'error'` is
54
+ * surfaced via DaemonAgentResult — the dispatcher (caller) maps
55
+ * to triggerBus.markFailed / deadLetter per the retry matrix.
56
+ */
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.RETRY_DECISION = void 0;
59
+ exports.createRealAgentRunner = createRealAgentRunner;
60
+ exports.computeRetryCooldownMs = computeRetryCooldownMs;
61
+ const agentRunner_1 = require("./agentRunner");
62
+ const resolveModel_1 = require("./resolveModel");
63
+ const daemonApproval_1 = require("./daemonApproval");
64
+ const dailyBudgetTracker_1 = require("./dailyBudgetTracker");
65
+ const budgetGate_1 = require("./budgetGate");
66
+ // ── Implementation ─────────────────────────────────────────────────────────
67
+ const ENV_DAEMON_MODEL = 'AIDEN_DAEMON_MODEL';
68
+ const ENV_DAILY_BUDGET = 'AIDEN_DAEMON_DAILY_BUDGET';
69
+ function createRealAgentRunner(opts) {
70
+ const log = opts.log ?? (() => { });
71
+ const now = opts.now ?? Date.now;
72
+ const tracker = (0, dailyBudgetTracker_1.createDailyBudgetTracker)({
73
+ db: opts.db,
74
+ budget: opts.dailyBudget ?? readDailyBudgetFromEnv(),
75
+ });
76
+ return {
77
+ async invoke(input) {
78
+ const dailyBudget = opts.dailyBudget ?? readDailyBudgetFromEnv();
79
+ // ── 1: pre-turn budget gate ────────────────────────────────────────
80
+ const verdict = (0, budgetGate_1.evaluatePreTurn)({ tracker, dailyBudget, now: now() });
81
+ if (!verdict.allowed) {
82
+ // Reject without invoking the agent. Surface as trigger_quota.
83
+ const runId = opts.runStore.create({
84
+ sessionId: input.sessionId,
85
+ instanceId: input.instanceId,
86
+ triggerEventId: input.triggerEventId,
87
+ status: 'running',
88
+ });
89
+ opts.runStore.emitEvent(runId, 'dispatcher:rejected', {
90
+ reason: verdict.reason ?? 'trigger_quota',
91
+ dailySnapshot: verdict.daily,
92
+ });
93
+ opts.runStore.setStatus(runId, 'failed', { finishReason: 'budget_exhausted' });
94
+ log('warn', `[real-runner] rejected eventId=${input.triggerEventId}: ${verdict.reason}`);
95
+ return {
96
+ runId,
97
+ finishReason: 'error',
98
+ error: verdict.reason ?? 'trigger_quota: daily budget exhausted',
99
+ };
100
+ }
101
+ // ── 2: resolve model from chain ───────────────────────────────────
102
+ const triggerSpec = readTriggerSpec(opts.db, input.triggerContext.triggerId);
103
+ const resolved = (0, resolveModel_1.resolveDaemonModel)({
104
+ triggerSpec: {
105
+ provider: triggerSpec?.provider ?? null,
106
+ model: triggerSpec?.model ?? null,
107
+ },
108
+ envOverride: process.env[ENV_DAEMON_MODEL],
109
+ persistedDefault: opts.persistedDefault ?? { provider: '', model: '' },
110
+ });
111
+ // ── 3: approval callbacks ─────────────────────────────────────────
112
+ const approvalPolicy = triggerSpec?.daemonApproval && (0, daemonApproval_1.isDaemonApprovalPolicy)(triggerSpec.daemonApproval)
113
+ ? triggerSpec.daemonApproval
114
+ : daemonApproval_1.DEFAULT_DAEMON_APPROVAL_POLICY;
115
+ // ── 4: per-turn budget watcher ────────────────────────────────────
116
+ const perTurnWatcher = (0, budgetGate_1.createPerTurnBudgetWatcher)({
117
+ maxTokensPerFire: triggerSpec?.maxTokensPerFire ?? null,
118
+ });
119
+ // ── 5: run row + dispatcher:invoked event ─────────────────────────
120
+ const runId = opts.runStore.create({
121
+ sessionId: input.sessionId,
122
+ instanceId: input.instanceId,
123
+ triggerEventId: input.triggerEventId,
124
+ status: 'running',
125
+ });
126
+ opts.runStore.emitEvent(runId, 'dispatcher:invoked', {
127
+ source: input.triggerContext.source,
128
+ triggerId: input.triggerContext.triggerId,
129
+ eventId: input.triggerEventId,
130
+ sessionId: input.sessionId,
131
+ templated: input.triggerContext.promptTemplate !== null,
132
+ messageLen: input.initialMessage.length,
133
+ attempt: input.triggerContext.attempt,
134
+ maxAttempts: input.triggerContext.maxAttempts,
135
+ model: resolved.model,
136
+ provider: resolved.provider,
137
+ modelSource: resolved.source,
138
+ approvalPolicy,
139
+ dailySnapshot: verdict.daily,
140
+ maxTokensPerFire: triggerSpec?.maxTokensPerFire ?? null,
141
+ });
142
+ const approvalCallbacks = (0, daemonApproval_1.buildDaemonApprovalCallbacks)({
143
+ policy: approvalPolicy,
144
+ runStore: opts.runStore,
145
+ runId,
146
+ log: (lvl, msg) => log(lvl, msg),
147
+ });
148
+ // ── 6: initial history ────────────────────────────────────────────
149
+ const history = (0, agentRunner_1.buildInitialHistory)(input);
150
+ // ── 7: build agent via injected factory ───────────────────────────
151
+ let agent;
152
+ const startedAt = now();
153
+ try {
154
+ agent = await opts.agentBuilder({
155
+ sessionId: input.sessionId,
156
+ resolvedModel: resolved,
157
+ approvalPolicy,
158
+ approvalCallbacks,
159
+ hooks: {
160
+ onToolCall: (call, phase, result) => emitToolEvent(opts.runStore, runId, call, phase, result, startedAt, now),
161
+ onBudgetWarning: (level, turn, max) => {
162
+ opts.runStore.emitEvent(runId, 'budget_warning', { level, turn, max });
163
+ },
164
+ },
165
+ abortSignal: perTurnWatcher.signal,
166
+ });
167
+ }
168
+ catch (e) {
169
+ const msg = e instanceof Error ? e.message : String(e);
170
+ log('error', `[real-runner] agentBuilder threw eventId=${input.triggerEventId}: ${msg}`);
171
+ opts.runStore.emitEvent(runId, 'dispatcher:builder_failed', { error: msg });
172
+ opts.runStore.setStatus(runId, 'failed', { finishReason: 'error' });
173
+ return { runId, finishReason: 'error', error: msg };
174
+ }
175
+ // ── 8: invoke runConversation ─────────────────────────────────────
176
+ let result = null;
177
+ let invocationError = null;
178
+ try {
179
+ result = await agent.runConversation(history, {
180
+ // The agent honours its own abort signal via per-tool aborts;
181
+ // tools that respect AbortSignal (shell_exec, fetch_*) will
182
+ // bail when perTurnWatcher trips.
183
+ //
184
+ // Note: runConversation doesn't currently take an abort
185
+ // signal in its options — the budget watcher is best-effort
186
+ // observability via tally(). Future enhancement: thread the
187
+ // signal into the loop body via options.
188
+ });
189
+ // Stamp the actual token usage onto the watcher for the
190
+ // post-turn snapshot below.
191
+ const tokens = extractTokens(result);
192
+ if (tokens > 0)
193
+ perTurnWatcher.tally(tokens);
194
+ }
195
+ catch (e) {
196
+ invocationError = e instanceof Error ? (e.stack ?? e.message) : String(e);
197
+ log('error', `[real-runner] runConversation threw eventId=${input.triggerEventId}: ${invocationError.slice(0, 500)}`);
198
+ }
199
+ // ── 9: post-turn budget consume + dispatcher:completed ─────────────
200
+ const finalSnapshot = (0, budgetGate_1.consumePostTurn)({
201
+ tracker,
202
+ actualTokens: perTurnWatcher.used(),
203
+ dailyBudget,
204
+ now: now(),
205
+ });
206
+ const finishReason = pickFinishReason(result, invocationError, perTurnWatcher.hit());
207
+ opts.runStore.emitEvent(runId, 'dispatcher:completed', {
208
+ finishReason,
209
+ totalTokens: perTurnWatcher.used(),
210
+ durationMs: now() - startedAt,
211
+ dailySnapshot: finalSnapshot,
212
+ perTurnBudgetHit: perTurnWatcher.hit(),
213
+ perTurnReason: perTurnWatcher.reason(),
214
+ invocationError: invocationError ? invocationError.slice(0, 200) : null,
215
+ });
216
+ // ── 10: map result → DaemonAgentResult ────────────────────────────
217
+ const runStatus = finishReason === 'stop' ? 'completed' :
218
+ finishReason === 'budget_exhausted' ? 'failed' :
219
+ finishReason === 'error' ? 'failed' :
220
+ finishReason === 'tool_loop' ? 'failed' : 'completed';
221
+ opts.runStore.setStatus(runId, runStatus, { finishReason });
222
+ return {
223
+ runId,
224
+ finishReason,
225
+ totalTokens: perTurnWatcher.used() > 0 ? perTurnWatcher.used() : undefined,
226
+ error: invocationError ?? (perTurnWatcher.hit() ? perTurnWatcher.reason() ?? undefined : undefined),
227
+ };
228
+ },
229
+ };
230
+ }
231
+ // ── Helpers ────────────────────────────────────────────────────────────────
232
+ /** Read the trigger spec row + extract Phase 7 spec fields. */
233
+ function readTriggerSpec(db, triggerId) {
234
+ try {
235
+ const row = db.prepare(`SELECT spec_json FROM triggers WHERE id = ?`).get(triggerId);
236
+ if (!row)
237
+ return null;
238
+ const parsed = JSON.parse(row.spec_json);
239
+ return {
240
+ provider: typeof parsed.provider === 'string' ? parsed.provider : undefined,
241
+ model: typeof parsed.model === 'string' ? parsed.model : undefined,
242
+ daemonApproval: typeof parsed.daemonApproval === 'string' ? parsed.daemonApproval : undefined,
243
+ maxTokensPerFire: typeof parsed.maxTokensPerFire === 'number' ? parsed.maxTokensPerFire : undefined,
244
+ };
245
+ }
246
+ catch {
247
+ return null;
248
+ }
249
+ }
250
+ /** Read AIDEN_DAEMON_DAILY_BUDGET, parse as positive integer; null otherwise. */
251
+ function readDailyBudgetFromEnv() {
252
+ const raw = process.env[ENV_DAILY_BUDGET];
253
+ if (!raw)
254
+ return null;
255
+ const n = Number.parseInt(raw, 10);
256
+ return Number.isFinite(n) && n > 0 ? n : null;
257
+ }
258
+ /** Major-events run_event emitter for tool calls. Truncated payload. */
259
+ function emitToolEvent(runStore, runId, call, phase, result, startedAt, now) {
260
+ try {
261
+ if (phase === 'before') {
262
+ const argsSummary = safeShortJson(call.arguments, 200);
263
+ runStore.emitEvent(runId, 'tool_call_started', {
264
+ toolName: call.name,
265
+ args: argsSummary,
266
+ ts: now(),
267
+ });
268
+ return;
269
+ }
270
+ runStore.emitEvent(runId, 'tool_call_completed', {
271
+ toolName: call.name,
272
+ error: result?.error ?? null,
273
+ hasResult: result?.result !== undefined && result?.result !== null,
274
+ durationMs: now() - startedAt,
275
+ });
276
+ }
277
+ catch { /* never let observability crash the agent loop */ }
278
+ }
279
+ function safeShortJson(value, maxBytes) {
280
+ try {
281
+ const s = JSON.stringify(value);
282
+ return s.length > maxBytes ? s.slice(0, maxBytes) + '…' : s;
283
+ }
284
+ catch {
285
+ return String(value).slice(0, maxBytes);
286
+ }
287
+ }
288
+ /**
289
+ * Pull the total-tokens count off an AidenAgentResult. The agent
290
+ * exposes per-turn token usage via its result's `usage` field
291
+ * (mirrors provider conventions). Falls back to 0 when missing.
292
+ */
293
+ function extractTokens(result) {
294
+ if (!result)
295
+ return 0;
296
+ const r = result;
297
+ return r.usage?.totalTokens ?? r.usage?.total ?? 0;
298
+ }
299
+ /**
300
+ * Map the agent's finishReason + invocation outcome → the
301
+ * DaemonAgentResult finishReason vocab the dispatcher expects.
302
+ */
303
+ function pickFinishReason(result, invocationError, perTurnHit) {
304
+ if (invocationError)
305
+ return 'error';
306
+ if (perTurnHit)
307
+ return 'budget_exhausted';
308
+ if (!result)
309
+ return 'error';
310
+ const fr = result.finishReason;
311
+ if (fr === 'stop')
312
+ return 'stop';
313
+ if (fr === 'tool_loop')
314
+ return 'tool_loop';
315
+ if (fr === 'budget_exhausted')
316
+ return 'budget_exhausted';
317
+ if (fr === 'error')
318
+ return 'error';
319
+ // Default conservative — the dispatcher will markDone unless we
320
+ // say error; treat unknown finishes as success for forward-compat.
321
+ return 'stop';
322
+ }
323
+ exports.RETRY_DECISION = Object.freeze({
324
+ // Transient — retry with backoff
325
+ timeout: 'retry',
326
+ network: 'retry',
327
+ rate_limit: 'retry',
328
+ dependency_missing: 'retry',
329
+ hallucination: 'retry',
330
+ stale_ref: 'retry',
331
+ // Permanent — dead-letter immediately
332
+ auth: 'dead_letter',
333
+ permission: 'dead_letter',
334
+ sandbox_violation: 'dead_letter',
335
+ manual_blocker: 'dead_letter',
336
+ trigger_misconfigured: 'dead_letter',
337
+ trigger_quota: 'dead_letter',
338
+ trigger_dead_lettered: 'dead_letter',
339
+ invalid_input: 'dead_letter',
340
+ not_found: 'dead_letter',
341
+ other: 'dead_letter',
342
+ });
343
+ /**
344
+ * Compute the cooldown to wait before re-claiming a transient
345
+ * failure. Formula: `min(2^attempts * 1000, 60000)` ms.
346
+ * Exposed as a pure function so tests can assert the schedule:
347
+ *
348
+ * attempts=1 → 2_000 ms
349
+ * attempts=2 → 4_000 ms
350
+ * attempts=3 → 8_000 ms
351
+ * attempts=6 → 60_000 ms (capped)
352
+ */
353
+ function computeRetryCooldownMs(attempts) {
354
+ const expo = Math.pow(2, Math.max(1, attempts)) * 1000;
355
+ return Math.min(expo, 60000);
356
+ }
@@ -0,0 +1,93 @@
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/resolveModel.ts — v4.5 Phase 7.
10
+ *
11
+ * Per-trigger model selection chain (Q-P7-2a):
12
+ *
13
+ * 1. Trigger spec — `triggers.spec_json.provider/model` if either
14
+ * is non-empty. Per-trigger configuration is the whole point
15
+ * of trigger registration.
16
+ * 2. Environment override — `AIDEN_DAEMON_MODEL` env var encoded
17
+ * as `<provider>/<model>` (e.g. "ollama/llama3.2:latest").
18
+ * For operators who want one model across every trigger.
19
+ * 3. Persisted user default — last interactive-REPL choice loaded
20
+ * via the caller-provided `persistedDefault` argument.
21
+ *
22
+ * The `source` field on the result is captured in the
23
+ * `dispatcher:invoked` run_event so operators can audit which leg
24
+ * of the chain won for a given turn.
25
+ *
26
+ * Pure module — no I/O, no fs reads. Caller is responsible for
27
+ * loading the persisted default (a separate concern from the
28
+ * chain logic).
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.resolveDaemonModel = resolveDaemonModel;
32
+ /**
33
+ * Resolve the (provider, model) pair to use for a daemon-fired
34
+ * agent turn. Deterministic per input.
35
+ *
36
+ * Edge cases:
37
+ * - Both spec fields set → trigger wins (source='trigger').
38
+ * - Only spec.provider set → trigger.provider + env/persisted.model
39
+ * (still source='trigger' because at least one piece came from
40
+ * the trigger).
41
+ * - Only spec.model set → ditto.
42
+ * - Neither spec field set → fall through to env / persisted.
43
+ * - Env value present but malformed (no slash) → ignored; falls
44
+ * through to persisted.
45
+ */
46
+ function resolveDaemonModel(input) {
47
+ const spec = input.triggerSpec ?? {};
48
+ const env = parseEnvOverride(input.envOverride);
49
+ const specProvider = nonEmpty(spec.provider);
50
+ const specModel = nonEmpty(spec.model);
51
+ // Trigger wins if it contributes anything.
52
+ if (specProvider || specModel) {
53
+ return {
54
+ provider: specProvider ?? env?.provider ?? input.persistedDefault.provider,
55
+ model: specModel ?? env?.model ?? input.persistedDefault.model,
56
+ source: 'trigger',
57
+ };
58
+ }
59
+ // Env wins next.
60
+ if (env) {
61
+ return {
62
+ provider: env.provider,
63
+ model: env.model,
64
+ source: 'env',
65
+ };
66
+ }
67
+ // Fallback: persisted user default.
68
+ return {
69
+ provider: input.persistedDefault.provider,
70
+ model: input.persistedDefault.model,
71
+ source: 'persisted',
72
+ };
73
+ }
74
+ /** Parse the `AIDEN_DAEMON_MODEL` env value. Returns null on malformed input. */
75
+ function parseEnvOverride(raw) {
76
+ if (typeof raw !== 'string' || raw.length === 0)
77
+ return null;
78
+ const slash = raw.indexOf('/');
79
+ // Require both halves non-empty: "<provider>/<model>".
80
+ if (slash <= 0 || slash >= raw.length - 1)
81
+ return null;
82
+ const provider = raw.slice(0, slash).trim();
83
+ const model = raw.slice(slash + 1).trim();
84
+ if (provider.length === 0 || model.length === 0)
85
+ return null;
86
+ return { provider, model };
87
+ }
88
+ function nonEmpty(v) {
89
+ if (typeof v !== 'string')
90
+ return undefined;
91
+ const t = v.trim();
92
+ return t.length > 0 ? t : undefined;
93
+ }
@@ -0,0 +1,93 @@
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/sessionId.ts — v4.5 Phase 5a.
10
+ *
11
+ * Per-trigger sessionId derivation. Stable across retries — the
12
+ * same trigger event (same idempotencyKey) always produces the
13
+ * same sessionId, so:
14
+ *
15
+ * - v4.4 docker session cache reuses one container per trigger
16
+ * across retry attempts (cold-start cost amortised).
17
+ * - v4.3 browser observer keeps page state observable for the
18
+ * same trigger across re-attempts.
19
+ * - v4.2 TurnState tracks repeated tool calls within a
20
+ * single retry session (still fresh per turn — TurnState
21
+ * lives per `runConversation`, not per sessionId).
22
+ *
23
+ * Format: `trigger:<source>:<sourceKey>:<idemHash>` where
24
+ * - source: TriggerSource literal ('file' / 'webhook' / 'email'
25
+ * / 'schedule' / 'manual')
26
+ * - sourceKey: the trigger spec id (FK to triggers.id)
27
+ * - idemHash: base64url(sha256(idempotencyKey)).slice(0,12)
28
+ *
29
+ * 12-char b64url prefix gives ~72 bits — far more than enough to
30
+ * keep distinct triggers separate without bloating the sessionId
31
+ * the docker cache / runStore have to carry around.
32
+ *
33
+ * `idempotencyKey === null` falls back to the literal `no-idem`
34
+ * sentinel — keeps the sessionId stable for sources that don't
35
+ * dedup (rare; mostly schedule).
36
+ */
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.buildTriggerSessionId = buildTriggerSessionId;
39
+ exports.parseTriggerSessionId = parseTriggerSessionId;
40
+ const node_crypto_1 = require("node:crypto");
41
+ const NO_IDEM_SENTINEL = 'no-idem';
42
+ /**
43
+ * Build a stable per-trigger sessionId.
44
+ *
45
+ * Deterministic: same input → same output. Tested for stability
46
+ * across retries (the trigger bus reclaims its existing event row;
47
+ * the dispatcher hands the same triple to this function on every
48
+ * attempt → same sessionId → same docker container / browser tab
49
+ * reused).
50
+ */
51
+ function buildTriggerSessionId(input) {
52
+ const idem = input.idempotencyKey ?? NO_IDEM_SENTINEL;
53
+ const hash = sha256B64url(idem).slice(0, 12);
54
+ return `trigger:${input.source}:${input.sourceKey}:${hash}`;
55
+ }
56
+ /**
57
+ * Inverse-ish parser. Returns the structural pieces of a sessionId
58
+ * built by `buildTriggerSessionId`. Returns `null` for sessionIds
59
+ * not built by this helper (interactive REPL sessions, plain
60
+ * UUIDs, etc.).
61
+ *
62
+ * Used by recoveryReport to detect daemon-triggered runs and
63
+ * surface the `triggerContext` pill.
64
+ */
65
+ function parseTriggerSessionId(sessionId) {
66
+ if (!sessionId.startsWith('trigger:'))
67
+ return null;
68
+ const parts = sessionId.split(':');
69
+ // ['trigger', source, sourceKey, idemHash]
70
+ if (parts.length !== 4)
71
+ return null;
72
+ const [, source, sourceKey, idemHash] = parts;
73
+ if (!isTriggerSource(source))
74
+ return null;
75
+ if (!sourceKey || sourceKey.length === 0)
76
+ return null;
77
+ if (!idemHash || idemHash.length === 0)
78
+ return null;
79
+ return { source, sourceKey, idemHash };
80
+ }
81
+ /** Type-guard for TriggerSource literals. */
82
+ function isTriggerSource(s) {
83
+ return s === 'file' || s === 'webhook' || s === 'email'
84
+ || s === 'schedule' || s === 'manual';
85
+ }
86
+ function sha256B64url(input) {
87
+ return (0, node_crypto_1.createHash)('sha256')
88
+ .update(input)
89
+ .digest('base64')
90
+ .replace(/\+/g, '-')
91
+ .replace(/\//g, '_')
92
+ .replace(/=+$/, '');
93
+ }