@steipete/oracle 0.20.0 → 0.20.2

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 (32) hide show
  1. package/dist/bin/oracle-cli.js +66 -25
  2. package/dist/src/browser/actions/modelSelection.js +36 -4
  3. package/dist/src/browser/actions/thinkingTime.js +11 -5
  4. package/dist/src/browser/browserConnection.js +59 -0
  5. package/dist/src/browser/chatgptImages.js +1 -1
  6. package/dist/src/browser/chromeLifecycle.js +105 -36
  7. package/dist/src/browser/index.js +41 -8
  8. package/dist/src/browser/liveTabs.js +94 -29
  9. package/dist/src/browser/profileState.js +6 -1
  10. package/dist/src/browser/promptFingerprint.js +54 -0
  11. package/dist/src/browser/reattach.js +157 -108
  12. package/dist/src/browser/recoveryTarget.js +31 -6
  13. package/dist/src/browser/sessionRunner.js +30 -7
  14. package/dist/src/browser/targetClaim.js +2 -2
  15. package/dist/src/cli/browserDefaults.js +3 -0
  16. package/dist/src/cli/browserTabs.js +105 -20
  17. package/dist/src/cli/detach.js +10 -1
  18. package/dist/src/cli/detachedSession.js +36 -0
  19. package/dist/src/cli/errorUtils.js +9 -0
  20. package/dist/src/cli/sessionDisplay.js +11 -3
  21. package/dist/src/cli/sessionRunner.js +150 -37
  22. package/dist/src/mcp/tools/consult.js +1 -7
  23. package/dist/src/remote/client.js +71 -14
  24. package/dist/src/remote/health.js +1 -0
  25. package/dist/src/remote/server.js +52 -10
  26. package/dist/src/remote/types.js +13 -0
  27. package/dist/src/sessionManager.js +8 -1
  28. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  29. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  30. package/package.json +9 -9
  31. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  32. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -1,3 +1,4 @@
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";
@@ -708,6 +709,7 @@ async function runBrowserModeInternal(options, cancellation) {
708
709
  let lastTargetId;
709
710
  let lastUrl;
710
711
  let promptSubmitted = false;
712
+ let submittedPromptHash = null;
711
713
  let ownedRecoveryTarget;
712
714
  const targetClaimId = randomUUID();
713
715
  let modelSelectionEvidence;
@@ -728,6 +730,7 @@ async function runBrowserModeInternal(options, cancellation) {
728
730
  tabUrl: lastUrl,
729
731
  conversationId,
730
732
  promptSubmitted,
733
+ submittedPromptHash,
731
734
  ownedRecoveryTarget,
732
735
  userDataDir,
733
736
  controllerPid: process.pid,
@@ -748,10 +751,8 @@ async function runBrowserModeInternal(options, cancellation) {
748
751
  }
749
752
  };
750
753
  const markPromptSubmitted = async () => {
751
- if (promptSubmitted) {
752
- return;
753
- }
754
754
  promptSubmitted = true;
755
+ submittedPromptHash = null;
755
756
  await emitRuntimeHint();
756
757
  void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
757
758
  };
@@ -986,6 +987,7 @@ async function runBrowserModeInternal(options, cancellation) {
986
987
  ? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
987
988
  : undefined,
988
989
  promptSubmitted,
990
+ submittedPromptHash,
989
991
  ownedRecoveryTarget,
990
992
  controllerPid: process.pid,
991
993
  researchPlan,
@@ -1208,7 +1210,9 @@ async function runBrowserModeInternal(options, cancellation) {
1208
1210
  await captureRuntimeSnapshot();
1209
1211
  const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
1210
1212
  if (config.desiredModel && modelStrategy !== "ignore" && !isResumingConversation) {
1211
- modelSelectionEvidence = await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
1213
+ modelSelectionEvidence = await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy, {
1214
+ implicitDefault: config.modelIsImplicitDefault,
1215
+ }), {
1212
1216
  retries: 2,
1213
1217
  delayMs: 300,
1214
1218
  onRetry: (attempt, error) => {
@@ -1332,6 +1336,7 @@ async function runBrowserModeInternal(options, cancellation) {
1332
1336
  const deepResearchTargetBaseline = deepResearch && client
1333
1337
  ? await captureDeepResearchTargetBaseline(client, logger)
1334
1338
  : undefined;
1339
+ const previousUserMessageIds = await readUserMessageIds(Runtime, config.inputTimeoutMs);
1335
1340
  await runProviderSubmissionFlow(chatgptDomProvider, {
1336
1341
  prompt,
1337
1342
  evaluate: async () => undefined,
@@ -1341,6 +1346,11 @@ async function runBrowserModeInternal(options, cancellation) {
1341
1346
  });
1342
1347
  await markPromptSubmitted();
1343
1348
  const providerBaselineTurns = providerState.baselineTurns;
1349
+ const renderedPromptHash = await readSubmittedPromptFingerprint(Runtime, previousUserMessageIds, config.inputTimeoutMs);
1350
+ if (renderedPromptHash) {
1351
+ submittedPromptHash = renderedPromptHash;
1352
+ await emitRuntimeHint();
1353
+ }
1344
1354
  if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
1345
1355
  baselineTurns = providerBaselineTurns;
1346
1356
  }
@@ -1463,6 +1473,7 @@ async function runBrowserModeInternal(options, cancellation) {
1463
1473
  tabUrl: lastUrl,
1464
1474
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1465
1475
  promptSubmitted,
1476
+ submittedPromptHash,
1466
1477
  ownedRecoveryTarget,
1467
1478
  controllerPid: process.pid,
1468
1479
  researchPlan,
@@ -1555,6 +1566,7 @@ async function runBrowserModeInternal(options, cancellation) {
1555
1566
  tabUrl: lastUrl,
1556
1567
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1557
1568
  promptSubmitted,
1569
+ submittedPromptHash,
1558
1570
  ownedRecoveryTarget,
1559
1571
  controllerPid: process.pid,
1560
1572
  },
@@ -1612,6 +1624,7 @@ async function runBrowserModeInternal(options, cancellation) {
1612
1624
  tabUrl: lastUrl,
1613
1625
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1614
1626
  promptSubmitted,
1627
+ submittedPromptHash,
1615
1628
  ownedRecoveryTarget,
1616
1629
  controllerPid: process.pid,
1617
1630
  };
@@ -1838,6 +1851,7 @@ async function runBrowserModeInternal(options, cancellation) {
1838
1851
  tabUrl: lastUrl,
1839
1852
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1840
1853
  promptSubmitted,
1854
+ submittedPromptHash,
1841
1855
  ownedRecoveryTarget,
1842
1856
  controllerPid: process.pid,
1843
1857
  },
@@ -1906,6 +1920,7 @@ async function runBrowserModeInternal(options, cancellation) {
1906
1920
  tabUrl: lastUrl,
1907
1921
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1908
1922
  promptSubmitted,
1923
+ submittedPromptHash,
1909
1924
  ownedRecoveryTarget,
1910
1925
  controllerPid: process.pid,
1911
1926
  };
@@ -1933,6 +1948,7 @@ async function runBrowserModeInternal(options, cancellation) {
1933
1948
  chromeTargetId: lastTargetId,
1934
1949
  tabUrl: lastUrl,
1935
1950
  promptSubmitted,
1951
+ submittedPromptHash,
1936
1952
  ownedRecoveryTarget,
1937
1953
  controllerPid: process.pid,
1938
1954
  };
@@ -1997,6 +2013,7 @@ async function runBrowserModeInternal(options, cancellation) {
1997
2013
  ? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
1998
2014
  : undefined,
1999
2015
  promptSubmitted,
2016
+ submittedPromptHash,
2000
2017
  ownedRecoveryTarget,
2001
2018
  controllerPid: process.pid,
2002
2019
  researchPlan,
@@ -2397,6 +2414,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2397
2414
  let tabLease = null;
2398
2415
  let lastUrl;
2399
2416
  let promptSubmitted = false;
2417
+ let submittedPromptHash = null;
2400
2418
  let ownedRecoveryTarget;
2401
2419
  const targetClaimId = randomUUID();
2402
2420
  let modelSelectionEvidence;
@@ -2419,6 +2437,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2419
2437
  tabUrl: lastUrl,
2420
2438
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2421
2439
  promptSubmitted,
2440
+ submittedPromptHash,
2422
2441
  ownedRecoveryTarget,
2423
2442
  controllerPid: process.pid,
2424
2443
  researchPlan,
@@ -2436,10 +2455,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2436
2455
  }
2437
2456
  };
2438
2457
  const markPromptSubmitted = async () => {
2439
- if (promptSubmitted) {
2440
- return;
2441
- }
2442
2458
  promptSubmitted = true;
2459
+ submittedPromptHash = null;
2443
2460
  await emitRuntimeHint();
2444
2461
  void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
2445
2462
  };
@@ -2475,6 +2492,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2475
2492
  const attached = await cancellation.acquire(() => connectToExistingChatGptTab({
2476
2493
  host,
2477
2494
  port,
2495
+ browserWSEndpoint,
2496
+ approvalWaitMs: config.approvalWaitMs,
2478
2497
  ref: tabRef,
2479
2498
  }), (attached) => attached.client.close());
2480
2499
  client = cancellation.client(attached.client);
@@ -2590,7 +2609,9 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2590
2609
  }
2591
2610
  const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
2592
2611
  if (config.desiredModel && modelStrategy !== "ignore" && !config.resumeConversationUrl) {
2593
- modelSelectionEvidence = await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
2612
+ modelSelectionEvidence = await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy, {
2613
+ implicitDefault: config.modelIsImplicitDefault,
2614
+ }), {
2594
2615
  retries: 2,
2595
2616
  delayMs: 300,
2596
2617
  onRetry: (attempt, error) => {
@@ -2685,6 +2706,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2685
2706
  const deepResearchTargetBaseline = deepResearch && client
2686
2707
  ? await captureDeepResearchTargetBaseline(client, logger)
2687
2708
  : undefined;
2709
+ const previousUserMessageIds = await readUserMessageIds(Runtime, config.inputTimeoutMs);
2688
2710
  await runProviderSubmissionFlow(chatgptDomProvider, {
2689
2711
  prompt,
2690
2712
  evaluate: async () => undefined,
@@ -2694,6 +2716,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2694
2716
  });
2695
2717
  await markPromptSubmitted();
2696
2718
  const providerBaselineTurns = providerState.baselineTurns;
2719
+ const renderedPromptHash = await readSubmittedPromptFingerprint(Runtime, previousUserMessageIds, config.inputTimeoutMs);
2720
+ if (renderedPromptHash) {
2721
+ submittedPromptHash = renderedPromptHash;
2722
+ await emitRuntimeHint();
2723
+ }
2697
2724
  if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
2698
2725
  baselineTurns = providerBaselineTurns;
2699
2726
  }
@@ -2790,6 +2817,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2790
2817
  tabUrl: lastUrl,
2791
2818
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2792
2819
  promptSubmitted,
2820
+ submittedPromptHash,
2793
2821
  ownedRecoveryTarget,
2794
2822
  controllerPid: process.pid,
2795
2823
  researchPlan,
@@ -2881,6 +2909,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2881
2909
  tabUrl: lastUrl,
2882
2910
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2883
2911
  promptSubmitted,
2912
+ submittedPromptHash,
2884
2913
  ownedRecoveryTarget,
2885
2914
  controllerPid: process.pid,
2886
2915
  },
@@ -2940,6 +2969,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2940
2969
  tabUrl: lastUrl,
2941
2970
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2942
2971
  promptSubmitted,
2972
+ submittedPromptHash,
2943
2973
  ownedRecoveryTarget,
2944
2974
  controllerPid: process.pid,
2945
2975
  };
@@ -3124,6 +3154,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
3124
3154
  tabUrl: lastUrl,
3125
3155
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
3126
3156
  promptSubmitted,
3157
+ submittedPromptHash,
3127
3158
  ownedRecoveryTarget,
3128
3159
  controllerPid: process.pid,
3129
3160
  },
@@ -3187,6 +3218,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
3187
3218
  tabUrl: lastUrl,
3188
3219
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
3189
3220
  promptSubmitted,
3221
+ submittedPromptHash,
3190
3222
  ownedRecoveryTarget,
3191
3223
  artifacts: savedArtifacts,
3192
3224
  generatedImages: imageArtifacts.generatedImages,
@@ -3236,6 +3268,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
3236
3268
  ? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
3237
3269
  : undefined,
3238
3270
  promptSubmitted,
3271
+ submittedPromptHash,
3239
3272
  ownedRecoveryTarget,
3240
3273
  controllerPid: process.pid,
3241
3274
  researchPlan,
@@ -5,6 +5,7 @@ import { captureAssistantMarkdown, readAssistantSnapshot } from "./actions/assis
5
5
  import { buildConversationTurnListExpression } from "./conversationTurns.js";
6
6
  import { extractStableConversationIdFromUrl } from "./conversationUrl.js";
7
7
  import { delay } from "./utils.js";
8
+ import { connectToRemoteChromeTarget, listRemoteChromeTargets } from "./chromeLifecycle.js";
8
9
  export const DEFAULT_REMOTE_CHROME_HOST = "127.0.0.1";
9
10
  export const DEFAULT_REMOTE_CHROME_PORT = 9222;
10
11
  const LOGIN_CTA_PATTERN = /\b(log in|login|sign up|sign in|continue with google|continue with microsoft)\b/i;
@@ -22,8 +23,15 @@ function normalizeHostPort(input = {}) {
22
23
  return {
23
24
  host: input.host ?? DEFAULT_REMOTE_CHROME_HOST,
24
25
  port: input.port ?? DEFAULT_REMOTE_CHROME_PORT,
26
+ ...(input.browserWSEndpoint
27
+ ? { browserWSEndpoint: input.browserWSEndpoint, approvalWaitMs: input.approvalWaitMs }
28
+ : {}),
25
29
  };
26
30
  }
31
+ function chromeTransportError(operation, endpoint, error) {
32
+ const detail = error instanceof Error ? error.message.trim() : String(error ?? "").trim();
33
+ return new Error(`Unable to ${operation} on Chrome at ${endpoint.host}:${endpoint.port}. ${detail || "Chrome returned no error details."} Check that Chrome is running and remote debugging is enabled, then retry and allow the connection prompt.`, { cause: error });
34
+ }
27
35
  function normalizeUrl(value) {
28
36
  return String(value ?? "").trim();
29
37
  }
@@ -162,6 +170,9 @@ function buildTabInspectionExpression() {
162
170
  const assistantCount = new Set(assistantOwners).size;
163
171
  const lastAssistantText = normalize(lastAssistantNode?.textContent);
164
172
  const lastUserText = normalize(lastUserTurn?.textContent);
173
+ const lastUserMessage = lastUserTurn?.matches?.('[data-message-author-role="user"]')
174
+ ? lastUserTurn : lastUserTurn?.querySelector?.('[data-message-author-role="user"]');
175
+ const userContent = lastUserMessage?.querySelectorAll?.('[class~="whitespace-pre-wrap"]');
165
176
  const authenticated = !loginButtonExists && (promptReady || sendExists || stopExists || assistantCount > 0);
166
177
  return {
167
178
  title: normalize(document.title),
@@ -178,6 +189,9 @@ function buildTabInspectionExpression() {
178
189
  lastAssistantTurnIndex,
179
190
  lastUserTurnIndex,
180
191
  lastUserText,
192
+ lastUserTextRaw: lastUserMessage?.textContent,
193
+ lastUserContentText: userContent?.length === 1 ? userContent[0].textContent : undefined,
194
+ lastUserMessageId: lastUserMessage?.getAttribute?.('data-message-id'),
181
195
  visibilityState: document.visibilityState,
182
196
  focused: Boolean(document.hasFocus?.()),
183
197
  };
@@ -187,26 +201,67 @@ export function buildTabInspectionExpressionForTest() {
187
201
  return buildTabInspectionExpression();
188
202
  }
189
203
  export async function listChatGptTargets(options = {}) {
190
- const { host, port } = normalizeHostPort(options);
191
- const targets = (await CDP.List({ host, port }));
192
- return targets.filter(isChatGptTarget);
204
+ const endpoint = normalizeHostPort(options);
205
+ try {
206
+ const targets = await listRemoteChromeTargets(endpoint);
207
+ return targets.filter(isChatGptTarget);
208
+ }
209
+ catch (error) {
210
+ throw chromeTransportError("list ChatGPT tabs", endpoint, error);
211
+ }
193
212
  }
194
213
  export async function openChatGptTarget(options = {}) {
195
- const { host, port } = normalizeHostPort(options);
214
+ const endpoint = normalizeHostPort(options);
215
+ const { host, port } = endpoint;
196
216
  const url = options.url ?? "https://chatgpt.com/";
197
- const target = await CDP.New({ host, port, url });
198
- return target.id;
217
+ try {
218
+ if (endpoint.browserWSEndpoint) {
219
+ const connection = await connectToRemoteChromeTarget(host, port, noopLogger, {
220
+ ...endpoint,
221
+ targetUrl: url,
222
+ });
223
+ try {
224
+ if (!connection.targetId)
225
+ throw new Error("Chrome did not return a target ID.");
226
+ return connection.targetId;
227
+ }
228
+ finally {
229
+ await connection.close();
230
+ }
231
+ }
232
+ const target = await CDP.New({ host, port, url });
233
+ return target.id;
234
+ }
235
+ catch (error) {
236
+ throw chromeTransportError("open saved ChatGPT conversation", endpoint, error);
237
+ }
199
238
  }
200
- async function connectToTarget(host, port, targetId) {
201
- const client = await CDP({ host, port, target: targetId });
239
+ async function connectToTarget(options, targetId) {
240
+ const endpoint = normalizeHostPort(options);
241
+ const { host, port } = endpoint;
242
+ const connection = await connectToRemoteChromeTarget(host, port, noopLogger, {
243
+ ...endpoint,
244
+ targetId,
245
+ }).catch((error) => {
246
+ throw chromeTransportError("inspect ChatGPT tab", endpoint, error);
247
+ });
248
+ const client = Object.create(connection.client);
249
+ // Session-bound clients detach only; the owning connection also closes its browser socket.
250
+ client.close = connection.close;
202
251
  const { Runtime, DOM } = client;
203
- if (Runtime?.enable) {
204
- await Runtime.enable();
252
+ try {
253
+ if (Runtime?.enable) {
254
+ await Runtime.enable();
255
+ }
256
+ if (DOM?.enable) {
257
+ await DOM.enable();
258
+ }
259
+ return client;
205
260
  }
206
- if (DOM?.enable) {
207
- await DOM.enable();
261
+ catch (error) {
262
+ await connection.close().catch(() => undefined);
263
+ throw error;
208
264
  }
209
- return client;
210
265
  }
211
266
  export async function inspectChatGptTab(options) {
212
267
  const { host, port } = normalizeHostPort(options);
@@ -215,7 +270,7 @@ export async function inspectChatGptTab(options) {
215
270
  if (!targetId) {
216
271
  throw new Error("inspectChatGptTab requires a target with targetId.");
217
272
  }
218
- const client = await connectToTarget(host, port, targetId);
273
+ const client = await connectToTarget(options, targetId);
219
274
  try {
220
275
  const { Runtime } = client;
221
276
  const evaluation = await Runtime.evaluate({
@@ -263,6 +318,9 @@ export async function inspectChatGptTab(options) {
263
318
  : undefined,
264
319
  lastAssistantSnippet: trimToSnippet(lastAssistantText),
265
320
  lastUserText,
321
+ lastUserTextRaw: info.lastUserTextRaw,
322
+ lastUserContentText: info.lastUserContentText,
323
+ lastUserMessageId: info.lastUserMessageId,
266
324
  lastUserSnippet: trimToSnippet(lastUserText),
267
325
  focused: Boolean(info.focused),
268
326
  visibilityState: typeof info.visibilityState === "string" ? info.visibilityState : "",
@@ -299,11 +357,11 @@ export function classifyTabState(summary) {
299
357
  }
300
358
  export async function collectChatGptTabs(options = {}) {
301
359
  const { host, port } = normalizeHostPort(options);
302
- const targets = await listChatGptTargets({ host, port });
360
+ const targets = await listChatGptTargets(options);
303
361
  const summaries = [];
304
362
  for (const target of targets) {
305
363
  try {
306
- const summary = await inspectChatGptTab({ host, port, target });
364
+ const summary = await inspectChatGptTab({ ...options, target });
307
365
  summaries.push(summary);
308
366
  }
309
367
  catch (error) {
@@ -377,28 +435,33 @@ export function resolveChatGptTabFromSummariesForTest(summaries, ref) {
377
435
  return resolveChatGptTabFromSummaries(summaries, ref);
378
436
  }
379
437
  export async function resolveChatGptTab(options = {}) {
380
- const { host, port } = normalizeHostPort(options);
381
- const summaries = await collectChatGptTabs({ host, port });
438
+ const ref = options.ref?.trim();
439
+ if (ref && ref.toLowerCase() !== "current") {
440
+ const targets = await listChatGptTargets(options);
441
+ const exact = targets.find((target) => extractTargetId(target) === ref ||
442
+ target.url === ref ||
443
+ extractConversationIdFromUrl(target.url ?? "") === ref);
444
+ if (exact)
445
+ return inspectChatGptTab({ ...options, target: exact });
446
+ }
447
+ const summaries = await collectChatGptTabs(options);
382
448
  return resolveChatGptTabFromSummaries(summaries, options.ref);
383
449
  }
384
450
  export async function connectToExistingChatGptTab(options = {}) {
385
- const { host, port } = normalizeHostPort(options);
386
- const tab = await resolveChatGptTab({ host, port, ref: options.ref });
387
- const client = await connectToTarget(host, port, tab.targetId);
451
+ const tab = await resolveChatGptTab(options);
452
+ const client = await connectToTarget(options, tab.targetId);
388
453
  return { client, targetId: tab.targetId, tab };
389
454
  }
390
455
  export async function harvestChatGptTab(options = {}) {
391
- const { host, port } = normalizeHostPort(options);
392
456
  const resolved = options.target
393
- ? await inspectChatGptTab({ host, port, target: options.target })
394
- : await resolveChatGptTab({ host, port, ref: options.ref });
395
- const client = await connectToTarget(host, port, resolved.targetId);
457
+ ? await inspectChatGptTab({ ...options, target: options.target })
458
+ : await resolveChatGptTab(options);
459
+ const client = await connectToTarget(options, resolved.targetId);
396
460
  try {
397
461
  const { Runtime } = client;
398
462
  const snapshot = await readAssistantSnapshot(Runtime).catch(() => null);
399
463
  const nowSummary = await inspectChatGptTab({
400
- host,
401
- port,
464
+ ...options,
402
465
  target: {
403
466
  targetId: resolved.targetId,
404
467
  title: resolved.title,
@@ -442,8 +505,7 @@ export async function harvestChatGptTab(options = {}) {
442
505
  const firstFingerprint = harvested.fingerprint;
443
506
  await delay(options.stallWindowMs);
444
507
  const followup = await inspectChatGptTab({
445
- host,
446
- port,
508
+ ...options,
447
509
  target: {
448
510
  targetId: harvested.targetId,
449
511
  title: harvested.title,
@@ -461,6 +523,9 @@ export async function harvestChatGptTab(options = {}) {
461
523
  harvested.authenticated = followup.authenticated;
462
524
  harvested.loginButtonExists = followup.loginButtonExists;
463
525
  harvested.lastUserText = followup.lastUserText;
526
+ harvested.lastUserTextRaw = followup.lastUserTextRaw;
527
+ harvested.lastUserContentText = followup.lastUserContentText;
528
+ harvested.lastUserMessageId = followup.lastUserMessageId;
464
529
  harvested.lastUserSnippet = followup.lastUserSnippet;
465
530
  harvested.assistantFollowsLatestUser = followup.assistantFollowsLatestUser;
466
531
  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
+ }