@steipete/oracle 0.13.0 → 0.14.1

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 (71) hide show
  1. package/README.md +22 -11
  2. package/dist/bin/oracle-cli.js +71 -71
  3. package/dist/src/browser/actions/assistantResponse.js +93 -10
  4. package/dist/src/browser/actions/deepResearch.js +180 -52
  5. package/dist/src/browser/actions/modelSelection.js +357 -40
  6. package/dist/src/browser/actions/navigation.js +294 -65
  7. package/dist/src/browser/actions/promptComposer.js +50 -4
  8. package/dist/src/browser/actions/thinkingTime.js +517 -105
  9. package/dist/src/browser/artifacts.js +19 -0
  10. package/dist/src/browser/chatgptFiles.js +797 -0
  11. package/dist/src/browser/chatgptImages.js +256 -27
  12. package/dist/src/browser/chromeLifecycle.js +4 -0
  13. package/dist/src/browser/config.js +2 -0
  14. package/dist/src/browser/index.js +404 -41
  15. package/dist/src/browser/pageActions.js +1 -1
  16. package/dist/src/browser/policies.js +2 -6
  17. package/dist/src/browser/projectSourcesRunner.js +16 -3
  18. package/dist/src/browser/prompt.js +161 -36
  19. package/dist/src/browser/reattach.js +6 -2
  20. package/dist/src/browser/reattachability.js +22 -10
  21. package/dist/src/browser/recoverConversation.js +73 -0
  22. package/dist/src/browser/sessionRunner.js +4 -1
  23. package/dist/src/cli/browserConfig.js +6 -2
  24. package/dist/src/cli/browserDefaults.js +2 -1
  25. package/dist/src/cli/browserTabs.js +129 -54
  26. package/dist/src/cli/followup.js +72 -0
  27. package/dist/src/cli/help.js +1 -1
  28. package/dist/src/cli/options.js +24 -0
  29. package/dist/src/cli/reattachGuidance.js +8 -0
  30. package/dist/src/cli/runOptions.js +2 -12
  31. package/dist/src/cli/sessionCommand.js +4 -0
  32. package/dist/src/cli/sessionDisplay.js +17 -3
  33. package/dist/src/cli/sessionLineage.js +7 -4
  34. package/dist/src/cli/sessionRunner.js +16 -1
  35. package/dist/src/gemini-web/client.js +5 -10
  36. package/dist/src/gemini-web/executor.js +1 -24
  37. package/dist/src/gemini-web/models.js +83 -0
  38. package/dist/src/mcp/server.js +2 -0
  39. package/dist/src/mcp/tools/chatgptImage.js +132 -0
  40. package/dist/src/mcp/tools/consult.js +270 -175
  41. package/dist/src/mcp/types.js +13 -2
  42. package/dist/src/mcp/utils.js +87 -1
  43. package/dist/src/oracle/config.js +27 -1
  44. package/dist/src/oracle/files.js +4 -6
  45. package/dist/src/oracle/geminiModels.js +2 -0
  46. package/dist/src/oracle/markdown.js +36 -3
  47. package/dist/src/oracle/modelResolver.js +21 -13
  48. package/dist/src/oracle/promptAssembly.js +2 -4
  49. package/dist/src/oracle/request.js +3 -5
  50. package/dist/src/oracle/thinkingTime.js +40 -0
  51. package/dist/src/oracle/tokenStats.js +5 -1
  52. package/dist/src/oracle.js +1 -1
  53. package/dist/src/sessionManager.js +1 -0
  54. package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
  55. package/package.json +47 -58
  56. package/vendor/oracle-notifier/build-notifier.sh +0 -0
  57. package/dist/bin/oracle.js +0 -569
  58. package/dist/src/browser/chromeCookies.js +0 -312
  59. package/dist/src/browser/keytarShim.js +0 -56
  60. package/dist/src/browser/windowsCookies.js +0 -219
  61. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  62. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
  63. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  64. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  65. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
  66. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  67. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
  68. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  69. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  70. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
  71. package/vendor/oracle-notifier/README.md +0 -26
@@ -5,12 +5,12 @@ import net from "node:net";
5
5
  import { resolveBrowserConfig } from "./config.js";
6
6
  import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToRemoteChrome, connectWithNewTab, closeTab, closeRemoteChromeTarget, closeBlankChromeTabs, } from "./chromeLifecycle.js";
7
7
  import { syncCookies } from "./cookies.js";
8
- import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, installJavaScriptDialogAutoDismissal, ensureModelSelection, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from "./pageActions.js";
8
+ import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, ensureModelSelection, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from "./pageActions.js";
9
9
  import { INPUT_SELECTORS } from "./constants.js";
10
10
  import { uploadAttachmentViaDataTransfer } from "./actions/remoteFileTransfer.js";
11
11
  import { ensureThinkingTime } from "./actions/thinkingTime.js";
12
12
  import { startThinkingStatusMonitor } from "./actions/thinkingStatus.js";
13
- import { activateDeepResearch, waitForDeepResearchCompletion, waitForResearchPlanAutoConfirm, } from "./actions/deepResearch.js";
13
+ import { activateDeepResearch, captureDeepResearchTargetKeys, waitForDeepResearchCompletion, waitForResearchPlanAutoConfirm, } from "./actions/deepResearch.js";
14
14
  import { estimateTokenCount, withRetries, delay } from "./utils.js";
15
15
  import { formatElapsed } from "../oracle/format.js";
16
16
  import { CHATGPT_URL, CONVERSATION_TURN_SELECTOR, DEFAULT_MODEL_STRATEGY } from "./constants.js";
@@ -20,6 +20,7 @@ import { cleanupStaleProfileState, acquireProfileRunLock, findRunningChromeDebug
20
20
  import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLeaseRegistry.js";
21
21
  import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
22
22
  import { collectGeneratedImageArtifacts } from "./chatgptImages.js";
23
+ import { collectChatGptFileArtifacts } from "./chatgptFiles.js";
23
24
  import { runProviderSubmissionFlow } from "./providerDomFlow.js";
24
25
  import { chatgptDomProvider } from "./providers/index.js";
25
26
  import { resolveAttachRunningConnection } from "./attachRunning.js";
@@ -71,12 +72,207 @@ export function shouldPreserveBrowserOnErrorForTest(error, headless) {
71
72
  export function classifyPreservedBrowserErrorForTest(error, headless) {
72
73
  return classifyPreservedBrowserError(error, headless);
73
74
  }
74
- // NOTE: Previously, shouldSkipThinkingTimeSelection() would skip the thinking
75
- // time UI step when desiredModel was gpt-5.5-pro and thinkingTime was "extended",
76
- // assuming that selecting "Pro Extended" in the old UI already implied Extended
77
- // effort. This is wrong for lower-tier plans ($100/mo Pro) where selecting "Pro"
78
- // defaults to Standard effort. ensureThinkingTime() already handles the
79
- // "already-selected" case as a no-op, so always attempting it is safe.
75
+ const MAX_CHATGPT_UI_WARNING_CHARS = 300;
76
+ const MAX_CHATGPT_UI_WARNINGS = 3;
77
+ function classifyChatGptUiWarningText(text) {
78
+ const normalized = text.toLowerCase();
79
+ if (/\btoo many requests\b/.test(normalized) ||
80
+ /\bsending too many requests\b/.test(normalized) ||
81
+ /\btoo quickly\b/.test(normalized) ||
82
+ /\btemporarily limited access\b/.test(normalized) ||
83
+ /\bplease wait a few minutes\b/.test(normalized) ||
84
+ /\brate limit(?:ed)?\b/.test(normalized) ||
85
+ /\bslow down\b/.test(normalized)) {
86
+ return "rate_limit";
87
+ }
88
+ if (/\btemporarily unavailable\b/.test(normalized) ||
89
+ /\bsomething went wrong\b/.test(normalized) ||
90
+ /\bfailed to generate\b/.test(normalized) ||
91
+ /\btry again later\b/.test(normalized)) {
92
+ return "temporary_unavailable";
93
+ }
94
+ if (/\bverify you are human\b/.test(normalized) ||
95
+ /\bunusual activity\b/.test(normalized) ||
96
+ /\bcloudflare\b/.test(normalized) ||
97
+ /\bchallenge\b/.test(normalized) ||
98
+ /\blogin required\b/.test(normalized) ||
99
+ /\bsign in\b/.test(normalized)) {
100
+ return "auth_or_challenge";
101
+ }
102
+ return null;
103
+ }
104
+ function sanitizeChatGptUiWarningText(text) {
105
+ return text
106
+ .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[redacted-email]")
107
+ .replace(/\b((?:access|auth|session)[-_ ]?token|token)\s*[:=]\s*["']?[^\s"',;]+/gi, "$1=[redacted]")
108
+ .replace(/\b(?:sk-(?:ant-|or-)?|xai-)[A-Za-z0-9_-]{8,}\b/g, "[redacted-token]");
109
+ }
110
+ function normalizeUiWarningCandidate(value) {
111
+ if (!value || typeof value !== "object")
112
+ return null;
113
+ const candidate = value;
114
+ const text = typeof candidate.text === "string"
115
+ ? sanitizeChatGptUiWarningText(candidate.text.replace(/\s+/g, " ").trim())
116
+ : "";
117
+ if (!text)
118
+ return null;
119
+ return {
120
+ text: text.slice(0, MAX_CHATGPT_UI_WARNING_CHARS),
121
+ source: typeof candidate.source === "string" ? candidate.source : null,
122
+ role: typeof candidate.role === "string" ? candidate.role : null,
123
+ ariaLive: typeof candidate.ariaLive === "string" ? candidate.ariaLive : null,
124
+ selector: typeof candidate.selector === "string" ? candidate.selector : null,
125
+ };
126
+ }
127
+ async function collectChatGptUiWarnings(Runtime) {
128
+ try {
129
+ const { result } = await Runtime.evaluate({
130
+ awaitPromise: true,
131
+ returnByValue: true,
132
+ expression: `(() => {
133
+ const warningPattern = /too many requests|sending too many requests|too quickly|temporarily limited access|please wait a few minutes|rate limit|rate limited|slow down|try again later|temporarily unavailable|something went wrong|failed to generate|verify you are human|unusual activity|cloudflare|challenge|login required|sign in/i;
134
+ const selectors = [
135
+ '[role="alert"]',
136
+ '[role="status"]',
137
+ '[role="dialog"]',
138
+ '[aria-live]',
139
+ '[data-testid*="toast" i]',
140
+ '[data-testid*="banner" i]',
141
+ '[data-testid*="error" i]',
142
+ '[class*="toast" i]',
143
+ '[class*="banner" i]'
144
+ ];
145
+ const isVisible = (element) => {
146
+ if (!(element instanceof HTMLElement)) return false;
147
+ let current = element;
148
+ while (current) {
149
+ const currentStyle = window.getComputedStyle(current);
150
+ if (
151
+ !currentStyle ||
152
+ currentStyle.display === 'none' ||
153
+ currentStyle.visibility === 'hidden' ||
154
+ currentStyle.visibility === 'collapse' ||
155
+ Number.parseFloat(currentStyle.opacity || '1') === 0
156
+ ) {
157
+ return false;
158
+ }
159
+ current = current.parentElement;
160
+ }
161
+ const rect = element.getBoundingClientRect();
162
+ return rect.width > 0 && rect.height > 0;
163
+ };
164
+ const describe = (element, source, selector = null) => ({
165
+ text: (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 1000),
166
+ source,
167
+ selector,
168
+ role: element.getAttribute('role'),
169
+ ariaLive: element.getAttribute('aria-live')
170
+ });
171
+ const out = [];
172
+ const seen = new Set();
173
+ const warningContainers = [];
174
+ const overlapsWarningContainer = (element) => warningContainers.some((container) => (
175
+ container !== element && (container.contains(element) || element.contains(container))
176
+ ));
177
+ const add = (element, entry) => {
178
+ if (!entry.text || !warningPattern.test(entry.text)) return;
179
+ const key = entry.text + '|' + (entry.role || '') + '|' + (entry.ariaLive || '');
180
+ if (seen.has(key)) return;
181
+ seen.add(key);
182
+ warningContainers.push(element);
183
+ out.push(entry);
184
+ };
185
+ for (const selector of selectors) {
186
+ if (out.length >= 5) break;
187
+ let elements = [];
188
+ try {
189
+ elements = Array.from(document.querySelectorAll(selector));
190
+ } catch {
191
+ elements = [];
192
+ }
193
+ for (const element of elements) {
194
+ if (out.length >= 5) break;
195
+ if (overlapsWarningContainer(element)) continue;
196
+ if (isVisible(element)) add(element, describe(element, 'selector', selector));
197
+ }
198
+ }
199
+ return out.slice(0, 5);
200
+ })()`,
201
+ });
202
+ const rawWarnings = Array.isArray(result?.value) ? result.value : [];
203
+ const warnings = [];
204
+ const seen = new Set();
205
+ for (const raw of rawWarnings) {
206
+ const candidate = normalizeUiWarningCandidate(raw);
207
+ if (!candidate)
208
+ continue;
209
+ const type = classifyChatGptUiWarningText(candidate.text);
210
+ if (!type)
211
+ continue;
212
+ const key = `${type}:${candidate.text}`;
213
+ if (seen.has(key))
214
+ continue;
215
+ seen.add(key);
216
+ warnings.push({
217
+ type,
218
+ message: candidate.text,
219
+ source: candidate.source,
220
+ role: candidate.role,
221
+ ariaLive: candidate.ariaLive,
222
+ selector: candidate.selector,
223
+ });
224
+ if (warnings.length >= MAX_CHATGPT_UI_WARNINGS)
225
+ break;
226
+ }
227
+ return warnings;
228
+ }
229
+ catch {
230
+ return [];
231
+ }
232
+ }
233
+ function formatChatGptUiWarningType(type) {
234
+ switch (type) {
235
+ case "rate_limit":
236
+ return "rate-limit";
237
+ case "temporary_unavailable":
238
+ return "temporary-unavailable";
239
+ case "auth_or_challenge":
240
+ return "authentication/challenge";
241
+ }
242
+ }
243
+ async function createChatGptUiWarningError(params) {
244
+ const [uiWarning] = await collectChatGptUiWarnings(params.Runtime);
245
+ if (!uiWarning)
246
+ return null;
247
+ params.logger(`[browser] ChatGPT UI warning detected (${uiWarning.type}): ${uiWarning.message}`);
248
+ return new BrowserAutomationError(`ChatGPT displayed a ${formatChatGptUiWarningType(uiWarning.type)} warning while waiting for ${params.waitTarget}: ${uiWarning.message}`, {
249
+ stage: params.stage,
250
+ code: "chatgpt-ui-warning",
251
+ uiWarning,
252
+ runtime: params.runtime,
253
+ diagnostics: params.diagnostics,
254
+ }, params.cause);
255
+ }
256
+ async function throwChatGptUiWarningIfPresent(params) {
257
+ const error = await createChatGptUiWarningError(params);
258
+ if (error)
259
+ throw error;
260
+ }
261
+ async function createAssistantTimeoutError(params) {
262
+ const warningError = await createChatGptUiWarningError({
263
+ Runtime: params.Runtime,
264
+ logger: params.logger,
265
+ runtime: params.runtime,
266
+ stage: "assistant-timeout",
267
+ waitTarget: "the assistant",
268
+ diagnostics: params.diagnostics,
269
+ cause: params.cause,
270
+ });
271
+ if (!warningError) {
272
+ return new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime: params.runtime, diagnostics: params.diagnostics }, params.cause);
273
+ }
274
+ return warningError;
275
+ }
80
276
  function listIgnoredRemoteChromeFlags(config) {
81
277
  return [
82
278
  config.headless ? "--browser-headless" : null,
@@ -157,7 +353,8 @@ function isImageOnlyUiChromeText(text) {
157
353
  return (normalized.length === 0 ||
158
354
  normalized === "edit" ||
159
355
  normalized === "stopped thinking" ||
160
- normalized === "stopped thinking edit");
356
+ normalized === "stopped thinking edit" ||
357
+ /^thought for \d+(?:\.\d+)?\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)\s+edit$/.test(normalized));
161
358
  }
162
359
  function normalizeBrowserFollowUpPrompts(values) {
163
360
  return (values ?? []).map((entry) => entry.trim()).filter(Boolean);
@@ -273,6 +470,19 @@ function resolveRemoteTabLeaseProfileDir(config) {
273
470
  export function resolveRemoteTabLeaseProfileDirForTest(config) {
274
471
  return resolveRemoteTabLeaseProfileDir(config);
275
472
  }
473
+ function isLocalChromeHost(host) {
474
+ const normalized = host
475
+ .trim()
476
+ .toLowerCase()
477
+ .replace(/^\[|\]$/g, "");
478
+ if (normalized === "localhost" || normalized === "::1") {
479
+ return true;
480
+ }
481
+ return net.isIPv4(normalized) && normalized.startsWith("127.");
482
+ }
483
+ export function isLocalChromeHostForTest(host) {
484
+ return isLocalChromeHost(host);
485
+ }
276
486
  async function closeRemoteConnectionAfterRun(options) {
277
487
  if (options.connectionClosedUnexpectedly) {
278
488
  return;
@@ -310,6 +520,7 @@ export async function runBrowserMode(options) {
310
520
  const attachments = options.attachments ?? [];
311
521
  const fallbackSubmission = options.fallbackSubmission;
312
522
  let config = resolveBrowserConfig(options.config);
523
+ const isResumingConversation = Boolean(config.resumeConversationUrl);
313
524
  const followUpPrompts = normalizeBrowserFollowUpPrompts(options.followUpPrompts);
314
525
  if (config.researchMode === "deep" && followUpPrompts.length > 0) {
315
526
  throw new BrowserAutomationError("Browser follow-ups are not supported with Deep Research mode. Put the full research plan into the initial prompt or run a normal browser consult for multi-turn review.", {
@@ -499,9 +710,10 @@ export async function runBrowserMode(options) {
499
710
  }
500
711
  else {
501
712
  const strictTabIsolation = Boolean(manualLogin && reusedChrome);
713
+ const devtoolsRetries = manualLogin ? 6 : 0;
502
714
  const connection = await connectWithNewTab(chrome.port, logger, config.url, chromeHost, {
503
715
  fallbackToDefault: !strictTabIsolation,
504
- retries: strictTabIsolation ? 3 : 0,
716
+ retries: devtoolsRetries,
505
717
  retryDelayMs: 500,
506
718
  });
507
719
  client = connection.client;
@@ -596,9 +808,18 @@ export async function runBrowserMode(options) {
596
808
  });
597
809
  }
598
810
  if (config.browserTabRef) {
811
+ if (isResumingConversation) {
812
+ await raceWithDisconnect(navigateToChatGPT(Page, Runtime, config.resumeConversationUrl, logger));
813
+ }
599
814
  await raceWithDisconnect(ensureNotBlocked(Runtime, config.headless, logger));
600
815
  await raceWithDisconnect(ensureLoggedIn(Runtime, logger));
601
816
  await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
817
+ if (isResumingConversation) {
818
+ await raceWithDisconnect(waitForResumedConversationHydration(Runtime, config.inputTimeoutMs, logger, {
819
+ requirePriorTurns: true,
820
+ expectedConversationUrl: config.resumeConversationUrl,
821
+ }));
822
+ }
602
823
  }
603
824
  else {
604
825
  const baseUrl = CHATGPT_URL;
@@ -616,7 +837,12 @@ export async function runBrowserMode(options) {
616
837
  profileDir: userDataDir,
617
838
  keepBrowser: effectiveKeepBrowser,
618
839
  }));
619
- if (config.url !== baseUrl) {
840
+ if (isResumingConversation) {
841
+ await raceWithDisconnect(navigateToChatGPT(Page, Runtime, config.resumeConversationUrl, logger));
842
+ await raceWithDisconnect(ensureNotBlocked(Runtime, config.headless, logger));
843
+ await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
844
+ }
845
+ else if (config.url !== baseUrl) {
620
846
  await raceWithDisconnect(navigateToPromptReadyWithFallback(Page, Runtime, {
621
847
  url: config.url,
622
848
  fallbackUrl: baseUrl,
@@ -628,6 +854,17 @@ export async function runBrowserMode(options) {
628
854
  else {
629
855
  await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
630
856
  }
857
+ if (isResumingConversation) {
858
+ // A resumed thread loads its prior history after navigation; ChatGPT can reset the
859
+ // composer mid-hydration and wipe a freshly-typed prompt. Wait for hydration to settle
860
+ // and re-confirm the composer before the prompt is typed/submitted below. Wrapped in
861
+ // raceWithDisconnect so a dropped client aborts immediately instead of polling to the
862
+ // hydration deadline. Shared with the remote path via the same helper.
863
+ await raceWithDisconnect(waitForResumedConversationHydration(Runtime, config.inputTimeoutMs, logger, {
864
+ requirePriorTurns: true,
865
+ expectedConversationUrl: config.resumeConversationUrl,
866
+ }));
867
+ }
631
868
  }
632
869
  logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
633
870
  const captureRuntimeSnapshot = async () => {
@@ -707,7 +944,7 @@ export async function runBrowserMode(options) {
707
944
  };
708
945
  await captureRuntimeSnapshot();
709
946
  const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
710
- if (config.desiredModel && modelStrategy !== "ignore") {
947
+ if (config.desiredModel && modelStrategy !== "ignore" && !isResumingConversation) {
711
948
  modelSelectionEvidence = await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
712
949
  retries: 2,
713
950
  delayMs: 300,
@@ -726,9 +963,11 @@ export async function runBrowserMode(options) {
726
963
  await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
727
964
  logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
728
965
  }
729
- else if (modelStrategy === "ignore") {
966
+ else if (modelStrategy === "ignore" || isResumingConversation) {
730
967
  modelSelectionEvidence = buildSkippedModelSelectionEvidence(config.desiredModel, modelStrategy);
731
- logger("Model picker: skipped (strategy=ignore)");
968
+ logger(isResumingConversation
969
+ ? "Model picker: skipped (resumed conversation)"
970
+ : "Model picker: skipped (strategy=ignore)");
732
971
  }
733
972
  const deepResearch = config.researchMode === "deep";
734
973
  // Handle thinking time selection if specified. Deep Research owns its own effort flow.
@@ -779,6 +1018,10 @@ export async function runBrowserMode(options) {
779
1018
  const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
780
1019
  const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
781
1020
  const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
1021
+ const attachmentExpectations = submissionAttachments.map((a) => ({
1022
+ name: path.basename(a.path),
1023
+ generatedBundle: a.generatedBundle === true,
1024
+ }));
782
1025
  let inputOnlyAttachments = false;
783
1026
  await raceWithDisconnect(clearPromptComposer(Runtime, logger));
784
1027
  await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
@@ -814,9 +1057,12 @@ export async function runBrowserMode(options) {
814
1057
  inputTimeoutMs: config.inputTimeoutMs ?? undefined,
815
1058
  attachmentTimeoutMs: config.attachmentTimeoutMs ?? undefined,
816
1059
  baselineTurns: baselineTurns ?? undefined,
817
- attachmentNames,
1060
+ attachmentNames: attachmentExpectations,
818
1061
  onPromptSubmitted: markPromptSubmitted,
819
1062
  };
1063
+ const deepResearchTargetKeys = deepResearch && client
1064
+ ? await captureDeepResearchTargetKeys(client).catch(() => [])
1065
+ : undefined;
820
1066
  await runProviderSubmissionFlow(chatgptDomProvider, {
821
1067
  prompt,
822
1068
  evaluate: async () => undefined,
@@ -849,7 +1095,7 @@ export async function runBrowserMode(options) {
849
1095
  }
850
1096
  // Reattach needs a /c/ URL; ChatGPT can update it late, so poll in the background.
851
1097
  scheduleConversationHint("post-submit", config.timeoutMs ?? 120_000);
852
- return { baselineTurns, baselineAssistantText };
1098
+ return { baselineTurns, baselineAssistantText, deepResearchTargetKeys };
853
1099
  };
854
1100
  const reloadPromptComposer = async () => {
855
1101
  logger("[browser] Composer became unresponsive; reloading page and retrying once.");
@@ -858,6 +1104,7 @@ export async function runBrowserMode(options) {
858
1104
  };
859
1105
  let baselineTurns = null;
860
1106
  let baselineAssistantText = null;
1107
+ let deepResearchTargetKeys = [];
861
1108
  await acquireProfileLockIfNeeded();
862
1109
  try {
863
1110
  const submission = await runSubmissionWithRecovery({
@@ -874,6 +1121,7 @@ export async function runBrowserMode(options) {
874
1121
  });
875
1122
  baselineTurns = submission.baselineTurns;
876
1123
  baselineAssistantText = submission.baselineAssistantText;
1124
+ deepResearchTargetKeys = submission.deepResearchTargetKeys ?? [];
877
1125
  }
878
1126
  finally {
879
1127
  await releaseProfileLockIfHeld();
@@ -881,7 +1129,7 @@ export async function runBrowserMode(options) {
881
1129
  const imageArtifactMinTurnIndex = baselineTurns;
882
1130
  if (deepResearch) {
883
1131
  await raceWithDisconnect(waitForResearchPlanAutoConfirm(Runtime, logger));
884
- const researchResult = await raceWithDisconnect(waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client));
1132
+ const researchResult = await raceWithDisconnect(waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client, { ignoredTargetKeys: deepResearchTargetKeys }));
885
1133
  await updateConversationHint("post-deep-research", 15_000).catch(() => false);
886
1134
  runStatus = "complete";
887
1135
  const durationMs = Date.now() - startedAt;
@@ -1070,7 +1318,13 @@ export async function runBrowserMode(options) {
1070
1318
  promptSubmitted,
1071
1319
  controllerPid: process.pid,
1072
1320
  };
1073
- throw new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime, diagnostics }, error);
1321
+ throw await createAssistantTimeoutError({
1322
+ Runtime,
1323
+ logger,
1324
+ runtime,
1325
+ diagnostics,
1326
+ cause: error,
1327
+ });
1074
1328
  }
1075
1329
  }
1076
1330
  else {
@@ -1259,6 +1513,9 @@ export async function runBrowserMode(options) {
1259
1513
  throw new Error("Chrome disconnected before completion");
1260
1514
  }
1261
1515
  const imageArtifacts = await collectGeneratedImageArtifacts({
1516
+ Browser: client.Browser,
1517
+ Client: client,
1518
+ Page,
1262
1519
  Runtime,
1263
1520
  Network,
1264
1521
  logger,
@@ -1268,21 +1525,50 @@ export async function runBrowserMode(options) {
1268
1525
  outputPath: options.outputPath,
1269
1526
  answerText,
1270
1527
  waitTimeoutMs: options.config?.timeoutMs,
1528
+ checkBlockingUiWarning: () => throwChatGptUiWarningIfPresent({
1529
+ Runtime,
1530
+ logger,
1531
+ stage: "image-artifact-wait",
1532
+ waitTarget: "generated image artifacts",
1533
+ runtime: {
1534
+ chromePid: chrome.pid,
1535
+ chromePort: chrome.port,
1536
+ chromeHost,
1537
+ userDataDir,
1538
+ chromeTargetId: lastTargetId,
1539
+ tabUrl: lastUrl,
1540
+ conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1541
+ promptSubmitted,
1542
+ controllerPid: process.pid,
1543
+ },
1544
+ }),
1271
1545
  });
1272
1546
  answerText = imageArtifacts.answerText || answerText;
1273
1547
  if (imageArtifacts.markdownSuffix) {
1274
1548
  answerMarkdown += imageArtifacts.markdownSuffix;
1275
1549
  }
1550
+ const fileArtifacts = await collectChatGptFileArtifacts({
1551
+ Browser: client.Browser,
1552
+ Client: client,
1553
+ Page,
1554
+ Runtime,
1555
+ Network,
1556
+ answerText: [answerText, answerMarkdown, answerHtml].filter(Boolean).join("\n"),
1557
+ logger,
1558
+ minTurnIndex: imageArtifactMinTurnIndex,
1559
+ sessionId: options.sessionId,
1560
+ });
1276
1561
  const savedImageArtifacts = appendArtifacts(undefined, imageArtifacts.savedImages);
1562
+ const savedBrowserArtifacts = appendArtifacts(savedImageArtifacts, fileArtifacts.savedFiles);
1277
1563
  const transcriptArtifact = await saveOptionalArtifact(() => saveBrowserTranscriptArtifact({
1278
1564
  sessionId: options.sessionId,
1279
1565
  prompt: promptText,
1280
1566
  answerMarkdown,
1281
1567
  conversationUrl: lastUrl,
1282
- artifacts: savedImageArtifacts,
1568
+ artifacts: savedBrowserArtifacts,
1283
1569
  logger,
1284
1570
  }), logger);
1285
- const savedArtifacts = appendArtifacts(savedImageArtifacts, [transcriptArtifact]);
1571
+ const savedArtifacts = appendArtifacts(savedBrowserArtifacts, [transcriptArtifact]);
1286
1572
  const archive = await maybeArchiveCompletedConversation({
1287
1573
  Runtime,
1288
1574
  logger,
@@ -1290,7 +1576,8 @@ export async function runBrowserMode(options) {
1290
1576
  conversationUrl: lastUrl,
1291
1577
  followUpCount: followUpPrompts.length,
1292
1578
  requiredArtifactsSaved: Boolean(transcriptArtifact) &&
1293
- imageArtifacts.savedImages.length === imageArtifacts.imageCount,
1579
+ imageArtifacts.savedImages.length === imageArtifacts.imageCount &&
1580
+ fileArtifacts.savedFiles.length === fileArtifacts.fileCount,
1294
1581
  });
1295
1582
  runStatus = "complete";
1296
1583
  const durationMs = Date.now() - startedAt;
@@ -1303,6 +1590,8 @@ export async function runBrowserMode(options) {
1303
1590
  artifacts: savedArtifacts,
1304
1591
  generatedImages: imageArtifacts.generatedImages,
1305
1592
  savedImages: imageArtifacts.savedImages,
1593
+ downloadableFiles: fileArtifacts.files,
1594
+ savedFiles: fileArtifacts.savedFiles,
1306
1595
  archive,
1307
1596
  modelSelection: modelSelectionEvidence,
1308
1597
  tookMs: durationMs,
@@ -1676,13 +1965,23 @@ async function maybeReuseRunningChrome(userDataDir, logger, options = {}) {
1676
1965
  let pid = await readChromePid(userDataDir);
1677
1966
  if (!port) {
1678
1967
  const discovered = await findRunningChromeDebugTargetForProfile(userDataDir);
1679
- if (!discovered)
1968
+ if (!discovered) {
1969
+ if (pid) {
1970
+ logger(`No reachable Chrome DevTools target found for ${userDataDir}; clearing stale profile state before launching new Chrome.`);
1971
+ await cleanupStaleProfileState(userDataDir, logger, {
1972
+ lockRemovalMode: "if_oracle_pid_dead",
1973
+ });
1974
+ }
1680
1975
  return null;
1976
+ }
1681
1977
  const discoveredProbe = await (options.probe ?? verifyDevToolsReachable)({
1682
1978
  port: discovered.port,
1683
1979
  });
1684
1980
  if (!discoveredProbe.ok) {
1685
1981
  logger(`Discovered Chrome for ${userDataDir} on port ${discovered.port} but it was unreachable (${discoveredProbe.error}); launching new Chrome.`);
1982
+ await cleanupStaleProfileState(userDataDir, logger, {
1983
+ lockRemovalMode: "if_oracle_pid_dead",
1984
+ });
1686
1985
  return null;
1687
1986
  }
1688
1987
  await writeDevToolsActivePort(userDataDir, discovered.port);
@@ -1830,16 +2129,20 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1830
2129
  removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
1831
2130
  // Skip cookie sync for remote Chrome - it already has cookies
1832
2131
  logger("Skipping cookie sync for remote Chrome (using existing session)");
1833
- if (!attachedExistingTab) {
2132
+ if (config.resumeConversationUrl) {
2133
+ await navigateToChatGPT(Page, Runtime, config.resumeConversationUrl, logger);
2134
+ }
2135
+ else if (!attachedExistingTab) {
1834
2136
  await navigateToChatGPT(Page, Runtime, config.url, logger);
1835
- await ensureNotBlocked(Runtime, config.headless, logger);
1836
- await ensureLoggedIn(Runtime, logger, { remoteSession: true });
1837
- await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
1838
2137
  }
1839
- else {
1840
- await ensureNotBlocked(Runtime, config.headless, logger);
1841
- await ensureLoggedIn(Runtime, logger, { remoteSession: true });
1842
- await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
2138
+ await ensureNotBlocked(Runtime, config.headless, logger);
2139
+ await ensureLoggedIn(Runtime, logger, { remoteSession: true });
2140
+ await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
2141
+ if (config.resumeConversationUrl) {
2142
+ await waitForResumedConversationHydration(Runtime, config.inputTimeoutMs, logger, {
2143
+ requirePriorTurns: true,
2144
+ expectedConversationUrl: config.resumeConversationUrl,
2145
+ });
1843
2146
  }
1844
2147
  logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
1845
2148
  try {
@@ -1856,7 +2159,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1856
2159
  // ignore
1857
2160
  }
1858
2161
  const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
1859
- if (config.desiredModel && modelStrategy !== "ignore") {
2162
+ if (config.desiredModel && modelStrategy !== "ignore" && !config.resumeConversationUrl) {
1860
2163
  modelSelectionEvidence = await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
1861
2164
  retries: 2,
1862
2165
  delayMs: 300,
@@ -1869,9 +2172,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1869
2172
  await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
1870
2173
  logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
1871
2174
  }
1872
- else if (modelStrategy === "ignore") {
2175
+ else if (modelStrategy === "ignore" || config.resumeConversationUrl) {
1873
2176
  modelSelectionEvidence = buildSkippedModelSelectionEvidence(config.desiredModel, modelStrategy);
1874
- logger("Model picker: skipped (strategy=ignore)");
2177
+ logger(config.resumeConversationUrl
2178
+ ? "Model picker: skipped (resumed conversation)"
2179
+ : "Model picker: skipped (strategy=ignore)");
1875
2180
  }
1876
2181
  const deepResearch = config.researchMode === "deep";
1877
2182
  // Handle thinking time selection if specified. Deep Research owns its own effort flow.
@@ -1905,6 +2210,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1905
2210
  const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
1906
2211
  const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
1907
2212
  const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
2213
+ const attachmentExpectations = submissionAttachments.map((a) => ({
2214
+ name: path.basename(a.path),
2215
+ generatedBundle: a.generatedBundle === true,
2216
+ }));
1908
2217
  await clearPromptComposer(Runtime, logger);
1909
2218
  await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
1910
2219
  if (submissionAttachments.length > 0) {
@@ -1935,9 +2244,12 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1935
2244
  inputTimeoutMs: config.inputTimeoutMs ?? undefined,
1936
2245
  attachmentTimeoutMs: config.attachmentTimeoutMs ?? undefined,
1937
2246
  baselineTurns: baselineTurns ?? undefined,
1938
- attachmentNames,
2247
+ attachmentNames: attachmentExpectations,
1939
2248
  onPromptSubmitted: markPromptSubmitted,
1940
2249
  };
2250
+ const deepResearchTargetKeys = deepResearch && client
2251
+ ? await captureDeepResearchTargetKeys(client).catch(() => [])
2252
+ : undefined;
1941
2253
  await runProviderSubmissionFlow(chatgptDomProvider, {
1942
2254
  prompt,
1943
2255
  evaluate: async () => undefined,
@@ -1950,7 +2262,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1950
2262
  if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
1951
2263
  baselineTurns = providerBaselineTurns;
1952
2264
  }
1953
- return { baselineTurns, baselineAssistantText };
2265
+ return { baselineTurns, baselineAssistantText, deepResearchTargetKeys };
1954
2266
  };
1955
2267
  const reloadPromptComposer = async () => {
1956
2268
  logger("[browser] Composer became unresponsive; reloading page and retrying once.");
@@ -1959,6 +2271,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1959
2271
  };
1960
2272
  let baselineTurns = null;
1961
2273
  let baselineAssistantText = null;
2274
+ let deepResearchTargetKeys = [];
1962
2275
  const submission = await runSubmissionWithRecovery({
1963
2276
  prompt: promptText,
1964
2277
  attachments,
@@ -1973,10 +2286,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1973
2286
  });
1974
2287
  baselineTurns = submission.baselineTurns;
1975
2288
  baselineAssistantText = submission.baselineAssistantText;
2289
+ deepResearchTargetKeys = submission.deepResearchTargetKeys ?? [];
1976
2290
  const imageArtifactMinTurnIndex = baselineTurns;
1977
2291
  if (deepResearch) {
1978
2292
  await waitForResearchPlanAutoConfirm(Runtime, logger);
1979
- const researchResult = await waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client);
2293
+ const researchResult = await waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client, { ignoredTargetKeys: deepResearchTargetKeys });
1980
2294
  await emitRuntimeHint();
1981
2295
  const durationMs = Date.now() - startedAt;
1982
2296
  const tokens = estimateTokenCount(researchResult.text);
@@ -2175,7 +2489,13 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2175
2489
  promptSubmitted,
2176
2490
  controllerPid: process.pid,
2177
2491
  };
2178
- throw new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime, diagnostics }, error);
2492
+ throw await createAssistantTimeoutError({
2493
+ Runtime,
2494
+ logger,
2495
+ runtime,
2496
+ diagnostics,
2497
+ cause: error,
2498
+ });
2179
2499
  }
2180
2500
  }
2181
2501
  else {
@@ -2320,7 +2640,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2320
2640
  answerMarkdown = formatted.answerMarkdown;
2321
2641
  answerHtml = "";
2322
2642
  }
2643
+ const canSaveBrowserDownloadsLocally = isLocalChromeHost(host);
2323
2644
  const imageArtifacts = await collectGeneratedImageArtifacts({
2645
+ Browser: canSaveBrowserDownloadsLocally ? client.Browser : undefined,
2646
+ Client: canSaveBrowserDownloadsLocally ? client : undefined,
2647
+ Page: canSaveBrowserDownloadsLocally ? Page : undefined,
2324
2648
  Runtime,
2325
2649
  Network,
2326
2650
  logger,
@@ -2330,21 +2654,50 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2330
2654
  outputPath: options.outputPath,
2331
2655
  answerText,
2332
2656
  waitTimeoutMs: options.config?.timeoutMs,
2657
+ checkBlockingUiWarning: () => throwChatGptUiWarningIfPresent({
2658
+ Runtime,
2659
+ logger,
2660
+ stage: "image-artifact-wait",
2661
+ waitTarget: "generated image artifacts",
2662
+ runtime: {
2663
+ chromePort: port,
2664
+ chromeHost: host,
2665
+ chromeBrowserWSEndpoint: browserWSEndpoint,
2666
+ chromeProfileRoot,
2667
+ chromeTargetId: remoteTargetId ?? undefined,
2668
+ tabUrl: lastUrl,
2669
+ conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2670
+ promptSubmitted,
2671
+ controllerPid: process.pid,
2672
+ },
2673
+ }),
2333
2674
  });
2334
2675
  answerText = imageArtifacts.answerText || answerText;
2335
2676
  if (imageArtifacts.markdownSuffix) {
2336
2677
  answerMarkdown += imageArtifacts.markdownSuffix;
2337
2678
  }
2679
+ const fileArtifacts = await collectChatGptFileArtifacts({
2680
+ Browser: client.Browser,
2681
+ Client: client,
2682
+ Page,
2683
+ Runtime,
2684
+ Network,
2685
+ answerText: [answerText, answerMarkdown, answerHtml].filter(Boolean).join("\n"),
2686
+ logger,
2687
+ minTurnIndex: imageArtifactMinTurnIndex,
2688
+ sessionId: options.sessionId,
2689
+ });
2338
2690
  const savedImageArtifacts = appendArtifacts(undefined, imageArtifacts.savedImages);
2691
+ const savedBrowserArtifacts = appendArtifacts(savedImageArtifacts, fileArtifacts.savedFiles);
2339
2692
  const transcriptArtifact = await saveOptionalArtifact(() => saveBrowserTranscriptArtifact({
2340
2693
  sessionId: options.sessionId,
2341
2694
  prompt: promptText,
2342
2695
  answerMarkdown,
2343
2696
  conversationUrl: lastUrl,
2344
- artifacts: savedImageArtifacts,
2697
+ artifacts: savedBrowserArtifacts,
2345
2698
  logger,
2346
2699
  }), logger);
2347
- const savedArtifacts = appendArtifacts(savedImageArtifacts, [transcriptArtifact]);
2700
+ const savedArtifacts = appendArtifacts(savedBrowserArtifacts, [transcriptArtifact]);
2348
2701
  const archive = await maybeArchiveCompletedConversation({
2349
2702
  Runtime,
2350
2703
  logger,
@@ -2352,7 +2705,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2352
2705
  conversationUrl: lastUrl,
2353
2706
  followUpCount: followUpPrompts.length,
2354
2707
  requiredArtifactsSaved: Boolean(transcriptArtifact) &&
2355
- imageArtifacts.savedImages.length === imageArtifacts.imageCount,
2708
+ imageArtifacts.savedImages.length === imageArtifacts.imageCount &&
2709
+ fileArtifacts.savedFiles.length === fileArtifacts.fileCount,
2356
2710
  });
2357
2711
  const durationMs = Date.now() - startedAt;
2358
2712
  const answerChars = answerText.length;
@@ -2377,6 +2731,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2377
2731
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2378
2732
  promptSubmitted,
2379
2733
  artifacts: savedArtifacts,
2734
+ generatedImages: imageArtifacts.generatedImages,
2735
+ savedImages: imageArtifacts.savedImages,
2736
+ downloadableFiles: fileArtifacts.files,
2737
+ savedFiles: fileArtifacts.savedFiles,
2380
2738
  archive,
2381
2739
  modelSelection: modelSelectionEvidence,
2382
2740
  controllerPid: process.pid,
@@ -2443,8 +2801,12 @@ export { resolveBrowserConfig, DEFAULT_BROWSER_CONFIG } from "./config.js";
2443
2801
  export const __test__ = {
2444
2802
  assertManualLoginProfileReadyForRun,
2445
2803
  closeRemoteConnectionAfterRun,
2804
+ classifyChatGptUiWarningText,
2805
+ collectChatGptUiWarnings,
2806
+ createAssistantTimeoutError,
2446
2807
  detachKeptChromeProcess,
2447
2808
  formatManualLoginSetupCommand,
2809
+ isAssistantResponseTimeoutError,
2448
2810
  isManualLoginProfileInitialized,
2449
2811
  isImageOnlyUiChromeText,
2450
2812
  listIgnoredRemoteChromeFlags,
@@ -2500,7 +2862,8 @@ function isAssistantResponseTimeoutError(error) {
2500
2862
  const message = error.message.toLowerCase();
2501
2863
  if (!message)
2502
2864
  return false;
2503
- return (message.includes("assistant-response") ||
2865
+ return (message === "response timeout" ||
2866
+ message.includes("assistant-response") ||
2504
2867
  message.includes("assistant response") ||
2505
2868
  message.includes("watchdog") ||
2506
2869
  message.includes("capture assistant response"));