@steipete/oracle 0.12.0 → 0.12.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/src/browser/actions/modelSelection.js +26 -10
- package/dist/src/browser/actions/promptComposer.js +1 -0
- package/dist/src/browser/actions/thinkingTime.js +111 -12
- package/dist/src/browser/index.js +37 -2
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattachability.js +22 -0
- package/dist/src/browser/sessionRunner.js +1 -0
- package/dist/src/cli/sessionDisplay.js +8 -0
- package/dist/src/cli/sessionRunner.js +49 -5
- package/dist/src/oracle/providerRoutePlan.js +29 -2
- package/dist/src/oracle/run.js +50 -156
- package/dist/src/sessionManager.js +38 -22
- package/package.json +13 -12
|
@@ -59,7 +59,7 @@ function assertResolvedModelSelection(desiredModel, resolvedLabel) {
|
|
|
59
59
|
}
|
|
60
60
|
if (!hasCurrentProSignal(resolved) ||
|
|
61
61
|
hasLegacyProVersionLabel(resolved) ||
|
|
62
|
-
|
|
62
|
+
resolved.includes("thinking")) {
|
|
63
63
|
throw new Error(`Model picker selected "${resolvedLabel}" while "${desiredModel}" requires GPT-5.5 Pro. Use model "gpt-5.5" with browser thinking time for the Thinking variant.`);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -70,12 +70,7 @@ function normalizeResolvedModelLabel(value) {
|
|
|
70
70
|
.trim();
|
|
71
71
|
}
|
|
72
72
|
function hasCurrentProSignal(resolved) {
|
|
73
|
-
return (resolved.includes("
|
|
74
|
-
resolved.endsWith("pro") ||
|
|
75
|
-
resolved.includes("pro ") ||
|
|
76
|
-
resolved.includes("extended") ||
|
|
77
|
-
resolved.includes("gpt-5.5-pro") ||
|
|
78
|
-
resolved.includes("gpt 5 5 pro"));
|
|
73
|
+
return normalizeResolvedModelLabel(resolved).split(" ").includes("pro");
|
|
79
74
|
}
|
|
80
75
|
function hasLegacyProVersionLabel(resolved) {
|
|
81
76
|
const normalized = normalizeResolvedModelLabel(resolved);
|
|
@@ -128,6 +123,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
128
123
|
.replace(/\\s+/g, ' ')
|
|
129
124
|
.trim();
|
|
130
125
|
};
|
|
126
|
+
const hasToken = (value, token) => normalizeText(value).split(' ').includes(token);
|
|
131
127
|
// Normalize every candidate token to keep fuzzy matching deterministic.
|
|
132
128
|
const normalizedTarget = normalizeText(PRIMARY_LABEL);
|
|
133
129
|
const normalizedTokens = Array.from(new Set([normalizedTarget, ...LABEL_TOKENS]))
|
|
@@ -173,7 +169,17 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
173
169
|
return false;
|
|
174
170
|
};
|
|
175
171
|
const hasProComposerPill = () => Boolean(
|
|
176
|
-
document.
|
|
172
|
+
Array.from(document.querySelectorAll('button.__composer-pill, button[aria-label]'))
|
|
173
|
+
.filter((node) => {
|
|
174
|
+
const label = normalizeText(node.getAttribute?.('aria-label') ?? '');
|
|
175
|
+
return node.matches?.('button.__composer-pill') || label.includes('click to remove');
|
|
176
|
+
})
|
|
177
|
+
.some((node) => {
|
|
178
|
+
const label = normalizeText(
|
|
179
|
+
(node.getAttribute?.('aria-label') ?? '') + ' ' + (node.textContent ?? '')
|
|
180
|
+
);
|
|
181
|
+
return hasToken(label, 'pro') && !hasToken(label, 'thinking');
|
|
182
|
+
})
|
|
177
183
|
);
|
|
178
184
|
|
|
179
185
|
const button = document.querySelector(BUTTON_SELECTOR);
|
|
@@ -209,7 +215,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
209
215
|
const resolved = label || '';
|
|
210
216
|
if (!wantsPro || !hasProComposerPill()) return resolved;
|
|
211
217
|
const normalized = normalizeText(resolved);
|
|
212
|
-
if (!normalized
|
|
218
|
+
if (!normalized) return resolved;
|
|
219
|
+
if (normalized.includes('thinking')) return 'Pro';
|
|
220
|
+
if (normalized.includes('pro')) return resolved;
|
|
213
221
|
return resolved + ' + Pro';
|
|
214
222
|
};
|
|
215
223
|
const getResolvedLabel = (fallback) =>
|
|
@@ -225,7 +233,15 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
225
233
|
const normalizedLabel = normalizeText(getButtonLabel());
|
|
226
234
|
if (!normalizedLabel) return false;
|
|
227
235
|
if (isTargetGpt55VisibleAlias(normalizedLabel)) return true;
|
|
228
|
-
if (
|
|
236
|
+
if (
|
|
237
|
+
wantsPro &&
|
|
238
|
+
hasProComposerPill() &&
|
|
239
|
+
(normalizedLabel === 'chatgpt' ||
|
|
240
|
+
normalizedLabel === 'extended' ||
|
|
241
|
+
normalizedLabel === 'standard' ||
|
|
242
|
+
normalizedLabel === 'heavy' ||
|
|
243
|
+
normalizedLabel === 'light')
|
|
244
|
+
) {
|
|
229
245
|
return true;
|
|
230
246
|
}
|
|
231
247
|
if (desiredVersion) {
|
|
@@ -183,6 +183,7 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
183
183
|
else {
|
|
184
184
|
logger("Clicked send button");
|
|
185
185
|
}
|
|
186
|
+
await deps.onPromptSubmitted?.();
|
|
186
187
|
const commitTimeoutMs = Math.max(60_000, deps.inputTimeoutMs ?? 0);
|
|
187
188
|
// Learned: the send button can succeed but the turn doesn't appear immediately; verify commit via turns/stop button.
|
|
188
189
|
return await verifyPromptCommitted(runtime, prompt, commitTimeoutMs, logger, deps.baselineTurns ?? undefined);
|
|
@@ -11,9 +11,11 @@ import { buildClickDispatcher } from "./domEvents.js";
|
|
|
11
11
|
*
|
|
12
12
|
* @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
|
|
13
13
|
*/
|
|
14
|
-
export async function ensureThinkingTime(Runtime, level, logger) {
|
|
15
|
-
const result = await evaluateThinkingTimeSelection(Runtime, level);
|
|
14
|
+
export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
15
|
+
const result = await evaluateThinkingTimeSelection(Runtime, level, desiredModel);
|
|
16
16
|
const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
|
|
17
|
+
const targetModelKind = inferThinkingTargetModelKind(desiredModel);
|
|
18
|
+
const strictProEffort = targetModelKind === "pro" && level === "extended";
|
|
17
19
|
switch (result?.status) {
|
|
18
20
|
case "already-selected":
|
|
19
21
|
logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
|
|
@@ -23,13 +25,26 @@ export async function ensureThinkingTime(Runtime, level, logger) {
|
|
|
23
25
|
return;
|
|
24
26
|
case "chip-not-found":
|
|
25
27
|
case "menu-not-found":
|
|
26
|
-
case "option-not-found":
|
|
28
|
+
case "option-not-found":
|
|
29
|
+
case "model-kind-not-found": {
|
|
27
30
|
await logDomFailure(Runtime, logger, `thinking-${result.status}`);
|
|
28
|
-
|
|
31
|
+
const kindHint = result.status === "model-kind-not-found" && result.modelKind
|
|
32
|
+
? ` for ${result.modelKind}`
|
|
33
|
+
: targetModelKind
|
|
34
|
+
? ` for ${targetModelKind}`
|
|
35
|
+
: "";
|
|
36
|
+
const message = `Thinking time: ${result.status.replaceAll("-", " ")}${kindHint} (requested ${capitalizedLevel})`;
|
|
37
|
+
if (strictProEffort) {
|
|
38
|
+
throw new Error(`${message}; refusing to submit without confirmed Pro Extended.`);
|
|
39
|
+
}
|
|
40
|
+
logger(`${message}; continuing with ChatGPT default.`);
|
|
29
41
|
return;
|
|
30
42
|
}
|
|
31
43
|
default: {
|
|
32
44
|
await logDomFailure(Runtime, logger, "thinking-time-unknown");
|
|
45
|
+
if (strictProEffort) {
|
|
46
|
+
throw new Error(`Thinking time: unknown outcome selecting ${capitalizedLevel}; refusing to submit without confirmed Pro Extended.`);
|
|
47
|
+
}
|
|
33
48
|
logger(`Thinking time: unknown outcome selecting ${capitalizedLevel}; continuing with ChatGPT default.`);
|
|
34
49
|
return;
|
|
35
50
|
}
|
|
@@ -40,9 +55,9 @@ export async function ensureThinkingTime(Runtime, level, logger) {
|
|
|
40
55
|
* Safe by default: if the pill/menu/option isn't present, we continue without throwing.
|
|
41
56
|
* @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
|
|
42
57
|
*/
|
|
43
|
-
export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
|
|
58
|
+
export async function ensureThinkingTimeIfAvailable(Runtime, level, logger, desiredModel) {
|
|
44
59
|
try {
|
|
45
|
-
const result = await evaluateThinkingTimeSelection(Runtime, level);
|
|
60
|
+
const result = await evaluateThinkingTimeSelection(Runtime, level, desiredModel);
|
|
46
61
|
const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
|
|
47
62
|
switch (result?.status) {
|
|
48
63
|
case "already-selected":
|
|
@@ -54,6 +69,7 @@ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
|
|
|
54
69
|
case "chip-not-found":
|
|
55
70
|
case "menu-not-found":
|
|
56
71
|
case "option-not-found":
|
|
72
|
+
case "model-kind-not-found":
|
|
57
73
|
if (logger.verbose) {
|
|
58
74
|
logger(`Thinking time: ${result.status.replaceAll("-", " ")}; continuing with default.`);
|
|
59
75
|
}
|
|
@@ -74,19 +90,20 @@ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
|
|
|
74
90
|
return false;
|
|
75
91
|
}
|
|
76
92
|
}
|
|
77
|
-
async function evaluateThinkingTimeSelection(Runtime, level) {
|
|
93
|
+
async function evaluateThinkingTimeSelection(Runtime, level, desiredModel) {
|
|
78
94
|
const outcome = await Runtime.evaluate({
|
|
79
|
-
expression: buildThinkingTimeExpression(level),
|
|
95
|
+
expression: buildThinkingTimeExpression(level, desiredModel),
|
|
80
96
|
awaitPromise: true,
|
|
81
97
|
returnByValue: true,
|
|
82
98
|
});
|
|
83
99
|
return outcome.result?.value;
|
|
84
100
|
}
|
|
85
|
-
function buildThinkingTimeExpression(level) {
|
|
101
|
+
function buildThinkingTimeExpression(level, desiredModel) {
|
|
86
102
|
const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
|
|
87
103
|
const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
|
|
88
104
|
const modelButtonLiteral = JSON.stringify(MODEL_BUTTON_SELECTOR);
|
|
89
105
|
const targetLevelLiteral = JSON.stringify(level.toLowerCase());
|
|
106
|
+
const targetModelKindLiteral = JSON.stringify(inferThinkingTargetModelKind(desiredModel));
|
|
90
107
|
return `(async () => {
|
|
91
108
|
${buildClickDispatcher()}
|
|
92
109
|
|
|
@@ -94,6 +111,7 @@ function buildThinkingTimeExpression(level) {
|
|
|
94
111
|
const MENU_ITEM_SELECTOR = ${menuItemLiteral};
|
|
95
112
|
const MODEL_BUTTON_SELECTOR = ${modelButtonLiteral};
|
|
96
113
|
const TARGET_LEVEL = ${targetLevelLiteral};
|
|
114
|
+
const TARGET_MODEL_KIND = ${targetModelKindLiteral};
|
|
97
115
|
|
|
98
116
|
// Bilingual matchers: English level token + observed Chinese variants.
|
|
99
117
|
const LEVEL_TOKENS = {
|
|
@@ -119,6 +137,7 @@ function buildThinkingTimeExpression(level) {
|
|
|
119
137
|
const t = normalize(text);
|
|
120
138
|
return targetTokens.some((tok) => t.includes(String(tok).toLowerCase()));
|
|
121
139
|
};
|
|
140
|
+
const hasToken = (text, token) => normalize(text).split(' ').includes(token);
|
|
122
141
|
const optionIsSelected = (node) => {
|
|
123
142
|
if (!(node instanceof HTMLElement)) return false;
|
|
124
143
|
const ariaChecked = node.getAttribute('aria-checked');
|
|
@@ -207,6 +226,7 @@ function buildThinkingTimeExpression(level) {
|
|
|
207
226
|
|
|
208
227
|
const findModelButton = () => document.querySelector(MODEL_BUTTON_SELECTOR);
|
|
209
228
|
const findTrailingButtons = () => Array.from(document.querySelectorAll(TRAILING_SELECTOR));
|
|
229
|
+
const KIND_NOT_FOUND = { kindNotFound: true };
|
|
210
230
|
const findEffortRow = (node) => {
|
|
211
231
|
let current = node instanceof HTMLElement ? node.parentElement : null;
|
|
212
232
|
while (current && current !== document.body) {
|
|
@@ -227,6 +247,58 @@ function buildThinkingTimeExpression(level) {
|
|
|
227
247
|
),
|
|
228
248
|
);
|
|
229
249
|
};
|
|
250
|
+
const rowForTrailing = (trailing) =>
|
|
251
|
+
trailing.closest('[role="menuitem"], [role="menuitemradio"], [data-radix-collection-item]');
|
|
252
|
+
const rowTextForTrailing = (trailing) => {
|
|
253
|
+
const row = rowForTrailing(trailing) || findEffortRow(trailing);
|
|
254
|
+
return normalize(
|
|
255
|
+
(row?.getAttribute?.('aria-label') ?? '') + ' ' +
|
|
256
|
+
(row?.getAttribute?.('data-testid') ?? '') + ' ' +
|
|
257
|
+
(row?.textContent ?? '') + ' ' +
|
|
258
|
+
(trailing.getAttribute?.('aria-label') ?? '') + ' ' +
|
|
259
|
+
(trailing.getAttribute?.('data-testid') ?? '')
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
const testIdTextForTrailing = (trailing) => {
|
|
263
|
+
const row = rowForTrailing(trailing) || findEffortRow(trailing);
|
|
264
|
+
return normalize(
|
|
265
|
+
(row?.getAttribute?.('data-testid') ?? '') + ' ' +
|
|
266
|
+
(trailing.getAttribute?.('data-testid') ?? '')
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
const modelKindFromTrailing = (trailing) => {
|
|
270
|
+
const idText = testIdTextForTrailing(trailing);
|
|
271
|
+
if (!idText.includes('model switcher')) return null;
|
|
272
|
+
const modelPart = normalize(idText.replace(/\\bthinking effort\\b.*$/, ''));
|
|
273
|
+
if (hasToken(modelPart, 'pro')) return 'pro';
|
|
274
|
+
if (hasToken(modelPart, 'thinking')) return 'thinking';
|
|
275
|
+
if (hasToken(modelPart, 'instant')) return 'instant';
|
|
276
|
+
return null;
|
|
277
|
+
};
|
|
278
|
+
const trailingMatchesTargetModelKind = (trailing) => {
|
|
279
|
+
if (!TARGET_MODEL_KIND) return false;
|
|
280
|
+
const idKind = modelKindFromTrailing(trailing);
|
|
281
|
+
if (idKind) return idKind === TARGET_MODEL_KIND;
|
|
282
|
+
const text = rowTextForTrailing(trailing);
|
|
283
|
+
if (TARGET_MODEL_KIND === 'pro') {
|
|
284
|
+
return hasToken(text, 'pro') && !hasToken(text, 'thinking');
|
|
285
|
+
}
|
|
286
|
+
if (TARGET_MODEL_KIND === 'thinking') {
|
|
287
|
+
return hasToken(text, 'thinking') && !hasToken(text, 'pro');
|
|
288
|
+
}
|
|
289
|
+
if (TARGET_MODEL_KIND === 'instant') {
|
|
290
|
+
return hasToken(text, 'instant') && !hasToken(text, 'thinking') && !hasToken(text, 'pro');
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
};
|
|
294
|
+
const hasStableBox = (node) => {
|
|
295
|
+
const r = node.getBoundingClientRect?.();
|
|
296
|
+
return Boolean(r && r.width > 0 && r.height > 0 && node.getAttribute?.('aria-hidden') !== 'true');
|
|
297
|
+
};
|
|
298
|
+
const pickSingleStableTrailing = (trailings) => {
|
|
299
|
+
const visible = trailings.filter((t) => hasStableBox(t));
|
|
300
|
+
return visible.length === 1 ? visible[0] : null;
|
|
301
|
+
};
|
|
230
302
|
const pickTrailingForCurrentModel = () => {
|
|
231
303
|
const trailings = findTrailingButtons();
|
|
232
304
|
if (trailings.length === 0) return null;
|
|
@@ -236,6 +308,10 @@ function buildThinkingTimeExpression(level) {
|
|
|
236
308
|
const row = findEffortRow(t);
|
|
237
309
|
if (rowIsSelected(row)) return t;
|
|
238
310
|
}
|
|
311
|
+
if (TARGET_MODEL_KIND) {
|
|
312
|
+
const targetTrailings = trailings.filter((t) => trailingMatchesTargetModelKind(t));
|
|
313
|
+
return pickSingleStableTrailing(targetTrailings) || KIND_NOT_FOUND;
|
|
314
|
+
}
|
|
239
315
|
return null;
|
|
240
316
|
};
|
|
241
317
|
|
|
@@ -243,7 +319,6 @@ function buildThinkingTimeExpression(level) {
|
|
|
243
319
|
if (!modelBtn) {
|
|
244
320
|
return { status: 'chip-not-found' };
|
|
245
321
|
}
|
|
246
|
-
|
|
247
322
|
// Open model menu (idempotent — leaves it open if already open).
|
|
248
323
|
if (modelBtn.getAttribute('aria-expanded') !== 'true') {
|
|
249
324
|
dispatchClickSequence(modelBtn);
|
|
@@ -261,6 +336,10 @@ function buildThinkingTimeExpression(level) {
|
|
|
261
336
|
closeOpenMenus();
|
|
262
337
|
return { status: 'chip-not-found' };
|
|
263
338
|
}
|
|
339
|
+
if (trailing.kindNotFound) {
|
|
340
|
+
closeOpenMenus();
|
|
341
|
+
return { status: 'model-kind-not-found', modelKind: TARGET_MODEL_KIND };
|
|
342
|
+
}
|
|
264
343
|
|
|
265
344
|
dispatchClickSequence(trailing);
|
|
266
345
|
await sleep(STEP_WAIT_MS);
|
|
@@ -313,6 +392,26 @@ function buildThinkingTimeExpression(level) {
|
|
|
313
392
|
return { status: already ? 'already-selected' : 'switched', label };
|
|
314
393
|
})()`;
|
|
315
394
|
}
|
|
316
|
-
export function buildThinkingTimeExpressionForTest(level = "extended") {
|
|
317
|
-
return buildThinkingTimeExpression(level);
|
|
395
|
+
export function buildThinkingTimeExpressionForTest(level = "extended", desiredModel) {
|
|
396
|
+
return buildThinkingTimeExpression(level, desiredModel);
|
|
397
|
+
}
|
|
398
|
+
function inferThinkingTargetModelKind(desiredModel) {
|
|
399
|
+
const normalized = (desiredModel ?? "")
|
|
400
|
+
.toLowerCase()
|
|
401
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
402
|
+
.replace(/\s+/g, " ")
|
|
403
|
+
.trim();
|
|
404
|
+
if (!normalized)
|
|
405
|
+
return null;
|
|
406
|
+
const tokens = normalized.split(" ");
|
|
407
|
+
if (tokens.includes("pro"))
|
|
408
|
+
return "pro";
|
|
409
|
+
if (tokens.includes("thinking"))
|
|
410
|
+
return "thinking";
|
|
411
|
+
if (tokens.includes("instant"))
|
|
412
|
+
return "instant";
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
export function inferThinkingTargetModelKindForTest(desiredModel) {
|
|
416
|
+
return inferThinkingTargetModelKind(desiredModel);
|
|
318
417
|
}
|
|
@@ -327,6 +327,7 @@ export async function runBrowserMode(options) {
|
|
|
327
327
|
const runtimeHintCb = options.runtimeHintCb;
|
|
328
328
|
let lastTargetId;
|
|
329
329
|
let lastUrl;
|
|
330
|
+
let promptSubmitted = false;
|
|
330
331
|
let tabLease = null;
|
|
331
332
|
const emitRuntimeHint = async () => {
|
|
332
333
|
if (!chrome?.port) {
|
|
@@ -340,6 +341,7 @@ export async function runBrowserMode(options) {
|
|
|
340
341
|
chromeTargetId: lastTargetId,
|
|
341
342
|
tabUrl: lastUrl,
|
|
342
343
|
conversationId,
|
|
344
|
+
promptSubmitted,
|
|
343
345
|
userDataDir,
|
|
344
346
|
controllerPid: process.pid,
|
|
345
347
|
};
|
|
@@ -357,6 +359,13 @@ export async function runBrowserMode(options) {
|
|
|
357
359
|
logger(`Failed to persist runtime hint: ${message}`);
|
|
358
360
|
}
|
|
359
361
|
};
|
|
362
|
+
const markPromptSubmitted = async () => {
|
|
363
|
+
if (promptSubmitted) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
promptSubmitted = true;
|
|
367
|
+
await emitRuntimeHint();
|
|
368
|
+
};
|
|
360
369
|
if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") {
|
|
361
370
|
logger(`[browser-mode] config: ${JSON.stringify({
|
|
362
371
|
...redactBrowserConfigForDebugLog(config),
|
|
@@ -725,7 +734,8 @@ export async function runBrowserMode(options) {
|
|
|
725
734
|
// Handle thinking time selection if specified. Deep Research owns its own effort flow.
|
|
726
735
|
const thinkingTime = config.thinkingTime;
|
|
727
736
|
if (thinkingTime && !deepResearch) {
|
|
728
|
-
|
|
737
|
+
const thinkingTargetModel = modelStrategy === "select" ? config.desiredModel : null;
|
|
738
|
+
await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger, thinkingTargetModel), {
|
|
729
739
|
retries: 2,
|
|
730
740
|
delayMs: 300,
|
|
731
741
|
onRetry: (attempt, error) => {
|
|
@@ -803,6 +813,7 @@ export async function runBrowserMode(options) {
|
|
|
803
813
|
inputTimeoutMs: config.inputTimeoutMs ?? undefined,
|
|
804
814
|
baselineTurns: baselineTurns ?? undefined,
|
|
805
815
|
attachmentNames,
|
|
816
|
+
onPromptSubmitted: markPromptSubmitted,
|
|
806
817
|
};
|
|
807
818
|
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
808
819
|
prompt,
|
|
@@ -811,6 +822,7 @@ export async function runBrowserMode(options) {
|
|
|
811
822
|
log: logger,
|
|
812
823
|
state: providerState,
|
|
813
824
|
});
|
|
825
|
+
await markPromptSubmitted();
|
|
814
826
|
const providerBaselineTurns = providerState.baselineTurns;
|
|
815
827
|
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
816
828
|
baselineTurns = providerBaselineTurns;
|
|
@@ -912,6 +924,7 @@ export async function runBrowserMode(options) {
|
|
|
912
924
|
chromeTargetId: lastTargetId,
|
|
913
925
|
tabUrl: lastUrl,
|
|
914
926
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
927
|
+
promptSubmitted,
|
|
915
928
|
controllerPid: process.pid,
|
|
916
929
|
};
|
|
917
930
|
}
|
|
@@ -996,6 +1009,7 @@ export async function runBrowserMode(options) {
|
|
|
996
1009
|
chromeTargetId: lastTargetId,
|
|
997
1010
|
tabUrl: lastUrl,
|
|
998
1011
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1012
|
+
promptSubmitted,
|
|
999
1013
|
controllerPid: process.pid,
|
|
1000
1014
|
},
|
|
1001
1015
|
});
|
|
@@ -1051,6 +1065,7 @@ export async function runBrowserMode(options) {
|
|
|
1051
1065
|
chromeTargetId: lastTargetId,
|
|
1052
1066
|
tabUrl: lastUrl,
|
|
1053
1067
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1068
|
+
promptSubmitted,
|
|
1054
1069
|
controllerPid: process.pid,
|
|
1055
1070
|
};
|
|
1056
1071
|
throw new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime, diagnostics }, error);
|
|
@@ -1298,6 +1313,7 @@ export async function runBrowserMode(options) {
|
|
|
1298
1313
|
chromeTargetId: lastTargetId,
|
|
1299
1314
|
tabUrl: lastUrl,
|
|
1300
1315
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1316
|
+
promptSubmitted,
|
|
1301
1317
|
controllerPid: process.pid,
|
|
1302
1318
|
};
|
|
1303
1319
|
}
|
|
@@ -1315,6 +1331,7 @@ export async function runBrowserMode(options) {
|
|
|
1315
1331
|
userDataDir,
|
|
1316
1332
|
chromeTargetId: lastTargetId,
|
|
1317
1333
|
tabUrl: lastUrl,
|
|
1334
|
+
promptSubmitted,
|
|
1318
1335
|
controllerPid: process.pid,
|
|
1319
1336
|
};
|
|
1320
1337
|
const reuseProfileHint = `oracle --engine browser --browser-manual-login ` +
|
|
@@ -1355,6 +1372,7 @@ export async function runBrowserMode(options) {
|
|
|
1355
1372
|
userDataDir,
|
|
1356
1373
|
chromeTargetId: lastTargetId,
|
|
1357
1374
|
tabUrl: lastUrl,
|
|
1375
|
+
promptSubmitted,
|
|
1358
1376
|
controllerPid: process.pid,
|
|
1359
1377
|
},
|
|
1360
1378
|
}, normalizedError);
|
|
@@ -1703,6 +1721,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1703
1721
|
let remoteTargetId = null;
|
|
1704
1722
|
let tabLease = null;
|
|
1705
1723
|
let lastUrl;
|
|
1724
|
+
let promptSubmitted = false;
|
|
1706
1725
|
let attachedExistingTab = false;
|
|
1707
1726
|
let ownsTarget = true;
|
|
1708
1727
|
const runtimeHintCb = options.runtimeHintCb;
|
|
@@ -1718,6 +1737,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1718
1737
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
1719
1738
|
tabUrl: lastUrl,
|
|
1720
1739
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1740
|
+
promptSubmitted,
|
|
1721
1741
|
controllerPid: process.pid,
|
|
1722
1742
|
});
|
|
1723
1743
|
await tabLease?.update({
|
|
@@ -1732,6 +1752,13 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1732
1752
|
logger(`Failed to persist runtime hint: ${message}`);
|
|
1733
1753
|
}
|
|
1734
1754
|
};
|
|
1755
|
+
const markPromptSubmitted = async () => {
|
|
1756
|
+
if (promptSubmitted) {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
promptSubmitted = true;
|
|
1760
|
+
await emitRuntimeHint();
|
|
1761
|
+
};
|
|
1735
1762
|
const startedAt = Date.now();
|
|
1736
1763
|
let answerText = "";
|
|
1737
1764
|
let answerMarkdown = "";
|
|
@@ -1848,7 +1875,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1848
1875
|
// Handle thinking time selection if specified. Deep Research owns its own effort flow.
|
|
1849
1876
|
const thinkingTime = config.thinkingTime;
|
|
1850
1877
|
if (thinkingTime && !deepResearch) {
|
|
1851
|
-
|
|
1878
|
+
const thinkingTargetModel = modelStrategy === "select" ? config.desiredModel : null;
|
|
1879
|
+
await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger, thinkingTargetModel), {
|
|
1852
1880
|
retries: 2,
|
|
1853
1881
|
delayMs: 300,
|
|
1854
1882
|
onRetry: (attempt, error) => {
|
|
@@ -1904,6 +1932,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1904
1932
|
inputTimeoutMs: config.inputTimeoutMs ?? undefined,
|
|
1905
1933
|
baselineTurns: baselineTurns ?? undefined,
|
|
1906
1934
|
attachmentNames,
|
|
1935
|
+
onPromptSubmitted: markPromptSubmitted,
|
|
1907
1936
|
};
|
|
1908
1937
|
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
1909
1938
|
prompt,
|
|
@@ -1912,6 +1941,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1912
1941
|
log: logger,
|
|
1913
1942
|
state: providerState,
|
|
1914
1943
|
});
|
|
1944
|
+
await markPromptSubmitted();
|
|
1915
1945
|
const providerBaselineTurns = providerState.baselineTurns;
|
|
1916
1946
|
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
1917
1947
|
baselineTurns = providerBaselineTurns;
|
|
@@ -1985,6 +2015,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1985
2015
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
1986
2016
|
tabUrl: lastUrl,
|
|
1987
2017
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2018
|
+
promptSubmitted,
|
|
1988
2019
|
controllerPid: process.pid,
|
|
1989
2020
|
};
|
|
1990
2021
|
}
|
|
@@ -2068,6 +2099,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2068
2099
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
2069
2100
|
tabUrl: lastUrl,
|
|
2070
2101
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2102
|
+
promptSubmitted,
|
|
2071
2103
|
controllerPid: process.pid,
|
|
2072
2104
|
},
|
|
2073
2105
|
});
|
|
@@ -2136,6 +2168,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2136
2168
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
2137
2169
|
tabUrl: lastUrl,
|
|
2138
2170
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2171
|
+
promptSubmitted,
|
|
2139
2172
|
controllerPid: process.pid,
|
|
2140
2173
|
};
|
|
2141
2174
|
throw new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime, diagnostics }, error);
|
|
@@ -2338,6 +2371,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2338
2371
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
2339
2372
|
tabUrl: lastUrl,
|
|
2340
2373
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2374
|
+
promptSubmitted,
|
|
2341
2375
|
artifacts: savedArtifacts,
|
|
2342
2376
|
archive,
|
|
2343
2377
|
modelSelection: modelSelectionEvidence,
|
|
@@ -2364,6 +2398,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2364
2398
|
chromeProfileRoot,
|
|
2365
2399
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
2366
2400
|
tabUrl: lastUrl,
|
|
2401
|
+
promptSubmitted,
|
|
2367
2402
|
controllerPid: process.pid,
|
|
2368
2403
|
},
|
|
2369
2404
|
});
|
|
@@ -23,6 +23,7 @@ async function submitPromptViaAdapter(ctx) {
|
|
|
23
23
|
attachmentNames: state.attachmentNames ?? [],
|
|
24
24
|
baselineTurns: state.baselineTurns ?? undefined,
|
|
25
25
|
inputTimeoutMs: state.inputTimeoutMs ?? undefined,
|
|
26
|
+
onPromptSubmitted: state.onPromptSubmitted,
|
|
26
27
|
}, ctx.prompt, state.logger);
|
|
27
28
|
state.committedTurns =
|
|
28
29
|
typeof committedTurns === "number" && Number.isFinite(committedTurns) ? committedTurns : null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function hasRecoverableChatGptConversation(runtime) {
|
|
2
|
+
if (!runtime) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
if (runtime.conversationId?.trim()) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
const tabUrl = runtime.tabUrl?.trim();
|
|
9
|
+
if (!tabUrl) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(tabUrl);
|
|
14
|
+
if (url.hostname !== "chatgpt.com" && url.hostname !== "chat.openai.com") {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return /(?:^|\/)c\/[^/]+/.test(url.pathname);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -214,6 +214,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
214
214
|
chromeTargetId: browserResult.chromeTargetId,
|
|
215
215
|
tabUrl: browserResult.tabUrl,
|
|
216
216
|
conversationId: browserResult.conversationId,
|
|
217
|
+
promptSubmitted: browserResult.promptSubmitted,
|
|
217
218
|
controllerPid: browserResult.controllerPid ?? process.pid,
|
|
218
219
|
},
|
|
219
220
|
archive: browserResult.archive,
|
|
@@ -6,6 +6,7 @@ import { formatFinishLine } from "../oracle/finishLine.js";
|
|
|
6
6
|
import { sessionStore, wait } from "../sessionStore.js";
|
|
7
7
|
import { formatTokenCount, formatTokenValue } from "../oracle/runUtils.js";
|
|
8
8
|
import { resumeBrowserSession } from "../browser/reattach.js";
|
|
9
|
+
import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
|
|
9
10
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "../browser/artifacts.js";
|
|
10
11
|
import { estimateTokenCount } from "../browser/utils.js";
|
|
11
12
|
import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
|
|
@@ -174,9 +175,16 @@ export async function attachSession(sessionId, options) {
|
|
|
174
175
|
hasFallbackSessionInfo &&
|
|
175
176
|
isDeepResearchPlaceholderCapture(metadata, await sessionStore.readLog(sessionId).catch(() => ""));
|
|
176
177
|
const completedDeepResearchPlaceholder = metadata.status === "completed" && deepResearchPlaceholderCapture;
|
|
178
|
+
const hasRecoverableConversation = hasRecoverableChatGptConversation(runtime);
|
|
179
|
+
const hasLiveChromeFallback = Boolean((metadata.status === "running" || hasIncompleteCapture || completedDeepResearchPlaceholder) &&
|
|
180
|
+
(runtime?.chromePort || runtime?.chromeBrowserWSEndpoint || runtime?.chromeProfileRoot));
|
|
177
181
|
const canReattach = (statusAllowsReattach || completedDeepResearchPlaceholder) &&
|
|
178
182
|
metadata.mode === "browser" &&
|
|
179
183
|
hasFallbackSessionInfo &&
|
|
184
|
+
(hasRecoverableConversation ||
|
|
185
|
+
runtime?.promptSubmitted ||
|
|
186
|
+
hasLiveChromeFallback ||
|
|
187
|
+
completedDeepResearchPlaceholder) &&
|
|
180
188
|
(hasChromeDisconnect ||
|
|
181
189
|
hasIncompleteCapture ||
|
|
182
190
|
completedDeepResearchPlaceholder ||
|
|
@@ -21,6 +21,7 @@ import { sanitizeOscProgress } from "./oscUtils.js";
|
|
|
21
21
|
import { readFiles } from "../oracle/files.js";
|
|
22
22
|
import { cwd as getCwd } from "node:process";
|
|
23
23
|
import { resumeBrowserSession } from "../browser/reattach.js";
|
|
24
|
+
import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
|
|
24
25
|
import { estimateTokenCount } from "../browser/utils.js";
|
|
25
26
|
import { formatElapsed } from "../oracle/format.js";
|
|
26
27
|
const isTty = process.stdout.isTTY;
|
|
@@ -390,6 +391,40 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
390
391
|
if (connectionLost && mode === "browser") {
|
|
391
392
|
const runtime = userError.details
|
|
392
393
|
?.runtime;
|
|
394
|
+
const recoverableRuntime = runtime ?? sessionMeta.browser?.runtime;
|
|
395
|
+
if (!hasRecoverableChatGptConversation(recoverableRuntime) &&
|
|
396
|
+
recoverableRuntime?.promptSubmitted !== true) {
|
|
397
|
+
log(dim("Chrome disconnected before a ChatGPT conversation was created; marking session error."));
|
|
398
|
+
if (modelForStatus) {
|
|
399
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
400
|
+
status: "error",
|
|
401
|
+
completedAt: new Date().toISOString(),
|
|
402
|
+
response: { status: "error", incompleteReason: "chrome-disconnected" },
|
|
403
|
+
error: {
|
|
404
|
+
category: userError.category,
|
|
405
|
+
message: userError.message,
|
|
406
|
+
details: userError.details,
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
411
|
+
status: "error",
|
|
412
|
+
completedAt: new Date().toISOString(),
|
|
413
|
+
errorMessage: message,
|
|
414
|
+
mode,
|
|
415
|
+
browser: {
|
|
416
|
+
config: browserConfig,
|
|
417
|
+
runtime: recoverableRuntime,
|
|
418
|
+
},
|
|
419
|
+
response: { status: "error", incompleteReason: "chrome-disconnected" },
|
|
420
|
+
error: {
|
|
421
|
+
category: userError.category,
|
|
422
|
+
message: userError.message,
|
|
423
|
+
details: userError.details,
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
393
428
|
log(dim("Chrome disconnected before completion; keeping session running for reattach."));
|
|
394
429
|
if (modelForStatus) {
|
|
395
430
|
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
@@ -576,13 +611,22 @@ function sanitizeMultiModelFailureForThrow(error, context) {
|
|
|
576
611
|
if (!(error instanceof Error)) {
|
|
577
612
|
return new Error(message);
|
|
578
613
|
}
|
|
579
|
-
|
|
614
|
+
let sanitized;
|
|
615
|
+
if (error instanceof OracleTransportError) {
|
|
616
|
+
sanitized = new OracleTransportError(error.reason, message);
|
|
617
|
+
}
|
|
618
|
+
else if (error instanceof OracleResponseError) {
|
|
619
|
+
sanitized = new OracleResponseError(message, error.response);
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
sanitized = new Error(message);
|
|
623
|
+
sanitized.name = error.name;
|
|
624
|
+
}
|
|
580
625
|
if (error.stack) {
|
|
581
|
-
const [
|
|
582
|
-
|
|
583
|
-
error.stack = [prefix ? `${prefix}: ${message}` : message, ...rest].join("\n");
|
|
626
|
+
const [, ...rest] = error.stack.split("\n");
|
|
627
|
+
sanitized.stack = [sanitized.name ? `${sanitized.name}: ${message}` : message, ...rest].join("\n");
|
|
584
628
|
}
|
|
585
|
-
return
|
|
629
|
+
return sanitized;
|
|
586
630
|
}
|
|
587
631
|
export function deriveOutputManifestPath(basePath) {
|
|
588
632
|
const ext = path.extname(basePath);
|
|
@@ -8,7 +8,14 @@ const DEFAULT_PROVIDER_HOSTS = {
|
|
|
8
8
|
openai: "api.openai.com",
|
|
9
9
|
xai: "api.x.ai",
|
|
10
10
|
};
|
|
11
|
+
export function resolveProviderRoute(input) {
|
|
12
|
+
return buildResolvedProviderRoute(input);
|
|
13
|
+
}
|
|
11
14
|
export function buildProviderRoutePlan(input) {
|
|
15
|
+
const { apiKey: _apiKey, baseUrl: _baseUrl, nativeProvider: _nativeProvider, openRouterFallback: _openRouterFallback, azureEndpoint: _azureEndpoint, ...plan } = buildResolvedProviderRoute(input);
|
|
16
|
+
return plan;
|
|
17
|
+
}
|
|
18
|
+
function buildResolvedProviderRoute(input) {
|
|
12
19
|
const env = input.env ?? process.env;
|
|
13
20
|
const providerMode = input.providerMode ?? "auto";
|
|
14
21
|
const azureConfigured = Boolean(input.azure?.endpoint?.trim());
|
|
@@ -49,7 +56,12 @@ export function buildProviderRoutePlan(input) {
|
|
|
49
56
|
keySource: key.source,
|
|
50
57
|
keyPreview: key.preview,
|
|
51
58
|
keyPresent: key.present,
|
|
59
|
+
apiKey: key.value,
|
|
60
|
+
nativeProvider: provider,
|
|
61
|
+
baseUrl: input.baseUrl,
|
|
62
|
+
openRouterFallback: false,
|
|
52
63
|
isAzureOpenAI,
|
|
64
|
+
azureEndpoint: state?.azureEndpoint ?? input.azure?.endpoint,
|
|
53
65
|
azureConfigured,
|
|
54
66
|
azureDeploymentName: state?.azureDeploymentName,
|
|
55
67
|
azureNote: azureNote(providerMode, azureConfigured, isAzureOpenAI),
|
|
@@ -145,7 +157,12 @@ export function buildProviderRoutePlan(input) {
|
|
|
145
157
|
keySource: key.source,
|
|
146
158
|
keyPreview: key.preview,
|
|
147
159
|
keyPresent: key.present,
|
|
160
|
+
apiKey: key.value,
|
|
161
|
+
nativeProvider: provider,
|
|
162
|
+
baseUrl,
|
|
163
|
+
openRouterFallback,
|
|
148
164
|
isAzureOpenAI,
|
|
165
|
+
azureEndpoint: state.azureEndpoint,
|
|
149
166
|
azureConfigured,
|
|
150
167
|
azureDeploymentName: state.azureDeploymentName,
|
|
151
168
|
azureNote: azureNote(providerMode, azureConfigured, isAzureOpenAI),
|
|
@@ -166,7 +183,12 @@ function getNativeKey({ model, provider, providerMode, isAzureOpenAI, apiKey, en
|
|
|
166
183
|
}
|
|
167
184
|
function getKeyForRoute({ model, provider, providerMode, isAzureOpenAI, baseUrl, openRouterFallback, apiKey, env, }) {
|
|
168
185
|
if (apiKey) {
|
|
169
|
-
return {
|
|
186
|
+
return {
|
|
187
|
+
source: "apiKey option",
|
|
188
|
+
preview: maskApiKey(apiKey) ?? "set",
|
|
189
|
+
present: true,
|
|
190
|
+
value: apiKey,
|
|
191
|
+
};
|
|
170
192
|
}
|
|
171
193
|
if (isAzureOpenAI) {
|
|
172
194
|
return readKey(["AZURE_OPENAI_API_KEY", "OPENAI_API_KEY"], env);
|
|
@@ -201,7 +223,12 @@ function readKey(names, env) {
|
|
|
201
223
|
for (const name of names) {
|
|
202
224
|
const value = env[name]?.trim();
|
|
203
225
|
if (value) {
|
|
204
|
-
return {
|
|
226
|
+
return {
|
|
227
|
+
source: name,
|
|
228
|
+
preview: `${name}=${maskApiKey(value) ?? "set"}`,
|
|
229
|
+
present: true,
|
|
230
|
+
value,
|
|
231
|
+
};
|
|
205
232
|
}
|
|
206
233
|
}
|
|
207
234
|
return { source: names.join("|"), preview: "missing", present: false };
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -14,7 +14,7 @@ import { getFileTokenStats, printFileTokenStats } from "./tokenStats.js";
|
|
|
14
14
|
import { OracleResponseError, OracleTransportError, PromptValidationError, describeTransportError, toTransportError, } from "./errors.js";
|
|
15
15
|
import { isCustomBaseUrl } from "./baseUrl.js";
|
|
16
16
|
import { createDefaultClientFactory } from "./client.js";
|
|
17
|
-
import {
|
|
17
|
+
import { maskApiKey } from "./logging.js";
|
|
18
18
|
import { startHeartbeat } from "../heartbeat.js";
|
|
19
19
|
import { startOscProgress } from "./oscProgress.js";
|
|
20
20
|
import { createFsAdapter } from "./fsAdapter.js";
|
|
@@ -25,60 +25,49 @@ import { createMarkdownStreamer } from "markdansi";
|
|
|
25
25
|
import { executeBackgroundResponse } from "./background.js";
|
|
26
26
|
import { formatTokenEstimate, formatTokenValue, resolvePreviewMode } from "./runUtils.js";
|
|
27
27
|
import { estimateUsdCost } from "tokentally";
|
|
28
|
-
import {
|
|
28
|
+
import { isOpenRouterBaseUrl, isProModel, resolveModelConfig } from "./modelResolver.js";
|
|
29
29
|
import { validateProviderRouting } from "./providerRouting.js";
|
|
30
|
+
import { formatRouteTargetForLog, resolveProviderRoute, } from "./providerRoutePlan.js";
|
|
30
31
|
const isStdoutTty = process.stdout.isTTY && chalk.level > 0;
|
|
31
32
|
const dim = (text) => (isStdoutTty ? kleur.dim(text) : text);
|
|
32
33
|
// Default timeout for non-pro API runs (fast models) — give them up to 120s.
|
|
33
34
|
const DEFAULT_TIMEOUT_NON_PRO_MS = 120_000;
|
|
34
35
|
const DEFAULT_TIMEOUT_PRO_MS = 60 * 60 * 1000;
|
|
35
|
-
const DEFAULT_PROVIDER_HOSTS = {
|
|
36
|
-
anthropic: "api.anthropic.com",
|
|
37
|
-
google: "generativelanguage.googleapis.com",
|
|
38
|
-
openai: "api.openai.com",
|
|
39
|
-
xai: "api.x.ai",
|
|
40
|
-
};
|
|
41
36
|
const defaultWait = (ms) => new Promise((resolve) => {
|
|
42
37
|
setTimeout(resolve, ms);
|
|
43
38
|
});
|
|
44
|
-
function
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
47
|
-
try {
|
|
48
|
-
const parsed = new URL(raw);
|
|
49
|
-
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
50
|
-
let path = "";
|
|
51
|
-
if (segments.length > 0) {
|
|
52
|
-
path = `/${segments[0]}`;
|
|
53
|
-
if (segments.length > 1) {
|
|
54
|
-
path += "/...";
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return `${parsed.host}${path}`;
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
const formatted = formatBaseUrlForLog(raw).replace(/^https?:\/\//u, "");
|
|
61
|
-
return formatted || fallbackHost;
|
|
39
|
+
function formatProviderRouteLogLine(route, keySource) {
|
|
40
|
+
if (route.isAzureOpenAI) {
|
|
41
|
+
return `Provider: Azure OpenAI | endpoint: ${formatRouteTargetForLog(route.azureEndpoint)} | deployment: ${route.azureDeploymentName || "none"} | key: ${keySource}`;
|
|
62
42
|
}
|
|
43
|
+
return `Provider: ${route.providerLabel} | base: ${route.base} | key: ${keySource}`;
|
|
63
44
|
}
|
|
64
|
-
function
|
|
65
|
-
if (
|
|
66
|
-
|
|
45
|
+
function runtimeKeySource({ route, providerMode, optionsApiKey, }) {
|
|
46
|
+
if (optionsApiKey &&
|
|
47
|
+
(route.isAzureOpenAI ||
|
|
48
|
+
providerMode === "openai" ||
|
|
49
|
+
route.provider === "openai" ||
|
|
50
|
+
route.providerLabel === "OpenAI-compatible")) {
|
|
51
|
+
return "apiKey option";
|
|
67
52
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
53
|
+
if (route.isAzureOpenAI) {
|
|
54
|
+
return "AZURE_OPENAI_API_KEY|OPENAI_API_KEY";
|
|
55
|
+
}
|
|
56
|
+
if (providerMode === "openai") {
|
|
57
|
+
return "OPENAI_API_KEY";
|
|
58
|
+
}
|
|
59
|
+
if (isOpenRouterBaseUrl(route.baseUrl) || route.openRouterFallback || route.model.includes("/")) {
|
|
60
|
+
return "OPENROUTER_API_KEY";
|
|
61
|
+
}
|
|
62
|
+
if (route.model.startsWith("gpt"))
|
|
63
|
+
return "OPENAI_API_KEY";
|
|
64
|
+
if (route.model.startsWith("gemini"))
|
|
65
|
+
return "GEMINI_API_KEY";
|
|
66
|
+
if (route.model.startsWith("claude"))
|
|
67
|
+
return "ANTHROPIC_API_KEY";
|
|
68
|
+
if (route.model.startsWith("grok"))
|
|
69
|
+
return "XAI_API_KEY";
|
|
70
|
+
return optionsApiKey ? "apiKey option" : route.keySource;
|
|
82
71
|
}
|
|
83
72
|
export async function runOracle(options, deps = {}) {
|
|
84
73
|
const { apiKey: optionsApiKey = options.apiKey, cwd = process.cwd(), fs: fsModule = createFsAdapter(fs), log = console.log, write: sinkWrite = (_text) => true, allowStdout = true, stdoutWrite: stdoutWriteDep, now = () => performance.now(), clientFactory = createDefaultClientFactory(), client, wait = defaultWait, } = deps;
|
|
@@ -86,13 +75,10 @@ export async function runOracle(options, deps = {}) {
|
|
|
86
75
|
? (stdoutWriteDep ?? process.stdout.write.bind(process.stdout))
|
|
87
76
|
: () => true;
|
|
88
77
|
const isTty = allowStdout && isStdoutTty;
|
|
89
|
-
const resolvedXaiBaseUrl = process.env.XAI_BASE_URL?.trim() || "https://api.x.ai/v1";
|
|
90
|
-
const openRouterApiKey = process.env.OPENROUTER_API_KEY?.trim();
|
|
91
|
-
const defaultOpenRouterBase = defaultOpenRouterBaseUrl();
|
|
92
78
|
const previewMode = resolvePreviewMode(options.previewMode ?? options.preview);
|
|
93
79
|
const isPreview = Boolean(previewMode);
|
|
94
80
|
const providerMode = options.provider ?? "auto";
|
|
95
|
-
|
|
81
|
+
validateProviderRouting({
|
|
96
82
|
model: options.model,
|
|
97
83
|
providerMode,
|
|
98
84
|
azure: options.azure,
|
|
@@ -103,99 +89,23 @@ export async function runOracle(options, deps = {}) {
|
|
|
103
89
|
}
|
|
104
90
|
},
|
|
105
91
|
});
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
!isOpenRouterBaseUrl(baseUrl) &&
|
|
118
|
-
!isCustomBaseUrl(baseUrl)) {
|
|
119
|
-
baseUrl = undefined;
|
|
120
|
-
}
|
|
121
|
-
if (!baseUrl) {
|
|
122
|
-
let envBaseUrl;
|
|
123
|
-
if (options.model.startsWith("grok")) {
|
|
124
|
-
envBaseUrl = resolvedXaiBaseUrl;
|
|
125
|
-
}
|
|
126
|
-
else if (provider === "anthropic") {
|
|
127
|
-
envBaseUrl = process.env.ANTHROPIC_BASE_URL?.trim();
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
envBaseUrl = process.env.OPENAI_BASE_URL?.trim();
|
|
131
|
-
}
|
|
132
|
-
if (!providerQualifiedOpenRouterCandidate || (envBaseUrl && isCustomBaseUrl(envBaseUrl))) {
|
|
133
|
-
baseUrl = envBaseUrl;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const providerKeyMissing = !isAzureOpenAI &&
|
|
137
|
-
(providerMode === "openai"
|
|
138
|
-
? !hasOpenAIKey
|
|
139
|
-
: (provider === "openai" && !hasOpenAIKey) ||
|
|
140
|
-
(provider === "anthropic" && !hasAnthropicKey) ||
|
|
141
|
-
(provider === "google" && !hasGeminiKey) ||
|
|
142
|
-
(provider === "xai" && !hasXaiKey) ||
|
|
143
|
-
provider === "other");
|
|
144
|
-
const providerQualifiedOpenRouterRoute = providerQualifiedOpenRouterCandidate && !baseUrl;
|
|
145
|
-
const openRouterFallback = !baseUrl &&
|
|
146
|
-
(providerQualifiedOpenRouterRoute ||
|
|
147
|
-
(providerMode !== "openai" &&
|
|
148
|
-
providerKeyMissing &&
|
|
149
|
-
(provider === "other" || Boolean(openRouterApiKey))));
|
|
150
|
-
if (!baseUrl || openRouterFallback) {
|
|
151
|
-
if (openRouterFallback) {
|
|
152
|
-
baseUrl = defaultOpenRouterBase;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (baseUrl && isOpenRouterBaseUrl(baseUrl)) {
|
|
156
|
-
baseUrl = normalizeOpenRouterBaseUrl(baseUrl);
|
|
157
|
-
}
|
|
92
|
+
const route = resolveProviderRoute({
|
|
93
|
+
model: options.model,
|
|
94
|
+
providerMode,
|
|
95
|
+
azure: options.azure,
|
|
96
|
+
baseUrl: options.baseUrl,
|
|
97
|
+
apiKey: optionsApiKey,
|
|
98
|
+
env: process.env,
|
|
99
|
+
});
|
|
100
|
+
const { isAzureOpenAI, azureDeploymentName } = route;
|
|
101
|
+
const baseUrl = route.baseUrl;
|
|
102
|
+
const openRouterFallback = route.openRouterFallback;
|
|
158
103
|
const logVerbose = (message) => {
|
|
159
104
|
if (options.verbose) {
|
|
160
105
|
log(dim(`[verbose] ${message}`));
|
|
161
106
|
}
|
|
162
107
|
};
|
|
163
|
-
const
|
|
164
|
-
if (isAzureOpenAI) {
|
|
165
|
-
if (optionsApiKey)
|
|
166
|
-
return { key: optionsApiKey, source: "apiKey option" };
|
|
167
|
-
const key = process.env.AZURE_OPENAI_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
168
|
-
return { key, source: "AZURE_OPENAI_API_KEY|OPENAI_API_KEY" };
|
|
169
|
-
}
|
|
170
|
-
if (providerMode === "openai") {
|
|
171
|
-
if (optionsApiKey)
|
|
172
|
-
return { key: optionsApiKey, source: "apiKey option" };
|
|
173
|
-
return { key: process.env.OPENAI_API_KEY, source: "OPENAI_API_KEY" };
|
|
174
|
-
}
|
|
175
|
-
if (isOpenRouterBaseUrl(baseUrl) || openRouterFallback) {
|
|
176
|
-
return { key: optionsApiKey ?? openRouterApiKey, source: "OPENROUTER_API_KEY" };
|
|
177
|
-
}
|
|
178
|
-
if (typeof model === "string" && model.startsWith("gpt")) {
|
|
179
|
-
if (optionsApiKey)
|
|
180
|
-
return { key: optionsApiKey, source: "apiKey option" };
|
|
181
|
-
return { key: process.env.OPENAI_API_KEY, source: "OPENAI_API_KEY" };
|
|
182
|
-
}
|
|
183
|
-
if (typeof model === "string" && model.startsWith("gemini")) {
|
|
184
|
-
return { key: optionsApiKey ?? process.env.GEMINI_API_KEY, source: "GEMINI_API_KEY" };
|
|
185
|
-
}
|
|
186
|
-
if (typeof model === "string" && model.startsWith("claude")) {
|
|
187
|
-
return { key: optionsApiKey ?? process.env.ANTHROPIC_API_KEY, source: "ANTHROPIC_API_KEY" };
|
|
188
|
-
}
|
|
189
|
-
if (typeof model === "string" && model.startsWith("grok")) {
|
|
190
|
-
return { key: optionsApiKey ?? process.env.XAI_API_KEY, source: "XAI_API_KEY" };
|
|
191
|
-
}
|
|
192
|
-
return {
|
|
193
|
-
key: optionsApiKey ?? openRouterApiKey,
|
|
194
|
-
source: optionsApiKey ? "apiKey option" : "OPENROUTER_API_KEY",
|
|
195
|
-
};
|
|
196
|
-
};
|
|
197
|
-
const apiKeyResult = getApiKeyForModel(options.model);
|
|
198
|
-
const apiKey = apiKeyResult.key;
|
|
108
|
+
const apiKey = route.apiKey;
|
|
199
109
|
if (!apiKey) {
|
|
200
110
|
const envVar = isAzureOpenAI
|
|
201
111
|
? "AZURE_OPENAI_API_KEY (or OPENAI_API_KEY)"
|
|
@@ -203,15 +113,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
203
113
|
? "OPENAI_API_KEY"
|
|
204
114
|
: isOpenRouterBaseUrl(baseUrl) || openRouterFallback
|
|
205
115
|
? "OPENROUTER_API_KEY"
|
|
206
|
-
:
|
|
207
|
-
? "OPENAI_API_KEY"
|
|
208
|
-
: options.model.startsWith("gemini")
|
|
209
|
-
? "GEMINI_API_KEY"
|
|
210
|
-
: options.model.startsWith("claude")
|
|
211
|
-
? "ANTHROPIC_API_KEY"
|
|
212
|
-
: options.model.startsWith("grok")
|
|
213
|
-
? "XAI_API_KEY"
|
|
214
|
-
: "OPENROUTER_API_KEY";
|
|
116
|
+
: route.keySource;
|
|
215
117
|
const browserModeHint = options.model.startsWith("gpt")
|
|
216
118
|
? ' If you have a ChatGPT Pro subscription, retry with --engine browser (or MCP engine:"browser" / preset:"chatgpt-pro-heavy"); browser mode uses your signed-in ChatGPT session instead of an API key.'
|
|
217
119
|
: "";
|
|
@@ -219,7 +121,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
219
121
|
env: envVar,
|
|
220
122
|
});
|
|
221
123
|
}
|
|
222
|
-
const envVar =
|
|
124
|
+
const envVar = runtimeKeySource({ route, providerMode, optionsApiKey });
|
|
223
125
|
const minPromptLength = Number.parseInt(process.env.ORACLE_MIN_PROMPT_CHARS ?? "10", 10);
|
|
224
126
|
const promptLength = options.prompt?.trim().length ?? 0;
|
|
225
127
|
// Enforce the short-prompt guardrail on pro-tier models because they're costly; cheaper models can run short prompts without blocking.
|
|
@@ -227,7 +129,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
227
129
|
if (isProTierModel && !Number.isNaN(minPromptLength) && promptLength < minPromptLength) {
|
|
228
130
|
throw new PromptValidationError(`Prompt is too short (<${minPromptLength} chars). This was likely accidental; please provide more detail.`, { minPromptLength, promptLength });
|
|
229
131
|
}
|
|
230
|
-
const resolverOpenRouterApiKey = openRouterFallback || isOpenRouterBaseUrl(baseUrl) ?
|
|
132
|
+
const resolverOpenRouterApiKey = openRouterFallback || isOpenRouterBaseUrl(baseUrl) ? apiKey : undefined;
|
|
231
133
|
const modelConfig = await resolveModelConfig(options.model, {
|
|
232
134
|
baseUrl,
|
|
233
135
|
openRouterApiKey: resolverOpenRouterApiKey,
|
|
@@ -326,15 +228,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
326
228
|
if (!isPreview) {
|
|
327
229
|
if (!options.suppressHeader) {
|
|
328
230
|
log(headerLine);
|
|
329
|
-
log(dim(formatProviderRouteLogLine(
|
|
330
|
-
provider,
|
|
331
|
-
baseUrl,
|
|
332
|
-
openRouterFallback,
|
|
333
|
-
isAzureOpenAI,
|
|
334
|
-
azureEndpoint,
|
|
335
|
-
azureDeploymentName,
|
|
336
|
-
envVar,
|
|
337
|
-
})));
|
|
231
|
+
log(dim(formatProviderRouteLogLine(route, envVar)));
|
|
338
232
|
}
|
|
339
233
|
const maskedKey = maskApiKey(apiKey);
|
|
340
234
|
if (maskedKey && options.verbose) {
|
|
@@ -409,7 +303,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
409
303
|
: proxyCompatibleBaseUrl
|
|
410
304
|
? proxyCompatibleBaseUrl
|
|
411
305
|
: modelConfig.model.startsWith("claude")
|
|
412
|
-
?
|
|
306
|
+
? baseUrl
|
|
413
307
|
: baseUrl;
|
|
414
308
|
const clientInstance = client ??
|
|
415
309
|
clientFactory(apiKey, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
-
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { createWriteStream, mkdirSync } from "node:fs";
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import { DEFAULT_MODEL } from "./oracle/config.js";
|
|
6
6
|
import { formatElapsed } from "./oracle/format.js";
|
|
@@ -86,14 +86,26 @@ async function fileExists(targetPath) {
|
|
|
86
86
|
return false;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
|
|
89
|
+
function isFileExistsError(error) {
|
|
90
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
91
|
+
}
|
|
92
|
+
async function reserveUniqueSessionDir(baseSlug) {
|
|
90
93
|
let candidate = baseSlug;
|
|
91
94
|
let suffix = 2;
|
|
92
|
-
|
|
95
|
+
for (;;) {
|
|
96
|
+
const dir = sessionDir(candidate);
|
|
97
|
+
try {
|
|
98
|
+
await fs.mkdir(dir, { recursive: false });
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (!isFileExistsError(error)) {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
93
106
|
candidate = `${baseSlug}-${suffix}`;
|
|
94
107
|
suffix += 1;
|
|
95
108
|
}
|
|
96
|
-
return candidate;
|
|
97
109
|
}
|
|
98
110
|
async function listModelRunFiles(sessionId) {
|
|
99
111
|
const dir = modelsDir(sessionId);
|
|
@@ -153,9 +165,7 @@ export async function readModelRunMetadata(sessionId, model) {
|
|
|
153
165
|
export async function initializeSession(options, cwd, notifications, baseSlugOverride) {
|
|
154
166
|
await ensureSessionStorage();
|
|
155
167
|
const baseSlug = baseSlugOverride || createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
|
|
156
|
-
const sessionId = await
|
|
157
|
-
const dir = sessionDir(sessionId);
|
|
158
|
-
await ensureDir(dir);
|
|
168
|
+
const sessionId = await reserveUniqueSessionDir(baseSlug);
|
|
159
169
|
const mode = options.mode ?? "api";
|
|
160
170
|
const browserConfig = options.browserConfig;
|
|
161
171
|
const modelList = Array.isArray(options.models) && options.models.length > 0
|
|
@@ -239,25 +249,25 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
|
|
|
239
249
|
return metadata;
|
|
240
250
|
}
|
|
241
251
|
export async function readSessionMetadata(sessionId) {
|
|
242
|
-
const modern = await readModernSessionMetadata(sessionId);
|
|
252
|
+
const modern = await readModernSessionMetadata(sessionId, { reconcile: true, persist: false });
|
|
243
253
|
if (modern) {
|
|
244
254
|
return modern;
|
|
245
255
|
}
|
|
246
|
-
const legacy = await readLegacySessionMetadata(sessionId);
|
|
256
|
+
const legacy = await readLegacySessionMetadata(sessionId, { reconcile: true, persist: false });
|
|
247
257
|
if (legacy) {
|
|
248
258
|
return legacy;
|
|
249
259
|
}
|
|
250
260
|
return null;
|
|
251
261
|
}
|
|
252
262
|
export async function updateSessionMetadata(sessionId, updates) {
|
|
253
|
-
const existing = (await readModernSessionMetadata(sessionId)) ??
|
|
254
|
-
(await readLegacySessionMetadata(sessionId)) ??
|
|
263
|
+
const existing = (await readModernSessionMetadata(sessionId, { reconcile: false, persist: false })) ??
|
|
264
|
+
(await readLegacySessionMetadata(sessionId, { reconcile: false, persist: false })) ??
|
|
255
265
|
{ id: sessionId };
|
|
256
266
|
const next = { ...existing, ...updates };
|
|
257
267
|
await fs.writeFile(metaPath(sessionId), JSON.stringify(next, null, 2), "utf8");
|
|
258
268
|
return next;
|
|
259
269
|
}
|
|
260
|
-
async function readModernSessionMetadata(sessionId) {
|
|
270
|
+
async function readModernSessionMetadata(sessionId, options) {
|
|
261
271
|
try {
|
|
262
272
|
const raw = await fs.readFile(metaPath(sessionId), "utf8");
|
|
263
273
|
const parsed = JSON.parse(raw);
|
|
@@ -265,25 +275,31 @@ async function readModernSessionMetadata(sessionId) {
|
|
|
265
275
|
return null;
|
|
266
276
|
}
|
|
267
277
|
const enriched = await attachModelRuns(parsed, sessionId);
|
|
268
|
-
|
|
269
|
-
return await markZombie(runtimeChecked, { persist: false });
|
|
278
|
+
return options.reconcile ? reconcileSessionMetadata(enriched, options) : enriched;
|
|
270
279
|
}
|
|
271
280
|
catch {
|
|
272
281
|
return null;
|
|
273
282
|
}
|
|
274
283
|
}
|
|
275
|
-
async function readLegacySessionMetadata(sessionId) {
|
|
284
|
+
async function readLegacySessionMetadata(sessionId, options) {
|
|
276
285
|
try {
|
|
277
286
|
const raw = await fs.readFile(legacySessionPath(sessionId), "utf8");
|
|
278
287
|
const parsed = JSON.parse(raw);
|
|
279
288
|
const enriched = await attachModelRuns(parsed, sessionId);
|
|
280
|
-
|
|
281
|
-
return await markZombie(runtimeChecked, { persist: false });
|
|
289
|
+
return options.reconcile ? reconcileSessionMetadata(enriched, options) : enriched;
|
|
282
290
|
}
|
|
283
291
|
catch {
|
|
284
292
|
return null;
|
|
285
293
|
}
|
|
286
294
|
}
|
|
295
|
+
async function readRawSessionMetadata(sessionId) {
|
|
296
|
+
return ((await readModernSessionMetadata(sessionId, { reconcile: false, persist: false })) ??
|
|
297
|
+
(await readLegacySessionMetadata(sessionId, { reconcile: false, persist: false })));
|
|
298
|
+
}
|
|
299
|
+
async function reconcileSessionMetadata(meta, { persist }) {
|
|
300
|
+
const runtimeChecked = await markDeadBrowser(meta, { persist });
|
|
301
|
+
return await markZombie(runtimeChecked, { persist });
|
|
302
|
+
}
|
|
287
303
|
function isSessionMetadataRecord(value) {
|
|
288
304
|
return Boolean(value && typeof value.id === "string" && value.status);
|
|
289
305
|
}
|
|
@@ -297,7 +313,7 @@ async function attachModelRuns(meta, sessionId) {
|
|
|
297
313
|
export function createSessionLogWriter(sessionId, model) {
|
|
298
314
|
const targetPath = model ? modelLogPath(sessionId, model) : logPath(sessionId);
|
|
299
315
|
if (model) {
|
|
300
|
-
|
|
316
|
+
mkdirSync(modelsDir(sessionId), { recursive: true });
|
|
301
317
|
}
|
|
302
318
|
const stream = createWriteStream(targetPath, { flags: "a" });
|
|
303
319
|
const logLine = (line = "") => {
|
|
@@ -314,10 +330,10 @@ export async function listSessionsMetadata() {
|
|
|
314
330
|
const entries = await fs.readdir(getSessionsDir()).catch(() => []);
|
|
315
331
|
const metas = [];
|
|
316
332
|
for (const entry of entries) {
|
|
317
|
-
let meta = await
|
|
333
|
+
let meta = await readRawSessionMetadata(entry);
|
|
318
334
|
if (meta) {
|
|
319
|
-
|
|
320
|
-
meta = await
|
|
335
|
+
// Keep stored metadata consistent with status reconciliation done by `oracle status`.
|
|
336
|
+
meta = await reconcileSessionMetadata(meta, { persist: true });
|
|
321
337
|
metas.push(meta);
|
|
322
338
|
}
|
|
323
339
|
}
|
|
@@ -388,7 +404,7 @@ export async function readModelLog(sessionId, model) {
|
|
|
388
404
|
}
|
|
389
405
|
}
|
|
390
406
|
export async function readSessionRequest(sessionId) {
|
|
391
|
-
const modern = await readModernSessionMetadata(sessionId);
|
|
407
|
+
const modern = await readModernSessionMetadata(sessionId, { reconcile: false, persist: false });
|
|
392
408
|
if (modern?.options) {
|
|
393
409
|
return modern.options;
|
|
394
410
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "CLI wrapper around OpenAI Responses API with GPT-5.5 Pro, GPT-5.5, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://github.com/steipete/oracle#readme",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"test": "vitest run",
|
|
47
47
|
"test:mcp": "pnpm run build && pnpm run test:mcp:unit && pnpm run test:mcp:mcporter",
|
|
48
48
|
"test:mcp:unit": "vitest run tests/mcp*.test.ts tests/mcp/**/*.test.ts",
|
|
49
|
-
"test:mcp:mcporter": "pnpm dlx mcporter list oracle-local --schema --config config/mcporter.json && pnpm dlx mcporter call oracle-local.sessions limit:1 --config config/mcporter.json",
|
|
49
|
+
"test:mcp:mcporter": "CHROME_DEVTOOLS_URL=http://127.0.0.1:0 pnpm dlx mcporter list oracle-local --schema --config config/mcporter.json && CHROME_DEVTOOLS_URL=http://127.0.0.1:0 pnpm dlx mcporter call oracle-local.sessions limit:1 --config config/mcporter.json",
|
|
50
50
|
"test:browser": "pnpm run build && tsx scripts/test-browser.ts && ./scripts/browser-smoke.sh",
|
|
51
51
|
"test:packed-cli": "node scripts/packed-cli-smoke.mjs",
|
|
52
52
|
"test:live": "ORACLE_LIVE_TEST=1 vitest run tests/live --exclude tests/live/openai-live.test.ts",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@google/genai": "^2.0.1",
|
|
62
62
|
"@google/generative-ai": "^0.24.1",
|
|
63
63
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
64
|
-
"@steipete/sweet-cookie": "^0.
|
|
64
|
+
"@steipete/sweet-cookie": "^0.3.0",
|
|
65
65
|
"chalk": "^5.6.2",
|
|
66
66
|
"chrome-launcher": "^1.2.1",
|
|
67
67
|
"chrome-remote-interface": "^0.34.0",
|
|
@@ -70,11 +70,11 @@
|
|
|
70
70
|
"dotenv": "^17.4.2",
|
|
71
71
|
"fast-glob": "^3.3.3",
|
|
72
72
|
"gpt-tokenizer": "^3.4.0",
|
|
73
|
-
"inquirer": "13.4.
|
|
73
|
+
"inquirer": "13.4.3",
|
|
74
74
|
"json5": "^2.2.3",
|
|
75
75
|
"kleur": "^4.1.5",
|
|
76
76
|
"markdansi": "0.2.1",
|
|
77
|
-
"openai": "^6.
|
|
77
|
+
"openai": "^6.38.0",
|
|
78
78
|
"osc-progress": "^0.3.0",
|
|
79
79
|
"qs": "^6.15.1",
|
|
80
80
|
"shiki": "^4.0.2",
|
|
@@ -84,17 +84,17 @@
|
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
87
|
-
"@types/chrome-remote-interface": "^0.
|
|
87
|
+
"@types/chrome-remote-interface": "^0.34.0",
|
|
88
88
|
"@types/inquirer": "^9.0.9",
|
|
89
89
|
"@types/node": "^25.6.0",
|
|
90
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
91
|
-
"@vitest/coverage-v8": "4.1.
|
|
92
|
-
"devtools-protocol": "0.0.
|
|
90
|
+
"@typescript/native-preview": "7.0.0-dev.20260516.1",
|
|
91
|
+
"@vitest/coverage-v8": "4.1.6",
|
|
92
|
+
"devtools-protocol": "0.0.1629771",
|
|
93
93
|
"es-toolkit": "^1.46.1",
|
|
94
94
|
"esbuild": "^0.28.0",
|
|
95
|
-
"oxfmt": "0.
|
|
95
|
+
"oxfmt": "0.50.0",
|
|
96
96
|
"oxlint": "^1.62.0",
|
|
97
|
-
"puppeteer-core": "^
|
|
97
|
+
"puppeteer-core": "^25.0.2",
|
|
98
98
|
"tsx": "^4.21.0",
|
|
99
99
|
"typescript": "^6.0.3",
|
|
100
100
|
"vitest": "^4.1.5"
|
|
@@ -113,7 +113,8 @@
|
|
|
113
113
|
"packageManager": "pnpm@10.33.2",
|
|
114
114
|
"pnpm": {
|
|
115
115
|
"overrides": {
|
|
116
|
-
"devtools-protocol": "0.0.
|
|
116
|
+
"devtools-protocol": "0.0.1629771",
|
|
117
|
+
"vite": "7.3.2"
|
|
117
118
|
},
|
|
118
119
|
"onlyBuiltDependencies": [
|
|
119
120
|
"esbuild"
|