aiden-runtime 4.1.4 → 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 (169) hide show
  1. package/README.md +250 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +379 -53
  4. package/dist/cli/v4/callbacks.js +248 -0
  5. package/dist/cli/v4/chatSession.js +292 -4
  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 +75 -3
  21. package/dist/cli/v4/display/capabilityCard.js +26 -0
  22. package/dist/cli/v4/display/progressBar.js +41 -8
  23. package/dist/cli/v4/display.js +258 -15
  24. package/dist/cli/v4/replyRenderer.js +31 -23
  25. package/dist/cli/v4/toolPreview.js +10 -0
  26. package/dist/cli/v4/updateBootPrompt.js +170 -0
  27. package/dist/core/playwrightBridge.js +129 -0
  28. package/dist/core/toolRegistry.js +7 -1
  29. package/dist/core/v4/aidenAgent.js +371 -4
  30. package/dist/core/v4/browserState.js +436 -0
  31. package/dist/core/v4/checkpoint.js +79 -0
  32. package/dist/core/v4/daemon/bootstrap.js +604 -0
  33. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  34. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  35. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  36. package/dist/core/v4/daemon/cron/migration.js +199 -0
  37. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  38. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  39. package/dist/core/v4/daemon/db/connection.js +106 -0
  40. package/dist/core/v4/daemon/db/migrations.js +296 -0
  41. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  42. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  43. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  44. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  45. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  46. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  47. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  48. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  49. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  50. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  51. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  52. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  53. package/dist/core/v4/daemon/drain.js +156 -0
  54. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  55. package/dist/core/v4/daemon/health.js +159 -0
  56. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  57. package/dist/core/v4/daemon/index.js +179 -0
  58. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  59. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  60. package/dist/core/v4/daemon/restartCode.js +32 -0
  61. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  62. package/dist/core/v4/daemon/runStore.js +114 -0
  63. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  64. package/dist/core/v4/daemon/signals.js +50 -0
  65. package/dist/core/v4/daemon/supervisor.js +272 -0
  66. package/dist/core/v4/daemon/triggerBus.js +279 -0
  67. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  68. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  69. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  70. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  71. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  72. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  73. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  74. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  75. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  76. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  77. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  78. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  79. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  80. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  81. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  82. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  83. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  84. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  85. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  86. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  87. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  88. package/dist/core/v4/daemon/types.js +15 -0
  89. package/dist/core/v4/dockerSession.js +461 -0
  90. package/dist/core/v4/dryRun.js +117 -0
  91. package/dist/core/v4/failureClassifier.js +779 -0
  92. package/dist/core/v4/loopTrace.js +257 -0
  93. package/dist/core/v4/recoveryReport.js +449 -0
  94. package/dist/core/v4/runtimeToggles.js +187 -0
  95. package/dist/core/v4/sandboxConfig.js +285 -0
  96. package/dist/core/v4/sandboxFs.js +316 -0
  97. package/dist/core/v4/suggestionCatalog.js +41 -0
  98. package/dist/core/v4/suggestionEngine.js +210 -0
  99. package/dist/core/v4/toolRegistry.js +18 -0
  100. package/dist/core/v4/turnState.js +587 -0
  101. package/dist/core/v4/update/checkUpdate.js +63 -3
  102. package/dist/core/v4/update/installMethodDetect.js +115 -0
  103. package/dist/core/v4/update/registryClient.js +121 -0
  104. package/dist/core/v4/update/skipState.js +75 -0
  105. package/dist/core/v4/verifier.js +448 -0
  106. package/dist/core/version.js +1 -1
  107. package/dist/core/webSearch.js +64 -24
  108. package/dist/tools/v4/browser/_observer.js +224 -0
  109. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  110. package/dist/tools/v4/browser/browserClick.js +18 -1
  111. package/dist/tools/v4/browser/browserClose.js +18 -1
  112. package/dist/tools/v4/browser/browserExtract.js +5 -1
  113. package/dist/tools/v4/browser/browserFill.js +17 -1
  114. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  115. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  116. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  117. package/dist/tools/v4/browser/browserScroll.js +18 -1
  118. package/dist/tools/v4/browser/browserType.js +17 -1
  119. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  120. package/dist/tools/v4/executeCode.js +1 -0
  121. package/dist/tools/v4/files/fileCopy.js +56 -2
  122. package/dist/tools/v4/files/fileDelete.js +38 -1
  123. package/dist/tools/v4/files/fileList.js +12 -1
  124. package/dist/tools/v4/files/fileMove.js +59 -2
  125. package/dist/tools/v4/files/filePatch.js +43 -1
  126. package/dist/tools/v4/files/fileRead.js +12 -1
  127. package/dist/tools/v4/files/fileWrite.js +41 -1
  128. package/dist/tools/v4/index.js +71 -58
  129. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  130. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  131. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  132. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  133. package/dist/tools/v4/process/processKill.js +19 -0
  134. package/dist/tools/v4/process/processList.js +1 -0
  135. package/dist/tools/v4/process/processLogRead.js +1 -0
  136. package/dist/tools/v4/process/processSpawn.js +13 -0
  137. package/dist/tools/v4/process/processWait.js +1 -0
  138. package/dist/tools/v4/sessions/recallSession.js +1 -0
  139. package/dist/tools/v4/sessions/sessionList.js +1 -0
  140. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  141. package/dist/tools/v4/skills/lookupToolSchema.js +2 -0
  142. package/dist/tools/v4/skills/skillManage.js +13 -0
  143. package/dist/tools/v4/skills/skillView.js +1 -0
  144. package/dist/tools/v4/skills/skillsList.js +1 -0
  145. package/dist/tools/v4/subagent/subagentFanout.js +1 -0
  146. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  147. package/dist/tools/v4/system/appClose.js +13 -0
  148. package/dist/tools/v4/system/appInput.js +13 -0
  149. package/dist/tools/v4/system/appLaunch.js +13 -0
  150. package/dist/tools/v4/system/clipboardRead.js +1 -0
  151. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  152. package/dist/tools/v4/system/mediaKey.js +12 -0
  153. package/dist/tools/v4/system/mediaSessions.js +1 -0
  154. package/dist/tools/v4/system/mediaTransport.js +13 -0
  155. package/dist/tools/v4/system/naturalEvents.js +1 -0
  156. package/dist/tools/v4/system/nowPlaying.js +1 -0
  157. package/dist/tools/v4/system/osProcessList.js +1 -0
  158. package/dist/tools/v4/system/screenshot.js +1 -0
  159. package/dist/tools/v4/system/systemInfo.js +1 -0
  160. package/dist/tools/v4/system/volumeSet.js +17 -0
  161. package/dist/tools/v4/terminal/shellExec.js +81 -9
  162. package/dist/tools/v4/web/deepResearch.js +1 -0
  163. package/dist/tools/v4/web/openUrl.js +1 -0
  164. package/dist/tools/v4/web/webFetch.js +1 -0
  165. package/dist/tools/v4/web/webPage.js +1 -0
  166. package/dist/tools/v4/web/webSearch.js +1 -0
  167. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  168. package/package.json +7 -1
  169. package/plugins/aiden-plugin-cdp-browser/.granted-permissions.json +8 -0
@@ -0,0 +1,170 @@
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
+ * cli/v4/updateBootPrompt.ts — v4.5 update system.
10
+ *
11
+ * Boxed three-option prompt rendered after the boot card / status
12
+ * pills, before the bottomPromptHint (Q-U5(b) position). When an
13
+ * update is available AND not skipped, the user sees:
14
+ *
15
+ * ┌─────────────────────────────────────────────────────────┐
16
+ * │ ◆ Aiden 4.5.1 available (you're on 4.5.0) │
17
+ * │ │
18
+ * │ What's new: bug fix for IMAP reconnect on Windows │
19
+ * │ │
20
+ * │ Update now? (y/n/later) │
21
+ * │ y — update now, restart after │
22
+ * │ n — skip this version (don't ask again) │
23
+ * │ later — remind me next session │
24
+ * └─────────────────────────────────────────────────────────┘
25
+ *
26
+ * Behavior (Q-U2(a)):
27
+ * - 5-second timeout defaults to 'later' (no state change)
28
+ * - 'y' triggers `executeInstall` via the method-aware dispatch
29
+ * - 'n' persists `skippedVersion = status.latest` to the cache
30
+ * - 'later' is a no-op (re-prompt next session)
31
+ *
32
+ * Display sink: writes via the supplied `display`. Keypress capture
33
+ * uses raw stdin mode so the user can press a single key — no Enter
34
+ * needed for y/n. 'later' is the timeout default; an explicit 'l'
35
+ * keypress also maps to it.
36
+ *
37
+ * Designed to be skippable: if stdin isn't a TTY (CI / piped /
38
+ * non-interactive), short-circuits to 'later' immediately so boot
39
+ * doesn't hang.
40
+ */
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.renderBootUpdateBox = renderBootUpdateBox;
43
+ exports.showBootUpdatePrompt = showBootUpdatePrompt;
44
+ const DEFAULT_TIMEOUT_MS = 5000;
45
+ /**
46
+ * Pure box renderer — public so tests can assert the rendered shape
47
+ * without driving stdin.
48
+ */
49
+ function renderBootUpdateBox(status, method) {
50
+ const innerWidth = 60;
51
+ const pad = (s) => {
52
+ const visible = s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
53
+ const len = visible.length;
54
+ if (len >= innerWidth)
55
+ return s.slice(0, innerWidth);
56
+ return s + ' '.repeat(innerWidth - len);
57
+ };
58
+ const top = '┌' + '─'.repeat(innerWidth) + '┐';
59
+ const bottom = '└' + '─'.repeat(innerWidth) + '┘';
60
+ const blank = '│' + ' '.repeat(innerWidth) + '│';
61
+ const lines = [];
62
+ lines.push(top);
63
+ lines.push(blank);
64
+ lines.push('│' + pad(` ◆ Aiden ${status.latest} available (you're on ${status.installed})`) + '│');
65
+ if (status.releaseNotes && status.releaseNotes.length > 0) {
66
+ lines.push(blank);
67
+ lines.push('│' + pad(` What's new: ${status.releaseNotes}`) + '│');
68
+ }
69
+ lines.push(blank);
70
+ lines.push('│' + pad(` Update now? (y/n/later)`) + '│');
71
+ lines.push('│' + pad(` y — update via ${method.method}`) + '│');
72
+ lines.push('│' + pad(` n — skip ${status.latest} (don't ask again)`) + '│');
73
+ lines.push('│' + pad(` later — remind me next session (default in 5s)`) + '│');
74
+ lines.push(blank);
75
+ lines.push(bottom);
76
+ return lines;
77
+ }
78
+ /**
79
+ * Show the prompt and resolve with the user's choice. Never throws
80
+ * — returns 'later' on any error / timeout / non-TTY.
81
+ */
82
+ async function showBootUpdatePrompt(input) {
83
+ // Test seam — short-circuit before any I/O.
84
+ if (input._testChoice)
85
+ return input._testChoice;
86
+ const isTTY = input.isTTY ?? Boolean(input.stdin?.isTTY ?? process.stdin.isTTY);
87
+ // Non-interactive stdin → silently default to 'later'. Boot must
88
+ // not hang in CI / piped contexts.
89
+ if (!isTTY)
90
+ return 'later';
91
+ if (!input.status.updateAvailable || !input.status.latest)
92
+ return 'later';
93
+ if (input.status.skipped)
94
+ return 'later';
95
+ // Render the box.
96
+ for (const line of renderBootUpdateBox(input.status, input.method)) {
97
+ input.display.write(line + '\n');
98
+ }
99
+ const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
100
+ const stdin = input.stdin ?? process.stdin;
101
+ return await captureSingleKey(stdin, timeoutMs);
102
+ }
103
+ /**
104
+ * Read ONE keypress from stdin in raw mode. Maps:
105
+ * - 'y' / 'Y' → 'install'
106
+ * - 'n' / 'N' → 'skip'
107
+ * - 'l' / 'L' / Enter / any other key → 'later'
108
+ * - Timeout → 'later'
109
+ *
110
+ * Restores stdin's prior pause/resume + rawMode state so the
111
+ * subsequent REPL prompt isn't broken.
112
+ */
113
+ function captureSingleKey(stdin, timeoutMs) {
114
+ return new Promise((resolve) => {
115
+ let done = false;
116
+ const wasRaw = stdin.isRaw === true;
117
+ const wasPaused = stdin.isPaused();
118
+ const cleanup = () => {
119
+ if (done)
120
+ return;
121
+ done = true;
122
+ try {
123
+ stdin.removeListener('data', onData);
124
+ }
125
+ catch { /* noop */ }
126
+ try {
127
+ if (!wasRaw && stdin.setRawMode)
128
+ stdin.setRawMode(false);
129
+ }
130
+ catch { /* noop */ }
131
+ if (wasPaused)
132
+ try {
133
+ stdin.pause();
134
+ }
135
+ catch { /* noop */ }
136
+ clearTimeout(timer);
137
+ };
138
+ const onData = (chunk) => {
139
+ const raw = chunk.toString();
140
+ const ch = raw.length > 0 ? raw[0].toLowerCase() : '';
141
+ let choice;
142
+ if (ch === 'y')
143
+ choice = 'install';
144
+ else if (ch === 'n')
145
+ choice = 'skip';
146
+ else
147
+ choice = 'later';
148
+ cleanup();
149
+ resolve(choice);
150
+ };
151
+ try {
152
+ if (stdin.setRawMode && !wasRaw)
153
+ stdin.setRawMode(true);
154
+ if (wasPaused)
155
+ stdin.resume();
156
+ stdin.on('data', onData);
157
+ }
158
+ catch {
159
+ cleanup();
160
+ resolve('later');
161
+ return;
162
+ }
163
+ const timer = setTimeout(() => {
164
+ cleanup();
165
+ resolve('later');
166
+ }, timeoutMs);
167
+ if (typeof timer.unref === 'function')
168
+ timer.unref();
169
+ });
170
+ }
@@ -58,11 +58,14 @@ exports.pwClickFirstResult = pwClickFirstResult;
58
58
  exports.pwType = pwType;
59
59
  exports.pwScroll = pwScroll;
60
60
  exports.pwSnapshot = pwSnapshot;
61
+ exports.pwSnapshotHash = pwSnapshotHash;
62
+ exports.pwSnapshotTabs = pwSnapshotTabs;
61
63
  exports.pwGetUrl = pwGetUrl;
62
64
  exports.pwClose = pwClose;
63
65
  exports.getActiveBrowserPage = getActiveBrowserPage;
64
66
  const path_1 = __importDefault(require("path"));
65
67
  const fs_1 = __importDefault(require("fs"));
68
+ const crypto_1 = __importDefault(require("crypto"));
66
69
  const paths_1 = require("./paths");
67
70
  // ── Lazy-import Playwright so the server boots even if playwright
68
71
  // is not installed (tools will return a clear error message).
@@ -410,6 +413,132 @@ async function pwSnapshot() {
410
413
  return { ok: false, error: e.message };
411
414
  }
412
415
  }
416
+ /**
417
+ * v4.3 Phase 1 — structured page-state snapshot used by the BrowserState
418
+ * observer. Captures URL + title + body-text hash + recursive iframe-tree
419
+ * hash in a single in-page evaluate. Truncates body innerText to 5 000
420
+ * chars before hashing so cost stays bounded for large pages.
421
+ *
422
+ * Cross-origin iframe srcs are surfaced (URL is visible); attempting to
423
+ * read `iframe.contentDocument` on a cross-origin frame throws — the
424
+ * recursive walker catches and skips, recording only the iframe's src.
425
+ *
426
+ * Returns `ok: false` when the browser is closed or evaluate fails.
427
+ * Caller (BrowserState.captureState) treats `ok: false` as "snapshot
428
+ * unavailable, embed no sidecar this call".
429
+ */
430
+ async function pwSnapshotHash() {
431
+ try {
432
+ const page = await ensurePage();
433
+ const url = page.url();
434
+ const title = await page.title();
435
+ // eslint-disable-next-line no-undef
436
+ const data = await page.evaluate(() => {
437
+ const doc = globalThis.document;
438
+ const text = (doc?.body?.innerText ?? '');
439
+ // Recursive iframe URL walk. Cross-origin iframes throw on
440
+ // contentDocument access — catch and record just the src.
441
+ const urls = [];
442
+ function walk(d) {
443
+ try {
444
+ const iframes = Array.from(d.querySelectorAll('iframe'));
445
+ for (const f of iframes) {
446
+ urls.push(String(f.src ?? ''));
447
+ try {
448
+ if (f.contentDocument)
449
+ walk(f.contentDocument);
450
+ }
451
+ catch { /* cross-origin */ }
452
+ }
453
+ }
454
+ catch { /* defensive */ }
455
+ }
456
+ walk(doc);
457
+ return { text, frame_urls: urls.join('|') };
458
+ });
459
+ const dom_text_hash = crypto_1.default.createHash('sha256').update(data.text.slice(0, 5000)).digest('hex');
460
+ const frame_tree_hash = crypto_1.default.createHash('sha256').update(data.frame_urls).digest('hex');
461
+ return { ok: true, url, title, dom_text_hash, frame_tree_hash };
462
+ }
463
+ catch (e) {
464
+ return { ok: false, error: e.message };
465
+ }
466
+ }
467
+ // ── v4.3 Phase 4 — multi-tab snapshot ─────────────────────────────────────
468
+ //
469
+ // Persistent context can hold many pages (target=_blank, window.open,
470
+ // CDP popups). Aiden's tools still target `_activePage` only — Phase 4
471
+ // is DATA-ONLY: it records what tabs exist, who opened whom, and which
472
+ // is the active one, so v4.4+ cross-tab orchestration has a foundation.
473
+ //
474
+ // `_tabIdMap` is a WeakMap that assigns each observed Page a stable
475
+ // `tab_id` (`tab-1`, `tab-2`, ...). When a Page closes, Playwright drops
476
+ // its reference and the WeakMap entry GCs naturally — no manual cleanup.
477
+ const _tabIdMap = new WeakMap();
478
+ let _nextTabIdCounter = 0;
479
+ function getOrAssignTabId(page) {
480
+ let id = _tabIdMap.get(page);
481
+ if (!id) {
482
+ _nextTabIdCounter += 1;
483
+ id = `tab-${_nextTabIdCounter}`;
484
+ _tabIdMap.set(page, id);
485
+ }
486
+ return id;
487
+ }
488
+ /**
489
+ * v4.3 Phase 4 — enumerate all pages in the persistent context and
490
+ * return their wire-data form. Stable `tab_id` per Page via the
491
+ * `_tabIdMap` WeakMap. The opener Page is looked up the same way, so
492
+ * `opener_id` is stable across reconciliations as long as the parent
493
+ * still exists.
494
+ *
495
+ * Cheap — `context.pages()` is in-process; the per-page work is one
496
+ * `url()` getter and one async `title()` call. Total cost scales
497
+ * linearly with the number of tabs; for typical sessions (1-5 tabs)
498
+ * it's well under 50ms.
499
+ *
500
+ * Returns `ok: false` when the browser is closed. Caller (BrowserState
501
+ * .reconcileTabs) treats `ok: false` as "no reconciliation this cycle".
502
+ */
503
+ async function pwSnapshotTabs() {
504
+ try {
505
+ const ctx = await ensureContext();
506
+ const pages = ctx.pages();
507
+ const tabs = [];
508
+ for (const p of pages) {
509
+ // Skip pages already closed mid-walk (rare race).
510
+ if (typeof p.isClosed === 'function' && p.isClosed())
511
+ continue;
512
+ const tab_id = getOrAssignTabId(p);
513
+ const url = (typeof p.url === 'function') ? p.url() : '';
514
+ // title() can throw if the page navigated mid-walk; default to empty.
515
+ let title = '';
516
+ try {
517
+ title = await p.title();
518
+ }
519
+ catch { /* defensive */ }
520
+ // opener() returns the parent Page (or null). Same WeakMap lookup.
521
+ let opener_id = null;
522
+ try {
523
+ const opener = typeof p.opener === 'function' ? await p.opener() : null;
524
+ if (opener)
525
+ opener_id = _tabIdMap.get(opener) ?? null;
526
+ }
527
+ catch { /* defensive */ }
528
+ tabs.push({
529
+ tab_id,
530
+ url,
531
+ title,
532
+ is_active: p === _activePage,
533
+ opener_id,
534
+ });
535
+ }
536
+ return { ok: true, tabs };
537
+ }
538
+ catch (e) {
539
+ return { ok: false, error: e.message };
540
+ }
541
+ }
413
542
  /** Return the URL currently loaded in the active browser page. */
414
543
  async function pwGetUrl() {
415
544
  try {
@@ -1388,7 +1388,13 @@ exports.TOOLS = {
1388
1388
  return { success: false, output: '', error: `No research results for: ${topic}` };
1389
1389
  }
1390
1390
  const combined = results.join('\n\n');
1391
- console.log(`[deep_research] Complete: ${combined.length} chars across ${results.length} passes`);
1391
+ // v4.1.5 Issue O gated behind AIDEN_DEBUG_WEB to match the
1392
+ // webSearch.ts debug-helper convention. Default off; power users
1393
+ // export the env var to see the research chain.
1394
+ if (process.env.AIDEN_DEBUG_WEB === '1') {
1395
+ // eslint-disable-next-line no-console
1396
+ console.log(`[deep_research] Complete: ${combined.length} chars across ${results.length} passes`);
1397
+ }
1392
1398
  return { success: true, output: combined.slice(0, 15000) };
1393
1399
  },
1394
1400
  // Activate a specialist agent persona — actual synthesis happens in respond phase