@steipete/oracle 0.16.1 → 0.17.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/README.md +72 -337
- package/dist/bin/oracle-cli.js +185 -56
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +1 -0
- package/dist/docs-site/RELEASING.html +410 -0
- package/dist/docs-site/agents.html +374 -0
- package/dist/docs-site/anthropic.html +368 -0
- package/dist/docs-site/bridge.html +416 -0
- package/dist/docs-site/browser-mode.html +594 -0
- package/dist/docs-site/chromium-forks.html +347 -0
- package/dist/docs-site/cli-reference.html +346 -0
- package/dist/docs-site/configuration.html +462 -0
- package/dist/docs-site/favicon.svg +14 -0
- package/dist/docs-site/followup.html +375 -0
- package/dist/docs-site/gemini.html +383 -0
- package/dist/docs-site/grok.html +325 -0
- package/dist/docs-site/index.html +360 -0
- package/dist/docs-site/install.html +335 -0
- package/dist/docs-site/linux.html +321 -0
- package/dist/docs-site/llms.txt +43 -0
- package/dist/docs-site/manual-tests.html +596 -0
- package/dist/docs-site/mcp.html +391 -0
- package/dist/docs-site/multimodel.html +364 -0
- package/dist/docs-site/mythical-pro-agents.html +360 -0
- package/dist/docs-site/notifier.html +338 -0
- package/dist/docs-site/openai-endpoints.html +410 -0
- package/dist/docs-site/openrouter.html +344 -0
- package/dist/docs-site/quickstart.html +369 -0
- package/dist/docs-site/refactor/ux.html +532 -0
- package/dist/docs-site/sessions.html +389 -0
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +79 -0
- package/dist/docs-site/spec.html +363 -0
- package/dist/docs-site/testing.html +320 -0
- package/dist/docs-site/tui-debug.html +326 -0
- package/dist/docs-site/windows-work.html +324 -0
- package/dist/docs-site/windows.html +320 -0
- package/dist/scripts/test-browser.js +1 -25
- package/dist/src/browser/actions/modelSelection.js +36 -34
- package/dist/src/browser/actions/navigation.js +8 -3
- package/dist/src/browser/actions/thinkingTime.js +96 -45
- package/dist/src/browser/chromeLifecycle.js +8 -37
- package/dist/src/browser/index.js +23 -8
- package/dist/src/browser/modelDisplay.js +67 -0
- package/dist/src/browser/reattach.js +14 -1
- package/dist/src/browser/recoverConversation.js +2 -1
- package/dist/src/browser/sessionRunner.js +9 -10
- package/dist/src/browser/wslHost.js +50 -0
- package/dist/src/cli/browserConfig.js +31 -11
- package/dist/src/cli/detach.js +21 -4
- package/dist/src/cli/dryRun.js +13 -2
- package/dist/src/cli/engine.js +2 -2
- package/dist/src/cli/options.js +5 -1
- package/dist/src/cli/sessionDisplay.js +60 -8
- package/dist/src/cli/sessionLifecycle.js +2 -1
- package/dist/src/cli/sessionRunner.js +110 -60
- package/dist/src/cli/sessionTable.js +5 -1
- package/dist/src/cli/tui/index.js +12 -4
- package/dist/src/duration.js +3 -0
- package/dist/src/gemini-web/client.js +19 -1
- package/dist/src/oracle/modelResolver.js +8 -1
- package/dist/src/oracle/request.js +9 -2
- package/dist/src/oracle/run.js +43 -3
- package/dist/src/oracle/thinkingTime.js +9 -3
- package/dist/src/sessionManager.js +41 -12
- 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 +17 -17
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -55,7 +55,9 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
55
55
|
if (strictProEffort) {
|
|
56
56
|
throw new Error(`${message}; refusing to submit without confirmed Pro Extended.`);
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
// Nothing was clicked, so the tab keeps whatever effort it already had —
|
|
59
|
+
// which is not necessarily ChatGPT's default.
|
|
60
|
+
logger(formatBrowserThinkingLog(`${message}; keeping the effort already selected in ChatGPT.`));
|
|
59
61
|
return;
|
|
60
62
|
}
|
|
61
63
|
default: {
|
|
@@ -72,7 +74,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
72
74
|
/**
|
|
73
75
|
* Best-effort selection of a thinking time level in ChatGPT's composer pill menu.
|
|
74
76
|
* Safe by default: if the pill/menu/option isn't present, we continue without throwing.
|
|
75
|
-
* @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
|
|
77
|
+
* @param level - The thinking time intensity: 'light', 'standard', 'extended', 'extra-high', or 'heavy'
|
|
76
78
|
*/
|
|
77
79
|
export async function ensureThinkingTimeIfAvailable(Runtime, level, logger, desiredModel) {
|
|
78
80
|
try {
|
|
@@ -135,12 +137,13 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
135
137
|
const TARGET_MODEL_KIND = ${targetModelKindLiteral};
|
|
136
138
|
const TARGET_IS_GPT56_MODEL = ${targetIsGpt56ModelLiteral};
|
|
137
139
|
|
|
138
|
-
//
|
|
140
|
+
// Multilingual matchers: English level token + observed German/Chinese variants.
|
|
139
141
|
const LEVEL_TOKENS = {
|
|
140
|
-
light: ['light', 'instant', '轻', '极速'],
|
|
141
|
-
standard: ['standard', 'medium', '标准', '中'],
|
|
142
|
-
extended: ['extended', 'high', '扩展', '深度', '加强', '高'],
|
|
143
|
-
|
|
142
|
+
light: ['light', 'instant', 'sofort', 'leicht', '轻', '极速'],
|
|
143
|
+
standard: ['standard', 'medium', 'mittel', '标准', '中'],
|
|
144
|
+
extended: ['extended', 'high', 'hoch', 'erweitert', '扩展', '深度', '加强', '高'],
|
|
145
|
+
'extra-high': ['extra high', 'sehr hoch', '极高'],
|
|
146
|
+
heavy: ['heavy', 'schwer', '重度', '加重'],
|
|
144
147
|
};
|
|
145
148
|
const targetTokens = LEVEL_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL];
|
|
146
149
|
|
|
@@ -155,19 +158,43 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
155
158
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
156
159
|
// Keep CJK characters so we can match Chinese labels against LEVEL_TOKENS.
|
|
157
160
|
const normalize = (value) => (value || '')
|
|
161
|
+
// Compose first so NFD umlauts fold too, then map them onto ASCII before
|
|
162
|
+
// the strip below would drop them (and split the token in half).
|
|
163
|
+
.normalize('NFC')
|
|
158
164
|
.toLowerCase()
|
|
165
|
+
.replace(/ä/g, 'a')
|
|
166
|
+
.replace(/ö/g, 'o')
|
|
167
|
+
.replace(/ü/g, 'u')
|
|
168
|
+
.replace(/ß/g, 'ss')
|
|
159
169
|
.replace(/[^a-z0-9\\u4e00-\\u9fa5]+/g, ' ')
|
|
160
170
|
.replace(/\\s+/g, ' ')
|
|
161
171
|
.trim();
|
|
162
172
|
const hasToken = (text, token) => normalize(text).split(' ').includes(token);
|
|
163
|
-
|
|
173
|
+
// Whole-word/phrase containment. Latin effort labels are short words that also
|
|
174
|
+
// occur inside unrelated UI text ("Hochladen", "Ermitteln") and inside their own
|
|
175
|
+
// row descriptions ("Hoch – für sehr komplexe Aufgaben"), so plain substring
|
|
176
|
+
// matching misclassifies rows. CJK labels have no word separators, so they keep
|
|
177
|
+
// substring semantics.
|
|
178
|
+
const hasPhrase = (text, phrase) => {
|
|
179
|
+
const haystack = ' ' + normalize(text) + ' ';
|
|
180
|
+
const needle = normalize(phrase);
|
|
181
|
+
if (!needle) return false;
|
|
182
|
+
return /^[a-z0-9 ]+$/.test(needle)
|
|
183
|
+
? haystack.includes(' ' + needle + ' ')
|
|
184
|
+
: haystack.includes(needle);
|
|
185
|
+
};
|
|
186
|
+
// ChatGPT's Pro effort tiers are "Pro Extended"/"Pro Erweitert" per UI language.
|
|
187
|
+
const hasExtendedWord = (text) => hasPhrase(text, 'extended') || hasPhrase(text, 'erweitert');
|
|
188
|
+
const matchesTokens = (text, tokens) => {
|
|
164
189
|
const t = normalize(text);
|
|
165
190
|
if (!t) return false;
|
|
166
|
-
return
|
|
191
|
+
return tokens.some((tok) => {
|
|
167
192
|
const token = normalize(tok);
|
|
168
193
|
if (!token) return false;
|
|
169
|
-
if (token === 'high') return
|
|
170
|
-
if (token === 'extra high') return
|
|
194
|
+
if (token === 'high') return hasPhrase(t, 'high') && !hasPhrase(t, 'extra high');
|
|
195
|
+
if (token === 'extra high') return hasPhrase(t, 'extra high');
|
|
196
|
+
if (token === 'hoch') return hasPhrase(t, 'hoch') && !hasPhrase(t, 'sehr hoch');
|
|
197
|
+
if (token === 'sehr hoch') return hasPhrase(t, 'sehr hoch');
|
|
171
198
|
if (token === '极速') {
|
|
172
199
|
const suffix = t.slice(token.length);
|
|
173
200
|
return t === token || hasToken(t, token) || /^[0-9]/.test(suffix);
|
|
@@ -175,27 +202,15 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
175
202
|
if (['中', '高', '极高'].includes(token)) {
|
|
176
203
|
return t === token || hasToken(t, token);
|
|
177
204
|
}
|
|
205
|
+
if (/^[a-z0-9 ]+$/.test(token)) {
|
|
206
|
+
return hasPhrase(t, token);
|
|
207
|
+
}
|
|
178
208
|
return t === token || hasToken(t, token) || t.includes(token);
|
|
179
209
|
});
|
|
180
210
|
};
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
for (const tokens of Object.values(LEVEL_TOKENS)) {
|
|
185
|
-
for (const rawToken of tokens) {
|
|
186
|
-
const token = normalize(rawToken);
|
|
187
|
-
if (!token) continue;
|
|
188
|
-
if (token.includes(' ')) {
|
|
189
|
-
if (token.split(' ').every((part) => hasToken(normalizedText, part))) return true;
|
|
190
|
-
} else if (/^[a-z0-9]+$/.test(token)) {
|
|
191
|
-
if (hasToken(normalizedText, token)) return true;
|
|
192
|
-
} else if (normalizedText.includes(token)) {
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return false;
|
|
198
|
-
};
|
|
211
|
+
const matchesLevel = (text) => matchesTokens(text, targetTokens);
|
|
212
|
+
const matchesAnyEffortLevel = (text) =>
|
|
213
|
+
Object.values(LEVEL_TOKENS).some((tokens) => matchesTokens(text, tokens));
|
|
199
214
|
const optionIsSelected = (node) => {
|
|
200
215
|
if (!(node instanceof HTMLElement)) return false;
|
|
201
216
|
const ariaChecked = node.getAttribute('aria-checked');
|
|
@@ -347,7 +362,8 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
347
362
|
return true;
|
|
348
363
|
}
|
|
349
364
|
const label = menu?.querySelector?.('.__menu-label, [class*="menu-label"]');
|
|
350
|
-
|
|
365
|
+
// 'intelligen' matches both "Intelligence" and German "Intelligenz".
|
|
366
|
+
return normalize(label?.textContent ?? '').includes('intelligen');
|
|
351
367
|
};
|
|
352
368
|
const failure = (status, extra = {}) => ({
|
|
353
369
|
status,
|
|
@@ -390,6 +406,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
390
406
|
return null;
|
|
391
407
|
}
|
|
392
408
|
}
|
|
409
|
+
// Generic effort-label match for every model/level. GPT-5.6 heavy used to
|
|
410
|
+
// short-circuit to the Pro row before reaching here; it no longer does, so
|
|
411
|
+
// a UI without a matching tier (e.g. German, which has no "heavy") falls
|
|
412
|
+
// through to null and the caller keeps the current selection.
|
|
393
413
|
for (const item of items) {
|
|
394
414
|
const itemText = normalize(
|
|
395
415
|
(item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
|
|
@@ -404,10 +424,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
404
424
|
return item;
|
|
405
425
|
}
|
|
406
426
|
}
|
|
407
|
-
if (TARGET_LEVEL === '
|
|
408
|
-
// Older Chinese layouts used bare 高 for the highest effort.
|
|
409
|
-
// only as a second-pass exact fallback so a current 高 row can
|
|
410
|
-
// win before the primary 极高 row.
|
|
427
|
+
if (TARGET_LEVEL === 'extra-high') {
|
|
428
|
+
// Older Chinese layouts used bare 高 for the highest non-Pro effort.
|
|
429
|
+
// Keep it only as a second-pass exact fallback so a current 高 row can
|
|
430
|
+
// never win before the primary 极高 row.
|
|
411
431
|
for (const item of items) {
|
|
412
432
|
const itemText = normalize(item.textContent ?? '');
|
|
413
433
|
const ariaLabel = normalize(item.getAttribute?.('aria-label') ?? '');
|
|
@@ -416,11 +436,15 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
416
436
|
}
|
|
417
437
|
return null;
|
|
418
438
|
};
|
|
439
|
+
// Menu-shape heuristic only. This reads the whole menu's textContent, where
|
|
440
|
+
// adjacent row labels concatenate without a separator ("Pro StandardPro
|
|
441
|
+
// Extended"), so word-boundary matching does not apply here — substring is
|
|
442
|
+
// deliberate. Row-level classification uses matchesLevel/matchesTokens.
|
|
419
443
|
const countEffortLevels = (menu) => {
|
|
420
444
|
const text = normalize(menu?.textContent ?? '');
|
|
421
445
|
let hits = 0;
|
|
422
446
|
for (const tokens of Object.values(LEVEL_TOKENS)) {
|
|
423
|
-
if (tokens.some((token) => text.includes(
|
|
447
|
+
if (tokens.some((token) => text.includes(normalize(token)))) hits += 1;
|
|
424
448
|
}
|
|
425
449
|
return hits;
|
|
426
450
|
};
|
|
@@ -431,16 +455,22 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
431
455
|
const label = menu.querySelector?.('.__menu-label, [class*="menu-label"]');
|
|
432
456
|
const labelText = normalize(label?.textContent ?? '');
|
|
433
457
|
return (
|
|
434
|
-
labelText.includes('
|
|
458
|
+
labelText.includes('intelligen') ||
|
|
435
459
|
labelText.includes('thinking time') ||
|
|
436
460
|
labelText.includes('thinking effort') ||
|
|
461
|
+
labelText.includes('denkdauer') ||
|
|
462
|
+
labelText.includes('denkzeit') ||
|
|
437
463
|
countEffortLevels(menu) >= 2
|
|
438
464
|
);
|
|
439
465
|
};
|
|
440
466
|
const isProEffortMenu = (menu) => {
|
|
441
467
|
if (!isVisible(menu)) return false;
|
|
442
468
|
const text = normalize(menu?.textContent ?? '');
|
|
443
|
-
|
|
469
|
+
// Aggregate menu text, so plain substring only (see countEffortLevels).
|
|
470
|
+
return (
|
|
471
|
+
text.includes('pro standard') &&
|
|
472
|
+
(text.includes('pro extended') || text.includes('pro erweitert'))
|
|
473
|
+
);
|
|
444
474
|
};
|
|
445
475
|
const controlledMenu = (trigger) => {
|
|
446
476
|
const id = trigger?.getAttribute?.('aria-controls');
|
|
@@ -475,10 +505,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
475
505
|
(node?.textContent ?? '') + ' ' + (node?.getAttribute?.('aria-label') ?? ''),
|
|
476
506
|
);
|
|
477
507
|
if (TARGET_LEVEL === 'standard') {
|
|
478
|
-
return text
|
|
508
|
+
return hasPhrase(text, 'pro') && hasPhrase(text, 'standard');
|
|
479
509
|
}
|
|
480
510
|
if (TARGET_LEVEL === 'extended') {
|
|
481
|
-
return text
|
|
511
|
+
return hasPhrase(text, 'pro') && hasExtendedWord(text);
|
|
482
512
|
}
|
|
483
513
|
return false;
|
|
484
514
|
};
|
|
@@ -503,29 +533,50 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
503
533
|
}
|
|
504
534
|
const label = normalize(button?.textContent ?? '');
|
|
505
535
|
if (TARGET_LEVEL === 'standard') {
|
|
506
|
-
return hasToken(label, 'pro') && !
|
|
536
|
+
return hasToken(label, 'pro') && !hasExtendedWord(label);
|
|
507
537
|
}
|
|
508
538
|
if (TARGET_LEVEL === 'extended') {
|
|
509
|
-
return hasToken(label, 'pro') &&
|
|
539
|
+
return hasToken(label, 'pro') && hasExtendedWord(label);
|
|
510
540
|
}
|
|
511
541
|
return false;
|
|
512
542
|
};
|
|
513
543
|
const currentEffortPillMatchesTarget = (trigger, modelKindOverride = null) => {
|
|
514
544
|
if (currentProEffortPillMatchesTarget(trigger, modelKindOverride)) return true;
|
|
515
545
|
const button = freshComposerTrigger(trigger) || findModelButton();
|
|
546
|
+
const normalizedLabel = normalize(
|
|
547
|
+
(button?.textContent ?? '') + ' ' + (button?.getAttribute?.('aria-label') ?? ''),
|
|
548
|
+
);
|
|
549
|
+
// No 5.6-heavy "a Pro pill counts as heavy" shortcut here: that would also
|
|
550
|
+
// make post-click verification pass on an unchanged Pro pill. selectAndVerify
|
|
551
|
+
// handles the already-on-Pro case explicitly before any click.
|
|
516
552
|
if ((modelKindOverride || TARGET_MODEL_KIND || modelKindFromNode(button)) === 'pro') {
|
|
517
553
|
return false;
|
|
518
554
|
}
|
|
519
|
-
|
|
520
|
-
return matchesLevel(label);
|
|
555
|
+
return matchesLevel(normalizedLabel);
|
|
521
556
|
};
|
|
522
557
|
const selectAndVerify = async (trigger, findOption, modelKindOverride = null) => {
|
|
523
|
-
const option = findOption();
|
|
524
558
|
const triggerModelKind =
|
|
525
559
|
modelKindOverride ||
|
|
526
560
|
TARGET_MODEL_KIND ||
|
|
527
561
|
modelKindFromNode(trigger) ||
|
|
528
562
|
effectiveTargetModelKind();
|
|
563
|
+
const option = findOption();
|
|
564
|
+
if (!option && TARGET_IS_GPT56_MODEL && TARGET_LEVEL === 'heavy') {
|
|
565
|
+
// GPT-5.6 has no "heavy" tier: Pro is the closest thing. Accept a pill that
|
|
566
|
+
// is already on Pro as satisfying the request, but never click Pro to get
|
|
567
|
+
// there, and never let this stand in for post-click verification.
|
|
568
|
+
const pill = freshComposerTrigger(trigger) || findModelButton();
|
|
569
|
+
const pillLabel = normalize(
|
|
570
|
+
(pill?.textContent ?? '') + ' ' + (pill?.getAttribute?.('aria-label') ?? ''),
|
|
571
|
+
);
|
|
572
|
+
if (
|
|
573
|
+
hasToken(pillLabel, 'pro') ||
|
|
574
|
+
currentEffortPillMatchesTarget(trigger, triggerModelKind)
|
|
575
|
+
) {
|
|
576
|
+
closeOpenMenus();
|
|
577
|
+
return { status: 'already-selected', label: trigger.textContent?.trim?.() || null };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
529
580
|
if (!option) return failure('option-not-found', { modelKind: triggerModelKind });
|
|
530
581
|
const label = option.textContent?.trim?.() || null;
|
|
531
582
|
if (optionIsSelected(option)) {
|
|
@@ -821,7 +872,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
821
872
|
const text = normalize(
|
|
822
873
|
(node?.textContent ?? '') + ' ' + (node?.getAttribute?.('aria-label') ?? ''),
|
|
823
874
|
);
|
|
824
|
-
return text
|
|
875
|
+
return hasPhrase(text, 'pro') && hasExtendedWord(text);
|
|
825
876
|
};
|
|
826
877
|
const findProExtendedOption = () => {
|
|
827
878
|
const menu = document.querySelector(INTELLIGENCE_MENU_SELECTOR);
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
2
|
import net from "node:net";
|
|
5
3
|
import CDP from "chrome-remote-interface";
|
|
6
4
|
import { launch, Launcher } from "chrome-launcher";
|
|
7
5
|
import { cleanupStaleProfileState } from "./profileState.js";
|
|
8
6
|
import { delay } from "./utils.js";
|
|
7
|
+
import { isWsl, resolveWslChromeLaunchRoute } from "./wslHost.js";
|
|
9
8
|
export async function launchChrome(config, userDataDir, logger) {
|
|
10
|
-
const connectHost =
|
|
11
|
-
const debugBindAddress = connectHost && connectHost !== "127.0.0.1" ? "0.0.0.0" : connectHost;
|
|
9
|
+
const { connectHost, debugBindAddress, usePatchedLauncher } = resolveWslChromeLaunchRoute();
|
|
12
10
|
const debugPort = config.debugPort ?? parseDebugPortEnv();
|
|
13
11
|
const chromeFlags = buildChromeFlags(config.headless ?? false, debugBindAddress, config.hideWindow ?? false);
|
|
14
|
-
const usePatchedLauncher = Boolean(connectHost && connectHost !== "127.0.0.1");
|
|
15
12
|
// copy-profile reuses a copied signed-in profile whose cookies are
|
|
16
13
|
// Keychain-encrypted, so it must launch with the real Keychain (not mocked):
|
|
17
14
|
// strip the keychain-mocking flags from both chrome-launcher's defaults and
|
|
@@ -545,6 +542,12 @@ function buildChromeFlags(headless, debugBindAddress, hideWindow = false) {
|
|
|
545
542
|
"--disable-features=TranslateUI,AutomationControlled",
|
|
546
543
|
"--mute-audio",
|
|
547
544
|
"--window-size=1280,720",
|
|
545
|
+
// Chrome that *we* launch is pinned to English, so ChatGPT renders the labels
|
|
546
|
+
// our selectors were written against. This does not make English the only case
|
|
547
|
+
// to handle: --browser-attach-running and --remote-chrome never build these
|
|
548
|
+
// flags (see controlPlan.ts), so those runs inherit the user's own Chrome
|
|
549
|
+
// locale, and a ChatGPT account language setting can localize the UI even here.
|
|
550
|
+
// That is why the model/effort matchers must stay language-tolerant.
|
|
548
551
|
"--lang=en-US",
|
|
549
552
|
"--accept-lang=en-US,en",
|
|
550
553
|
];
|
|
@@ -595,38 +598,6 @@ function parseDebugPortEnv() {
|
|
|
595
598
|
}
|
|
596
599
|
return value;
|
|
597
600
|
}
|
|
598
|
-
function resolveRemoteDebugHost() {
|
|
599
|
-
const override = process.env.ORACLE_BROWSER_REMOTE_DEBUG_HOST?.trim() || process.env.WSL_HOST_IP?.trim();
|
|
600
|
-
if (override) {
|
|
601
|
-
return override;
|
|
602
|
-
}
|
|
603
|
-
if (!isWsl()) {
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
try {
|
|
607
|
-
const resolv = readFileSync("/etc/resolv.conf", "utf8");
|
|
608
|
-
for (const line of resolv.split("\n")) {
|
|
609
|
-
const match = line.match(/^nameserver\s+([0-9.]+)/);
|
|
610
|
-
if (match?.[1]) {
|
|
611
|
-
return match[1];
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
catch {
|
|
616
|
-
// ignore; fall back to localhost
|
|
617
|
-
}
|
|
618
|
-
return null;
|
|
619
|
-
}
|
|
620
|
-
function isWsl() {
|
|
621
|
-
if (process.platform !== "linux") {
|
|
622
|
-
return false;
|
|
623
|
-
}
|
|
624
|
-
if (process.env.WSL_DISTRO_NAME) {
|
|
625
|
-
return true;
|
|
626
|
-
}
|
|
627
|
-
const release = os.release();
|
|
628
|
-
return release.toLowerCase().includes("microsoft");
|
|
629
|
-
}
|
|
630
601
|
async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, ignoreDefaultFlags, }) {
|
|
631
602
|
const launcher = new Launcher({
|
|
632
603
|
chromePath: chromePath ?? undefined,
|
|
@@ -71,6 +71,9 @@ function classifyPreservedBrowserError(error, headless) {
|
|
|
71
71
|
function shouldPreserveBrowserOnError(error, headless) {
|
|
72
72
|
return classifyPreservedBrowserError(error, headless) !== null;
|
|
73
73
|
}
|
|
74
|
+
function normalizeAuthenticatedModelSelectionError(error) {
|
|
75
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
76
|
+
}
|
|
74
77
|
function shouldKeepLocalBrowserOpen(options) {
|
|
75
78
|
if (options.usingCopiedProfile)
|
|
76
79
|
return false;
|
|
@@ -1054,11 +1057,9 @@ export async function runBrowserMode(options) {
|
|
|
1054
1057
|
}
|
|
1055
1058
|
},
|
|
1056
1059
|
})).catch((error) => {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
: "";
|
|
1061
|
-
throw new Error(`${base}${hint}`);
|
|
1060
|
+
// Login has already been verified above. Preserve the picker failure instead of
|
|
1061
|
+
// misdiagnosing an unavailable model as missing cookies.
|
|
1062
|
+
throw normalizeAuthenticatedModelSelectionError(error);
|
|
1062
1063
|
});
|
|
1063
1064
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
1064
1065
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
@@ -1342,7 +1343,11 @@ export async function runBrowserMode(options) {
|
|
|
1342
1343
|
if (conversationUrl && isConversationUrl(conversationUrl)) {
|
|
1343
1344
|
logger(`[browser] Rechecking assistant response at ${conversationUrl}`);
|
|
1344
1345
|
await raceWithDisconnect(Page.navigate({ url: conversationUrl }));
|
|
1345
|
-
await raceWithDisconnect(
|
|
1346
|
+
await raceWithDisconnect(waitForResumedConversationHydration(Runtime, recheckTimeoutMs || 30_000, logger, {
|
|
1347
|
+
requirePriorTurns: true,
|
|
1348
|
+
requirePromptReady: false,
|
|
1349
|
+
expectedConversationUrl: conversationUrl,
|
|
1350
|
+
}));
|
|
1346
1351
|
}
|
|
1347
1352
|
// Validate session before attempting recheck - sessions can expire during the delay
|
|
1348
1353
|
const sessionValid = await validateChatGPTSession(Runtime, logger);
|
|
@@ -2607,7 +2612,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2607
2612
|
lastUrl = conversationUrl;
|
|
2608
2613
|
logger(`[browser] Rechecking assistant response at ${conversationUrl}`);
|
|
2609
2614
|
await Page.navigate({ url: conversationUrl });
|
|
2610
|
-
await
|
|
2615
|
+
await waitForResumedConversationHydration(Runtime, recheckTimeoutMs || 30_000, logger, {
|
|
2616
|
+
requirePriorTurns: true,
|
|
2617
|
+
requirePromptReady: false,
|
|
2618
|
+
expectedConversationUrl: conversationUrl,
|
|
2619
|
+
});
|
|
2611
2620
|
}
|
|
2612
2621
|
// Validate session before attempting recheck - sessions can expire during the delay
|
|
2613
2622
|
const sessionValid = await validateChatGPTSession(Runtime, logger);
|
|
@@ -3051,10 +3060,12 @@ export const __test__ = {
|
|
|
3051
3060
|
isManualLoginProfileInitialized,
|
|
3052
3061
|
isImageOnlyUiChromeText,
|
|
3053
3062
|
listIgnoredRemoteChromeFlags,
|
|
3063
|
+
normalizeAuthenticatedModelSelectionError,
|
|
3054
3064
|
resolveManualLoginWaitMs,
|
|
3055
3065
|
shouldCleanupBlankTabsAfterLastLease,
|
|
3056
3066
|
shouldCloseOwnedRunTargetAfterRun,
|
|
3057
3067
|
shouldKeepLocalBrowserOpen,
|
|
3068
|
+
waitForAssistantResponseWithReload,
|
|
3058
3069
|
};
|
|
3059
3070
|
export { syncCookies } from "./cookies.js";
|
|
3060
3071
|
export { navigateToChatGPT, ensureNotBlocked, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, } from "./pageActions.js";
|
|
@@ -3086,7 +3097,11 @@ async function waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logg
|
|
|
3086
3097
|
}
|
|
3087
3098
|
logger("Assistant response stalled; reloading conversation and retrying once");
|
|
3088
3099
|
await Page.navigate({ url: conversationUrl });
|
|
3089
|
-
await
|
|
3100
|
+
await waitForResumedConversationHydration(Runtime, timeoutMs, logger, {
|
|
3101
|
+
requirePriorTurns: true,
|
|
3102
|
+
requirePromptReady: false,
|
|
3103
|
+
expectedConversationUrl: conversationUrl,
|
|
3104
|
+
});
|
|
3090
3105
|
return await waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId);
|
|
3091
3106
|
}
|
|
3092
3107
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function cleanLabel(value) {
|
|
2
|
+
const label = value?.trim();
|
|
3
|
+
return label ? label : null;
|
|
4
|
+
}
|
|
5
|
+
function sameLabel(left, right) {
|
|
6
|
+
return left.localeCompare(right, undefined, { sensitivity: "accent" }) === 0;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Describe what a browser run will try to select without presenting the target as observed fact.
|
|
10
|
+
*/
|
|
11
|
+
export function formatBrowserModelTarget({ model, desiredModel, modelStrategy, }) {
|
|
12
|
+
const requested = cleanLabel(model) ?? "n/a";
|
|
13
|
+
if (modelStrategy === "current" || modelStrategy === "ignore") {
|
|
14
|
+
return `picker=${modelStrategy}; requested=${requested}`;
|
|
15
|
+
}
|
|
16
|
+
const target = cleanLabel(desiredModel);
|
|
17
|
+
if (!target) {
|
|
18
|
+
return requested;
|
|
19
|
+
}
|
|
20
|
+
return `target=${target}; requested=${requested}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Prefer picker evidence only when Oracle verified it. Otherwise retain the requested CLI key.
|
|
24
|
+
* In particular, a bare `Pro` picker label must not be expanded to a server-side model version.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveBrowserModelDisplayName({ model, evidence, }) {
|
|
27
|
+
const verifiedLabel = evidence?.verified ? cleanLabel(evidence.resolvedLabel) : null;
|
|
28
|
+
return verifiedLabel ?? cleanLabel(model) ?? "n/a";
|
|
29
|
+
}
|
|
30
|
+
export function formatBrowserModelWithRequestedKey(input) {
|
|
31
|
+
const displayName = resolveBrowserModelDisplayName(input);
|
|
32
|
+
const requested = cleanLabel(input.model);
|
|
33
|
+
if (!requested || sameLabel(displayName, requested)) {
|
|
34
|
+
return displayName;
|
|
35
|
+
}
|
|
36
|
+
return `${displayName} (requested ${requested})`;
|
|
37
|
+
}
|
|
38
|
+
export function resolveSessionBrowserModelDisplayName(metadata, model = metadata.model) {
|
|
39
|
+
const sessionModel = cleanLabel(metadata.model);
|
|
40
|
+
const requestedModel = cleanLabel(model);
|
|
41
|
+
const evidenceApplies = requestedModel === null
|
|
42
|
+
? sessionModel === null
|
|
43
|
+
: sessionModel !== null && sameLabel(requestedModel, sessionModel);
|
|
44
|
+
return resolveBrowserModelDisplayName({
|
|
45
|
+
model,
|
|
46
|
+
evidence: evidenceApplies ? metadata.browser?.modelSelection : undefined,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function formatSessionBrowserModelWithRequestedKey(metadata, model = metadata.model) {
|
|
50
|
+
const sessionModel = cleanLabel(metadata.model);
|
|
51
|
+
const requestedModel = cleanLabel(model);
|
|
52
|
+
const evidenceApplies = requestedModel === null
|
|
53
|
+
? sessionModel === null
|
|
54
|
+
: sessionModel !== null && sameLabel(requestedModel, sessionModel);
|
|
55
|
+
return formatBrowserModelWithRequestedKey({
|
|
56
|
+
model,
|
|
57
|
+
evidence: evidenceApplies ? metadata.browser?.modelSelection : undefined,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function formatBrowserModelSelectionEvidence(evidence, model) {
|
|
61
|
+
const requestedKey = cleanLabel(model) ?? "(none)";
|
|
62
|
+
const target = cleanLabel(evidence.requestedModel) ?? "(none)";
|
|
63
|
+
const resolvedLabel = cleanLabel(evidence.resolvedLabel) ?? "(unavailable)";
|
|
64
|
+
const strategy = evidence.strategy ?? "(default)";
|
|
65
|
+
const verified = evidence.verified ? "yes" : "no";
|
|
66
|
+
return `requestedKey=${requestedKey}; target=${target}; resolvedLabel=${resolvedLabel}; status=${evidence.status}; strategy=${strategy}; verified=${verified}; source=${evidence.source}; capturedAt=${evidence.capturedAt}`;
|
|
67
|
+
}
|
|
@@ -2,7 +2,7 @@ import CDP from "chrome-remote-interface";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
|
5
|
-
import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, } from "./pageActions.js";
|
|
5
|
+
import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, waitForResumedConversationHydration, } from "./pageActions.js";
|
|
6
6
|
import { launchChrome, connectToChrome, positionChromeWindowOffscreen, connectToRemoteChromeTarget, listRemoteChromeTargets, } from "./chromeLifecycle.js";
|
|
7
7
|
import { resolveBrowserConfig } from "./config.js";
|
|
8
8
|
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
|
|
@@ -98,6 +98,13 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
98
98
|
const pingTimeoutMs = Math.min(5_000, Math.max(1_500, Math.floor(timeoutMs * 0.05)));
|
|
99
99
|
await withTimeout(Runtime.evaluate({ expression: "1+1", returnByValue: true }), pingTimeoutMs, "Reattach target did not respond");
|
|
100
100
|
await ensureConversationOpen();
|
|
101
|
+
const waitForHydration = deps.waitForConversationHydration ?? waitForResumedConversationHydration;
|
|
102
|
+
const expectedConversationUrl = buildConversationUrl(runtime, resolveBrowserConfig(config ?? {}).url);
|
|
103
|
+
await waitForHydration(Runtime, timeoutMs, logger, {
|
|
104
|
+
requirePriorTurns: true,
|
|
105
|
+
requirePromptReady: false,
|
|
106
|
+
expectedConversationUrl: expectedConversationUrl ?? undefined,
|
|
107
|
+
});
|
|
101
108
|
const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
|
|
102
109
|
(deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
|
|
103
110
|
if (config?.researchMode === "deep") {
|
|
@@ -226,6 +233,12 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
226
233
|
}
|
|
227
234
|
await waitForLocationChange(Runtime, 15_000);
|
|
228
235
|
}
|
|
236
|
+
const waitForHydration = deps.waitForConversationHydration ?? waitForResumedConversationHydration;
|
|
237
|
+
await waitForHydration(Runtime, resolved.inputTimeoutMs, logger, {
|
|
238
|
+
requirePriorTurns: true,
|
|
239
|
+
requirePromptReady: false,
|
|
240
|
+
expectedConversationUrl: conversationUrl ?? undefined,
|
|
241
|
+
});
|
|
229
242
|
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
230
243
|
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
231
244
|
const timeoutMs = resolved.timeoutMs ?? 120_000;
|
|
@@ -67,7 +67,8 @@ export function isRecoveredConversationHarvestReady(harvested) {
|
|
|
67
67
|
(typeof harvested.lastAssistantTurnIndex === "number" &&
|
|
68
68
|
typeof harvested.lastUserTurnIndex === "number" &&
|
|
69
69
|
harvested.lastAssistantTurnIndex > harvested.lastUserTurnIndex);
|
|
70
|
-
|
|
70
|
+
const hasHydratedUserTurn = typeof harvested.lastUserTurnIndex === "number" && harvested.lastUserTurnIndex >= 0;
|
|
71
|
+
return ((harvested.stopExists === true && hasHydratedUserTurn) ||
|
|
71
72
|
((harvested.assistantCount ?? 0) > 0 &&
|
|
72
73
|
assistantFollowsLatestUser &&
|
|
73
74
|
latestAssistant.trim().length > 0 &&
|
|
@@ -5,6 +5,7 @@ import { runBrowserMode } from "../browserMode.js";
|
|
|
5
5
|
import { assembleBrowserPrompt } from "./prompt.js";
|
|
6
6
|
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
7
7
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
|
|
8
|
+
import { formatBrowserModelSelectionEvidence, formatBrowserModelTarget, resolveBrowserModelDisplayName, } from "./modelDisplay.js";
|
|
8
9
|
const LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD = 25_000;
|
|
9
10
|
const LARGE_PRO_FAST_ELAPSED_MS_THRESHOLD = 120_000;
|
|
10
11
|
function buildUnavailableModelSelectionEvidence(browserConfig) {
|
|
@@ -21,13 +22,6 @@ function buildUnavailableModelSelectionEvidence(browserConfig) {
|
|
|
21
22
|
capturedAt: new Date().toISOString(),
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
|
-
function formatModelSelectionEvidence(evidence) {
|
|
25
|
-
const requested = evidence.requestedModel ?? "(none)";
|
|
26
|
-
const resolved = evidence.resolvedLabel ?? "(unavailable)";
|
|
27
|
-
const strategy = evidence.strategy ?? "(default)";
|
|
28
|
-
const verified = evidence.verified ? "yes" : "no";
|
|
29
|
-
return `[browser] Model selection evidence: requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}.`;
|
|
30
|
-
}
|
|
31
25
|
function isRequestedProBrowserRun(runOptions, browserConfig, evidence) {
|
|
32
26
|
const candidates = [
|
|
33
27
|
runOptions.model,
|
|
@@ -87,7 +81,12 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
87
81
|
if (promptArtifacts.bundled) {
|
|
88
82
|
log(chalk.dim(`Packed ${promptArtifacts.bundled.originalCount} files into 1 bundle (contents counted in token estimate).`));
|
|
89
83
|
}
|
|
90
|
-
const
|
|
84
|
+
const launchModel = formatBrowserModelTarget({
|
|
85
|
+
model: runOptions.model,
|
|
86
|
+
desiredModel: browserConfig.desiredModel,
|
|
87
|
+
modelStrategy: browserConfig.modelStrategy,
|
|
88
|
+
});
|
|
89
|
+
const headerLine = `Launching browser mode (${launchModel}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens.`;
|
|
91
90
|
const automationLogger = ((message) => {
|
|
92
91
|
if (typeof message !== "string")
|
|
93
92
|
return;
|
|
@@ -150,7 +149,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
150
149
|
}
|
|
151
150
|
const modelSelection = browserResult.modelSelection ?? buildUnavailableModelSelectionEvidence(browserConfig);
|
|
152
151
|
if (modelSelection) {
|
|
153
|
-
log(
|
|
152
|
+
log(`[browser] Model selection evidence: ${formatBrowserModelSelectionEvidence(modelSelection, runOptions.model)}`);
|
|
154
153
|
}
|
|
155
154
|
const warnings = buildBrowserRunWarnings({
|
|
156
155
|
runOptions,
|
|
@@ -199,7 +198,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
199
198
|
})();
|
|
200
199
|
const { line1, line2 } = formatFinishLine({
|
|
201
200
|
elapsedMs: browserResult.tookMs,
|
|
202
|
-
model: `${runOptions.model}[browser]`,
|
|
201
|
+
model: `${resolveBrowserModelDisplayName({ model: runOptions.model, evidence: modelSelection })}[browser]`,
|
|
203
202
|
tokensPart,
|
|
204
203
|
detailParts: [
|
|
205
204
|
runOptions.file && runOptions.file.length > 0 ? `files=${runOptions.file.length}` : null,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
export function isWsl() {
|
|
4
|
+
if (process.platform !== "linux")
|
|
5
|
+
return false;
|
|
6
|
+
if (process.env.WSL_DISTRO_NAME)
|
|
7
|
+
return true;
|
|
8
|
+
return os.release().toLowerCase().includes("microsoft");
|
|
9
|
+
}
|
|
10
|
+
export function parseWslResolverHost(resolvConf) {
|
|
11
|
+
for (const line of resolvConf.split("\n")) {
|
|
12
|
+
const match = line.match(/^nameserver\s+([0-9.]+)/);
|
|
13
|
+
if (match?.[1]) {
|
|
14
|
+
return match[1].startsWith("127.") ? "127.0.0.1" : match[1];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export function resolveWslHost() {
|
|
20
|
+
if (!isWsl())
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
return parseWslResolverHost(readFileSync("/etc/resolv.conf", "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function resolveWslChromeHost(options = {}) {
|
|
30
|
+
const remoteDebugHost = options.remoteDebugHost === undefined
|
|
31
|
+
? process.env.ORACLE_BROWSER_REMOTE_DEBUG_HOST
|
|
32
|
+
: options.remoteDebugHost;
|
|
33
|
+
const wslHostIp = options.wslHostIp === undefined ? process.env.WSL_HOST_IP : options.wslHostIp;
|
|
34
|
+
const override = remoteDebugHost?.trim() || wslHostIp?.trim();
|
|
35
|
+
if (override)
|
|
36
|
+
return override;
|
|
37
|
+
if (options.resolvConf !== undefined) {
|
|
38
|
+
return options.resolvConf === null ? null : parseWslResolverHost(options.resolvConf);
|
|
39
|
+
}
|
|
40
|
+
return resolveWslHost();
|
|
41
|
+
}
|
|
42
|
+
export function resolveWslChromeLaunchRoute(options = {}) {
|
|
43
|
+
const connectHost = resolveWslChromeHost(options);
|
|
44
|
+
const usePatchedLauncher = Boolean(connectHost && connectHost !== "127.0.0.1");
|
|
45
|
+
return {
|
|
46
|
+
connectHost,
|
|
47
|
+
debugBindAddress: usePatchedLauncher ? "0.0.0.0" : connectHost,
|
|
48
|
+
usePatchedLauncher,
|
|
49
|
+
};
|
|
50
|
+
}
|