@steipete/oracle 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/oracle-cli.js +72 -29
- package/dist/src/browser/actions/deepResearch.js +168 -44
- package/dist/src/browser/actions/modelSelection.js +78 -1
- package/dist/src/browser/actions/promptComposer.js +3 -0
- package/dist/src/browser/actions/thinkingTime.js +49 -6
- package/dist/src/browser/actions/webSearch.js +96 -0
- package/dist/src/browser/chromeLifecycle.js +28 -16
- package/dist/src/browser/config.js +1 -1
- package/dist/src/browser/index.js +144 -49
- package/dist/src/browser/liveTabs.js +11 -4
- package/dist/src/browser/profileState.js +6 -1
- package/dist/src/browser/promptFingerprint.js +54 -0
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattach.js +178 -109
- package/dist/src/browser/recoveryTarget.js +156 -0
- package/dist/src/browser/sessionRunner.js +33 -8
- package/dist/src/browser/tabLeaseRegistry.js +20 -0
- package/dist/src/browser/targetClaim.js +54 -0
- package/dist/src/cli/browserConfig.js +33 -3
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/browserTabs.js +38 -3
- package/dist/src/cli/detach.js +10 -1
- package/dist/src/cli/detachedSession.js +36 -0
- package/dist/src/cli/options.js +15 -0
- package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
- package/dist/src/cli/runOptions.js +19 -4
- package/dist/src/cli/sessionDisplay.js +15 -8
- package/dist/src/cli/sessionRunner.js +153 -41
- package/dist/src/mcp/tools/consult.js +2 -2
- package/dist/src/mcp/types.js +1 -1
- package/dist/src/oracle/config.js +14 -0
- package/dist/src/oracle/geminiModels.js +1 -0
- package/dist/src/oracle/run.js +27 -4
- package/dist/src/remote/client.js +3 -0
- package/dist/src/remote/server.js +2 -0
- package/dist/src/sessionManager.js +8 -1
- 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 +10 -10
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -13,9 +13,21 @@ import { readDevToolsActivePortInfo } from "./detect.js";
|
|
|
13
13
|
import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from "./reattachHelpers.js";
|
|
14
14
|
import { waitForDeepResearchCompletion } from "./actions/deepResearch.js";
|
|
15
15
|
import { CHROME_COOKIE_SYNC_WARNING, shouldSyncBrowserCookies } from "./policies.js";
|
|
16
|
+
import { BrowserCancellation, withoutBrowserCancellation } from "./cancellation.js";
|
|
17
|
+
import { BrowserRunCancelledError } from "../oracle/errors.js";
|
|
16
18
|
export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
+
const cancellation = new BrowserCancellation(deps.signal, logger);
|
|
20
|
+
try {
|
|
21
|
+
return await cancellation.run(() => resumeBrowserSessionInternal(runtime, config, logger, deps, cancellation));
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
cancellation.dispose();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function resumeBrowserSessionInternal(runtime, config, logger, deps, cancellation) {
|
|
28
|
+
const recoverSession = deps.recoverSession
|
|
29
|
+
? (runtimeMeta, configMeta) => cancellation.call(() => deps.recoverSession(runtimeMeta, configMeta))
|
|
30
|
+
: (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps, cancellation);
|
|
19
31
|
let closeAttachedConnection = null;
|
|
20
32
|
const closeAttached = async () => {
|
|
21
33
|
const close = closeAttachedConnection;
|
|
@@ -39,17 +51,18 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
39
51
|
browserWSEndpoint,
|
|
40
52
|
approvalWaitMs,
|
|
41
53
|
logger,
|
|
54
|
+
signal: deps.signal,
|
|
42
55
|
})));
|
|
43
|
-
const targetList = (await listTargets
|
|
56
|
+
const targetList = (await cancellation.call(listTargets));
|
|
44
57
|
const target = pickTarget(targetList, liveRuntime);
|
|
45
|
-
const connection = browserWSEndpoint && !deps.connect
|
|
46
|
-
?
|
|
58
|
+
const connection = await cancellation.acquire(() => browserWSEndpoint && !deps.connect
|
|
59
|
+
? connectToRemoteChromeTarget(host, port ?? 9222, logger, {
|
|
47
60
|
browserWSEndpoint,
|
|
48
61
|
targetId: target?.targetId ?? target?.id,
|
|
49
62
|
closeTargetOnDispose: false,
|
|
50
63
|
approvalWaitMs,
|
|
51
64
|
})
|
|
52
|
-
:
|
|
65
|
+
: (async () => {
|
|
53
66
|
const client = (await (deps.connect ?? ((options) => CDP(options)))(browserWSEndpoint
|
|
54
67
|
? {
|
|
55
68
|
target: browserWSEndpoint,
|
|
@@ -62,10 +75,23 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
62
75
|
target: target?.targetId ?? target?.id,
|
|
63
76
|
}));
|
|
64
77
|
return { client, close: () => client.close() };
|
|
65
|
-
})();
|
|
78
|
+
})(), (lateConnection) => lateConnection.close());
|
|
66
79
|
closeAttachedConnection = () => connection.close();
|
|
67
|
-
const client = connection.client;
|
|
80
|
+
const client = cancellation.client(connection.client);
|
|
68
81
|
const { Runtime, DOM, Page } = client;
|
|
82
|
+
const captureIdentity = async () => {
|
|
83
|
+
const targetId = target?.targetId ?? target?.id;
|
|
84
|
+
if (!targetId || !port)
|
|
85
|
+
return undefined;
|
|
86
|
+
const { result } = await withTimeout(Runtime.evaluate({ expression: "location.href", returnByValue: true }), 2_000, "Recovery target identity unavailable");
|
|
87
|
+
return {
|
|
88
|
+
host,
|
|
89
|
+
port,
|
|
90
|
+
targetId,
|
|
91
|
+
browserWSEndpoint,
|
|
92
|
+
conversationId: extractConversationIdFromUrl(typeof result?.value === "string" ? result.value : ""),
|
|
93
|
+
};
|
|
94
|
+
};
|
|
69
95
|
if (Runtime?.enable) {
|
|
70
96
|
await Runtime.enable();
|
|
71
97
|
}
|
|
@@ -105,34 +131,44 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
105
131
|
await ensureConversationOpen();
|
|
106
132
|
const waitForHydration = deps.waitForConversationHydration ?? waitForResumedConversationHydration;
|
|
107
133
|
const expectedConversationUrl = buildConversationUrl(runtime, resolveBrowserConfig(config ?? {}).url);
|
|
108
|
-
await waitForHydration(Runtime, timeoutMs, logger, {
|
|
134
|
+
await cancellation.call(() => waitForHydration(Runtime, timeoutMs, logger, {
|
|
109
135
|
requirePriorTurns: true,
|
|
110
136
|
requirePromptReady: false,
|
|
111
137
|
expectedConversationUrl: expectedConversationUrl ?? undefined,
|
|
112
|
-
});
|
|
138
|
+
}));
|
|
113
139
|
const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
|
|
114
140
|
(deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
|
|
115
141
|
if (config?.researchMode === "deep") {
|
|
116
142
|
const waitForDeepResearch = deps.waitForDeepResearchCompletion ?? waitForDeepResearchCompletion;
|
|
117
|
-
const researchResult = await withTimeout(waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
|
|
143
|
+
const researchResult = await withTimeout(cancellation.call(() => waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
|
|
118
144
|
requireScopedTargetOwner: true,
|
|
119
|
-
}), timeoutMs + 5_000, "Reattach Deep Research response timed out");
|
|
145
|
+
})), timeoutMs + 5_000, "Reattach Deep Research response timed out");
|
|
146
|
+
const captureTarget = await captureIdentity().catch(() => undefined);
|
|
120
147
|
await closeAttached();
|
|
121
148
|
return {
|
|
122
149
|
answerText: researchResult.text,
|
|
123
150
|
answerMarkdown: researchResult.text,
|
|
151
|
+
captureTarget,
|
|
124
152
|
};
|
|
125
153
|
}
|
|
126
154
|
const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
|
|
127
|
-
const answer = await withTimeout(waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined), timeoutMs + 5_000, "Reattach response timed out");
|
|
128
|
-
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
129
|
-
const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000, "Reattach markdown capture timed out")) ?? recovered.text;
|
|
155
|
+
const answer = await withTimeout(cancellation.call(() => waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined)), timeoutMs + 5_000, "Reattach response timed out");
|
|
156
|
+
const recovered = await cancellation.call(() => recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs));
|
|
157
|
+
const markdown = (await withTimeout(cancellation.call(() => captureMarkdown(Runtime, recovered.meta, logger)), 15_000, "Reattach markdown capture timed out")) ?? recovered.text;
|
|
130
158
|
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
159
|
+
const captureTarget = await captureIdentity().catch(() => undefined);
|
|
131
160
|
await closeAttached();
|
|
132
|
-
return {
|
|
161
|
+
return {
|
|
162
|
+
answerText: aligned.answerText,
|
|
163
|
+
answerMarkdown: aligned.answerMarkdown,
|
|
164
|
+
captureTarget,
|
|
165
|
+
};
|
|
133
166
|
}
|
|
134
167
|
catch (error) {
|
|
135
168
|
await closeAttached();
|
|
169
|
+
if (deps.signal?.aborted || error instanceof BrowserRunCancelledError) {
|
|
170
|
+
throw new BrowserRunCancelledError();
|
|
171
|
+
}
|
|
136
172
|
const message = error instanceof Error ? error.message : String(error);
|
|
137
173
|
logger(`Existing Chrome reattach failed (${message}); reopening browser to locate the session.`);
|
|
138
174
|
return recoverSession(runtime, config);
|
|
@@ -172,26 +208,64 @@ function inferPortFromBrowserWSEndpoint(browserWSEndpoint) {
|
|
|
172
208
|
}
|
|
173
209
|
return undefined;
|
|
174
210
|
}
|
|
175
|
-
async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
211
|
+
async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps, cancellation) {
|
|
176
212
|
const resolved = resolveBrowserConfig(config ?? {});
|
|
177
213
|
const manualLogin = Boolean(resolved.manualLogin);
|
|
178
214
|
const userDataDir = manualLogin
|
|
179
215
|
? (resolved.manualLoginProfileDir ?? path.join(os.homedir(), ".oracle", "browser-profile"))
|
|
180
|
-
: await mkdtemp(path.join(os.tmpdir(), "oracle-reattach-"));
|
|
216
|
+
: await (deps.createTemporaryProfile ?? (() => mkdtemp(path.join(os.tmpdir(), "oracle-reattach-"))))();
|
|
181
217
|
if (manualLogin) {
|
|
182
218
|
await mkdir(userDataDir, { recursive: true });
|
|
183
219
|
}
|
|
220
|
+
const cleanupProfile = async () => {
|
|
221
|
+
if (manualLogin) {
|
|
222
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
if (deps.signal?.aborted) {
|
|
229
|
+
if (!manualLogin)
|
|
230
|
+
await cleanupProfile();
|
|
231
|
+
cancellation.check();
|
|
232
|
+
}
|
|
184
233
|
const launch = deps.launchChrome ?? launchChrome;
|
|
185
234
|
const connectToLaunchedChrome = deps.connectToChrome ?? connectToChrome;
|
|
186
|
-
const chrome = await launch(resolved, userDataDir, logger)
|
|
235
|
+
const chrome = await cancellation.acquire(() => launch(resolved, userDataDir, logger), async (lateChrome) => {
|
|
236
|
+
if (!resolved.keepBrowser) {
|
|
237
|
+
try {
|
|
238
|
+
await lateChrome.kill();
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// best-effort cleanup for a browser that finished launching after cancellation
|
|
242
|
+
}
|
|
243
|
+
await cleanupProfile();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
187
246
|
const chromeHost = chrome && typeof chrome === "object" && "host" in chrome && typeof chrome.host === "string"
|
|
188
247
|
? chrome.host
|
|
189
248
|
: "127.0.0.1";
|
|
190
|
-
|
|
249
|
+
let rawClient;
|
|
250
|
+
try {
|
|
251
|
+
rawClient = await cancellation.acquire(() => connectToLaunchedChrome(chrome.port, logger, chromeHost), (lateClient) => lateClient.close());
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (!resolved.keepBrowser) {
|
|
255
|
+
try {
|
|
256
|
+
await withoutBrowserCancellation(() => chrome.kill());
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// best-effort cleanup after a failed or cancelled CDP connection
|
|
260
|
+
}
|
|
261
|
+
await cleanupProfile();
|
|
262
|
+
}
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
191
265
|
const cleanup = async () => {
|
|
192
|
-
if (
|
|
266
|
+
if (rawClient && typeof rawClient.close === "function") {
|
|
193
267
|
try {
|
|
194
|
-
await
|
|
268
|
+
await rawClient.close();
|
|
195
269
|
}
|
|
196
270
|
catch {
|
|
197
271
|
// ignore
|
|
@@ -204,34 +278,30 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
204
278
|
catch {
|
|
205
279
|
// ignore
|
|
206
280
|
}
|
|
207
|
-
|
|
208
|
-
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
212
|
-
}
|
|
281
|
+
await cleanupProfile();
|
|
213
282
|
}
|
|
214
283
|
};
|
|
284
|
+
const client = cancellation.client(rawClient);
|
|
215
285
|
const { Network, Page, Runtime, DOM, Target } = client;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (DOM && typeof DOM.enable === "function") {
|
|
220
|
-
await DOM.enable();
|
|
221
|
-
}
|
|
222
|
-
if (!resolved.headless && resolved.hideWindow) {
|
|
223
|
-
await positionChromeWindowOffscreen(client, userDataDir, logger);
|
|
224
|
-
}
|
|
225
|
-
else if (!resolved.headless) {
|
|
226
|
-
await positionChromeWindowOnscreen(client, userDataDir, logger);
|
|
227
|
-
}
|
|
228
|
-
let appliedCookies = 0;
|
|
229
|
-
if (shouldSyncBrowserCookies(resolved, { manualLogin })) {
|
|
230
|
-
if (!resolved.inlineCookies) {
|
|
231
|
-
logger(CHROME_COOKIE_SYNC_WARNING);
|
|
286
|
+
try {
|
|
287
|
+
if (Runtime?.enable) {
|
|
288
|
+
await Runtime.enable();
|
|
232
289
|
}
|
|
233
|
-
|
|
234
|
-
|
|
290
|
+
if (DOM && typeof DOM.enable === "function") {
|
|
291
|
+
await DOM.enable();
|
|
292
|
+
}
|
|
293
|
+
if (!resolved.headless && resolved.hideWindow) {
|
|
294
|
+
await positionChromeWindowOffscreen(client, userDataDir, logger);
|
|
295
|
+
}
|
|
296
|
+
else if (!resolved.headless) {
|
|
297
|
+
await positionChromeWindowOnscreen(client, userDataDir, logger);
|
|
298
|
+
}
|
|
299
|
+
let appliedCookies = 0;
|
|
300
|
+
if (shouldSyncBrowserCookies(resolved, { manualLogin })) {
|
|
301
|
+
if (!resolved.inlineCookies) {
|
|
302
|
+
logger(CHROME_COOKIE_SYNC_WARNING);
|
|
303
|
+
}
|
|
304
|
+
const sync = deps.syncCookies ?? syncCookies;
|
|
235
305
|
appliedCookies = await sync(Network, resolved.url, resolved.chromeProfile, logger, {
|
|
236
306
|
allowErrors: resolved.allowCookieErrors,
|
|
237
307
|
filterNames: resolved.cookieNames ?? undefined,
|
|
@@ -240,74 +310,73 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
240
310
|
waitMs: resolved.cookieSyncWaitMs ?? 0,
|
|
241
311
|
});
|
|
242
312
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
extractConversationIdFromUrl(runtime.tabUrl ?? ""),
|
|
252
|
-
extractConversationIdFromUrl(resolved.url),
|
|
253
|
-
],
|
|
254
|
-
});
|
|
255
|
-
await navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger);
|
|
256
|
-
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
257
|
-
await ensureLoggedIn(Runtime, logger, { appliedCookies });
|
|
258
|
-
if (resolved.url !== CHATGPT_URL) {
|
|
259
|
-
await navigateToChatGPT(Page, Runtime, resolved.url, logger);
|
|
260
|
-
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
261
|
-
}
|
|
262
|
-
await ensurePromptReady(Runtime, resolved.inputTimeoutMs, logger);
|
|
263
|
-
const conversationUrl = buildConversationUrl(runtime, resolved.url);
|
|
264
|
-
if (conversationUrl) {
|
|
265
|
-
logger(`Reopening conversation at ${conversationUrl}`);
|
|
266
|
-
await navigateToChatGPT(Page, Runtime, conversationUrl, logger);
|
|
313
|
+
await clearStaleChatGptConversationCookies(Network, Target, logger, {
|
|
314
|
+
preserveConversationIds: [
|
|
315
|
+
runtime.conversationId,
|
|
316
|
+
extractConversationIdFromUrl(runtime.tabUrl ?? ""),
|
|
317
|
+
extractConversationIdFromUrl(resolved.url),
|
|
318
|
+
],
|
|
319
|
+
});
|
|
320
|
+
await navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger);
|
|
267
321
|
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
322
|
+
await ensureLoggedIn(Runtime, logger, { appliedCookies });
|
|
323
|
+
if (resolved.url !== CHATGPT_URL) {
|
|
324
|
+
await navigateToChatGPT(Page, Runtime, resolved.url, logger);
|
|
325
|
+
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
326
|
+
}
|
|
268
327
|
await ensurePromptReady(Runtime, resolved.inputTimeoutMs, logger);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
328
|
+
const conversationUrl = buildConversationUrl(runtime, resolved.url);
|
|
329
|
+
if (conversationUrl) {
|
|
330
|
+
logger(`Reopening conversation at ${conversationUrl}`);
|
|
331
|
+
await navigateToChatGPT(Page, Runtime, conversationUrl, logger);
|
|
332
|
+
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
333
|
+
await ensurePromptReady(Runtime, resolved.inputTimeoutMs, logger);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const opened = await openConversationFromSidebarWithRetry(Runtime, {
|
|
337
|
+
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ""),
|
|
338
|
+
preferProjects: resolved.url !== CHATGPT_URL ||
|
|
339
|
+
Boolean(runtime.tabUrl &&
|
|
340
|
+
(/\/g\//.test(runtime.tabUrl) || runtime.tabUrl.includes("/project"))),
|
|
341
|
+
promptPreview: deps.promptPreview,
|
|
342
|
+
}, 15_000);
|
|
343
|
+
if (!opened) {
|
|
344
|
+
throw new Error("Unable to locate prior ChatGPT conversation in sidebar.");
|
|
345
|
+
}
|
|
346
|
+
await waitForLocationChange(Runtime, 15_000);
|
|
347
|
+
}
|
|
348
|
+
const waitForHydration = deps.waitForConversationHydration ?? waitForResumedConversationHydration;
|
|
349
|
+
await cancellation.call(() => waitForHydration(Runtime, resolved.inputTimeoutMs, logger, {
|
|
350
|
+
requirePriorTurns: true,
|
|
351
|
+
requirePromptReady: false,
|
|
352
|
+
expectedConversationUrl: conversationUrl ?? undefined,
|
|
353
|
+
}));
|
|
354
|
+
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
355
|
+
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
356
|
+
const timeoutMs = resolved.timeoutMs ?? 120_000;
|
|
357
|
+
const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
|
|
358
|
+
(deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
|
|
359
|
+
if (resolved.researchMode === "deep") {
|
|
360
|
+
const waitForDeepResearch = deps.waitForDeepResearchCompletion ?? waitForDeepResearchCompletion;
|
|
361
|
+
const researchResult = await cancellation.call(() => waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
|
|
362
|
+
requireScopedTargetOwner: true,
|
|
363
|
+
}));
|
|
364
|
+
return {
|
|
365
|
+
answerText: researchResult.text,
|
|
366
|
+
answerMarkdown: researchResult.text,
|
|
367
|
+
};
|
|
279
368
|
}
|
|
280
|
-
|
|
369
|
+
const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
|
|
370
|
+
const answer = await cancellation.call(() => waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined));
|
|
371
|
+
const recovered = await cancellation.call(() => recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs));
|
|
372
|
+
const markdown = (await cancellation.call(() => captureMarkdown(Runtime, recovered.meta, logger))) ??
|
|
373
|
+
recovered.text;
|
|
374
|
+
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
375
|
+
return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
|
|
281
376
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
requirePriorTurns: true,
|
|
285
|
-
requirePromptReady: false,
|
|
286
|
-
expectedConversationUrl: conversationUrl ?? undefined,
|
|
287
|
-
});
|
|
288
|
-
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
289
|
-
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
290
|
-
const timeoutMs = resolved.timeoutMs ?? 120_000;
|
|
291
|
-
const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
|
|
292
|
-
(deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
|
|
293
|
-
if (resolved.researchMode === "deep") {
|
|
294
|
-
const waitForDeepResearch = deps.waitForDeepResearchCompletion ?? waitForDeepResearchCompletion;
|
|
295
|
-
const researchResult = await waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
|
|
296
|
-
requireScopedTargetOwner: true,
|
|
297
|
-
});
|
|
298
|
-
await cleanup();
|
|
299
|
-
return {
|
|
300
|
-
answerText: researchResult.text,
|
|
301
|
-
answerMarkdown: researchResult.text,
|
|
302
|
-
};
|
|
377
|
+
finally {
|
|
378
|
+
await withoutBrowserCancellation(cleanup);
|
|
303
379
|
}
|
|
304
|
-
const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
|
|
305
|
-
const answer = await waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined);
|
|
306
|
-
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
307
|
-
const markdown = (await captureMarkdown(Runtime, recovered.meta, logger)) ?? recovered.text;
|
|
308
|
-
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
309
|
-
await cleanup();
|
|
310
|
-
return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
|
|
311
380
|
}
|
|
312
381
|
async function readPromptPreviewTurnIndex(Runtime, promptPreview) {
|
|
313
382
|
const preview = promptPreview?.trim();
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { sessionStore } from "../sessionStore.js";
|
|
3
|
+
import { connectToRemoteChromeTarget } from "./chromeLifecycle.js";
|
|
4
|
+
import { resolveBrowserApprovalWait } from "./config.js";
|
|
5
|
+
import { extractConversationIdFromUrl } from "./reattachHelpers.js";
|
|
6
|
+
import { STOP_BUTTON_SELECTORS } from "./constants.js";
|
|
7
|
+
import { hasActiveBrowserTargetLease } from "./tabLeaseRegistry.js";
|
|
8
|
+
import { buildTargetRetirementExpression, buildTargetRetirementRollbackExpression, normalizeChromeHost, } from "./targetClaim.js";
|
|
9
|
+
function processMayBeAlive(pid) {
|
|
10
|
+
try {
|
|
11
|
+
process.kill(pid, 0);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return error.code !== "ESRCH";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function hasOtherLiveBrowserController(metadata) {
|
|
19
|
+
for (const pid of [metadata.browser?.runtime?.controllerPid, metadata.lifecycle?.workerPid]) {
|
|
20
|
+
if (!pid || pid === process.pid)
|
|
21
|
+
continue;
|
|
22
|
+
if (processMayBeAlive(pid))
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
async function targetHasActiveController(metadata, capture) {
|
|
28
|
+
for (const other of await sessionStore.listSessions()) {
|
|
29
|
+
if (other.id === metadata.id)
|
|
30
|
+
continue;
|
|
31
|
+
const runtime = other.browser?.runtime;
|
|
32
|
+
if (runtime?.chromeTargetId !== capture.targetId ||
|
|
33
|
+
runtime.chromePort !== capture.port ||
|
|
34
|
+
normalizeChromeHost(runtime.chromeHost ?? "127.0.0.1") !== normalizeChromeHost(capture.host))
|
|
35
|
+
continue;
|
|
36
|
+
if ([runtime.controllerPid, other.lifecycle?.workerPid].some((pid) => pid && processMayBeAlive(pid)))
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const runtime = metadata.browser?.runtime;
|
|
40
|
+
const profileDirs = new Set([
|
|
41
|
+
runtime?.userDataDir,
|
|
42
|
+
runtime?.chromeProfileRoot,
|
|
43
|
+
metadata.browser?.config?.manualLoginProfileDir,
|
|
44
|
+
].filter((dir) => Boolean(dir)));
|
|
45
|
+
for (const dir of profileDirs) {
|
|
46
|
+
if (await hasActiveBrowserTargetLease(dir, capture))
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
export function matchesOwnedRecoveryTarget(metadata, capture) {
|
|
52
|
+
const runtime = metadata.browser?.runtime;
|
|
53
|
+
const owned = runtime?.ownedRecoveryTarget;
|
|
54
|
+
const conversationId = runtime?.conversationId ?? extractConversationIdFromUrl(runtime?.tabUrl ?? "");
|
|
55
|
+
return Boolean(owned &&
|
|
56
|
+
capture &&
|
|
57
|
+
conversationId &&
|
|
58
|
+
capture.conversationId === conversationId &&
|
|
59
|
+
owned.targetId === capture.targetId &&
|
|
60
|
+
normalizeChromeHost(owned.host) === normalizeChromeHost(capture.host) &&
|
|
61
|
+
owned.port === capture.port &&
|
|
62
|
+
owned.browserWSEndpoint === capture.browserWSEndpoint);
|
|
63
|
+
}
|
|
64
|
+
export function recoveryCaptureFromRuntime(runtime) {
|
|
65
|
+
const owned = runtime?.ownedRecoveryTarget;
|
|
66
|
+
const conversationId = runtime?.conversationId ?? extractConversationIdFromUrl(runtime?.tabUrl ?? "");
|
|
67
|
+
return owned && conversationId ? { ...owned, conversationId } : undefined;
|
|
68
|
+
}
|
|
69
|
+
/** Call only after the full recovered answer and completed session have been saved. */
|
|
70
|
+
export async function retireRecoveredBrowserTarget(sessionId, capture, logger, deps = {}) {
|
|
71
|
+
await retireOwnedBrowserTarget(sessionId, capture, logger, {
|
|
72
|
+
...deps,
|
|
73
|
+
terminalStatus: "completed",
|
|
74
|
+
requireIdle: true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Call only after cancellation has been persisted for the owning session. */
|
|
78
|
+
export async function retireCancelledBrowserTarget(sessionId, capture, logger, deps = {}) {
|
|
79
|
+
await retireOwnedBrowserTarget(sessionId, capture, logger, {
|
|
80
|
+
...deps,
|
|
81
|
+
terminalStatus: "cancelled",
|
|
82
|
+
requireIdle: false,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function retireOwnedBrowserTarget(sessionId, capture, logger, options) {
|
|
86
|
+
if (!capture)
|
|
87
|
+
return;
|
|
88
|
+
let connection;
|
|
89
|
+
let lockedClaim;
|
|
90
|
+
const reservationId = randomUUID();
|
|
91
|
+
let retired = false;
|
|
92
|
+
try {
|
|
93
|
+
const metadata = await sessionStore.readSession(sessionId);
|
|
94
|
+
if (!metadata ||
|
|
95
|
+
metadata.status !== options.terminalStatus ||
|
|
96
|
+
metadata.browser?.config?.keepBrowser === true ||
|
|
97
|
+
hasOtherLiveBrowserController(metadata) ||
|
|
98
|
+
!matchesOwnedRecoveryTarget(metadata, capture))
|
|
99
|
+
return;
|
|
100
|
+
const claimId = metadata.browser?.runtime?.ownedRecoveryTarget?.claimId;
|
|
101
|
+
if (!claimId)
|
|
102
|
+
return;
|
|
103
|
+
const connect = options.connect ?? connectToRemoteChromeTarget;
|
|
104
|
+
connection = await connect(capture.host, capture.port, logger, {
|
|
105
|
+
targetId: capture.targetId,
|
|
106
|
+
browserWSEndpoint: capture.browserWSEndpoint,
|
|
107
|
+
closeTargetOnDispose: false,
|
|
108
|
+
approvalWaitMs: resolveBrowserApprovalWait(metadata.browser?.config?.approvalWaitMs),
|
|
109
|
+
});
|
|
110
|
+
const { Runtime, Target } = connection.client;
|
|
111
|
+
const state = await Runtime.evaluate({
|
|
112
|
+
expression: `(() => ({ url: location.href, generating: Array.from(document.querySelectorAll(${JSON.stringify(STOP_BUTTON_SELECTORS.join(","))})).some(node => node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0) }))()`,
|
|
113
|
+
returnByValue: true,
|
|
114
|
+
});
|
|
115
|
+
const value = state.result?.value;
|
|
116
|
+
if (!value ||
|
|
117
|
+
(options.requireIdle && value.generating !== false) ||
|
|
118
|
+
extractConversationIdFromUrl(value.url ?? "") !== capture.conversationId)
|
|
119
|
+
return;
|
|
120
|
+
const { targetInfos } = await Target.getTargets();
|
|
121
|
+
if (!targetInfos.some((target) => target.type === "page" && target.targetId !== capture.targetId)) {
|
|
122
|
+
const replacement = await Target.createTarget({ url: "about:blank" });
|
|
123
|
+
if (!replacement.targetId)
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (await targetHasActiveController(metadata, capture))
|
|
127
|
+
return;
|
|
128
|
+
lockedClaim = claimId;
|
|
129
|
+
const locked = await Runtime.evaluate({
|
|
130
|
+
expression: buildTargetRetirementExpression(claimId, capture.conversationId, reservationId, {
|
|
131
|
+
allowGenerating: !options.requireIdle,
|
|
132
|
+
}),
|
|
133
|
+
returnByValue: true,
|
|
134
|
+
});
|
|
135
|
+
if (locked.exceptionDetails || locked.result?.value !== true)
|
|
136
|
+
return;
|
|
137
|
+
const result = await Target.closeTarget({ targetId: capture.targetId });
|
|
138
|
+
if (!result.success)
|
|
139
|
+
throw new Error("Chrome refused target retirement");
|
|
140
|
+
retired = true;
|
|
141
|
+
logger(options.terminalStatus === "completed"
|
|
142
|
+
? "Retired Oracle-owned browser tab after saving the recovered answer."
|
|
143
|
+
: "Retired Oracle-owned browser tab after cancellation.");
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
logger(`${options.terminalStatus === "completed" ? "Recovered answer is saved" : "Cancellation is saved"}; browser tab cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
if (connection && lockedClaim && !retired) {
|
|
150
|
+
await connection.client.Runtime.evaluate({
|
|
151
|
+
expression: buildTargetRetirementRollbackExpression(lockedClaim, reservationId),
|
|
152
|
+
}).catch(() => undefined);
|
|
153
|
+
}
|
|
154
|
+
await connection?.close().catch(() => undefined);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -4,7 +4,7 @@ import { formatFinishLine } from "../oracle/finishLine.js";
|
|
|
4
4
|
import { runBrowserMode } from "../browserMode.js";
|
|
5
5
|
import { DEFAULT_BROWSER_CONFIG } from "./config.js";
|
|
6
6
|
import { assembleBrowserPrompt, cleanupGeneratedBrowserBundles, materializeBrowserFallback, } from "./prompt.js";
|
|
7
|
-
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
7
|
+
import { BrowserAutomationError, BrowserRunCancelledError } from "../oracle/errors.js";
|
|
8
8
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
|
|
9
9
|
import { formatBrowserModelSelectionEvidence, formatBrowserThinkingSelectionEvidence, formatBrowserModelTarget, resolveBrowserModelDisplayName, } from "./modelDisplay.js";
|
|
10
10
|
const LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD = 25_000;
|
|
@@ -55,24 +55,28 @@ function buildBrowserRunWarnings(args) {
|
|
|
55
55
|
},
|
|
56
56
|
];
|
|
57
57
|
}
|
|
58
|
-
export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, deps = {}) {
|
|
58
|
+
export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log, signal }, deps = {}) {
|
|
59
59
|
const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
|
|
60
60
|
const executeBrowser = deps.executeBrowser ?? runBrowserMode;
|
|
61
61
|
const persistRuntimeHint = deps.persistRuntimeHint ?? (() => { });
|
|
62
62
|
const inputTimeoutMs = browserConfig.inputTimeoutMs ?? DEFAULT_BROWSER_CONFIG.inputTimeoutMs;
|
|
63
|
-
let
|
|
63
|
+
let preparationAbandoned = false;
|
|
64
64
|
let preparationTimeout;
|
|
65
|
+
let removePreparationAbortListener;
|
|
65
66
|
let promptArtifacts;
|
|
67
|
+
if (signal?.aborted) {
|
|
68
|
+
throw new BrowserRunCancelledError();
|
|
69
|
+
}
|
|
66
70
|
try {
|
|
67
71
|
promptArtifacts = await Promise.race([
|
|
68
72
|
assemblePrompt(runOptions, { cwd }).then(async (artifacts) => {
|
|
69
|
-
if (
|
|
73
|
+
if (preparationAbandoned)
|
|
70
74
|
await cleanupGeneratedBrowserBundles(artifacts);
|
|
71
75
|
return artifacts;
|
|
72
76
|
}),
|
|
73
77
|
new Promise((_, reject) => {
|
|
74
78
|
preparationTimeout = setTimeout(() => {
|
|
75
|
-
|
|
79
|
+
preparationAbandoned = true;
|
|
76
80
|
reject(new BrowserAutomationError(`Browser prompt preparation timed out after ${inputTimeoutMs}ms; increase --browser-input-timeout if local files need more time.`, {
|
|
77
81
|
stage: "prepare-prompt",
|
|
78
82
|
code: "prompt-preparation-timeout",
|
|
@@ -80,12 +84,27 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
80
84
|
}));
|
|
81
85
|
}, inputTimeoutMs);
|
|
82
86
|
}),
|
|
87
|
+
new Promise((_, reject) => {
|
|
88
|
+
if (!signal)
|
|
89
|
+
return;
|
|
90
|
+
const abort = () => {
|
|
91
|
+
preparationAbandoned = true;
|
|
92
|
+
reject(new BrowserRunCancelledError());
|
|
93
|
+
};
|
|
94
|
+
if (signal.aborted) {
|
|
95
|
+
abort();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
99
|
+
removePreparationAbortListener = () => signal.removeEventListener("abort", abort);
|
|
100
|
+
}),
|
|
83
101
|
]);
|
|
84
102
|
}
|
|
85
103
|
finally {
|
|
86
104
|
if (preparationTimeout) {
|
|
87
105
|
clearTimeout(preparationTimeout);
|
|
88
106
|
}
|
|
107
|
+
removePreparationAbortListener?.();
|
|
89
108
|
}
|
|
90
109
|
try {
|
|
91
110
|
return await executeAssembledBrowserSession({
|
|
@@ -95,13 +114,14 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
95
114
|
promptArtifacts,
|
|
96
115
|
executeBrowser,
|
|
97
116
|
persistRuntimeHint,
|
|
117
|
+
signal,
|
|
98
118
|
});
|
|
99
119
|
}
|
|
100
120
|
finally {
|
|
101
121
|
await cleanupGeneratedBrowserBundles(promptArtifacts);
|
|
102
122
|
}
|
|
103
123
|
}
|
|
104
|
-
async function executeAssembledBrowserSession({ runOptions, browserConfig, log, promptArtifacts, executeBrowser, persistRuntimeHint, }) {
|
|
124
|
+
async function executeAssembledBrowserSession({ runOptions, browserConfig, log, promptArtifacts, executeBrowser, persistRuntimeHint, signal, }) {
|
|
105
125
|
if (runOptions.verbose) {
|
|
106
126
|
log(chalk.dim(`[verbose] Browser config: ${JSON.stringify({
|
|
107
127
|
...browserConfig,
|
|
@@ -135,7 +155,7 @@ async function executeAssembledBrowserSession({ runOptions, browserConfig, log,
|
|
|
135
155
|
if (typeof message !== "string")
|
|
136
156
|
return;
|
|
137
157
|
const shouldAlwaysPrint = message.startsWith("[browser] ") &&
|
|
138
|
-
/archive|fallback|follow-up|retry|thinking|waiting for chatgpt|remote debugging approval|browser slot|browser control|browser guidance|model selection|model picker/i.test(message);
|
|
158
|
+
/archive|fallback|follow-up|retry|thinking|research|waiting for chatgpt|remote debugging approval|browser slot|browser control|browser guidance|model selection|model picker/i.test(message);
|
|
139
159
|
if (!runOptions.verbose && !shouldAlwaysPrint)
|
|
140
160
|
return;
|
|
141
161
|
log(message);
|
|
@@ -180,6 +200,8 @@ async function executeAssembledBrowserSession({ runOptions, browserConfig, log,
|
|
|
180
200
|
generateImagePath: runOptions.generateImage,
|
|
181
201
|
outputPath: runOptions.outputPath,
|
|
182
202
|
followUpPrompts: runOptions.browserFollowUps,
|
|
203
|
+
signal,
|
|
204
|
+
closeOwnedTabOnCancel: !executionBrowserConfig.keepBrowser,
|
|
183
205
|
runtimeHintCb: async (runtime, modelSelection) => {
|
|
184
206
|
const runtimeWithController = {
|
|
185
207
|
...runtime,
|
|
@@ -195,7 +217,7 @@ async function executeAssembledBrowserSession({ runOptions, browserConfig, log,
|
|
|
195
217
|
});
|
|
196
218
|
}
|
|
197
219
|
catch (error) {
|
|
198
|
-
if (error instanceof BrowserAutomationError) {
|
|
220
|
+
if (error instanceof BrowserAutomationError || error instanceof BrowserRunCancelledError) {
|
|
199
221
|
throw error;
|
|
200
222
|
}
|
|
201
223
|
const message = error instanceof Error ? error.message : "Browser automation failed.";
|
|
@@ -278,9 +300,12 @@ async function executeAssembledBrowserSession({ runOptions, browserConfig, log,
|
|
|
278
300
|
chromeProfileRoot: browserResult.chromeProfileRoot,
|
|
279
301
|
userDataDir: browserResult.userDataDir,
|
|
280
302
|
chromeTargetId: browserResult.chromeTargetId,
|
|
303
|
+
ownedRecoveryTarget: browserResult.ownedRecoveryTarget,
|
|
281
304
|
tabUrl: browserResult.tabUrl,
|
|
282
305
|
conversationId: browserResult.conversationId,
|
|
283
306
|
promptSubmitted: browserResult.promptSubmitted,
|
|
307
|
+
submittedPromptHash: browserResult.submittedPromptHash,
|
|
308
|
+
researchPlan: browserResult.researchPlan,
|
|
284
309
|
controllerPid: browserResult.controllerPid ?? process.pid,
|
|
285
310
|
},
|
|
286
311
|
archive: browserResult.archive,
|