@steipete/oracle 0.19.0 → 0.20.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 (41) hide show
  1. package/dist/bin/oracle-cli.js +72 -29
  2. package/dist/src/browser/actions/deepResearch.js +168 -44
  3. package/dist/src/browser/actions/modelSelection.js +78 -1
  4. package/dist/src/browser/actions/promptComposer.js +3 -0
  5. package/dist/src/browser/actions/thinkingTime.js +49 -6
  6. package/dist/src/browser/actions/webSearch.js +96 -0
  7. package/dist/src/browser/chromeLifecycle.js +28 -16
  8. package/dist/src/browser/config.js +1 -1
  9. package/dist/src/browser/index.js +144 -49
  10. package/dist/src/browser/liveTabs.js +11 -4
  11. package/dist/src/browser/profileState.js +6 -1
  12. package/dist/src/browser/promptFingerprint.js +54 -0
  13. package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
  14. package/dist/src/browser/reattach.js +178 -109
  15. package/dist/src/browser/recoveryTarget.js +156 -0
  16. package/dist/src/browser/sessionRunner.js +33 -8
  17. package/dist/src/browser/tabLeaseRegistry.js +20 -0
  18. package/dist/src/browser/targetClaim.js +54 -0
  19. package/dist/src/cli/browserConfig.js +33 -3
  20. package/dist/src/cli/browserDefaults.js +3 -0
  21. package/dist/src/cli/browserTabs.js +38 -3
  22. package/dist/src/cli/detach.js +10 -1
  23. package/dist/src/cli/detachedSession.js +36 -0
  24. package/dist/src/cli/options.js +15 -0
  25. package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
  26. package/dist/src/cli/runOptions.js +19 -4
  27. package/dist/src/cli/sessionDisplay.js +15 -8
  28. package/dist/src/cli/sessionRunner.js +153 -41
  29. package/dist/src/mcp/tools/consult.js +2 -2
  30. package/dist/src/mcp/types.js +1 -1
  31. package/dist/src/oracle/config.js +14 -0
  32. package/dist/src/oracle/geminiModels.js +1 -0
  33. package/dist/src/oracle/run.js +27 -4
  34. package/dist/src/remote/client.js +3 -0
  35. package/dist/src/remote/server.js +2 -0
  36. package/dist/src/sessionManager.js +8 -1
  37. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  38. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  39. package/package.json +10 -10
  40. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  41. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -1,7 +1,10 @@
1
+ import { readSubmittedPromptFingerprint, readUserMessageIds } from "./promptFingerprint.js";
1
2
  import { mkdtemp, rm, mkdir } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import os from "node:os";
4
5
  import net from "node:net";
6
+ import { randomUUID } from "node:crypto";
7
+ import { claimBrowserTarget } from "./targetClaim.js";
5
8
  import { resolveBrowserConfig } from "./config.js";
6
9
  import { copyChromeProfile } from "./profileCopy.js";
7
10
  import { BrowserCancellation, withoutBrowserCancellation } from "./cancellation.js";
@@ -554,19 +557,13 @@ export function isLocalChromeHostForTest(host) {
554
557
  return isLocalChromeHost(host);
555
558
  }
556
559
  async function closeRemoteConnectionAfterRun(options) {
557
- if (options.connectionClosedUnexpectedly) {
558
- return;
559
- }
560
560
  if (!options.connection) {
561
561
  await options.client?.close();
562
562
  return;
563
563
  }
564
- if (options.runStatus === "complete") {
565
- await options.connection.close();
566
- }
567
- else {
568
- await options.client?.close();
569
- }
564
+ await options.connection.close({
565
+ preserveTarget: options.connectionClosedUnexpectedly || options.preserveTarget,
566
+ });
570
567
  }
571
568
  function shouldCloseOwnedRunTargetAfterRun(options) {
572
569
  return (options.ownsTarget &&
@@ -712,8 +709,12 @@ async function runBrowserModeInternal(options, cancellation) {
712
709
  let lastTargetId;
713
710
  let lastUrl;
714
711
  let promptSubmitted = false;
712
+ let submittedPromptHash = null;
713
+ let ownedRecoveryTarget;
714
+ const targetClaimId = randomUUID();
715
715
  let modelSelectionEvidence;
716
716
  let thinkingSelectionEvidence;
717
+ let researchPlan;
717
718
  let tabLease = null;
718
719
  let conversationUrlMonitor = null;
719
720
  const emitRuntimeHint = async () => {
@@ -729,8 +730,11 @@ async function runBrowserModeInternal(options, cancellation) {
729
730
  tabUrl: lastUrl,
730
731
  conversationId,
731
732
  promptSubmitted,
733
+ submittedPromptHash,
734
+ ownedRecoveryTarget,
732
735
  userDataDir,
733
736
  controllerPid: process.pid,
737
+ researchPlan,
734
738
  };
735
739
  try {
736
740
  await runtimeHintCb?.(hint, modelSelectionEvidence);
@@ -747,10 +751,8 @@ async function runBrowserModeInternal(options, cancellation) {
747
751
  }
748
752
  };
749
753
  const markPromptSubmitted = async () => {
750
- if (promptSubmitted) {
751
- return;
752
- }
753
754
  promptSubmitted = true;
755
+ submittedPromptHash = null;
754
756
  await emitRuntimeHint();
755
757
  void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
756
758
  };
@@ -929,7 +931,15 @@ async function runBrowserModeInternal(options, cancellation) {
929
931
  });
930
932
  client = cancellation.client(connection.client);
931
933
  isolatedTargetId = connection.targetId ?? null;
932
- ownsTarget = true;
934
+ ownsTarget = Boolean(connection.targetId);
935
+ if (connection.targetId && (!config.keepBrowser || options.closeOwnedTabOnComplete)) {
936
+ ownedRecoveryTarget = {
937
+ host: chromeHost,
938
+ port: chrome.port,
939
+ targetId: connection.targetId,
940
+ claimId: targetClaimId,
941
+ };
942
+ }
933
943
  }
934
944
  if (tabLease && isolatedTargetId) {
935
945
  await tabLease.update({
@@ -977,7 +987,10 @@ async function runBrowserModeInternal(options, cancellation) {
977
987
  ? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
978
988
  : undefined,
979
989
  promptSubmitted,
990
+ submittedPromptHash,
991
+ ownedRecoveryTarget,
980
992
  controllerPid: process.pid,
993
+ researchPlan,
981
994
  },
982
995
  }));
983
996
  })();
@@ -990,6 +1003,8 @@ async function runBrowserModeInternal(options, cancellation) {
990
1003
  domainEnablers.push(DOM.enable());
991
1004
  }
992
1005
  await Promise.all(domainEnablers);
1006
+ if (config.browserTabRef)
1007
+ await claimBrowserTarget(Runtime, targetClaimId);
993
1008
  if (!config.headless && config.hideWindow) {
994
1009
  await positionChromeWindowOffscreen(client, userDataDir, logger);
995
1010
  }
@@ -1251,6 +1266,7 @@ async function runBrowserModeInternal(options, cancellation) {
1251
1266
  await withoutBrowserCancellation(() => handle.release()).catch(() => undefined);
1252
1267
  };
1253
1268
  const submitOnce = async (prompt, submissionAttachments) => {
1269
+ await claimBrowserTarget(Runtime, targetClaimId);
1254
1270
  const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
1255
1271
  const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
1256
1272
  const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
@@ -1313,10 +1329,12 @@ async function runBrowserModeInternal(options, cancellation) {
1313
1329
  attachmentNames: attachmentExpectations,
1314
1330
  attachmentNavigationUrl,
1315
1331
  onPromptSubmitted: markPromptSubmitted,
1332
+ webSearch: config.researchMode === "search",
1316
1333
  };
1317
1334
  const deepResearchTargetBaseline = deepResearch && client
1318
1335
  ? await captureDeepResearchTargetBaseline(client, logger)
1319
1336
  : undefined;
1337
+ const previousUserMessageIds = await readUserMessageIds(Runtime, config.inputTimeoutMs);
1320
1338
  await runProviderSubmissionFlow(chatgptDomProvider, {
1321
1339
  prompt,
1322
1340
  evaluate: async () => undefined,
@@ -1326,6 +1344,11 @@ async function runBrowserModeInternal(options, cancellation) {
1326
1344
  });
1327
1345
  await markPromptSubmitted();
1328
1346
  const providerBaselineTurns = providerState.baselineTurns;
1347
+ const renderedPromptHash = await readSubmittedPromptFingerprint(Runtime, previousUserMessageIds, config.inputTimeoutMs);
1348
+ if (renderedPromptHash) {
1349
+ submittedPromptHash = renderedPromptHash;
1350
+ await emitRuntimeHint();
1351
+ }
1329
1352
  if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
1330
1353
  baselineTurns = providerBaselineTurns;
1331
1354
  }
@@ -1387,7 +1410,17 @@ async function runBrowserModeInternal(options, cancellation) {
1387
1410
  }
1388
1411
  const imageArtifactMinTurnIndex = baselineTurns;
1389
1412
  if (deepResearch) {
1390
- await raceWithDisconnect(waitForResearchPlanAutoConfirm(Runtime, logger));
1413
+ await raceWithDisconnect(waitForResearchPlanAutoConfirm(Runtime, logger, undefined, {
1414
+ Page,
1415
+ client,
1416
+ ignoredTargetKeys: deepResearchTargetKeys,
1417
+ targetBaselineCaptured: deepResearchTargetBaselineCaptured,
1418
+ minTurnIndex: baselineTurns,
1419
+ onPlan: async (plan) => {
1420
+ researchPlan = plan;
1421
+ await emitRuntimeHint();
1422
+ },
1423
+ }));
1391
1424
  const researchResult = await raceWithDisconnect(waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client, {
1392
1425
  ignoredTargetKeys: deepResearchTargetKeys,
1393
1426
  targetBaselineCaptured: deepResearchTargetBaselineCaptured,
@@ -1438,7 +1471,10 @@ async function runBrowserModeInternal(options, cancellation) {
1438
1471
  tabUrl: lastUrl,
1439
1472
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1440
1473
  promptSubmitted,
1474
+ submittedPromptHash,
1475
+ ownedRecoveryTarget,
1441
1476
  controllerPid: process.pid,
1477
+ researchPlan,
1442
1478
  };
1443
1479
  }
1444
1480
  // Helper to normalize text for echo detection (collapse whitespace, lowercase)
@@ -1528,6 +1564,8 @@ async function runBrowserModeInternal(options, cancellation) {
1528
1564
  tabUrl: lastUrl,
1529
1565
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1530
1566
  promptSubmitted,
1567
+ submittedPromptHash,
1568
+ ownedRecoveryTarget,
1531
1569
  controllerPid: process.pid,
1532
1570
  },
1533
1571
  });
@@ -1584,6 +1622,8 @@ async function runBrowserModeInternal(options, cancellation) {
1584
1622
  tabUrl: lastUrl,
1585
1623
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1586
1624
  promptSubmitted,
1625
+ submittedPromptHash,
1626
+ ownedRecoveryTarget,
1587
1627
  controllerPid: process.pid,
1588
1628
  };
1589
1629
  throw await createAssistantTimeoutError({
@@ -1809,6 +1849,8 @@ async function runBrowserModeInternal(options, cancellation) {
1809
1849
  tabUrl: lastUrl,
1810
1850
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1811
1851
  promptSubmitted,
1852
+ submittedPromptHash,
1853
+ ownedRecoveryTarget,
1812
1854
  controllerPid: process.pid,
1813
1855
  },
1814
1856
  }),
@@ -1876,6 +1918,8 @@ async function runBrowserModeInternal(options, cancellation) {
1876
1918
  tabUrl: lastUrl,
1877
1919
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1878
1920
  promptSubmitted,
1921
+ submittedPromptHash,
1922
+ ownedRecoveryTarget,
1879
1923
  controllerPid: process.pid,
1880
1924
  };
1881
1925
  }
@@ -1902,6 +1946,8 @@ async function runBrowserModeInternal(options, cancellation) {
1902
1946
  chromeTargetId: lastTargetId,
1903
1947
  tabUrl: lastUrl,
1904
1948
  promptSubmitted,
1949
+ submittedPromptHash,
1950
+ ownedRecoveryTarget,
1905
1951
  controllerPid: process.pid,
1906
1952
  };
1907
1953
  const reuseProfileHint = `oracle --engine browser --browser-manual-login ` +
@@ -1965,7 +2011,10 @@ async function runBrowserModeInternal(options, cancellation) {
1965
2011
  ? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
1966
2012
  : undefined,
1967
2013
  promptSubmitted,
2014
+ submittedPromptHash,
2015
+ ownedRecoveryTarget,
1968
2016
  controllerPid: process.pid,
2017
+ researchPlan,
1969
2018
  },
1970
2019
  }, normalizedError);
1971
2020
  }
@@ -2363,8 +2412,12 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2363
2412
  let tabLease = null;
2364
2413
  let lastUrl;
2365
2414
  let promptSubmitted = false;
2415
+ let submittedPromptHash = null;
2416
+ let ownedRecoveryTarget;
2417
+ const targetClaimId = randomUUID();
2366
2418
  let modelSelectionEvidence;
2367
2419
  let thinkingSelectionEvidence;
2420
+ let researchPlan;
2368
2421
  let attachedExistingTab = false;
2369
2422
  let ownsTarget = true;
2370
2423
  let conversationUrlMonitor = null;
@@ -2382,7 +2435,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2382
2435
  tabUrl: lastUrl,
2383
2436
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2384
2437
  promptSubmitted,
2438
+ submittedPromptHash,
2439
+ ownedRecoveryTarget,
2385
2440
  controllerPid: process.pid,
2441
+ researchPlan,
2386
2442
  }, modelSelectionEvidence);
2387
2443
  await tabLease?.update({
2388
2444
  chromeHost: host,
@@ -2397,10 +2453,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2397
2453
  }
2398
2454
  };
2399
2455
  const markPromptSubmitted = async () => {
2400
- if (promptSubmitted) {
2401
- return;
2402
- }
2403
2456
  promptSubmitted = true;
2457
+ submittedPromptHash = null;
2404
2458
  await emitRuntimeHint();
2405
2459
  void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
2406
2460
  };
@@ -2452,7 +2506,16 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2452
2506
  }), (connection) => connection.close());
2453
2507
  client = cancellation.client(connection.client);
2454
2508
  remoteTargetId = connection.targetId ?? null;
2455
- ownsTarget = true;
2509
+ ownsTarget = Boolean(connection.targetId);
2510
+ if (connection.targetId && (!config.keepBrowser || options.closeOwnedTabOnComplete)) {
2511
+ ownedRecoveryTarget = {
2512
+ host,
2513
+ port,
2514
+ targetId: connection.targetId,
2515
+ browserWSEndpoint,
2516
+ claimId: targetClaimId,
2517
+ };
2518
+ }
2456
2519
  }
2457
2520
  if (tabLease && remoteTargetId) {
2458
2521
  await tabLease.update({
@@ -2472,6 +2535,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2472
2535
  domainEnablers.push(DOM.enable());
2473
2536
  }
2474
2537
  await Promise.all(domainEnablers);
2538
+ if (config.browserTabRef)
2539
+ await claimBrowserTarget(Runtime, targetClaimId);
2475
2540
  removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
2476
2541
  await enableFocusEmulation(client, logger, "remote target");
2477
2542
  const activeConversationUrlMonitor = createConversationUrlMonitor({
@@ -2572,6 +2637,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2572
2637
  });
2573
2638
  }
2574
2639
  const submitOnce = async (prompt, submissionAttachments) => {
2640
+ await claimBrowserTarget(Runtime, targetClaimId);
2575
2641
  const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
2576
2642
  const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
2577
2643
  const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
@@ -2629,10 +2695,12 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2629
2695
  attachmentNames: attachmentExpectations,
2630
2696
  attachmentNavigationUrl,
2631
2697
  onPromptSubmitted: markPromptSubmitted,
2698
+ webSearch: config.researchMode === "search",
2632
2699
  };
2633
2700
  const deepResearchTargetBaseline = deepResearch && client
2634
2701
  ? await captureDeepResearchTargetBaseline(client, logger)
2635
2702
  : undefined;
2703
+ const previousUserMessageIds = await readUserMessageIds(Runtime, config.inputTimeoutMs);
2636
2704
  await runProviderSubmissionFlow(chatgptDomProvider, {
2637
2705
  prompt,
2638
2706
  evaluate: async () => undefined,
@@ -2642,6 +2710,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2642
2710
  });
2643
2711
  await markPromptSubmitted();
2644
2712
  const providerBaselineTurns = providerState.baselineTurns;
2713
+ const renderedPromptHash = await readSubmittedPromptFingerprint(Runtime, previousUserMessageIds, config.inputTimeoutMs);
2714
+ if (renderedPromptHash) {
2715
+ submittedPromptHash = renderedPromptHash;
2716
+ await emitRuntimeHint();
2717
+ }
2645
2718
  if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
2646
2719
  baselineTurns = providerBaselineTurns;
2647
2720
  }
@@ -2679,7 +2752,17 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2679
2752
  deepResearchTargetBaselineCaptured = submission.deepResearchTargetBaselineCaptured ?? false;
2680
2753
  const imageArtifactMinTurnIndex = baselineTurns;
2681
2754
  if (deepResearch) {
2682
- await waitForResearchPlanAutoConfirm(Runtime, logger);
2755
+ await waitForResearchPlanAutoConfirm(Runtime, logger, undefined, {
2756
+ Page,
2757
+ client,
2758
+ ignoredTargetKeys: deepResearchTargetKeys,
2759
+ targetBaselineCaptured: deepResearchTargetBaselineCaptured,
2760
+ minTurnIndex: baselineTurns,
2761
+ onPlan: async (plan) => {
2762
+ researchPlan = plan;
2763
+ await emitRuntimeHint();
2764
+ },
2765
+ });
2683
2766
  const researchResult = await waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client, {
2684
2767
  ignoredTargetKeys: deepResearchTargetKeys,
2685
2768
  targetBaselineCaptured: deepResearchTargetBaselineCaptured,
@@ -2728,7 +2811,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2728
2811
  tabUrl: lastUrl,
2729
2812
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2730
2813
  promptSubmitted,
2814
+ submittedPromptHash,
2815
+ ownedRecoveryTarget,
2731
2816
  controllerPid: process.pid,
2817
+ researchPlan,
2732
2818
  };
2733
2819
  }
2734
2820
  // Helper to normalize text for echo detection (collapse whitespace, lowercase)
@@ -2817,6 +2903,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2817
2903
  tabUrl: lastUrl,
2818
2904
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2819
2905
  promptSubmitted,
2906
+ submittedPromptHash,
2907
+ ownedRecoveryTarget,
2820
2908
  controllerPid: process.pid,
2821
2909
  },
2822
2910
  });
@@ -2875,6 +2963,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2875
2963
  tabUrl: lastUrl,
2876
2964
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2877
2965
  promptSubmitted,
2966
+ submittedPromptHash,
2967
+ ownedRecoveryTarget,
2878
2968
  controllerPid: process.pid,
2879
2969
  };
2880
2970
  throw await createAssistantTimeoutError({
@@ -3058,6 +3148,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
3058
3148
  tabUrl: lastUrl,
3059
3149
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
3060
3150
  promptSubmitted,
3151
+ submittedPromptHash,
3152
+ ownedRecoveryTarget,
3061
3153
  controllerPid: process.pid,
3062
3154
  },
3063
3155
  }),
@@ -3120,6 +3212,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
3120
3212
  tabUrl: lastUrl,
3121
3213
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
3122
3214
  promptSubmitted,
3215
+ submittedPromptHash,
3216
+ ownedRecoveryTarget,
3123
3217
  artifacts: savedArtifacts,
3124
3218
  generatedImages: imageArtifacts.generatedImages,
3125
3219
  savedImages: imageArtifacts.savedImages,
@@ -3168,7 +3262,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
3168
3262
  ? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
3169
3263
  : undefined,
3170
3264
  promptSubmitted,
3265
+ submittedPromptHash,
3266
+ ownedRecoveryTarget,
3171
3267
  controllerPid: process.pid,
3268
+ researchPlan,
3172
3269
  },
3173
3270
  });
3174
3271
  }
@@ -3176,17 +3273,6 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
3176
3273
  await withoutBrowserCancellation(async () => {
3177
3274
  stopThinkingMonitor?.();
3178
3275
  await conversationUrlMonitor?.stop();
3179
- try {
3180
- await closeRemoteConnectionAfterRun({
3181
- connectionClosedUnexpectedly,
3182
- connection,
3183
- client,
3184
- runStatus,
3185
- });
3186
- }
3187
- catch {
3188
- // ignore
3189
- }
3190
3276
  removeDialogHandler?.();
3191
3277
  const keepRemoteBrowser = Boolean(config.keepBrowser);
3192
3278
  const shouldCloseOwnedRemoteTarget = shouldCloseOwnedRunTargetAfterRun({
@@ -3196,33 +3282,42 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
3196
3282
  closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
3197
3283
  closeOwnedTabOnCancel: options.closeOwnedTabOnCancel,
3198
3284
  });
3199
- const closeOwnedRemoteTarget = async () => {
3200
- if (!shouldCloseOwnedRemoteTarget || !remoteTargetId) {
3201
- return;
3202
- }
3203
- const safeToClose = !keepRemoteBrowser ||
3204
- Boolean(await ensureChromePageTargetAfterClose(port, remoteTargetId, logger, host));
3205
- if (!safeToClose) {
3206
- logger(`[browser] Leaving completed remote browser tab open because Chrome has no replacement page target.`);
3207
- return;
3208
- }
3209
- const closeConfirmed = await closeTab(port, remoteTargetId, logger, host);
3210
- if (!closeConfirmed && keepRemoteBrowser) {
3211
- const replacementTargetId = await createChromePageTarget(port, logger, host);
3212
- if (!replacementTargetId) {
3213
- logger(`[browser] Remote Chrome page retention could not be verified after closing ${remoteTargetId}.`);
3285
+ const closeConnection = async () => {
3286
+ let preserveTarget = !shouldCloseOwnedRemoteTarget;
3287
+ if (!preserveTarget && keepRemoteBrowser && client && remoteTargetId) {
3288
+ try {
3289
+ const { targetInfos } = await client.Target.getTargets();
3290
+ if (!targetInfos.some((target) => target.type === "page" && target.targetId !== remoteTargetId)) {
3291
+ const replacement = await client.Target.createTarget({ url: "about:blank" });
3292
+ if (!replacement.targetId)
3293
+ preserveTarget = true;
3294
+ }
3295
+ }
3296
+ catch {
3297
+ preserveTarget = true;
3214
3298
  }
3215
3299
  }
3300
+ await closeRemoteConnectionAfterRun({
3301
+ connectionClosedUnexpectedly,
3302
+ connection,
3303
+ client,
3304
+ preserveTarget,
3305
+ });
3216
3306
  };
3217
3307
  if (tabLease) {
3218
3308
  const handle = tabLease;
3219
3309
  tabLease = null;
3220
- await handle
3221
- .release({ onRelease: async () => closeOwnedRemoteTarget() })
3222
- .catch(() => undefined);
3310
+ await handle.release({ onRelease: closeConnection }).catch(async () => {
3311
+ await closeRemoteConnectionAfterRun({
3312
+ connectionClosedUnexpectedly,
3313
+ connection,
3314
+ client,
3315
+ preserveTarget: true,
3316
+ }).catch(() => undefined);
3317
+ });
3223
3318
  }
3224
3319
  else {
3225
- await closeOwnedRemoteTarget();
3320
+ await closeConnection().catch(() => undefined);
3226
3321
  }
3227
3322
  // Don't kill remote Chrome - it's not ours to manage
3228
3323
  const totalSeconds = (Date.now() - startedAt) / 1000;
@@ -1,6 +1,6 @@
1
1
  import CDP from "chrome-remote-interface";
2
2
  import { createHash } from "node:crypto";
3
- import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, INPUT_SELECTORS, MODEL_BUTTON_SELECTOR, SEND_BUTTON_SELECTORS, STOP_BUTTON_SELECTOR, } from "./constants.js";
3
+ import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, INPUT_SELECTORS, MODEL_BUTTON_SELECTOR, SEND_BUTTON_SELECTORS, STOP_BUTTON_SELECTORS, } from "./constants.js";
4
4
  import { captureAssistantMarkdown, readAssistantSnapshot } from "./actions/assistantResponse.js";
5
5
  import { buildConversationTurnListExpression } from "./conversationTurns.js";
6
6
  import { extractStableConversationIdFromUrl } from "./conversationUrl.js";
@@ -62,7 +62,7 @@ function buildTabInspectionExpression() {
62
62
  const answerSelectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
63
63
  const assistantRoleLiteral = escapeLiteral(ASSISTANT_ROLE_SELECTOR);
64
64
  const modelButtonSelectorLiteral = escapeLiteral(MODEL_BUTTON_SELECTOR);
65
- const stopSelectorLiteral = escapeLiteral(STOP_BUTTON_SELECTOR);
65
+ const stopSelectorLiteral = escapeLiteral(STOP_BUTTON_SELECTORS.join(","));
66
66
  return `(() => {
67
67
  const INPUT_SELECTORS = ${inputSelectorsLiteral};
68
68
  const SEND_SELECTORS = ${sendSelectorsLiteral};
@@ -91,8 +91,7 @@ function buildTabInspectionExpression() {
91
91
  const label = normalize(node.textContent || node.getAttribute('aria-label') || node.getAttribute('title'));
92
92
  return LOGIN_CTA.test(label);
93
93
  });
94
- const stopButton = document.querySelector(STOP_BUTTON_SELECTOR);
95
- const stopExists = Boolean(stopButton && isVisible(stopButton));
94
+ const stopExists = Array.from(document.querySelectorAll(STOP_BUTTON_SELECTOR)).some(isVisible);
96
95
  const sendButton = firstVisible(SEND_SELECTORS);
97
96
  const sendExists = Boolean(sendButton);
98
97
  const promptNode = firstVisible(INPUT_SELECTORS);
@@ -163,6 +162,8 @@ function buildTabInspectionExpression() {
163
162
  const assistantCount = new Set(assistantOwners).size;
164
163
  const lastAssistantText = normalize(lastAssistantNode?.textContent);
165
164
  const lastUserText = normalize(lastUserTurn?.textContent);
165
+ const lastUserMessage = lastUserTurn?.matches?.('[data-message-author-role="user"]')
166
+ ? lastUserTurn : lastUserTurn?.querySelector?.('[data-message-author-role="user"]');
166
167
  const authenticated = !loginButtonExists && (promptReady || sendExists || stopExists || assistantCount > 0);
167
168
  return {
168
169
  title: normalize(document.title),
@@ -179,6 +180,8 @@ function buildTabInspectionExpression() {
179
180
  lastAssistantTurnIndex,
180
181
  lastUserTurnIndex,
181
182
  lastUserText,
183
+ lastUserTextRaw: lastUserMessage?.textContent,
184
+ lastUserMessageId: lastUserMessage?.getAttribute?.('data-message-id'),
182
185
  visibilityState: document.visibilityState,
183
186
  focused: Boolean(document.hasFocus?.()),
184
187
  };
@@ -264,6 +267,8 @@ export async function inspectChatGptTab(options) {
264
267
  : undefined,
265
268
  lastAssistantSnippet: trimToSnippet(lastAssistantText),
266
269
  lastUserText,
270
+ lastUserTextRaw: info.lastUserTextRaw,
271
+ lastUserMessageId: info.lastUserMessageId,
267
272
  lastUserSnippet: trimToSnippet(lastUserText),
268
273
  focused: Boolean(info.focused),
269
274
  visibilityState: typeof info.visibilityState === "string" ? info.visibilityState : "",
@@ -462,6 +467,8 @@ export async function harvestChatGptTab(options = {}) {
462
467
  harvested.authenticated = followup.authenticated;
463
468
  harvested.loginButtonExists = followup.loginButtonExists;
464
469
  harvested.lastUserText = followup.lastUserText;
470
+ harvested.lastUserTextRaw = followup.lastUserTextRaw;
471
+ harvested.lastUserMessageId = followup.lastUserMessageId;
465
472
  harvested.lastUserSnippet = followup.lastUserSnippet;
466
473
  harvested.assistantFollowsLatestUser = followup.assistantFollowsLatestUser;
467
474
  harvested.lastAssistantTurnIndex = followup.lastAssistantTurnIndex;
@@ -224,7 +224,12 @@ export async function readProcessStartTimeMs(pid) {
224
224
  if (Math.trunc(pid) === process.pid) {
225
225
  // Use the same OS identity as peer controllers, not wall time minus uptime.
226
226
  // Cache our own PID only: it cannot be reused during this process's lifetime.
227
- return (ownProcessStartTime ??= queryProcessStartTimeMs(process.pid));
227
+ return (ownProcessStartTime ??= queryProcessStartTimeMs(process.pid).then((startedAt) => {
228
+ // A transient probe failure is not a permanent process identity.
229
+ if (startedAt === null)
230
+ ownProcessStartTime = undefined;
231
+ return startedAt;
232
+ }));
228
233
  }
229
234
  return queryProcessStartTimeMs(pid);
230
235
  }
@@ -0,0 +1,54 @@
1
+ import { createHash } from "node:crypto";
2
+ import { buildConversationTurnListExpression } from "./conversationTurns.js";
3
+ export function browserPromptFingerprint(value, messageId) {
4
+ return createHash("sha256")
5
+ .update(JSON.stringify([messageId, String(value ?? "").replace(/\r\n?/g, "\n")]))
6
+ .digest("hex");
7
+ }
8
+ export function readUserMessageIds(runtime, timeoutMs = 0) {
9
+ return readDomUntil(runtime, `Array.from(document.querySelectorAll('[data-message-author-role="user"]'), user => user.getAttribute('data-message-id'))`, timeoutMs, (value) => Array.isArray(value) && value.every((id) => typeof id === "string" && id.trim())
10
+ ? value
11
+ : undefined);
12
+ }
13
+ export async function readSubmittedPromptFingerprint(runtime, previousMessageIds, timeoutMs = 0) {
14
+ if (previousMessageIds === undefined)
15
+ return undefined;
16
+ const previous = new Set(previousMessageIds);
17
+ return readDomUntil(runtime, `(() => {
18
+ const turns = ${buildConversationTurnListExpression()};
19
+ for (let index = turns.length - 1; index >= 0; index--) {
20
+ const turn = turns[index];
21
+ const user = turn.matches('[data-message-author-role="user"]') ? turn : turn.querySelector('[data-message-author-role="user"]');
22
+ if (user) return { text: user.textContent, messageId: user.getAttribute('data-message-id') };
23
+ }
24
+ return null;
25
+ })()`, timeoutMs, (value) => {
26
+ const turn = value;
27
+ if (typeof turn?.text === "string" &&
28
+ turn.text.trim() &&
29
+ typeof turn.messageId === "string" &&
30
+ turn.messageId.trim() &&
31
+ !previous.has(turn.messageId))
32
+ return browserPromptFingerprint(turn.text, turn.messageId);
33
+ return undefined;
34
+ });
35
+ }
36
+ async function readDomUntil(runtime, expression, timeoutMs, select) {
37
+ const deadline = Date.now() + Math.max(0, timeoutMs);
38
+ for (;;) {
39
+ try {
40
+ const result = await runtime.evaluate({ expression, returnByValue: true });
41
+ const selected = select(result.result?.value);
42
+ if (selected !== undefined)
43
+ return selected;
44
+ }
45
+ catch (error) {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ if (!/Cannot find (?:default )?(?:execution )?context|Execution context (?:was destroyed|is not available)/i.test(message))
48
+ return undefined;
49
+ }
50
+ if (Date.now() >= deadline)
51
+ return undefined;
52
+ await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
53
+ }
54
+ }
@@ -27,6 +27,7 @@ async function submitPromptViaAdapter(ctx) {
27
27
  inputTimeoutMs: state.inputTimeoutMs ?? undefined,
28
28
  attachmentTimeoutMs: state.attachmentTimeoutMs ?? undefined,
29
29
  onPromptSubmitted: state.onPromptSubmitted,
30
+ webSearch: state.webSearch,
30
31
  }, ctx.prompt, state.logger);
31
32
  state.committedTurns =
32
33
  typeof committedTurns === "number" && Number.isFinite(committedTurns) ? committedTurns : null;