@steipete/oracle 0.15.0 → 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/bin/oracle-cli.js +14 -6
- package/dist/docs-site/bridge.html +17 -1
- package/dist/docs-site/browser-mode.html +2 -2
- package/dist/docs-site/configuration.html +12 -2
- package/dist/docs-site/openai-endpoints.html +12 -0
- package/dist/scripts/test-browser.js +13 -2
- package/dist/src/browser/actions/assistantResponse.js +81 -50
- package/dist/src/browser/actions/attachments.js +31 -5
- package/dist/src/browser/actions/deepResearch.js +218 -73
- package/dist/src/browser/actions/modelSelection.js +30 -7
- package/dist/src/browser/actions/promptComposer.js +75 -19
- package/dist/src/browser/actions/thinkingStatus.js +19 -1
- package/dist/src/browser/artifacts.js +191 -6
- package/dist/src/browser/chatgptFiles.js +529 -98
- package/dist/src/browser/chatgptImages.js +3 -4
- package/dist/src/browser/chromeLifecycle.js +1 -0
- package/dist/src/browser/constants.js +6 -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 +103 -94
- 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/browser/sessionRunner.js +9 -3
- package/dist/src/cli/bridge/client.js +4 -1
- package/dist/src/cli/bridge/doctor.js +19 -0
- package/dist/src/cli/runOptions.js +11 -2
- package/dist/src/cli/sessionDisplay.js +6 -1
- package/dist/src/cli/sessionRunner.js +28 -10
- package/dist/src/config.js +3 -0
- package/dist/src/oracle/client.js +2 -0
- package/dist/src/oracle/modelResolver.js +85 -0
- package/dist/src/oracle/multiModelRunner.js +4 -1
- package/dist/src/oracle/oscProgress.js +3 -2
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/remote/client.js +253 -22
- package/dist/src/remote/health.js +27 -0
- package/dist/src/remote/server.js +239 -4
- package/dist/src/remote/types.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- 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 +20 -20
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -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";
|
|
@@ -63,6 +64,11 @@ export const UPLOAD_STATUS_SELECTORS = [
|
|
|
63
64
|
'[aria-live="assertive"]',
|
|
64
65
|
];
|
|
65
66
|
export const STOP_BUTTON_SELECTOR = '[data-testid="stop-button"]';
|
|
67
|
+
export const STOP_BUTTON_SELECTORS = [
|
|
68
|
+
STOP_BUTTON_SELECTOR,
|
|
69
|
+
'[data-testid="composer-stop-button"]',
|
|
70
|
+
'button[aria-label*="stop" i]',
|
|
71
|
+
];
|
|
66
72
|
export const SEND_BUTTON_SELECTORS = [
|
|
67
73
|
'button[data-testid="send-button"]',
|
|
68
74
|
'button[data-testid*="composer-send"]',
|
|
@@ -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";
|
|
@@ -558,7 +560,9 @@ export async function runBrowserMode(options) {
|
|
|
558
560
|
let lastTargetId;
|
|
559
561
|
let lastUrl;
|
|
560
562
|
let promptSubmitted = false;
|
|
563
|
+
let modelSelectionEvidence;
|
|
561
564
|
let tabLease = null;
|
|
565
|
+
let conversationUrlMonitor = null;
|
|
562
566
|
const emitRuntimeHint = async () => {
|
|
563
567
|
if (!chrome?.port) {
|
|
564
568
|
return;
|
|
@@ -576,7 +580,7 @@ export async function runBrowserMode(options) {
|
|
|
576
580
|
controllerPid: process.pid,
|
|
577
581
|
};
|
|
578
582
|
try {
|
|
579
|
-
await runtimeHintCb?.(hint);
|
|
583
|
+
await runtimeHintCb?.(hint, modelSelectionEvidence);
|
|
580
584
|
await tabLease?.update({
|
|
581
585
|
chromeHost,
|
|
582
586
|
chromePort: chrome.port,
|
|
@@ -595,6 +599,7 @@ export async function runBrowserMode(options) {
|
|
|
595
599
|
}
|
|
596
600
|
promptSubmitted = true;
|
|
597
601
|
await emitRuntimeHint();
|
|
602
|
+
void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
|
|
598
603
|
};
|
|
599
604
|
if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") {
|
|
600
605
|
logger(`[browser-mode] config: ${JSON.stringify({
|
|
@@ -722,7 +727,6 @@ export async function runBrowserMode(options) {
|
|
|
722
727
|
let answerMarkdown = "";
|
|
723
728
|
let answerHtml = "";
|
|
724
729
|
let runStatus = "attempted";
|
|
725
|
-
let modelSelectionEvidence;
|
|
726
730
|
let connectionClosedUnexpectedly = false;
|
|
727
731
|
let stopThinkingMonitor = null;
|
|
728
732
|
let removeDialogHandler = null;
|
|
@@ -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) {
|
|
@@ -1019,19 +1007,6 @@ export async function runBrowserMode(options) {
|
|
|
1019
1007
|
},
|
|
1020
1008
|
}));
|
|
1021
1009
|
}
|
|
1022
|
-
if (deepResearch) {
|
|
1023
|
-
await raceWithDisconnect(withRetries(() => activateDeepResearch(Runtime, Input, logger), {
|
|
1024
|
-
retries: 2,
|
|
1025
|
-
delayMs: 500,
|
|
1026
|
-
onRetry: (attempt, error) => {
|
|
1027
|
-
if (options.verbose) {
|
|
1028
|
-
logger(`[retry] Deep Research activation attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
1029
|
-
}
|
|
1030
|
-
},
|
|
1031
|
-
}));
|
|
1032
|
-
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
1033
|
-
logger(`Prompt textarea ready (after Deep Research activation, ${promptText.length.toLocaleString()} chars queued)`);
|
|
1034
|
-
}
|
|
1035
1010
|
const profileLockTimeoutMs = manualLogin ? (config.profileLockTimeoutMs ?? 0) : 0;
|
|
1036
1011
|
let profileLock = null;
|
|
1037
1012
|
const acquireProfileLockIfNeeded = async () => {
|
|
@@ -1082,6 +1057,19 @@ export async function runBrowserMode(options) {
|
|
|
1082
1057
|
await waitForAttachmentCompletion(Runtime, attachmentWaitBudget, attachmentNames, logger);
|
|
1083
1058
|
logger("All attachments uploaded");
|
|
1084
1059
|
}
|
|
1060
|
+
if (deepResearch) {
|
|
1061
|
+
await raceWithDisconnect(withRetries(() => activateDeepResearch(Runtime, Input, logger), {
|
|
1062
|
+
retries: 2,
|
|
1063
|
+
delayMs: 500,
|
|
1064
|
+
onRetry: (attempt, error) => {
|
|
1065
|
+
if (options.verbose) {
|
|
1066
|
+
logger(`[retry] Deep Research activation attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
}));
|
|
1070
|
+
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
1071
|
+
logger(`Prompt textarea ready (after Deep Research activation, ${prompt.length.toLocaleString()} chars queued)`);
|
|
1072
|
+
}
|
|
1085
1073
|
let baselineTurns = await readConversationTurnCount(Runtime, logger);
|
|
1086
1074
|
// Learned: return baselineTurns so assistant polling can ignore earlier content.
|
|
1087
1075
|
const providerState = {
|
|
@@ -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();
|
|
@@ -2083,8 +2070,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2083
2070
|
let tabLease = null;
|
|
2084
2071
|
let lastUrl;
|
|
2085
2072
|
let promptSubmitted = false;
|
|
2073
|
+
let modelSelectionEvidence;
|
|
2086
2074
|
let attachedExistingTab = false;
|
|
2087
2075
|
let ownsTarget = true;
|
|
2076
|
+
let conversationUrlMonitor = null;
|
|
2088
2077
|
const runtimeHintCb = options.runtimeHintCb;
|
|
2089
2078
|
const emitRuntimeHint = async () => {
|
|
2090
2079
|
if (!runtimeHintCb)
|
|
@@ -2100,7 +2089,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2100
2089
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2101
2090
|
promptSubmitted,
|
|
2102
2091
|
controllerPid: process.pid,
|
|
2103
|
-
});
|
|
2092
|
+
}, modelSelectionEvidence);
|
|
2104
2093
|
await tabLease?.update({
|
|
2105
2094
|
chromeHost: host,
|
|
2106
2095
|
chromePort: port,
|
|
@@ -2119,6 +2108,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2119
2108
|
}
|
|
2120
2109
|
promptSubmitted = true;
|
|
2121
2110
|
await emitRuntimeHint();
|
|
2111
|
+
void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
|
|
2122
2112
|
};
|
|
2123
2113
|
const startedAt = Date.now();
|
|
2124
2114
|
let answerText = "";
|
|
@@ -2126,7 +2116,6 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2126
2116
|
let answerHtml = "";
|
|
2127
2117
|
let connectionClosedUnexpectedly = false;
|
|
2128
2118
|
let runStatus = "attempted";
|
|
2129
|
-
let modelSelectionEvidence;
|
|
2130
2119
|
let stopThinkingMonitor = null;
|
|
2131
2120
|
let removeDialogHandler = null;
|
|
2132
2121
|
let connection = null;
|
|
@@ -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
|
}
|
|
@@ -2253,19 +2271,6 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2253
2271
|
},
|
|
2254
2272
|
});
|
|
2255
2273
|
}
|
|
2256
|
-
if (deepResearch) {
|
|
2257
|
-
await withRetries(() => activateDeepResearch(Runtime, Input, logger), {
|
|
2258
|
-
retries: 2,
|
|
2259
|
-
delayMs: 500,
|
|
2260
|
-
onRetry: (attempt, error) => {
|
|
2261
|
-
if (options.verbose) {
|
|
2262
|
-
logger(`[retry] Deep Research activation attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
2263
|
-
}
|
|
2264
|
-
},
|
|
2265
|
-
});
|
|
2266
|
-
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
2267
|
-
logger(`Prompt textarea ready (after Deep Research activation, ${promptText.length.toLocaleString()} chars queued)`);
|
|
2268
|
-
}
|
|
2269
2274
|
const submitOnce = async (prompt, submissionAttachments) => {
|
|
2270
2275
|
const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
2271
2276
|
const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
|
|
@@ -2295,6 +2300,19 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2295
2300
|
await waitForAttachmentCompletion(Runtime, attachmentWaitBudget, attachmentNames, logger);
|
|
2296
2301
|
logger("All attachments uploaded");
|
|
2297
2302
|
}
|
|
2303
|
+
if (deepResearch) {
|
|
2304
|
+
await withRetries(() => activateDeepResearch(Runtime, Input, logger), {
|
|
2305
|
+
retries: 2,
|
|
2306
|
+
delayMs: 500,
|
|
2307
|
+
onRetry: (attempt, error) => {
|
|
2308
|
+
if (options.verbose) {
|
|
2309
|
+
logger(`[retry] Deep Research activation attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
2310
|
+
}
|
|
2311
|
+
},
|
|
2312
|
+
});
|
|
2313
|
+
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
2314
|
+
logger(`Prompt textarea ready (after Deep Research activation, ${prompt.length.toLocaleString()} chars queued)`);
|
|
2315
|
+
}
|
|
2298
2316
|
let baselineTurns = await readConversationTurnCount(Runtime, logger);
|
|
2299
2317
|
const providerState = {
|
|
2300
2318
|
runtime: Runtime,
|
|
@@ -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,
|