aiden-runtime 4.1.5 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +250 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +351 -53
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +138 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +92 -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/help.js +7 -0
  12. package/dist/cli/v4/commands/index.js +20 -1
  13. package/dist/cli/v4/commands/runs.js +203 -0
  14. package/dist/cli/v4/commands/sandbox.js +48 -0
  15. package/dist/cli/v4/commands/suggestions.js +68 -0
  16. package/dist/cli/v4/commands/tce.js +41 -0
  17. package/dist/cli/v4/commands/trigger.js +378 -0
  18. package/dist/cli/v4/commands/update.js +95 -3
  19. package/dist/cli/v4/daemonAgentBuilder.js +142 -0
  20. package/dist/cli/v4/defaultSoul.js +1 -1
  21. package/dist/cli/v4/display/capabilityCard.js +26 -0
  22. package/dist/cli/v4/display.js +18 -8
  23. package/dist/cli/v4/replyRenderer.js +31 -23
  24. package/dist/cli/v4/updateBootPrompt.js +170 -0
  25. package/dist/core/playwrightBridge.js +129 -0
  26. package/dist/core/v4/aidenAgent.js +308 -4
  27. package/dist/core/v4/browserState.js +436 -0
  28. package/dist/core/v4/checkpoint.js +79 -0
  29. package/dist/core/v4/daemon/bootstrap.js +604 -0
  30. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  31. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  32. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  33. package/dist/core/v4/daemon/cron/migration.js +199 -0
  34. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  35. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  36. package/dist/core/v4/daemon/db/connection.js +106 -0
  37. package/dist/core/v4/daemon/db/migrations.js +296 -0
  38. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  39. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  40. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  41. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  42. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  43. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  44. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  45. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  46. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  47. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  48. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  49. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  50. package/dist/core/v4/daemon/drain.js +156 -0
  51. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  52. package/dist/core/v4/daemon/health.js +159 -0
  53. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  54. package/dist/core/v4/daemon/index.js +179 -0
  55. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  56. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  57. package/dist/core/v4/daemon/restartCode.js +32 -0
  58. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  59. package/dist/core/v4/daemon/runStore.js +114 -0
  60. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  61. package/dist/core/v4/daemon/signals.js +50 -0
  62. package/dist/core/v4/daemon/supervisor.js +272 -0
  63. package/dist/core/v4/daemon/triggerBus.js +279 -0
  64. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  65. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  66. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  67. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  68. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  69. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  70. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  71. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  72. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  73. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  74. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  75. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  76. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  77. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  78. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  79. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  80. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  81. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  82. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  83. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  84. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  85. package/dist/core/v4/daemon/types.js +15 -0
  86. package/dist/core/v4/dockerSession.js +461 -0
  87. package/dist/core/v4/dryRun.js +117 -0
  88. package/dist/core/v4/failureClassifier.js +779 -0
  89. package/dist/core/v4/recoveryReport.js +449 -0
  90. package/dist/core/v4/runtimeToggles.js +187 -0
  91. package/dist/core/v4/sandboxConfig.js +285 -0
  92. package/dist/core/v4/sandboxFs.js +316 -0
  93. package/dist/core/v4/suggestionCatalog.js +41 -0
  94. package/dist/core/v4/suggestionEngine.js +210 -0
  95. package/dist/core/v4/toolRegistry.js +18 -0
  96. package/dist/core/v4/turnState.js +587 -0
  97. package/dist/core/v4/update/checkUpdate.js +63 -3
  98. package/dist/core/v4/update/installMethodDetect.js +115 -0
  99. package/dist/core/v4/update/registryClient.js +121 -0
  100. package/dist/core/v4/update/skipState.js +75 -0
  101. package/dist/core/v4/verifier.js +448 -0
  102. package/dist/core/version.js +1 -1
  103. package/dist/tools/v4/browser/_observer.js +224 -0
  104. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  105. package/dist/tools/v4/browser/browserClick.js +18 -1
  106. package/dist/tools/v4/browser/browserClose.js +18 -1
  107. package/dist/tools/v4/browser/browserExtract.js +5 -1
  108. package/dist/tools/v4/browser/browserFill.js +17 -1
  109. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  110. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  111. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  112. package/dist/tools/v4/browser/browserScroll.js +18 -1
  113. package/dist/tools/v4/browser/browserType.js +17 -1
  114. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  115. package/dist/tools/v4/executeCode.js +1 -0
  116. package/dist/tools/v4/files/fileCopy.js +56 -2
  117. package/dist/tools/v4/files/fileDelete.js +38 -1
  118. package/dist/tools/v4/files/fileList.js +12 -1
  119. package/dist/tools/v4/files/fileMove.js +59 -2
  120. package/dist/tools/v4/files/filePatch.js +43 -1
  121. package/dist/tools/v4/files/fileRead.js +12 -1
  122. package/dist/tools/v4/files/fileWrite.js +41 -1
  123. package/dist/tools/v4/index.js +71 -58
  124. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  125. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  126. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  127. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  128. package/dist/tools/v4/process/processKill.js +19 -0
  129. package/dist/tools/v4/process/processList.js +1 -0
  130. package/dist/tools/v4/process/processLogRead.js +1 -0
  131. package/dist/tools/v4/process/processSpawn.js +13 -0
  132. package/dist/tools/v4/process/processWait.js +1 -0
  133. package/dist/tools/v4/sessions/recallSession.js +1 -0
  134. package/dist/tools/v4/sessions/sessionList.js +1 -0
  135. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  136. package/dist/tools/v4/skills/lookupToolSchema.js +2 -0
  137. package/dist/tools/v4/skills/skillManage.js +13 -0
  138. package/dist/tools/v4/skills/skillView.js +1 -0
  139. package/dist/tools/v4/skills/skillsList.js +1 -0
  140. package/dist/tools/v4/subagent/subagentFanout.js +1 -0
  141. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  142. package/dist/tools/v4/system/appClose.js +13 -0
  143. package/dist/tools/v4/system/appInput.js +13 -0
  144. package/dist/tools/v4/system/appLaunch.js +13 -0
  145. package/dist/tools/v4/system/clipboardRead.js +1 -0
  146. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  147. package/dist/tools/v4/system/mediaKey.js +12 -0
  148. package/dist/tools/v4/system/mediaSessions.js +1 -0
  149. package/dist/tools/v4/system/mediaTransport.js +13 -0
  150. package/dist/tools/v4/system/naturalEvents.js +1 -0
  151. package/dist/tools/v4/system/nowPlaying.js +1 -0
  152. package/dist/tools/v4/system/osProcessList.js +1 -0
  153. package/dist/tools/v4/system/screenshot.js +1 -0
  154. package/dist/tools/v4/system/systemInfo.js +1 -0
  155. package/dist/tools/v4/system/volumeSet.js +17 -0
  156. package/dist/tools/v4/terminal/shellExec.js +81 -9
  157. package/dist/tools/v4/web/deepResearch.js +1 -0
  158. package/dist/tools/v4/web/openUrl.js +1 -0
  159. package/dist/tools/v4/web/webFetch.js +1 -0
  160. package/dist/tools/v4/web/webPage.js +1 -0
  161. package/dist/tools/v4/web/webSearch.js +1 -0
  162. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  163. package/package.json +7 -1
@@ -0,0 +1,449 @@
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/recoveryReport.ts — v4.2 Phase 3: Evidence Output +
10
+ * RecoveryReport.
11
+ *
12
+ * Pure synthesis. Consumes a TurnStateDiagnosticSnapshot (populated by
13
+ * Phase 1's verifier + Phase 2's classifier records) and produces a
14
+ * structured RecoveryReport that captures what the agent tried, what
15
+ * failed, why, what was recovered, and what's next.
16
+ *
17
+ * Surfaced ONLY when the TurnState recovery controller reaches the
18
+ * `surfaced` stage — quiet by design on hint/cooldown turns where the
19
+ * model self-corrects without user intervention. The report enriches
20
+ * the existing v4.1.6 tool_loop capability card by attaching summary
21
+ * lines (whatHappened) and a category breakdown (failuresByCategory).
22
+ *
23
+ * Reference notes: a comparable reference system's failure surface is
24
+ * text-only metadata (flat dict + appended guidance strings). Aiden's
25
+ * structured report is genuinely new — no patterns to port, but the
26
+ * single-source-of-truth synthesis approach mirrors the reference's
27
+ * `to_metadata()` style.
28
+ *
29
+ * Phase 3 stays consume-only: no changes to TurnState, verifier, or
30
+ * failureClassifier. Imports flow downstream (recoveryReport depends
31
+ * on TurnState's snapshot type and failureClassifier's category enum).
32
+ *
33
+ * Pure module — no I/O, no async, no side effects. Easy to unit test
34
+ * with synthetic snapshots.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.extractGoal = extractGoal;
38
+ exports.guidanceFor = guidanceFor;
39
+ exports.buildRecoveryReport = buildRecoveryReport;
40
+ exports.enrichCardWithReport = enrichCardWithReport;
41
+ // ── Goal extraction ────────────────────────────────────────────────────────
42
+ const MAX_GOAL_CHARS = 140;
43
+ /**
44
+ * Pull the first user message from the conversation as the turn's
45
+ * goal. Handles three shapes:
46
+ * - string content (the common path)
47
+ * - ContentBlock[] content (Anthropic structured shape) — concatenates
48
+ * text blocks; ignores tool_use / image blocks
49
+ * - missing user message — returns empty string
50
+ *
51
+ * Result truncated to MAX_GOAL_CHARS with ellipsis to keep the report
52
+ * line bounded.
53
+ */
54
+ function extractGoal(messages) {
55
+ const firstUser = messages.find((m) => m.role === 'user');
56
+ if (!firstUser)
57
+ return '';
58
+ const raw = stringifyContent(firstUser.content);
59
+ const trimmed = raw.trim();
60
+ if (trimmed.length <= MAX_GOAL_CHARS)
61
+ return trimmed;
62
+ return trimmed.slice(0, MAX_GOAL_CHARS - 3) + '...';
63
+ }
64
+ function stringifyContent(content) {
65
+ if (typeof content === 'string')
66
+ return content;
67
+ if (Array.isArray(content)) {
68
+ const parts = [];
69
+ for (const block of content) {
70
+ if (block && typeof block === 'object') {
71
+ const b = block;
72
+ if (typeof b.text === 'string')
73
+ parts.push(b.text);
74
+ else if (b.type === 'text' && typeof b.text === 'string')
75
+ parts.push(b.text);
76
+ }
77
+ }
78
+ return parts.join(' ');
79
+ }
80
+ return '';
81
+ }
82
+ // ── Guidance map ───────────────────────────────────────────────────────────
83
+ const GUIDANCE_BY_CATEGORY = {
84
+ permission: 'Adjust permissions or surface this to the user — the tool refused, so retrying without changes will not help.',
85
+ auth: 'Provide credentials before retrying — the tool needs auth that has not been supplied.',
86
+ timeout: 'Network or tool deadline exceeded. Retry with a longer budget or check connectivity.',
87
+ dependency_missing: 'A required binary or service is not available. Install it or use a different approach.',
88
+ rate_limit: 'Upstream rate-limited the call. Wait a moment and retry, or rotate to a different credential.',
89
+ network: 'Network unreachable or DNS failure. Check the connection and retry once it is stable.',
90
+ invalid_input: 'The tool arguments were rejected. Re-read the tool schema and fix the arguments before retrying.',
91
+ hallucination: 'The model used a path or name that does not exist. Re-read the surrounding state before retrying.',
92
+ not_found: 'The target resource was not found. Verify the path or name and try again with a corrected value.',
93
+ stale_ref: 'The page changed between snapshot and action. The observer already attempted resnapshot+retry once — re-read the visible state and try a different selector or approach.',
94
+ manual_blocker: 'The site requires a human action (login, 2FA, captcha, or verification). Surface this to the user and wait — do not retry automatically.',
95
+ sandbox_violation: 'Aiden\'s execution sandbox refused this operation. Surface the matched policy to the user and either widen the allowlist via AIDEN_SANDBOX_ALLOW=path1:path2 or disable the sandbox with AIDEN_SANDBOX=0 (not recommended). Denylist matches cannot be overridden — they signal sensitive paths the user explicitly wants protected.',
96
+ trigger_misconfigured: 'The trigger spec is invalid or its prompt template references variables the payload does not supply. Inspect the trigger via `aiden trigger list` and fix the spec — retrying without changes will produce the same failure.',
97
+ trigger_quota: 'The trigger\'s per-source fire-rate cap was hit. Investigate the upstream producer (file watcher loop, runaway webhook caller, mis-scheduled cron) or raise the fire_rate_limit on the trigger spec.',
98
+ trigger_dead_lettered: 'The trigger event exceeded its retry budget and moved to the dead-letter queue. Review the last_error on the trigger event row and either fix the root cause + re-queue, or accept the event as lost.',
99
+ other: 'The tool failed for an unclassified reason. Inspect the trace for details before retrying.',
100
+ };
101
+ /** Public for tests + plugin extensions. */
102
+ function guidanceFor(category) {
103
+ return GUIDANCE_BY_CATEGORY[category] ?? GUIDANCE_BY_CATEGORY.other;
104
+ }
105
+ /**
106
+ * Pure function. Given the per-turn diagnostic snapshot plus three
107
+ * scalar inputs, produces a deterministic RecoveryReport. No I/O,
108
+ * no async, no Date.now() — all timestamps come from the snapshot
109
+ * or are passed explicitly.
110
+ */
111
+ function buildRecoveryReport(input) {
112
+ const { snapshot, goal, exitReason, durationMs } = input;
113
+ // ── Attempts ────────────────────────────────────────────────────────────
114
+ // Total = every recorded tool call (toolCalls array).
115
+ // Succeeded = verifications with ok=true.
116
+ // Failed = verifications with ok=false.
117
+ //
118
+ // Note: total may exceed succeeded+failed when callers run without
119
+ // a verification (verifier disabled or threw). The arithmetic
120
+ // tolerates that — the counters report exactly what's recorded.
121
+ const total = snapshot.toolCalls.length;
122
+ const succeeded = snapshot.verifications.filter((v) => v.verification.ok).length;
123
+ const failed = snapshot.verifications.filter((v) => !v.verification.ok).length;
124
+ // ── Failure breakdown ───────────────────────────────────────────────────
125
+ const breakdown = {};
126
+ for (const entry of snapshot.classifications) {
127
+ const cat = entry.classification.category;
128
+ breakdown[cat] = (breakdown[cat] ?? 0) + 1;
129
+ }
130
+ // ── Failed tools (latest classification per tool name) ──────────────────
131
+ // Iterate forward; later entries overwrite earlier ones, so the
132
+ // resulting map holds the most recent classification per name.
133
+ const latestByName = new Map();
134
+ for (const entry of snapshot.classifications) {
135
+ latestByName.set(entry.name, {
136
+ name: entry.name,
137
+ category: entry.classification.category,
138
+ reason: entry.classification.reason,
139
+ confidence: entry.classification.confidence,
140
+ });
141
+ }
142
+ const failedTools = [...latestByName.values()];
143
+ // ── Recovery stages (passthrough — already ordered by recordToolCall) ──
144
+ const recoveryStages = snapshot.recoveryEvents.map((e) => ({
145
+ stage: e.stage,
146
+ toolName: e.toolName,
147
+ count: e.count,
148
+ }));
149
+ // ── Guidance — dominant failure category ────────────────────────────────
150
+ const guidance = synthesizeGuidance(breakdown);
151
+ // ── v4.3 Phase 5 — browserContext enrichment ────────────────────────────
152
+ // Populated when an optional BrowserStateLike is provided AND it
153
+ // reports at least one tab. Counts stale-ref retries from the
154
+ // recoveryStages signal indirectly — Phase 2's auto-retry fires
155
+ // via the HOC, not TurnState's recovery state machine, so we look
156
+ // for retried classifications in the snapshot instead.
157
+ const browserContext = buildBrowserContext(input.browserState, snapshot);
158
+ // ── v4.4 Phase 5 — sandboxContext enrichment ────────────────────────────
159
+ // Populated when any classification this turn has category
160
+ // `sandbox_violation`. The classifier (Phase 5) attaches the raw
161
+ // envelope to ClassificationResult.sandboxViolation, so we don't
162
+ // re-parse tool results here.
163
+ const sandboxContext = buildSandboxContext(snapshot);
164
+ // ── v4.5 Phase 5a — triggerContext passthrough ─────────────────────────
165
+ // The dispatcher hands the context in directly; this module just
166
+ // attaches it to the report shape without re-deriving fields. Keeps
167
+ // the report module decoupled from the daemon dispatcher.
168
+ const triggerContext = input.triggerContext;
169
+ return {
170
+ goal,
171
+ exitReason,
172
+ durationMs,
173
+ attempts: { total, succeeded, failed },
174
+ failureBreakdown: breakdown,
175
+ failedTools,
176
+ successfulTools: [...snapshot.successfulTools],
177
+ recoveryStages,
178
+ guidance,
179
+ ...(browserContext ? { browserContext } : {}),
180
+ ...(sandboxContext ? { sandboxContext } : {}),
181
+ ...(triggerContext ? { triggerContext } : {}),
182
+ };
183
+ }
184
+ /**
185
+ * v4.4 Phase 5 — build the `sandboxContext` sidecar from the turn's
186
+ * classifications. Returns null when no `sandbox_violation` fired
187
+ * (the common path).
188
+ *
189
+ * Aggregates FS vs shell violation counts (FS = code starts with
190
+ * `fs.`; shell = anything else under the sandbox_violation category)
191
+ * and surfaces the most recent envelope's matched policy +
192
+ * auto-derived override suggestion.
193
+ */
194
+ function buildSandboxContext(snapshot) {
195
+ const violations = snapshot.classifications.filter((c) => c.classification.category === 'sandbox_violation');
196
+ if (violations.length === 0)
197
+ return null;
198
+ const last = violations[violations.length - 1].classification;
199
+ const lastCode = last.matchedPattern ?? last.sandboxViolation?.code ?? '';
200
+ let fsViolations = 0;
201
+ let shellViolations = 0;
202
+ for (const v of violations) {
203
+ const code = v.classification.matchedPattern
204
+ ?? v.classification.sandboxViolation?.code
205
+ ?? '';
206
+ if (code.startsWith('fs.'))
207
+ fsViolations += 1;
208
+ else
209
+ shellViolations += 1;
210
+ }
211
+ const ctx = {
212
+ violationCount: violations.length,
213
+ fsViolations,
214
+ shellViolations,
215
+ lastCode,
216
+ lastMatched: last.sandboxViolation?.matchedPolicy ?? '',
217
+ };
218
+ if (last.sandboxViolation?.requestedPath) {
219
+ ctx.lastRequested = last.sandboxViolation.requestedPath;
220
+ }
221
+ if (last.recoveryHint?.detail) {
222
+ ctx.suggestedEnv = last.recoveryHint.detail;
223
+ }
224
+ return ctx;
225
+ }
226
+ /**
227
+ * v4.3 Phase 5 — build the `browserContext` sidecar from an optional
228
+ * BrowserStateLike + diagnostic snapshot. Returns null when no tabs
229
+ * exist (opt-out via AIDEN_BROWSER_DEPTH=0 or no browser action
230
+ * this turn) so
231
+ * the caller can decide whether to include the field.
232
+ *
233
+ * Stale-ref retry count derives from classifications with category
234
+ * `stale_ref` — Phase 5's classifier produces those when Phase 2's
235
+ * HOC-level retry attempted but failed. Successful retries don't
236
+ * appear in classifications (their final result has `success:true`).
237
+ */
238
+ function buildBrowserContext(browserState, snapshot) {
239
+ if (!browserState)
240
+ return null;
241
+ const tabs = browserState.getTabs();
242
+ if (tabs.length === 0)
243
+ return null;
244
+ const active = browserState.getActiveTab();
245
+ const otherTabCount = active
246
+ ? tabs.filter((t) => !t.is_active).length
247
+ : tabs.length;
248
+ // Count stale_ref classifications recorded by Phase 5's browser
249
+ // classifier in the turn's classifications log.
250
+ const staleRefRetries = snapshot.classifications.filter((c) => c.classification.category === 'stale_ref').length;
251
+ const ctx = {
252
+ otherTabCount,
253
+ staleRefRetries,
254
+ };
255
+ if (active?.url)
256
+ ctx.activeTabUrl = active.url;
257
+ if (active?.title)
258
+ ctx.activeTabTitle = active.title;
259
+ if (active?.last_blocker)
260
+ ctx.activeBlocker = active.last_blocker.kind;
261
+ return ctx;
262
+ }
263
+ /**
264
+ * Pick the most-frequent failure category and return its guidance
265
+ * string. Ties broken by category priority (more recoverable first):
266
+ * timeout > rate_limit > network > invalid_input > not_found >
267
+ * hallucination > dependency_missing > permission > auth > other.
268
+ *
269
+ * No failures recorded → returns the generic `other` guidance.
270
+ */
271
+ function synthesizeGuidance(breakdown) {
272
+ const entries = Object.entries(breakdown);
273
+ if (entries.length === 0)
274
+ return GUIDANCE_BY_CATEGORY.other;
275
+ const PRIORITY = [
276
+ 'timeout', 'rate_limit', 'network', 'invalid_input',
277
+ 'not_found',
278
+ 'stale_ref', // v4.3 Phase 5 — auto-recoverable via wait+retry
279
+ 'hallucination', 'dependency_missing',
280
+ 'manual_blocker', // v4.3 Phase 5 — requires human action; semi-blocking
281
+ 'sandbox_violation', // v4.4 Phase 5 — env-var override is the specific user action
282
+ 'trigger_misconfigured', // v4.5 Phase 5a — trigger spec fix required
283
+ 'trigger_quota', // v4.5 Phase 5a — anti-thrash, producer/cap fix
284
+ 'trigger_dead_lettered', // v4.5 Phase 5a — terminal; re-queue or accept loss
285
+ 'permission', 'auth', 'other',
286
+ ];
287
+ const rank = (c) => {
288
+ const i = PRIORITY.indexOf(c);
289
+ return i === -1 ? PRIORITY.length : i;
290
+ };
291
+ entries.sort((a, b) => {
292
+ if (b[1] !== a[1])
293
+ return b[1] - a[1]; // desc by count
294
+ return rank(a[0]) - rank(b[0]); // tie → priority rank asc
295
+ });
296
+ return GUIDANCE_BY_CATEGORY[entries[0][0]];
297
+ }
298
+ // ── Card enrichment ────────────────────────────────────────────────────────
299
+ /**
300
+ * Take a base CapabilityCardData (typically from TurnState's surface
301
+ * card) and overlay the RecoveryReport's summary lines. Returns a new
302
+ * card object — the base is not mutated. When report is undefined,
303
+ * returns the base unchanged.
304
+ *
305
+ * Three additions:
306
+ * - whatHappened: one-line summary string with attempt counts +
307
+ * duration (rendered above canStill section).
308
+ * - failuresByCategory: inline pill row of non-zero category counts,
309
+ * ordered by descending count then priority.
310
+ * - fix: replaced with the report's guidance text (one sentence,
311
+ * dominant-category aware).
312
+ *
313
+ * The base card's title / canStill / cannotReliably pass through.
314
+ */
315
+ function enrichCardWithReport(base, report) {
316
+ const whatHappened = buildWhatHappenedLine(report);
317
+ const failuresByCategory = buildFailuresPills(report.failureBreakdown);
318
+ // v4.3 Phase 5 — browser-context inline row. Only present when the
319
+ // report carries browserContext (which requires an active BrowserState
320
+ // with tabs). Renderer treats this as a single-line muted addition
321
+ // below whatHappened.
322
+ const browserContext = report.browserContext
323
+ ? buildBrowserContextLine(report.browserContext)
324
+ : undefined;
325
+ // v4.4 Phase 5 — sandbox-context inline row. Present when the
326
+ // report carries sandboxContext (any sandbox_violation this turn).
327
+ // Renderer surfaces this as another muted line right below
328
+ // browserContext (or whatHappened when no browser activity).
329
+ const sandboxContext = report.sandboxContext
330
+ ? buildSandboxContextLine(report.sandboxContext)
331
+ : undefined;
332
+ // v4.5 Phase 5a — trigger-context inline row. Present when the
333
+ // run was fired from the daemon trigger bus. Surfaces below
334
+ // browser/sandbox lines so the operator sees the trigger
335
+ // identity + attempt count at-a-glance.
336
+ const triggerContext = report.triggerContext
337
+ ? buildTriggerContextLine(report.triggerContext)
338
+ : undefined;
339
+ return {
340
+ title: base.title,
341
+ canStill: base.canStill,
342
+ cannotReliably: base.cannotReliably,
343
+ fix: report.guidance,
344
+ whatHappened,
345
+ failuresByCategory,
346
+ ...(browserContext ? { browserContext } : {}),
347
+ ...(sandboxContext ? { sandboxContext } : {}),
348
+ ...(triggerContext ? { triggerContext } : {}),
349
+ };
350
+ }
351
+ /**
352
+ * v4.5 Phase 5a — format the triggerContext fields into a compact
353
+ * single-line summary for the recovery card. Mirrors
354
+ * `buildBrowserContextLine` / `buildSandboxContextLine` shape.
355
+ * Returns empty string only when no signal worth surfacing
356
+ * (defensive — the dispatcher always sets the core fields).
357
+ */
358
+ function buildTriggerContextLine(ctx) {
359
+ const parts = [];
360
+ parts.push(`${ctx.source}/${ctx.triggerId}`);
361
+ parts.push(`attempt ${ctx.attempt}/${ctx.maxAttempts}`);
362
+ if (ctx.promptTemplateUsed)
363
+ parts.push('templated');
364
+ if (ctx.fireReason && ctx.fireReason !== 'trigger_fired') {
365
+ parts.push(`reason=${ctx.fireReason}`);
366
+ }
367
+ return parts.length > 0 ? `Trigger: ${parts.join(' · ')}` : '';
368
+ }
369
+ /**
370
+ * v4.3 Phase 5 — format the browserContext fields into a compact
371
+ * single-line summary for the recovery card. Returns empty string
372
+ * when no signal worth surfacing.
373
+ */
374
+ function buildBrowserContextLine(ctx) {
375
+ const parts = [];
376
+ if (ctx.activeTabUrl) {
377
+ try {
378
+ parts.push(`active=${new URL(ctx.activeTabUrl).hostname || ctx.activeTabUrl}`);
379
+ }
380
+ catch {
381
+ parts.push(`active=${ctx.activeTabUrl}`);
382
+ }
383
+ }
384
+ if (ctx.activeBlocker)
385
+ parts.push(`${ctx.activeBlocker} blocker`);
386
+ if (ctx.otherTabCount > 0) {
387
+ parts.push(`${ctx.otherTabCount} other tab${ctx.otherTabCount === 1 ? '' : 's'}`);
388
+ }
389
+ if (ctx.staleRefRetries > 0) {
390
+ parts.push(`${ctx.staleRefRetries} stale-ref retr${ctx.staleRefRetries === 1 ? 'y' : 'ies'}`);
391
+ }
392
+ return parts.length > 0 ? `Browser: ${parts.join(' · ')}` : '';
393
+ }
394
+ /**
395
+ * v4.4 Phase 5 — format the sandboxContext fields into a compact
396
+ * single-line summary for the recovery card. Mirrors
397
+ * `buildBrowserContextLine` shape. Returns empty string when no
398
+ * signal worth surfacing.
399
+ */
400
+ function buildSandboxContextLine(ctx) {
401
+ const parts = [];
402
+ parts.push(`${ctx.violationCount} blocked`);
403
+ if (ctx.fsViolations > 0 && ctx.shellViolations > 0) {
404
+ parts.push(`${ctx.fsViolations} fs · ${ctx.shellViolations} shell`);
405
+ }
406
+ else if (ctx.shellViolations > 0) {
407
+ parts.push(`${ctx.shellViolations} shell`);
408
+ }
409
+ if (ctx.lastCode)
410
+ parts.push(`last: ${ctx.lastCode}`);
411
+ if (ctx.suggestedEnv)
412
+ parts.push(`try: ${ctx.suggestedEnv}`);
413
+ return parts.length > 0 ? `Sandbox: ${parts.join(' · ')}` : '';
414
+ }
415
+ function buildWhatHappenedLine(report) {
416
+ const { attempts, durationMs } = report;
417
+ const dur = (durationMs / 1000).toFixed(1);
418
+ return (`Tried ${attempts.total} tool ${plural(attempts.total, 'call')} · ` +
419
+ `${attempts.succeeded} succeeded · ${attempts.failed} failed · ${dur}s`);
420
+ }
421
+ function plural(n, word) {
422
+ return n === 1 ? word : `${word}s`;
423
+ }
424
+ function buildFailuresPills(breakdown) {
425
+ const entries = Object.entries(breakdown);
426
+ // Same ordering rule as guidance synthesis: count desc, priority asc.
427
+ const PRIORITY = [
428
+ 'timeout', 'rate_limit', 'network', 'invalid_input',
429
+ 'not_found',
430
+ 'stale_ref', // v4.3 Phase 5 — auto-recoverable via wait+retry
431
+ 'hallucination', 'dependency_missing',
432
+ 'manual_blocker', // v4.3 Phase 5 — requires human action; semi-blocking
433
+ 'sandbox_violation', // v4.4 Phase 5 — env-var override is the specific user action
434
+ 'trigger_misconfigured', // v4.5 Phase 5a — trigger spec fix required
435
+ 'trigger_quota', // v4.5 Phase 5a — anti-thrash, producer/cap fix
436
+ 'trigger_dead_lettered', // v4.5 Phase 5a — terminal; re-queue or accept loss
437
+ 'permission', 'auth', 'other',
438
+ ];
439
+ const rank = (c) => {
440
+ const i = PRIORITY.indexOf(c);
441
+ return i === -1 ? PRIORITY.length : i;
442
+ };
443
+ entries.sort((a, b) => {
444
+ if (b[1] !== a[1])
445
+ return b[1] - a[1];
446
+ return rank(a[0]) - rank(b[0]);
447
+ });
448
+ return entries.map(([category, count]) => ({ category, count }));
449
+ }
@@ -0,0 +1,187 @@
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/runtimeToggles.ts — v4.5 Phase 8a.
10
+ *
11
+ * Single source of truth for the v4.2/v4.3/v4.4 subsystem
12
+ * default-on toggles (TCE, browser depth, sandbox). Replaces the
13
+ * direct `process.env.AIDEN_*` reads scattered across:
14
+ *
15
+ * - core/v4/sandboxConfig.ts (AIDEN_SANDBOX)
16
+ * - core/v4/turnState.ts (AIDEN_TCE)
17
+ * - core/v4/browserState.ts (AIDEN_BROWSER_DEPTH)
18
+ *
19
+ * with a centralised resolver that supports:
20
+ *
21
+ * - **Live flip** via slash commands (/sandbox on|off, /tce on|off,
22
+ * /browser-depth on|off). The slash command updates the in-process
23
+ * state + persists to config.yaml, and fires onChange callbacks so
24
+ * cached consumers (sandboxConfig's singleton) invalidate.
25
+ *
26
+ * - **Persistence** across restarts via
27
+ * `<AIDEN_HOME>/config.yaml :: runtime_toggles.{sandbox,tce,browser_depth}`.
28
+ *
29
+ * - **Env-var precedence** (Q-P8a-1a): explicit env var > config.yaml >
30
+ * default (true for all three). Matches the existing AIDEN_*
31
+ * escape-hatch contract.
32
+ *
33
+ * The singleton is initialised by the CLI at boot via `initRuntimeToggles`
34
+ * with a ConfigProvider seam. Core modules that read the toggles call
35
+ * `getRuntimeToggles().isEnabled(key)` — when the singleton hasn't been
36
+ * initialised (test bench, core-only invocation), an env-only fallback
37
+ * resolver is used so the modules keep working with their pre-v4.5
38
+ * semantics.
39
+ */
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports._TOGGLE_KEYS = void 0;
42
+ exports.buildRuntimeToggles = buildRuntimeToggles;
43
+ exports.getRuntimeToggles = getRuntimeToggles;
44
+ exports.initRuntimeToggles = initRuntimeToggles;
45
+ exports._resetRuntimeTogglesForTests = _resetRuntimeTogglesForTests;
46
+ // ── Env var mapping ────────────────────────────────────────────────────────
47
+ const ENV_VAR = {
48
+ sandbox: 'AIDEN_SANDBOX',
49
+ tce: 'AIDEN_TCE',
50
+ browser_depth: 'AIDEN_BROWSER_DEPTH',
51
+ // v4.5 Phase 8b — contextual capability suggestions. Rarely set as
52
+ // env (this is mostly a UX toggle) but included for symmetry with
53
+ // the other subsystem toggles.
54
+ suggestions: 'AIDEN_SUGGESTIONS',
55
+ };
56
+ const CONFIG_KEY = {
57
+ sandbox: 'runtime_toggles.sandbox',
58
+ tce: 'runtime_toggles.tce',
59
+ browser_depth: 'runtime_toggles.browser_depth',
60
+ suggestions: 'runtime_toggles.suggestions',
61
+ };
62
+ const ALL_KEYS = ['sandbox', 'tce', 'browser_depth', 'suggestions'];
63
+ // ── Resolver primitives ────────────────────────────────────────────────────
64
+ /**
65
+ * Strict env interpretation matching existing v4.2/v4.3/v4.4
66
+ * semantics: literal `'0'` (or `'false'` for forgiveness) means off;
67
+ * unset means defer to next leg; anything else means on. Returns
68
+ * `null` when the env var is unset / empty — caller falls through to
69
+ * config or default.
70
+ */
71
+ function readEnv(env, key) {
72
+ const raw = env[ENV_VAR[key]];
73
+ if (typeof raw !== 'string' || raw.length === 0)
74
+ return null;
75
+ const trimmed = raw.trim().toLowerCase();
76
+ if (trimmed === '0' || trimmed === 'false' || trimmed === 'off' || trimmed === 'no') {
77
+ return false;
78
+ }
79
+ return true;
80
+ }
81
+ function readConfig(cfg, key) {
82
+ if (!cfg)
83
+ return null;
84
+ const raw = cfg(CONFIG_KEY[key]);
85
+ if (raw === undefined || raw === null)
86
+ return null;
87
+ if (typeof raw === 'boolean')
88
+ return raw;
89
+ if (typeof raw === 'string') {
90
+ const t = raw.trim().toLowerCase();
91
+ if (t === 'true' || t === '1' || t === 'on' || t === 'yes')
92
+ return true;
93
+ if (t === 'false' || t === '0' || t === 'off' || t === 'no')
94
+ return false;
95
+ }
96
+ if (typeof raw === 'number')
97
+ return raw !== 0;
98
+ return null;
99
+ }
100
+ // ── Singleton ──────────────────────────────────────────────────────────────
101
+ let _singleton = null;
102
+ /**
103
+ * Build a RuntimeToggles instance bound to the supplied deps.
104
+ * Public so tests can construct isolated instances.
105
+ */
106
+ function buildRuntimeToggles(deps = {}) {
107
+ const env = deps.env ?? process.env;
108
+ // In-process overrides — set() updates this map; subsequent
109
+ // isEnabled() reads see the override before falling through to
110
+ // env/config/default.
111
+ const overrides = new Map();
112
+ const subscribers = new Map();
113
+ function resolve(key) {
114
+ // 1. env (Q-P8a-1a — explicit env always wins)
115
+ const envValue = readEnv(env, key);
116
+ if (envValue !== null)
117
+ return { value: envValue, source: 'env' };
118
+ // 2. in-process override (slash-command flip not yet persisted)
119
+ if (overrides.has(key))
120
+ return { value: overrides.get(key), source: 'config' };
121
+ // 3. config.yaml
122
+ const cfgValue = readConfig(deps.configRead, key);
123
+ if (cfgValue !== null)
124
+ return { value: cfgValue, source: 'config' };
125
+ // 4. default
126
+ return { value: true, source: 'default' };
127
+ }
128
+ function fire(key) {
129
+ const set = subscribers.get(key);
130
+ if (!set)
131
+ return;
132
+ for (const cb of set) {
133
+ try {
134
+ cb();
135
+ }
136
+ catch { /* never let an invalidation callback crash the flip */ }
137
+ }
138
+ }
139
+ return {
140
+ isEnabled(key) { return resolve(key).value; },
141
+ async set(key, value, opts = {}) {
142
+ overrides.set(key, value);
143
+ if (opts.persist !== false && deps.configWriteAndSave) {
144
+ await deps.configWriteAndSave(CONFIG_KEY[key], value);
145
+ }
146
+ fire(key);
147
+ },
148
+ snapshot() {
149
+ const out = {};
150
+ for (const k of ALL_KEYS)
151
+ out[k] = resolve(k);
152
+ return out;
153
+ },
154
+ onChange(key, cb) {
155
+ let set = subscribers.get(key);
156
+ if (!set) {
157
+ set = new Set();
158
+ subscribers.set(key, set);
159
+ }
160
+ set.add(cb);
161
+ },
162
+ };
163
+ }
164
+ /**
165
+ * Return the process-wide RuntimeToggles. When `initRuntimeToggles`
166
+ * hasn't been called, returns a env-only fallback resolver so core
167
+ * modules (sandboxConfig, turnState, browserState) keep working in
168
+ * test benches + core-only invocations.
169
+ */
170
+ function getRuntimeToggles() {
171
+ if (!_singleton)
172
+ _singleton = buildRuntimeToggles();
173
+ return _singleton;
174
+ }
175
+ /**
176
+ * Initialise the singleton with the CLI's ConfigManager seam. Called
177
+ * once by `aidenCLI.ts::buildAgentRuntime` after config.yaml is loaded.
178
+ */
179
+ function initRuntimeToggles(deps) {
180
+ _singleton = buildRuntimeToggles(deps);
181
+ return _singleton;
182
+ }
183
+ /** Test-only reset. */
184
+ function _resetRuntimeTogglesForTests() {
185
+ _singleton = null;
186
+ }
187
+ exports._TOGGLE_KEYS = ALL_KEYS;