@steipete/oracle 0.15.1 → 0.15.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.
- package/dist/scripts/test-browser.js +13 -2
- package/dist/src/browser/actions/assistantResponse.js +44 -22
- package/dist/src/browser/actions/attachments.js +3 -4
- package/dist/src/browser/actions/deepResearch.js +6 -6
- package/dist/src/browser/actions/promptComposer.js +4 -5
- package/dist/src/browser/chatgptFiles.js +4 -7
- package/dist/src/browser/chatgptImages.js +3 -4
- package/dist/src/browser/chromeLifecycle.js +1 -0
- package/dist/src/browser/constants.js +1 -0
- package/dist/src/browser/conversationTurns.js +16 -0
- package/dist/src/browser/conversationUrlMonitor.js +64 -0
- package/dist/src/browser/cookies.js +72 -0
- package/dist/src/browser/index.js +73 -64
- package/dist/src/browser/projectSourcesRunner.js +3 -2
- package/dist/src/browser/reattach.js +27 -11
- package/dist/src/browser/reattachHelpers.js +14 -5
- package/dist/src/oracle/oscProgress.js +3 -2
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +18 -18
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -58,9 +58,9 @@ function firewallHint(host, devtoolsPort) {
|
|
|
58
58
|
"Re-run ./runner pnpm test:browser after adding the rule.",
|
|
59
59
|
].join("\n");
|
|
60
60
|
}
|
|
61
|
-
async function fetchVersion(host, devtoolsPort) {
|
|
61
|
+
async function fetchVersion(host, devtoolsPort, timeoutMs = 5000) {
|
|
62
62
|
const controller = new AbortController();
|
|
63
|
-
const timer = setTimeout(() => controller.abort(),
|
|
63
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
64
64
|
try {
|
|
65
65
|
const res = await fetch(`http://${host}:${devtoolsPort}/json/version`, {
|
|
66
66
|
signal: controller.signal,
|
|
@@ -77,6 +77,16 @@ async function fetchVersion(host, devtoolsPort) {
|
|
|
77
77
|
clearTimeout(timer);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
async function waitForDevToolsShutdown(host, devtoolsPort) {
|
|
81
|
+
const deadline = Date.now() + 5000;
|
|
82
|
+
while (Date.now() < deadline) {
|
|
83
|
+
if (!(await fetchVersion(host, devtoolsPort, 250))) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await sleep(100);
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`DevTools did not stop listening at ${host}:${devtoolsPort}`);
|
|
89
|
+
}
|
|
80
90
|
async function main() {
|
|
81
91
|
console.log(`[browser-test] launching Chrome on ${targetHost}:${port} (headful)…`);
|
|
82
92
|
const chrome = await launch({
|
|
@@ -89,6 +99,7 @@ async function main() {
|
|
|
89
99
|
ok = await fetchVersion(targetHost, chrome.port);
|
|
90
100
|
}
|
|
91
101
|
await chrome.kill();
|
|
102
|
+
await waitForDevToolsShutdown(targetHost, chrome.port);
|
|
92
103
|
if (ok) {
|
|
93
104
|
console.log(`[browser-test] PASS: DevTools responding on ${targetHost}:${chrome.port}`);
|
|
94
105
|
process.exit(0);
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, COPY_BUTTON_SELECTOR, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTORS, } from "../constants.js";
|
|
2
|
+
import { buildConversationTurnListExpression } from "../conversationTurns.js";
|
|
2
3
|
import { delay } from "../utils.js";
|
|
3
4
|
import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression, } from "../domDebug.js";
|
|
4
5
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
5
6
|
const ASSISTANT_POLL_TIMEOUT_ERROR = "assistant-response-watchdog-timeout";
|
|
6
7
|
const STOP_CONTROL_SELECTOR = STOP_BUTTON_SELECTORS.join(", ");
|
|
8
|
+
const MIN_CONFIDENT_ANSWER_LENGTH = 16;
|
|
9
|
+
function isImplausiblyShortAnswer(candidateLength) {
|
|
10
|
+
return candidateLength > 0 && candidateLength < MIN_CONFIDENT_ANSWER_LENGTH;
|
|
11
|
+
}
|
|
12
|
+
export function shouldConfirmAssistantCompletion(args) {
|
|
13
|
+
if (args.stopVisible || args.completionVisible) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return isImplausiblyShortAnswer(args.candidateLength);
|
|
17
|
+
}
|
|
7
18
|
const THINKING_STATUS_LABELS = [
|
|
8
19
|
"thinking",
|
|
9
20
|
"pro thinking",
|
|
@@ -109,7 +120,9 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
109
120
|
if (source === "poll" &&
|
|
110
121
|
error instanceof Error &&
|
|
111
122
|
error.message === ASSISTANT_POLL_TIMEOUT_ERROR) {
|
|
112
|
-
|
|
123
|
+
evaluationPromise.catch(() => undefined);
|
|
124
|
+
await terminateRuntimeExecution(Runtime);
|
|
125
|
+
throw error;
|
|
113
126
|
}
|
|
114
127
|
else if (source === "poll") {
|
|
115
128
|
throw error;
|
|
@@ -162,6 +175,8 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
162
175
|
// The evaluation path can race ahead of completion. If ChatGPT is still streaming, wait for the watchdog poller.
|
|
163
176
|
const elapsedMs = Date.now() - start;
|
|
164
177
|
const remainingMs = Math.max(0, timeoutMs - elapsedMs);
|
|
178
|
+
const candidateText = String(candidate?.text ?? "").trim();
|
|
179
|
+
const suspiciouslyShort = isImplausiblyShortAnswer(candidateText.length);
|
|
165
180
|
if (remainingMs > 0) {
|
|
166
181
|
const [stopVisible, completionVisible] = await Promise.all([
|
|
167
182
|
isStopButtonVisible(Runtime),
|
|
@@ -170,17 +185,25 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
170
185
|
// Completion controls can appear briefly while Pro is still replacing its thinking UI.
|
|
171
186
|
// Confirm every capture from that transition with the stability-based watchdog; a
|
|
172
187
|
// partial first paragraph can be arbitrarily long.
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
if (shouldConfirmAssistantCompletion({
|
|
189
|
+
candidateLength: candidateText.length,
|
|
190
|
+
stopVisible,
|
|
191
|
+
completionVisible,
|
|
192
|
+
})) {
|
|
175
193
|
logger(stopVisible
|
|
176
194
|
? "Assistant still generating; waiting for completion"
|
|
177
|
-
:
|
|
195
|
+
: completionVisible
|
|
196
|
+
? "Completion controls surfaced; confirming stable assistant response"
|
|
197
|
+
: "Captured an implausibly short response; confirming it is not a mid-stream capture");
|
|
178
198
|
const completed = await pollAssistantCompletion(Runtime, remainingMs, minTurnIndex, expectedConversationId);
|
|
179
199
|
if (completed && String(completed.text ?? "").trim().length >= candidateText.length) {
|
|
180
200
|
return completed;
|
|
181
201
|
}
|
|
182
202
|
}
|
|
183
203
|
}
|
|
204
|
+
if (suspiciouslyShort) {
|
|
205
|
+
throw new Error("assistant-response short capture could not be confirmed before timeout; refusing to finalize it");
|
|
206
|
+
}
|
|
184
207
|
return candidate;
|
|
185
208
|
}
|
|
186
209
|
export async function readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId) {
|
|
@@ -243,11 +266,17 @@ async function recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex
|
|
|
243
266
|
if (recoveryTimeoutMs === 0) {
|
|
244
267
|
return null;
|
|
245
268
|
}
|
|
269
|
+
const recoveryStartedAt = Date.now();
|
|
246
270
|
const recovered = await waitForCondition(async () => {
|
|
247
271
|
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId);
|
|
248
272
|
return normalizeAssistantSnapshot(snapshot);
|
|
249
273
|
}, recoveryTimeoutMs, 400);
|
|
250
274
|
if (recovered) {
|
|
275
|
+
if (isImplausiblyShortAnswer(recovered.text.length)) {
|
|
276
|
+
logger("Recovered an implausibly short response; waiting for completion proof");
|
|
277
|
+
const remainingMs = Math.max(0, recoveryTimeoutMs - (Date.now() - recoveryStartedAt));
|
|
278
|
+
return pollAssistantCompletion(Runtime, remainingMs, minTurnIndex, expectedConversationId);
|
|
279
|
+
}
|
|
251
280
|
logger("Recovered assistant response via polling fallback");
|
|
252
281
|
return recovered;
|
|
253
282
|
}
|
|
@@ -364,8 +393,8 @@ async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expecte
|
|
|
364
393
|
if (isGeneratedImageAssistantAnswer(normalized)) {
|
|
365
394
|
return normalized;
|
|
366
395
|
}
|
|
367
|
-
const shortAnswer = currentLength
|
|
368
|
-
const mediumAnswer = currentLength >=
|
|
396
|
+
const shortAnswer = isImplausiblyShortAnswer(currentLength);
|
|
397
|
+
const mediumAnswer = currentLength >= MIN_CONFIDENT_ANSWER_LENGTH && currentLength < 40;
|
|
369
398
|
const longAnswer = currentLength >= 40 && currentLength < 500;
|
|
370
399
|
// Learned: short answers need a longer stability window or they truncate.
|
|
371
400
|
// Learned: long streaming responses (esp. thinking models) can pause mid-stream;
|
|
@@ -378,7 +407,7 @@ async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expecte
|
|
|
378
407
|
if (!stopVisible) {
|
|
379
408
|
const stableEnough = stableCycles >= requiredStableCycles && stableMs >= minStableMs;
|
|
380
409
|
const completionEnough = completionVisible && stableCycles >= completionStableTarget && stableMs >= minStableMs;
|
|
381
|
-
if (completionEnough || stableEnough) {
|
|
410
|
+
if (completionEnough || (!shortAnswer && stableEnough)) {
|
|
382
411
|
return normalized;
|
|
383
412
|
}
|
|
384
413
|
}
|
|
@@ -445,7 +474,7 @@ async function isCompletionVisible(Runtime) {
|
|
|
445
474
|
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
446
475
|
};
|
|
447
476
|
|
|
448
|
-
const turns =
|
|
477
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
449
478
|
let lastAssistantTurn = null;
|
|
450
479
|
for (let i = turns.length - 1; i >= 0; i--) {
|
|
451
480
|
if (isAssistantTurn(turns[i])) {
|
|
@@ -557,7 +586,6 @@ function buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId)
|
|
|
557
586
|
}
|
|
558
587
|
function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConversationId) {
|
|
559
588
|
const selectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
|
|
560
|
-
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
561
589
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
562
590
|
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
563
591
|
? Math.floor(minTurnIndex)
|
|
@@ -570,7 +598,6 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
570
598
|
const SELECTORS = ${selectorsLiteral};
|
|
571
599
|
const STOP_SELECTOR = ${JSON.stringify(STOP_CONTROL_SELECTOR)};
|
|
572
600
|
const FINISHED_SELECTOR = '${FINISHED_ACTIONS_SELECTOR}';
|
|
573
|
-
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
574
601
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
575
602
|
const EXPECTED_CONVERSATION_ID = ${expectedConversationLiteral};
|
|
576
603
|
// Learned: settling avoids capturing mid-stream HTML; keep short.
|
|
@@ -693,7 +720,7 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
693
720
|
|
|
694
721
|
// Check if the last assistant turn has finished (scoped to avoid detecting old turns).
|
|
695
722
|
const isLastAssistantTurnFinished = () => {
|
|
696
|
-
const turns =
|
|
723
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
697
724
|
let lastAssistantTurn = null;
|
|
698
725
|
for (let i = turns.length - 1; i >= 0; i--) {
|
|
699
726
|
if (isAssistantTurn(turns[i])) {
|
|
@@ -717,8 +744,8 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
717
744
|
// Learned: long streaming responses (esp. thinking models) can pause mid-stream;
|
|
718
745
|
// use progressively longer windows to avoid truncation (#71).
|
|
719
746
|
const initialLength = snapshot?.text?.length ?? 0;
|
|
720
|
-
const shortAnswer = initialLength > 0 && initialLength <
|
|
721
|
-
const mediumAnswer = initialLength >=
|
|
747
|
+
const shortAnswer = initialLength > 0 && initialLength < ${MIN_CONFIDENT_ANSWER_LENGTH};
|
|
748
|
+
const mediumAnswer = initialLength >= ${MIN_CONFIDENT_ANSWER_LENGTH} && initialLength < 40;
|
|
722
749
|
const longAnswer = initialLength >= 40 && initialLength < 500;
|
|
723
750
|
const settleWindowMs = shortAnswer ? 12_000 : mediumAnswer ? 5_000 : longAnswer ? 8_000 : 10_000;
|
|
724
751
|
const settleIntervalMs = 400;
|
|
@@ -792,11 +819,9 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
792
819
|
})()`;
|
|
793
820
|
}
|
|
794
821
|
function buildAssistantExtractor(functionName) {
|
|
795
|
-
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
796
822
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
797
823
|
return `const ${functionName} = () => {
|
|
798
824
|
${buildClickDispatcher()}
|
|
799
|
-
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
800
825
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
801
826
|
const isAssistantTurn = (node) => {
|
|
802
827
|
if (!(node instanceof HTMLElement)) return false;
|
|
@@ -832,7 +857,7 @@ function buildAssistantExtractor(functionName) {
|
|
|
832
857
|
}
|
|
833
858
|
};
|
|
834
859
|
|
|
835
|
-
const turns =
|
|
860
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
836
861
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
837
862
|
const turn = turns[index];
|
|
838
863
|
if (!isAssistantTurn(turn)) {
|
|
@@ -916,13 +941,10 @@ function buildMarkdownFallbackExtractor(minTurnLiteral) {
|
|
|
916
941
|
}
|
|
917
942
|
}
|
|
918
943
|
if (!root) return null;
|
|
919
|
-
const
|
|
920
|
-
const turnNodes = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
944
|
+
const turnNodes = ${buildConversationTurnListExpression()};
|
|
921
945
|
const hasTurns = turnNodes.length > 0;
|
|
922
946
|
const resolveTurnIndex = (node) => {
|
|
923
|
-
const
|
|
924
|
-
if (!turn) return null;
|
|
925
|
-
const idx = turnNodes.indexOf(turn);
|
|
947
|
+
const idx = turnNodes.findIndex((turn) => turn === node || turn.contains?.(node));
|
|
926
948
|
return idx >= 0 ? idx : null;
|
|
927
949
|
};
|
|
928
950
|
const isAfterMinTurn = (node) => {
|
|
@@ -1050,7 +1072,7 @@ function buildCopyExpression(meta) {
|
|
|
1050
1072
|
if (testId.includes('assistant')) return true;
|
|
1051
1073
|
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
1052
1074
|
};
|
|
1053
|
-
const turns =
|
|
1075
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
1054
1076
|
for (let i = turns.length - 1; i >= 0; i -= 1) {
|
|
1055
1077
|
const turn = turns[i];
|
|
1056
1078
|
if (!isAssistantTurn(turn)) continue;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { INPUT_SELECTORS, SEND_BUTTON_SELECTORS, UPLOAD_STATUS_SELECTORS } from "../constants.js";
|
|
3
|
+
import { buildConversationTurnListExpression } from "../conversationTurns.js";
|
|
3
4
|
import { delay } from "../utils.js";
|
|
4
5
|
import { logDomFailure } from "../domDebug.js";
|
|
5
6
|
import { transferAttachmentViaDataTransfer } from "./attachmentDataTransfer.js";
|
|
@@ -1594,14 +1595,12 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
1594
1595
|
throw new Error("Attachment was not present on the sent user message.");
|
|
1595
1596
|
}
|
|
1596
1597
|
function buildUserTurnAttachmentExpression(options) {
|
|
1597
|
-
const conversationSelectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
1598
1598
|
const minTurnLiteral = options.minTurnIndex === null ? "null" : String(options.minTurnIndex);
|
|
1599
1599
|
const expectedPromptLiteral = JSON.stringify(options.expectedPromptPrefix);
|
|
1600
1600
|
const expectedConversationLiteral = options.expectedConversationId
|
|
1601
1601
|
? JSON.stringify(options.expectedConversationId)
|
|
1602
1602
|
: "null";
|
|
1603
1603
|
return `(() => {
|
|
1604
|
-
const CONVERSATION_SELECTOR = ${conversationSelectorLiteral};
|
|
1605
1604
|
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
1606
1605
|
const EXPECTED_PROMPT_PREFIX = ${expectedPromptLiteral};
|
|
1607
1606
|
const EXPECTED_CONVERSATION_ID = ${expectedConversationLiteral};
|
|
@@ -1614,7 +1613,7 @@ function buildUserTurnAttachmentExpression(options) {
|
|
|
1614
1613
|
) {
|
|
1615
1614
|
return { ok: false, conversationMismatch: true };
|
|
1616
1615
|
}
|
|
1617
|
-
const turns =
|
|
1616
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
1618
1617
|
const userTurns = turns.map((node, index) => ({ node, index })).filter(({ node }) => {
|
|
1619
1618
|
const attr = (node.getAttribute('data-message-author-role') || node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
1620
1619
|
if (attr === 'user') return true;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { DEEP_RESEARCH_PLUS_BUTTON, DEEP_RESEARCH_DROPDOWN_ITEM_TEXT, DEEP_RESEARCH_PILL_LABEL, DEEP_RESEARCH_POLL_INTERVAL_MS, DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS, DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR,
|
|
1
|
+
import { DEEP_RESEARCH_PLUS_BUTTON, DEEP_RESEARCH_DROPDOWN_ITEM_TEXT, DEEP_RESEARCH_PILL_LABEL, DEEP_RESEARCH_POLL_INTERVAL_MS, DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS, DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, } from "../constants.js";
|
|
2
|
+
import { buildConversationTurnListExpression } from "../conversationTurns.js";
|
|
2
3
|
import { delay } from "../utils.js";
|
|
3
4
|
import { isDeepResearchIncompleteText } from "../deepResearchResult.js";
|
|
4
5
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
@@ -480,9 +481,9 @@ async function readDeepResearchTargetOwnerTurnIndex(rawClient, frameId, pageSess
|
|
|
480
481
|
.send("Runtime.callFunctionOn", {
|
|
481
482
|
objectId,
|
|
482
483
|
functionDeclaration: `function() {
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
return
|
|
484
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
485
|
+
const index = turns.findIndex((turn) => turn === this || turn.contains?.(this));
|
|
486
|
+
return index >= 0 ? index : null;
|
|
486
487
|
}`,
|
|
487
488
|
returnByValue: true,
|
|
488
489
|
}, pageSessionId)
|
|
@@ -703,7 +704,6 @@ function buildDeepResearchStatusExpression() {
|
|
|
703
704
|
function buildDeepResearchCompletionPollExpression(minTurnIndex) {
|
|
704
705
|
const finishedSelector = JSON.stringify(FINISHED_ACTIONS_SELECTOR);
|
|
705
706
|
const stopSelector = JSON.stringify(STOP_BUTTON_SELECTOR);
|
|
706
|
-
const turnSelector = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
707
707
|
return `(() => {
|
|
708
708
|
const MIN_TURN_INDEX = ${minTurnIndex};
|
|
709
709
|
const stopVisible = Boolean(document.querySelector(${stopSelector}));
|
|
@@ -719,7 +719,7 @@ function buildDeepResearchCompletionPollExpression(minTurnIndex) {
|
|
|
719
719
|
String(node.getAttribute('data-testid') || '').toLowerCase().includes('conversation-turn') &&
|
|
720
720
|
/chatgpt\\s+said/i.test(node.innerText || node.textContent || '');
|
|
721
721
|
};
|
|
722
|
-
const conversationTurns =
|
|
722
|
+
const conversationTurns = ${buildConversationTurnListExpression()};
|
|
723
723
|
const allAssistantTurns = Array.from(document.querySelectorAll('[data-message-author-role="assistant"], [data-turn="assistant"]'));
|
|
724
724
|
const scopedTurns = scopedToNewTurns
|
|
725
725
|
? conversationTurns.slice(MIN_TURN_INDEX).filter(isAssistantTurn)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR, SEND_BUTTON_SELECTORS,
|
|
1
|
+
import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR, SEND_BUTTON_SELECTORS, STOP_BUTTON_SELECTOR, ASSISTANT_ROLE_SELECTOR, } from "../constants.js";
|
|
2
|
+
import { buildConversationTurnCountExpression, buildConversationTurnListExpression, } from "../conversationTurns.js";
|
|
2
3
|
import { delay } from "../utils.js";
|
|
3
4
|
import { logDomFailure } from "../domDebug.js";
|
|
4
5
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
@@ -691,14 +692,13 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
691
692
|
const inputSelectorsLiteral = JSON.stringify(INPUT_SELECTORS);
|
|
692
693
|
const stopSelectorLiteral = JSON.stringify(STOP_BUTTON_SELECTOR);
|
|
693
694
|
const assistantSelectorLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
694
|
-
const turnSelectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
695
695
|
let baseline = typeof baselineTurns === "number" && Number.isFinite(baselineTurns) && baselineTurns >= 0
|
|
696
696
|
? Math.floor(baselineTurns)
|
|
697
697
|
: null;
|
|
698
698
|
if (baseline === null) {
|
|
699
699
|
try {
|
|
700
700
|
const { result } = await Runtime.evaluate({
|
|
701
|
-
expression:
|
|
701
|
+
expression: buildConversationTurnCountExpression(),
|
|
702
702
|
returnByValue: true,
|
|
703
703
|
});
|
|
704
704
|
const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
|
|
@@ -726,8 +726,7 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
726
726
|
};
|
|
727
727
|
const normalizedPrompt = normalize(${encodedPrompt});
|
|
728
728
|
const normalizedPromptPrefix = normalizedPrompt.slice(0, 120);
|
|
729
|
-
const
|
|
730
|
-
const articles = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
729
|
+
const articles = ${buildConversationTurnListExpression()};
|
|
731
730
|
const normalizedTurns = articles.map((node) => normalize(node?.innerText));
|
|
732
731
|
const readValue = (node) => {
|
|
733
732
|
if (!node) return '';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { ASSISTANT_ROLE_SELECTOR
|
|
3
|
+
import { ASSISTANT_ROLE_SELECTOR } from "./constants.js";
|
|
4
|
+
import { buildConversationTurnListExpression } from "./conversationTurns.js";
|
|
4
5
|
import { computeFileSha256, resolveSessionArtifactsDir, sanitizeArtifactFilename, validateArtifactFile, writeBinaryBrowserArtifact, } from "./artifacts.js";
|
|
5
6
|
const CHATGPT_DOWNLOAD_BASE_URL = "https://chatgpt.com/";
|
|
6
7
|
const DOWNLOAD_BUTTON_WAIT_MS = 15_000;
|
|
@@ -272,11 +273,9 @@ function buildAssistantDownloadableFilesExpression(minTurnIndex) {
|
|
|
272
273
|
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
273
274
|
? Math.floor(minTurnIndex)
|
|
274
275
|
: -1;
|
|
275
|
-
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
276
276
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
277
277
|
return `(() => {
|
|
278
278
|
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
279
|
-
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
280
279
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
281
280
|
const isAssistantTurn = (node) => {
|
|
282
281
|
if (!(node instanceof HTMLElement)) return false;
|
|
@@ -385,7 +384,7 @@ function buildAssistantDownloadableFilesExpression(minTurnIndex) {
|
|
|
385
384
|
].join(',')))
|
|
386
385
|
.map(serializeCandidate)
|
|
387
386
|
.filter(Boolean);
|
|
388
|
-
const turns =
|
|
387
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
389
388
|
const files = [];
|
|
390
389
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
391
390
|
const turn = turns[index];
|
|
@@ -626,7 +625,6 @@ function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabe
|
|
|
626
625
|
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
627
626
|
? Math.floor(minTurnIndex)
|
|
628
627
|
: -1;
|
|
629
|
-
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
630
628
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
631
629
|
const expectedLabelsLiteral = JSON.stringify(expectedLabels);
|
|
632
630
|
const allowGenericDownloadLabelsLiteral = JSON.stringify(allowGenericDownloadLabels);
|
|
@@ -639,7 +637,6 @@ function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabe
|
|
|
639
637
|
: 0;
|
|
640
638
|
return `(() => {
|
|
641
639
|
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
642
|
-
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
643
640
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
644
641
|
const EXPECTED_LABELS = ${expectedLabelsLiteral};
|
|
645
642
|
const ALLOW_GENERIC_DOWNLOAD_LABELS = ${allowGenericDownloadLabelsLiteral};
|
|
@@ -762,7 +759,7 @@ function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabe
|
|
|
762
759
|
const genericBehaviorButton = (info) =>
|
|
763
760
|
ALLOW_GENERIC_DOWNLOAD_LABELS && info.className.includes('behavior-btn') && hasDownloadIntent(info);
|
|
764
761
|
const genericFallbackButton = (info) => ALLOW_GENERIC_DOWNLOAD_LABELS && hasDownloadIntent(info);
|
|
765
|
-
const turns =
|
|
762
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
766
763
|
const expectedMatches = new Set();
|
|
767
764
|
const genericBehaviorMatches = new Set();
|
|
768
765
|
const genericFallbackMatches = new Set();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { ASSISTANT_ROLE_SELECTOR
|
|
4
|
+
import { ASSISTANT_ROLE_SELECTOR } from "./constants.js";
|
|
5
|
+
import { buildConversationTurnListExpression } from "./conversationTurns.js";
|
|
5
6
|
import { delay } from "./utils.js";
|
|
6
7
|
import { readAssistantSnapshot } from "./pageActions.js";
|
|
7
8
|
import { getOracleHomeDir } from "../oracleHome.js";
|
|
@@ -61,11 +62,9 @@ function buildAssistantImageExpression(minTurnIndex) {
|
|
|
61
62
|
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
62
63
|
? Math.floor(minTurnIndex)
|
|
63
64
|
: -1;
|
|
64
|
-
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
65
65
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
66
66
|
return `(() => {
|
|
67
67
|
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
68
|
-
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
69
68
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
70
69
|
const isGeneratedImage = (img) => {
|
|
71
70
|
const url = new URL(img?.src || '', location.origin || 'https://chatgpt.com');
|
|
@@ -101,7 +100,7 @@ function buildAssistantImageExpression(minTurnIndex) {
|
|
|
101
100
|
if (testId.includes('assistant')) return true;
|
|
102
101
|
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
103
102
|
};
|
|
104
|
-
const turns =
|
|
103
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
105
104
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
106
105
|
const turn = turns[index];
|
|
107
106
|
if (!isAssistantTurn(turn)) continue;
|
|
@@ -367,6 +367,7 @@ function createSessionBoundChromeClient(browser, sessionId) {
|
|
|
367
367
|
Runtime: bindDomain("Runtime"),
|
|
368
368
|
Input: bindDomain("Input"),
|
|
369
369
|
DOM: bindDomain("DOM"),
|
|
370
|
+
Emulation: bindDomain("Emulation"),
|
|
370
371
|
on: browserWithEvents.on.bind(browserWithEvents),
|
|
371
372
|
once: browserWithEvents.once.bind(browserWithEvents),
|
|
372
373
|
off: browserWithEvents.off?.bind(browserWithEvents) ??
|
|
@@ -32,6 +32,7 @@ export const ANSWER_SELECTORS = [
|
|
|
32
32
|
export const CONVERSATION_TURN_SELECTOR = 'article[data-testid^="conversation-turn"], div[data-testid^="conversation-turn"], section[data-testid^="conversation-turn"], ' +
|
|
33
33
|
"article[data-message-author-role], div[data-message-author-role], section[data-message-author-role], " +
|
|
34
34
|
"article[data-turn], div[data-turn], section[data-turn]";
|
|
35
|
+
export const CONVERSATION_TURN_CONTAINER_SELECTOR = '[data-testid^="conversation-turn"]';
|
|
35
36
|
export const ASSISTANT_ROLE_SELECTOR = '[data-message-author-role="assistant"], [data-turn="assistant"]';
|
|
36
37
|
export const CLOUDFLARE_SCRIPT_SELECTOR = 'script[src*="/challenge-platform/"]';
|
|
37
38
|
export const CLOUDFLARE_TITLE = "just a moment";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CONVERSATION_TURN_CONTAINER_SELECTOR, CONVERSATION_TURN_SELECTOR } from "./constants.js";
|
|
2
|
+
/** Build a browser-context expression that returns one DOM node per conversation turn. */
|
|
3
|
+
export function buildConversationTurnListExpression(rootExpression = "document") {
|
|
4
|
+
const containerSelector = JSON.stringify(CONVERSATION_TURN_CONTAINER_SELECTOR);
|
|
5
|
+
const fallbackSelector = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
6
|
+
return `(() => {
|
|
7
|
+
const root = ${rootExpression};
|
|
8
|
+
const containers = Array.from(root.querySelectorAll(${containerSelector}));
|
|
9
|
+
return containers.length > 0
|
|
10
|
+
? containers
|
|
11
|
+
: Array.from(root.querySelectorAll(${fallbackSelector}));
|
|
12
|
+
})()`;
|
|
13
|
+
}
|
|
14
|
+
export function buildConversationTurnCountExpression(rootExpression = "document") {
|
|
15
|
+
return `(${buildConversationTurnListExpression(rootExpression)}).length`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { delay } from "./utils.js";
|
|
2
|
+
export function createConversationUrlMonitor(options) {
|
|
3
|
+
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
4
|
+
const wait = options.wait ?? delay;
|
|
5
|
+
const now = options.now ?? Date.now;
|
|
6
|
+
let inFlight = null;
|
|
7
|
+
let stopped = false;
|
|
8
|
+
const activePersists = new Set();
|
|
9
|
+
const update = async (label, timeoutMs = 10_000) => {
|
|
10
|
+
const startedAt = now();
|
|
11
|
+
while (!stopped && now() - startedAt < timeoutMs) {
|
|
12
|
+
try {
|
|
13
|
+
const url = await options.readUrl();
|
|
14
|
+
if (stopped) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (url && isConversationUrl(url)) {
|
|
18
|
+
options.logger(`[browser] conversation url (${label}) = ${url}`);
|
|
19
|
+
const persist = options.persistUrl(url);
|
|
20
|
+
activePersists.add(persist);
|
|
21
|
+
try {
|
|
22
|
+
await persist;
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
activePersists.delete(persist);
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// The page can navigate or disconnect between polls; keep trying until timeout.
|
|
32
|
+
}
|
|
33
|
+
await wait(pollIntervalMs);
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
};
|
|
37
|
+
const schedule = (label, timeoutMs) => {
|
|
38
|
+
if (stopped) {
|
|
39
|
+
return Promise.resolve(false);
|
|
40
|
+
}
|
|
41
|
+
if (inFlight) {
|
|
42
|
+
return inFlight;
|
|
43
|
+
}
|
|
44
|
+
// The /c/ URL can appear after submit. Persist it without blocking response capture.
|
|
45
|
+
inFlight = update(label, timeoutMs)
|
|
46
|
+
.catch(() => false)
|
|
47
|
+
.finally(() => {
|
|
48
|
+
inFlight = null;
|
|
49
|
+
});
|
|
50
|
+
return inFlight;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
update,
|
|
54
|
+
schedule,
|
|
55
|
+
isInFlight: () => inFlight !== null,
|
|
56
|
+
stop: async () => {
|
|
57
|
+
stopped = true;
|
|
58
|
+
await Promise.allSettled(activePersists);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function isConversationUrl(url) {
|
|
63
|
+
return /\/c\/[a-z0-9-]+/i.test(url);
|
|
64
|
+
}
|
|
@@ -3,6 +3,56 @@ import { delay } from "./utils.js";
|
|
|
3
3
|
import { getCookies } from "@steipete/sweet-cookie";
|
|
4
4
|
export class ChromeCookieSyncError extends Error {
|
|
5
5
|
}
|
|
6
|
+
export async function clearStaleChatGptConversationCookies(Network, Target, logger, options = {}) {
|
|
7
|
+
try {
|
|
8
|
+
const preservedNames = new Set((options.preserveConversationIds ?? [])
|
|
9
|
+
.filter((id) => Boolean(id))
|
|
10
|
+
.map((id) => `conv_key_${id}`));
|
|
11
|
+
try {
|
|
12
|
+
const { targetInfos = [] } = await Target.getTargets();
|
|
13
|
+
for (const target of targetInfos) {
|
|
14
|
+
const conversationId = extractChatGptConversationId(target.url ?? "");
|
|
15
|
+
if (conversationId) {
|
|
16
|
+
preservedNames.add(`conv_key_${conversationId}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
logger(`[cookies] Failed to inspect active ChatGPT conversations; skipping stale cookie cleanup: ${message}`);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
const { cookies = [] } = await Network.getAllCookies();
|
|
26
|
+
const targets = cookies.filter((cookie) => isChatGptConversationCookie(cookie) && !preservedNames.has(String(cookie.name ?? "")));
|
|
27
|
+
if (targets.length === 0) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
let deleted = 0;
|
|
31
|
+
for (const cookie of targets) {
|
|
32
|
+
try {
|
|
33
|
+
await Network.deleteCookies({
|
|
34
|
+
name: cookie.name,
|
|
35
|
+
domain: cookie.domain,
|
|
36
|
+
path: cookie.path ?? "/",
|
|
37
|
+
});
|
|
38
|
+
deleted += 1;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
logger(`[cookies] Failed to clear a stale ChatGPT conversation cookie: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (deleted > 0) {
|
|
46
|
+
logger(`[cookies] Cleared ${deleted} stale ChatGPT conversation cookie${deleted === 1 ? "" : "s"}.`);
|
|
47
|
+
}
|
|
48
|
+
return deleted;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
logger(`[cookies] Failed to inspect ChatGPT conversation cookies: ${message}`);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
6
56
|
export async function syncCookies(Network, url, profile, logger, options = {}) {
|
|
7
57
|
const { allowErrors = false, filterNames, inlineCookies, cookiePath, waitMs = 0 } = options;
|
|
8
58
|
try {
|
|
@@ -94,6 +144,28 @@ async function readChromeCookies(url, profile, filterNames, cookiePath) {
|
|
|
94
144
|
}
|
|
95
145
|
return Array.from(merged.values());
|
|
96
146
|
}
|
|
147
|
+
function isChatGptConversationCookie(cookie) {
|
|
148
|
+
if (!cookie.name?.startsWith("conv_key_")) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const domain = String(cookie.domain ?? "")
|
|
152
|
+
.replace(/^\./, "")
|
|
153
|
+
.toLowerCase();
|
|
154
|
+
return domain === "chatgpt.com" || domain === "chat.openai.com";
|
|
155
|
+
}
|
|
156
|
+
function extractChatGptConversationId(url) {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = new URL(url);
|
|
159
|
+
const domain = parsed.hostname.toLowerCase();
|
|
160
|
+
if (domain !== "chatgpt.com" && domain !== "chat.openai.com") {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return parsed.pathname.match(/\/c\/([a-zA-Z0-9-]+)/)?.[1];
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
97
169
|
function normalizeInlineCookies(rawCookies, fallbackHost) {
|
|
98
170
|
const merged = new Map();
|
|
99
171
|
for (const cookie of rawCookies) {
|
|
@@ -5,7 +5,7 @@ import net from "node:net";
|
|
|
5
5
|
import { resolveBrowserConfig } from "./config.js";
|
|
6
6
|
import { copyChromeProfile } from "./profileCopy.js";
|
|
7
7
|
import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToRemoteChrome, connectWithNewTab, closeTab, closeRemoteChromeTarget, closeBlankChromeTabs, } from "./chromeLifecycle.js";
|
|
8
|
-
import { syncCookies } from "./cookies.js";
|
|
8
|
+
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
|
|
9
9
|
import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, ensureModelSelection, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from "./pageActions.js";
|
|
10
10
|
import { INPUT_SELECTORS } from "./constants.js";
|
|
11
11
|
import { uploadAttachmentViaDataTransfer } from "./actions/remoteFileTransfer.js";
|
|
@@ -14,9 +14,10 @@ import { startThinkingStatusMonitor } from "./actions/thinkingStatus.js";
|
|
|
14
14
|
import { activateDeepResearch, captureDeepResearchTargetKeys, waitForDeepResearchCompletion, waitForResearchPlanAutoConfirm, } from "./actions/deepResearch.js";
|
|
15
15
|
import { estimateTokenCount, withRetries, delay } from "./utils.js";
|
|
16
16
|
import { formatElapsed } from "../oracle/format.js";
|
|
17
|
-
import { CHATGPT_URL,
|
|
17
|
+
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY } from "./constants.js";
|
|
18
18
|
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
19
19
|
import { alignPromptEchoPair, buildPromptEchoMatcher } from "./reattachHelpers.js";
|
|
20
|
+
import { buildConversationTurnCountExpression } from "./conversationTurns.js";
|
|
20
21
|
import { cleanupStaleProfileState, acquireProfileRunLock, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, terminateRecordedChromeForProfile, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
|
|
21
22
|
import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLeaseRegistry.js";
|
|
22
23
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
|
|
@@ -30,6 +31,7 @@ import { captureBrowserDiagnostics } from "./domDebug.js";
|
|
|
30
31
|
import { archiveChatGptConversation, resolveBrowserArchiveDecision, } from "./actions/archiveConversation.js";
|
|
31
32
|
import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, formatManualLoginSetupCommand, isManualLoginProfileInitialized, resolveManualLoginWaitMs, } from "./manualLoginProfile.js";
|
|
32
33
|
import { describeBrowserControlPlan, formatBrowserControlPlan } from "./controlPlan.js";
|
|
34
|
+
import { createConversationUrlMonitor, } from "./conversationUrlMonitor.js";
|
|
33
35
|
export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "./constants.js";
|
|
34
36
|
export { parseDuration, delay, normalizeChatgptUrl, isTemporaryChatUrl } from "./utils.js";
|
|
35
37
|
export { formatThinkingLog, formatThinkingWaitingLog, buildThinkingStatusExpressionForTest, readThinkingStatusForTest, sanitizeThinkingText, startThinkingStatusMonitorForTest, } from "./actions/thinkingStatus.js";
|
|
@@ -560,6 +562,7 @@ export async function runBrowserMode(options) {
|
|
|
560
562
|
let promptSubmitted = false;
|
|
561
563
|
let modelSelectionEvidence;
|
|
562
564
|
let tabLease = null;
|
|
565
|
+
let conversationUrlMonitor = null;
|
|
563
566
|
const emitRuntimeHint = async () => {
|
|
564
567
|
if (!chrome?.port) {
|
|
565
568
|
return;
|
|
@@ -596,6 +599,7 @@ export async function runBrowserMode(options) {
|
|
|
596
599
|
}
|
|
597
600
|
promptSubmitted = true;
|
|
598
601
|
await emitRuntimeHint();
|
|
602
|
+
void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
|
|
599
603
|
};
|
|
600
604
|
if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") {
|
|
601
605
|
logger(`[browser-mode] config: ${JSON.stringify({
|
|
@@ -746,7 +750,7 @@ export async function runBrowserMode(options) {
|
|
|
746
750
|
else {
|
|
747
751
|
const strictTabIsolation = Boolean(manualLogin && reusedChrome);
|
|
748
752
|
const devtoolsRetries = manualLogin ? 6 : 0;
|
|
749
|
-
const connection = await connectWithNewTab(chrome.port, logger,
|
|
753
|
+
const connection = await connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
|
|
750
754
|
fallbackToDefault: !strictTabIsolation,
|
|
751
755
|
retries: devtoolsRetries,
|
|
752
756
|
retryDelayMs: 500,
|
|
@@ -778,7 +782,7 @@ export async function runBrowserMode(options) {
|
|
|
778
782
|
});
|
|
779
783
|
});
|
|
780
784
|
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
781
|
-
const { Network, Page, Runtime, Input, DOM } = client;
|
|
785
|
+
const { Network, Page, Runtime, Input, DOM, Target } = client;
|
|
782
786
|
if (!config.headless && config.hideWindow) {
|
|
783
787
|
await hideChromeWindow(chrome, logger);
|
|
784
788
|
}
|
|
@@ -828,6 +832,12 @@ export async function runBrowserMode(options) {
|
|
|
828
832
|
? "Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in."
|
|
829
833
|
: "Skipping Chrome cookie sync (--browser-no-cookie-sync)");
|
|
830
834
|
}
|
|
835
|
+
await clearStaleChatGptConversationCookies(Network, Target, logger, {
|
|
836
|
+
preserveConversationIds: [
|
|
837
|
+
extractConversationIdFromUrl(config.resumeConversationUrl ?? ""),
|
|
838
|
+
extractConversationIdFromUrl(lastUrl ?? ""),
|
|
839
|
+
],
|
|
840
|
+
});
|
|
831
841
|
if (cookieSyncEnabled && !manualLogin && (appliedCookies ?? 0) === 0 && !config.inlineCookies) {
|
|
832
842
|
// Learned: if the profile has no ChatGPT cookies, browser mode will just bounce to login.
|
|
833
843
|
// Fail early so the user knows to sign in.
|
|
@@ -939,44 +949,22 @@ export async function runBrowserMode(options) {
|
|
|
939
949
|
await emitRuntimeHint();
|
|
940
950
|
}
|
|
941
951
|
};
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
return true;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
catch {
|
|
962
|
-
// ignore; keep polling until timeout
|
|
963
|
-
}
|
|
964
|
-
await delay(250);
|
|
965
|
-
}
|
|
966
|
-
return false;
|
|
967
|
-
};
|
|
968
|
-
const scheduleConversationHint = (label, timeoutMs) => {
|
|
969
|
-
if (conversationHintInFlight) {
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
972
|
-
// Learned: the /c/ URL can update after the answer; emit hints in the background.
|
|
973
|
-
// Run in the background so prompt submission/streaming isn't blocked by slow URL updates.
|
|
974
|
-
conversationHintInFlight = updateConversationHint(label, timeoutMs)
|
|
975
|
-
.catch(() => false)
|
|
976
|
-
.finally(() => {
|
|
977
|
-
conversationHintInFlight = null;
|
|
978
|
-
});
|
|
979
|
-
};
|
|
952
|
+
const activeConversationUrlMonitor = createConversationUrlMonitor({
|
|
953
|
+
readUrl: async () => {
|
|
954
|
+
const { result } = await Runtime.evaluate({
|
|
955
|
+
expression: "location.href",
|
|
956
|
+
returnByValue: true,
|
|
957
|
+
});
|
|
958
|
+
return typeof result?.value === "string" ? result.value : null;
|
|
959
|
+
},
|
|
960
|
+
persistUrl: async (url) => {
|
|
961
|
+
lastUrl = url;
|
|
962
|
+
await emitRuntimeHint();
|
|
963
|
+
},
|
|
964
|
+
logger,
|
|
965
|
+
});
|
|
966
|
+
conversationUrlMonitor = activeConversationUrlMonitor;
|
|
967
|
+
const updateConversationHint = conversationUrlMonitor.update;
|
|
980
968
|
await captureRuntimeSnapshot();
|
|
981
969
|
const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
|
|
982
970
|
if (config.desiredModel && modelStrategy !== "ignore" && !isResumingConversation) {
|
|
@@ -1128,8 +1116,6 @@ export async function runBrowserMode(options) {
|
|
|
1128
1116
|
}
|
|
1129
1117
|
}
|
|
1130
1118
|
}
|
|
1131
|
-
// Reattach needs a /c/ URL; ChatGPT can update it late, so poll in the background.
|
|
1132
|
-
scheduleConversationHint("post-submit", config.timeoutMs ?? 120_000);
|
|
1133
1119
|
return {
|
|
1134
1120
|
baselineTurns,
|
|
1135
1121
|
baselineAssistantText,
|
|
@@ -1725,6 +1711,7 @@ export async function runBrowserMode(options) {
|
|
|
1725
1711
|
}, normalizedError);
|
|
1726
1712
|
}
|
|
1727
1713
|
finally {
|
|
1714
|
+
await conversationUrlMonitor?.stop();
|
|
1728
1715
|
try {
|
|
1729
1716
|
if (!connectionClosedUnexpectedly) {
|
|
1730
1717
|
await client?.close();
|
|
@@ -2086,6 +2073,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2086
2073
|
let modelSelectionEvidence;
|
|
2087
2074
|
let attachedExistingTab = false;
|
|
2088
2075
|
let ownsTarget = true;
|
|
2076
|
+
let conversationUrlMonitor = null;
|
|
2089
2077
|
const runtimeHintCb = options.runtimeHintCb;
|
|
2090
2078
|
const emitRuntimeHint = async () => {
|
|
2091
2079
|
if (!runtimeHintCb)
|
|
@@ -2120,6 +2108,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2120
2108
|
}
|
|
2121
2109
|
promptSubmitted = true;
|
|
2122
2110
|
await emitRuntimeHint();
|
|
2111
|
+
void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
|
|
2123
2112
|
};
|
|
2124
2113
|
const startedAt = Date.now();
|
|
2125
2114
|
let answerText = "";
|
|
@@ -2161,7 +2150,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2161
2150
|
logger(`Attached to existing remote ChatGPT tab ${attached.targetId}${attached.tab.url ? ` (${attached.tab.url})` : ""}`);
|
|
2162
2151
|
}
|
|
2163
2152
|
else {
|
|
2164
|
-
connection = await connectToRemoteChrome(host, port, logger,
|
|
2153
|
+
connection = await connectToRemoteChrome(host, port, logger, "about:blank", browserWSEndpoint, {
|
|
2165
2154
|
approvalWaitMs: config.attachRunning && browserWSEndpoint ? 20_000 : undefined,
|
|
2166
2155
|
});
|
|
2167
2156
|
client = connection.client;
|
|
@@ -2180,15 +2169,44 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2180
2169
|
connectionClosedUnexpectedly = true;
|
|
2181
2170
|
};
|
|
2182
2171
|
client.on("disconnect", markConnectionLost);
|
|
2183
|
-
const { Network, Page, Runtime, Input, DOM } = client;
|
|
2172
|
+
const { Network, Page, Runtime, Input, DOM, Target } = client;
|
|
2184
2173
|
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
2185
2174
|
if (DOM && typeof DOM.enable === "function") {
|
|
2186
2175
|
domainEnablers.push(DOM.enable());
|
|
2187
2176
|
}
|
|
2188
2177
|
await Promise.all(domainEnablers);
|
|
2189
2178
|
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
2179
|
+
try {
|
|
2180
|
+
await client.Emulation.setFocusEmulationEnabled({ enabled: true });
|
|
2181
|
+
logger("[browser] Focus emulation enabled for remote target");
|
|
2182
|
+
}
|
|
2183
|
+
catch (error) {
|
|
2184
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2185
|
+
logger(`[browser] Focus emulation unavailable: ${message}`);
|
|
2186
|
+
}
|
|
2187
|
+
const activeConversationUrlMonitor = createConversationUrlMonitor({
|
|
2188
|
+
readUrl: async () => {
|
|
2189
|
+
const { result } = await Runtime.evaluate({
|
|
2190
|
+
expression: "location.href",
|
|
2191
|
+
returnByValue: true,
|
|
2192
|
+
});
|
|
2193
|
+
return typeof result?.value === "string" ? result.value : null;
|
|
2194
|
+
},
|
|
2195
|
+
persistUrl: async (url) => {
|
|
2196
|
+
lastUrl = url;
|
|
2197
|
+
await emitRuntimeHint();
|
|
2198
|
+
},
|
|
2199
|
+
logger,
|
|
2200
|
+
});
|
|
2201
|
+
conversationUrlMonitor = activeConversationUrlMonitor;
|
|
2190
2202
|
// Skip cookie sync for remote Chrome - it already has cookies
|
|
2191
2203
|
logger("Skipping cookie sync for remote Chrome (using existing session)");
|
|
2204
|
+
await clearStaleChatGptConversationCookies(Network, Target, logger, {
|
|
2205
|
+
preserveConversationIds: [
|
|
2206
|
+
extractConversationIdFromUrl(config.resumeConversationUrl ?? ""),
|
|
2207
|
+
extractConversationIdFromUrl(lastUrl ?? ""),
|
|
2208
|
+
],
|
|
2209
|
+
});
|
|
2192
2210
|
if (config.resumeConversationUrl) {
|
|
2193
2211
|
await navigateToChatGPT(Page, Runtime, config.resumeConversationUrl, logger);
|
|
2194
2212
|
}
|
|
@@ -2361,7 +2379,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2361
2379
|
ignoredTargetKeys: deepResearchTargetKeys,
|
|
2362
2380
|
targetBaselineCaptured: deepResearchTargetBaselineCaptured,
|
|
2363
2381
|
});
|
|
2364
|
-
await
|
|
2382
|
+
await activeConversationUrlMonitor.update("post-deep-research", 15_000).catch(() => false);
|
|
2365
2383
|
const durationMs = Date.now() - startedAt;
|
|
2366
2384
|
const tokens = estimateTokenCount(researchResult.text);
|
|
2367
2385
|
const reportArtifact = await saveOptionalArtifact(() => saveDeepResearchReportArtifact({
|
|
@@ -2512,11 +2530,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2512
2530
|
const captureAssistantTurn = async (turnPrompt, label) => {
|
|
2513
2531
|
let turnAnswer;
|
|
2514
2532
|
try {
|
|
2515
|
-
|
|
2516
|
-
if (conversationUrl && isConversationUrl(conversationUrl)) {
|
|
2517
|
-
lastUrl = conversationUrl;
|
|
2518
|
-
await emitRuntimeHint();
|
|
2519
|
-
}
|
|
2533
|
+
await activeConversationUrlMonitor.update("assistant-wait", 15_000).catch(() => false);
|
|
2520
2534
|
turnAnswer = await waitWithThinkingMonitor(() => waitForAssistantOrGeneratedImageResponse({
|
|
2521
2535
|
Runtime,
|
|
2522
2536
|
waitForText: () => waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger, baselineTurns ?? undefined, expectedConversationId()),
|
|
@@ -2534,16 +2548,9 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2534
2548
|
turnAnswer = rechecked;
|
|
2535
2549
|
}
|
|
2536
2550
|
else {
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
lastUrl = conversationUrl;
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
catch {
|
|
2544
|
-
// ignore
|
|
2545
|
-
}
|
|
2546
|
-
await emitRuntimeHint();
|
|
2551
|
+
await activeConversationUrlMonitor
|
|
2552
|
+
.update("assistant-timeout", 15_000)
|
|
2553
|
+
.catch(() => false);
|
|
2547
2554
|
const diagnostics = await captureBrowserDiagnostics(Runtime, logger, "assistant-timeout", {
|
|
2548
2555
|
Page,
|
|
2549
2556
|
sessionId: options.sessionId,
|
|
@@ -2572,6 +2579,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2572
2579
|
throw error;
|
|
2573
2580
|
}
|
|
2574
2581
|
}
|
|
2582
|
+
await activeConversationUrlMonitor.update("post-response", 15_000).catch(() => false);
|
|
2575
2583
|
const baselineNormalized = baselineAssistantText
|
|
2576
2584
|
? normalizeForComparison(baselineAssistantText)
|
|
2577
2585
|
: "";
|
|
@@ -2836,6 +2844,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2836
2844
|
});
|
|
2837
2845
|
}
|
|
2838
2846
|
finally {
|
|
2847
|
+
await conversationUrlMonitor?.stop();
|
|
2839
2848
|
try {
|
|
2840
2849
|
await closeRemoteConnectionAfterRun({
|
|
2841
2850
|
connectionClosedUnexpectedly,
|
|
@@ -3053,12 +3062,12 @@ function buildSessionValidationExpression() {
|
|
|
3053
3062
|
})()`;
|
|
3054
3063
|
}
|
|
3055
3064
|
async function readConversationTurnCount(Runtime, logger) {
|
|
3056
|
-
const
|
|
3065
|
+
const expression = buildConversationTurnCountExpression();
|
|
3057
3066
|
const attempts = 4;
|
|
3058
3067
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
3059
3068
|
try {
|
|
3060
3069
|
const { result } = await Runtime.evaluate({
|
|
3061
|
-
expression
|
|
3070
|
+
expression,
|
|
3062
3071
|
returnByValue: true,
|
|
3063
3072
|
});
|
|
3064
3073
|
const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
|
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { closeTab, connectWithNewTab, hideChromeWindow, launchChrome, registerTerminationHooks, } from "./chromeLifecycle.js";
|
|
5
5
|
import { resolveBrowserConfig } from "./config.js";
|
|
6
|
-
import { syncCookies } from "./cookies.js";
|
|
6
|
+
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
|
|
7
7
|
import { installJavaScriptDialogAutoDismissal, navigateToChatGPT, ensureLoggedIn, } from "./pageActions.js";
|
|
8
8
|
import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLeaseRegistry.js";
|
|
9
9
|
import { acquireProfileRunLock, cleanupStaleProfileState, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
|
|
@@ -120,7 +120,7 @@ export async function runBrowserProjectSources(request) {
|
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
122
|
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
123
|
-
const { Network, Page, Runtime, Input, DOM } = client;
|
|
123
|
+
const { Network, Page, Runtime, Input, DOM, Target } = client;
|
|
124
124
|
if (!config.headless && config.hideWindow) {
|
|
125
125
|
await hideChromeWindow(chrome, logger);
|
|
126
126
|
}
|
|
@@ -139,6 +139,7 @@ export async function runBrowserProjectSources(request) {
|
|
|
139
139
|
manualLogin,
|
|
140
140
|
logger,
|
|
141
141
|
});
|
|
142
|
+
await clearStaleChatGptConversationCookies(Network, Target, logger);
|
|
142
143
|
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger));
|
|
143
144
|
await raceWithDisconnect(waitForProjectSourcesLogin({
|
|
144
145
|
runtime: Runtime,
|
|
@@ -5,8 +5,9 @@ import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
|
|
5
5
|
import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, } from "./pageActions.js";
|
|
6
6
|
import { launchChrome, connectToChrome, hideChromeWindow, connectToRemoteChromeTarget, listRemoteChromeTargets, } from "./chromeLifecycle.js";
|
|
7
7
|
import { resolveBrowserConfig } from "./config.js";
|
|
8
|
-
import { syncCookies } from "./cookies.js";
|
|
9
|
-
import { CHATGPT_URL
|
|
8
|
+
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
|
|
9
|
+
import { CHATGPT_URL } from "./constants.js";
|
|
10
|
+
import { buildConversationTurnListExpression } from "./conversationTurns.js";
|
|
10
11
|
import { cleanupStaleProfileState } from "./profileState.js";
|
|
11
12
|
import { readDevToolsActivePortInfo } from "./detect.js";
|
|
12
13
|
import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from "./reattachHelpers.js";
|
|
@@ -14,6 +15,12 @@ import { waitForDeepResearchCompletion } from "./actions/deepResearch.js";
|
|
|
14
15
|
export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
15
16
|
const recoverSession = deps.recoverSession ??
|
|
16
17
|
(async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
|
|
18
|
+
let closeAttachedConnection = null;
|
|
19
|
+
const closeAttached = async () => {
|
|
20
|
+
const close = closeAttachedConnection;
|
|
21
|
+
closeAttachedConnection = null;
|
|
22
|
+
await close?.().catch(() => undefined);
|
|
23
|
+
};
|
|
17
24
|
if (!runtime.chromePort && !runtime.chromeBrowserWSEndpoint) {
|
|
18
25
|
logger("No running Chrome detected; reopening browser to locate the session.");
|
|
19
26
|
return recoverSession(runtime, config);
|
|
@@ -37,8 +44,8 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
37
44
|
targetId: target?.targetId ?? target?.id,
|
|
38
45
|
closeTargetOnDispose: false,
|
|
39
46
|
})
|
|
40
|
-
: {
|
|
41
|
-
client
|
|
47
|
+
: await (async () => {
|
|
48
|
+
const client = (await (deps.connect ?? ((options) => CDP(options)))(browserWSEndpoint
|
|
42
49
|
? {
|
|
43
50
|
target: browserWSEndpoint,
|
|
44
51
|
local: true,
|
|
@@ -48,9 +55,10 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
48
55
|
host,
|
|
49
56
|
port,
|
|
50
57
|
target: target?.targetId ?? target?.id,
|
|
51
|
-
}))
|
|
52
|
-
close:
|
|
53
|
-
};
|
|
58
|
+
}));
|
|
59
|
+
return { client, close: () => client.close() };
|
|
60
|
+
})();
|
|
61
|
+
closeAttachedConnection = () => connection.close();
|
|
54
62
|
const client = connection.client;
|
|
55
63
|
const { Runtime, DOM, Page } = client;
|
|
56
64
|
if (Runtime?.enable) {
|
|
@@ -97,7 +105,7 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
97
105
|
const researchResult = await withTimeout(waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
|
|
98
106
|
requireScopedTargetOwner: true,
|
|
99
107
|
}), timeoutMs + 5_000, "Reattach Deep Research response timed out");
|
|
100
|
-
await
|
|
108
|
+
await closeAttached();
|
|
101
109
|
return {
|
|
102
110
|
answerText: researchResult.text,
|
|
103
111
|
answerMarkdown: researchResult.text,
|
|
@@ -108,10 +116,11 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
108
116
|
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
109
117
|
const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000, "Reattach markdown capture timed out")) ?? recovered.text;
|
|
110
118
|
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
111
|
-
await
|
|
119
|
+
await closeAttached();
|
|
112
120
|
return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
|
|
113
121
|
}
|
|
114
122
|
catch (error) {
|
|
123
|
+
await closeAttached();
|
|
115
124
|
const message = error instanceof Error ? error.message : String(error);
|
|
116
125
|
logger(`Existing Chrome reattach failed (${message}); reopening browser to locate the session.`);
|
|
117
126
|
return recoverSession(runtime, config);
|
|
@@ -163,7 +172,7 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
163
172
|
const chrome = await launchChrome(resolved, userDataDir, logger);
|
|
164
173
|
const chromeHost = chrome.host ?? "127.0.0.1";
|
|
165
174
|
const client = await connectToChrome(chrome.port, logger, chromeHost);
|
|
166
|
-
const { Network, Page, Runtime, DOM } = client;
|
|
175
|
+
const { Network, Page, Runtime, DOM, Target } = client;
|
|
167
176
|
if (Runtime?.enable) {
|
|
168
177
|
await Runtime.enable();
|
|
169
178
|
}
|
|
@@ -183,6 +192,13 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
183
192
|
waitMs: resolved.cookieSyncWaitMs ?? 0,
|
|
184
193
|
});
|
|
185
194
|
}
|
|
195
|
+
await clearStaleChatGptConversationCookies(Network, Target, logger, {
|
|
196
|
+
preserveConversationIds: [
|
|
197
|
+
runtime.conversationId,
|
|
198
|
+
extractConversationIdFromUrl(runtime.tabUrl ?? ""),
|
|
199
|
+
extractConversationIdFromUrl(resolved.url),
|
|
200
|
+
],
|
|
201
|
+
});
|
|
186
202
|
await navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger);
|
|
187
203
|
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
188
204
|
await ensureLoggedIn(Runtime, logger, { appliedCookies });
|
|
@@ -268,7 +284,7 @@ async function readPromptPreviewTurnIndex(Runtime, promptPreview) {
|
|
|
268
284
|
const needle = ${JSON.stringify(preview.toLowerCase().replace(/\s+/g, " ").slice(0, 120))};
|
|
269
285
|
if (!needle) return null;
|
|
270
286
|
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
271
|
-
const turns =
|
|
287
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
272
288
|
let matched = null;
|
|
273
289
|
for (const [index, node] of turns.entries()) {
|
|
274
290
|
const attr = (node.getAttribute('data-message-author-role') || node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { CONVERSATION_TURN_SELECTOR } from "./constants.js";
|
|
2
|
+
import { buildConversationTurnCountExpression } from "./conversationTurns.js";
|
|
2
3
|
import { delay } from "./utils.js";
|
|
3
4
|
import { readAssistantSnapshot } from "./pageActions.js";
|
|
4
5
|
export function pickTarget(targets, runtime) {
|
|
5
6
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
6
7
|
return undefined;
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const conversationId = runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? "");
|
|
10
|
+
const byId = runtime.chromeTargetId
|
|
11
|
+
? targets.find((target) => (target.targetId ?? target.id) === runtime.chromeTargetId)
|
|
12
|
+
: undefined;
|
|
13
|
+
if (conversationId) {
|
|
14
|
+
if (byId && extractConversationIdFromUrl(byId.url ?? "") === conversationId) {
|
|
11
15
|
return byId;
|
|
16
|
+
}
|
|
17
|
+
const byConversation = targets.find((target) => extractConversationIdFromUrl(target.url ?? "") === conversationId);
|
|
18
|
+
if (byConversation)
|
|
19
|
+
return byConversation;
|
|
12
20
|
}
|
|
21
|
+
if (byId)
|
|
22
|
+
return byId;
|
|
13
23
|
if (runtime.tabUrl) {
|
|
14
24
|
const byUrl = targets.find((t) => t.url?.startsWith(runtime.tabUrl)) ||
|
|
15
25
|
targets.find((t) => runtime.tabUrl.startsWith(t.url || ""));
|
|
@@ -253,10 +263,9 @@ export async function waitForLocationChange(Runtime, timeoutMs) {
|
|
|
253
263
|
}
|
|
254
264
|
}
|
|
255
265
|
export async function readConversationTurnIndex(Runtime, logger) {
|
|
256
|
-
const selectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
257
266
|
try {
|
|
258
267
|
const { result } = await Runtime.evaluate({
|
|
259
|
-
expression:
|
|
268
|
+
expression: buildConversationTurnCountExpression(),
|
|
260
269
|
returnByValue: true,
|
|
261
270
|
});
|
|
262
271
|
const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import { startOscProgress as startOscProgressShared, supportsOscProgress as supportsOscProgressShared, } from "osc-progress";
|
|
3
|
-
export function supportsOscProgress(env = process.env, isTty = process.stdout.isTTY) {
|
|
3
|
+
export function supportsOscProgress(env = process.env, isTty = process.stdout.isTTY === true) {
|
|
4
4
|
if (env.CODEX_MANAGED_BY_NPM === "1" && env.ORACLE_FORCE_OSC_PROGRESS !== "1") {
|
|
5
5
|
return false;
|
|
6
6
|
}
|
|
@@ -16,8 +16,9 @@ export function startOscProgress(options = {}) {
|
|
|
16
16
|
}
|
|
17
17
|
return startOscProgressShared({
|
|
18
18
|
...options,
|
|
19
|
-
// Preserve Oracle's previous
|
|
19
|
+
// Preserve Oracle's previous defaults: progress emits to and checks stdout.
|
|
20
20
|
write: options.write ?? ((text) => process.stdout.write(text)),
|
|
21
|
+
isTty: options.isTty ?? process.stdout.isTTY === true,
|
|
21
22
|
disableEnvVar: "ORACLE_NO_OSC_PROGRESS",
|
|
22
23
|
forceEnvVar: "ORACLE_FORCE_OSC_PROGRESS",
|
|
23
24
|
});
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "CLI wrapper around OpenAI Responses API with GPT-5.5 Pro, GPT-5.5, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://askoracle.sh",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
61
|
-
"@google/genai": "^2.
|
|
61
|
+
"@google/genai": "^2.10.0",
|
|
62
62
|
"@google/generative-ai": "^0.24.1",
|
|
63
63
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
64
64
|
"@steipete/sweet-cookie": "^0.4.0",
|
|
@@ -73,31 +73,31 @@
|
|
|
73
73
|
"inquirer": "14.0.2",
|
|
74
74
|
"json5": "^2.2.3",
|
|
75
75
|
"kleur": "^4.1.5",
|
|
76
|
-
"markdansi": "0.3.
|
|
77
|
-
"openai": "^6.
|
|
78
|
-
"osc-progress": "^0.3.
|
|
79
|
-
"qs": "^6.15.
|
|
80
|
-
"shiki": "^4.
|
|
76
|
+
"markdansi": "0.3.2",
|
|
77
|
+
"openai": "^6.45.0",
|
|
78
|
+
"osc-progress": "^0.3.2",
|
|
79
|
+
"qs": "^6.15.3",
|
|
80
|
+
"shiki": "^4.3.1",
|
|
81
81
|
"toasted-notifier": "^10.1.0",
|
|
82
|
-
"tokentally": "^0.1.
|
|
82
|
+
"tokentally": "^0.1.2",
|
|
83
83
|
"zod": "^4.4.3"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
87
87
|
"@types/chrome-remote-interface": "^0.34.0",
|
|
88
88
|
"@types/inquirer": "^9.0.10",
|
|
89
|
-
"@types/node": "^26.
|
|
90
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
91
|
-
"@vitest/coverage-v8": "4.1.
|
|
92
|
-
"devtools-protocol": "0.0.
|
|
93
|
-
"es-toolkit": "^1.
|
|
89
|
+
"@types/node": "^26.1.0",
|
|
90
|
+
"@typescript/native-preview": "7.0.0-dev.20260706.1",
|
|
91
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
92
|
+
"devtools-protocol": "0.0.1656784",
|
|
93
|
+
"es-toolkit": "^1.49.0",
|
|
94
94
|
"esbuild": "^0.28.1",
|
|
95
|
-
"oxfmt": "0.
|
|
96
|
-
"oxlint": "^1.
|
|
97
|
-
"puppeteer-core": "^25.
|
|
98
|
-
"tsx": "^4.
|
|
95
|
+
"oxfmt": "0.57.0",
|
|
96
|
+
"oxlint": "^1.72.0",
|
|
97
|
+
"puppeteer-core": "^25.3.0",
|
|
98
|
+
"tsx": "^4.23.0",
|
|
99
99
|
"typescript": "^6.0.3",
|
|
100
|
-
"vitest": "^4.1.
|
|
100
|
+
"vitest": "^4.1.10"
|
|
101
101
|
},
|
|
102
102
|
"devEngines": {
|
|
103
103
|
"runtime": [
|
|
Binary file
|
|
Binary file
|