@steipete/oracle 0.20.0 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/oracle-cli.js +66 -25
- package/dist/src/browser/actions/modelSelection.js +36 -4
- package/dist/src/browser/actions/thinkingTime.js +11 -5
- package/dist/src/browser/browserConnection.js +59 -0
- package/dist/src/browser/chatgptImages.js +1 -1
- package/dist/src/browser/chromeLifecycle.js +105 -36
- package/dist/src/browser/index.js +41 -8
- package/dist/src/browser/liveTabs.js +94 -29
- package/dist/src/browser/profileState.js +6 -1
- package/dist/src/browser/promptFingerprint.js +54 -0
- package/dist/src/browser/reattach.js +157 -108
- package/dist/src/browser/recoveryTarget.js +31 -6
- package/dist/src/browser/sessionRunner.js +30 -7
- package/dist/src/browser/targetClaim.js +2 -2
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/browserTabs.js +105 -20
- package/dist/src/cli/detach.js +10 -1
- package/dist/src/cli/detachedSession.js +36 -0
- package/dist/src/cli/errorUtils.js +9 -0
- package/dist/src/cli/sessionDisplay.js +11 -3
- package/dist/src/cli/sessionRunner.js +150 -37
- package/dist/src/mcp/tools/consult.js +1 -7
- package/dist/src/remote/client.js +71 -14
- package/dist/src/remote/health.js +1 -0
- package/dist/src/remote/server.js +52 -10
- package/dist/src/remote/types.js +13 -0
- package/dist/src/sessionManager.js +8 -1
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +9 -9
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -9,7 +9,6 @@ import { formatResponseMetadata, formatTransportMetadata } from "./sessionDispla
|
|
|
9
9
|
import { markErrorLogged } from "./errorUtils.js";
|
|
10
10
|
import { sendSessionNotification, deriveNotificationSettingsFromMetadata, } from "./notifier.js";
|
|
11
11
|
import { sessionStore } from "../sessionStore.js";
|
|
12
|
-
import { wait } from "../sessionManager.js";
|
|
13
12
|
import { runMultiModelApiSession } from "../oracle/multiModelRunner.js";
|
|
14
13
|
import { MODEL_CONFIGS, DEFAULT_SYSTEM_PROMPT } from "../oracle/config.js";
|
|
15
14
|
import { isKnownModel } from "../oracle/modelResolver.js";
|
|
@@ -22,31 +21,71 @@ import { sanitizeOscProgress } from "./oscUtils.js";
|
|
|
22
21
|
import { readFiles } from "../oracle/files.js";
|
|
23
22
|
import { cwd as getCwd } from "node:process";
|
|
24
23
|
import { resumeBrowserSession } from "../browser/reattach.js";
|
|
25
|
-
import { retireRecoveredBrowserTarget } from "../browser/recoveryTarget.js";
|
|
24
|
+
import { recoveryCaptureFromRuntime, retireCancelledBrowserTarget, retireRecoveredBrowserTarget, } from "../browser/recoveryTarget.js";
|
|
26
25
|
import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
|
|
27
|
-
import { estimateTokenCount } from "../browser/utils.js";
|
|
26
|
+
import { delay, estimateTokenCount } from "../browser/utils.js";
|
|
28
27
|
import { computeFileSha256, sanitizeArtifactFilename } from "../browser/artifacts.js";
|
|
29
28
|
import { formatElapsed } from "../oracle/format.js";
|
|
30
29
|
import { formatBrowserReattachGuidance } from "./reattachGuidance.js";
|
|
30
|
+
import { BrowserRunCancelledError } from "../oracle/errors.js";
|
|
31
31
|
const isTty = process.stdout.isTTY;
|
|
32
32
|
const dim = (text) => (isTty ? kleur.dim(text) : text);
|
|
33
|
-
export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, notifications, browserDeps, muteStdout = false, }) {
|
|
33
|
+
export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, notifications, browserDeps, muteStdout = false, signal, }) {
|
|
34
34
|
const writeInline = (chunk) => {
|
|
35
35
|
// Keep session logs intact while still echoing inline output to the user.
|
|
36
36
|
write(chunk);
|
|
37
37
|
return muteStdout ? true : process.stdout.write(chunk);
|
|
38
38
|
};
|
|
39
39
|
let currentBrowser = browserConfig
|
|
40
|
-
? {
|
|
40
|
+
? {
|
|
41
|
+
config: browserConfig,
|
|
42
|
+
...(mode === "browser" && sessionMeta.browser?.runtime?.submittedPromptHash !== undefined
|
|
43
|
+
? { runtime: { submittedPromptHash: null } }
|
|
44
|
+
: {}),
|
|
45
|
+
}
|
|
41
46
|
: sessionMeta.browser;
|
|
42
47
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
43
48
|
status: "running",
|
|
44
49
|
startedAt: new Date().toISOString(),
|
|
45
50
|
mode,
|
|
46
|
-
...(browserConfig ? { browser:
|
|
51
|
+
...(browserConfig ? { browser: currentBrowser } : {}),
|
|
47
52
|
});
|
|
48
53
|
const notificationSettings = notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env);
|
|
49
54
|
const modelForStatus = runOptions.model ?? sessionMeta.model;
|
|
55
|
+
const checkBrowserCancellation = () => {
|
|
56
|
+
if (mode === "browser" && signal?.aborted)
|
|
57
|
+
throw new BrowserRunCancelledError();
|
|
58
|
+
};
|
|
59
|
+
const finalizeBrowserCancellation = async (cancellationError) => {
|
|
60
|
+
const completedAt = new Date().toISOString();
|
|
61
|
+
log("Browser run cancelled.");
|
|
62
|
+
if (modelForStatus) {
|
|
63
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
64
|
+
status: "cancelled",
|
|
65
|
+
completedAt,
|
|
66
|
+
response: { status: "cancelled" },
|
|
67
|
+
error: undefined,
|
|
68
|
+
transport: undefined,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
72
|
+
status: "cancelled",
|
|
73
|
+
completedAt,
|
|
74
|
+
errorMessage: undefined,
|
|
75
|
+
mode,
|
|
76
|
+
browser: browserConfig
|
|
77
|
+
? {
|
|
78
|
+
...currentBrowser,
|
|
79
|
+
config: browserConfig,
|
|
80
|
+
}
|
|
81
|
+
: undefined,
|
|
82
|
+
response: { status: "cancelled" },
|
|
83
|
+
error: undefined,
|
|
84
|
+
transport: undefined,
|
|
85
|
+
});
|
|
86
|
+
await retireCancelledBrowserTarget(sessionMeta.id, recoveryCaptureFromRuntime(currentBrowser?.runtime), log);
|
|
87
|
+
throw cancellationError;
|
|
88
|
+
};
|
|
50
89
|
try {
|
|
51
90
|
if (mode === "browser") {
|
|
52
91
|
if (!browserConfig) {
|
|
@@ -80,13 +119,17 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
80
119
|
browserConfig,
|
|
81
120
|
cwd,
|
|
82
121
|
log,
|
|
122
|
+
signal,
|
|
83
123
|
}, runnerDeps);
|
|
124
|
+
checkBrowserCancellation();
|
|
84
125
|
const writtenOutputPath = await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
|
|
126
|
+
checkBrowserCancellation();
|
|
85
127
|
const outputArtifacts = await copyBrowserOutputArtifacts({
|
|
86
128
|
outputPath: writtenOutputPath,
|
|
87
129
|
savedFiles: runOptions.writeArtifacts ? result.savedFiles : undefined,
|
|
88
130
|
log,
|
|
89
131
|
});
|
|
132
|
+
checkBrowserCancellation();
|
|
90
133
|
const browserWarnings = [...(result.warnings ?? []), ...outputArtifacts.warnings];
|
|
91
134
|
await sendSessionNotification({
|
|
92
135
|
sessionId: sessionMeta.id,
|
|
@@ -96,13 +139,16 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
96
139
|
usage: result.usage,
|
|
97
140
|
characters: result.answerText?.length,
|
|
98
141
|
}, notificationSettings, log, result.answerText?.slice(0, 140));
|
|
142
|
+
checkBrowserCancellation();
|
|
99
143
|
if (modelForStatus) {
|
|
100
144
|
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
101
145
|
status: "completed",
|
|
102
146
|
completedAt: new Date().toISOString(),
|
|
103
147
|
usage: result.usage,
|
|
104
148
|
});
|
|
149
|
+
checkBrowserCancellation();
|
|
105
150
|
}
|
|
151
|
+
checkBrowserCancellation();
|
|
106
152
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
107
153
|
status: "completed",
|
|
108
154
|
completedAt: new Date().toISOString(),
|
|
@@ -401,6 +447,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
401
447
|
});
|
|
402
448
|
}
|
|
403
449
|
catch (error) {
|
|
450
|
+
if (error instanceof BrowserRunCancelledError) {
|
|
451
|
+
return await finalizeBrowserCancellation(error);
|
|
452
|
+
}
|
|
404
453
|
const message = formatError(error);
|
|
405
454
|
log(`ERROR: ${message}`);
|
|
406
455
|
markErrorLogged(error);
|
|
@@ -426,6 +475,11 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
426
475
|
const runtime = userError.details
|
|
427
476
|
?.runtime;
|
|
428
477
|
const recoverableRuntime = runtime ?? currentBrowser?.runtime;
|
|
478
|
+
currentBrowser = {
|
|
479
|
+
...currentBrowser,
|
|
480
|
+
config: browserConfig,
|
|
481
|
+
runtime: recoverableRuntime,
|
|
482
|
+
};
|
|
429
483
|
if (!hasRecoverableChatGptConversation(recoverableRuntime) &&
|
|
430
484
|
recoverableRuntime?.promptSubmitted !== true) {
|
|
431
485
|
log(dim("Chrome disconnected before a ChatGPT conversation was created; marking session error."));
|
|
@@ -495,22 +549,32 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
495
549
|
const connectionLostIntervalMs = configuredIntervalMs > 0
|
|
496
550
|
? configuredIntervalMs
|
|
497
551
|
: Math.max(1_000, Math.min(browserConfig?.timeoutMs ?? 30_000, 30_000));
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
552
|
+
let success;
|
|
553
|
+
try {
|
|
554
|
+
success = await autoReattachUntilComplete({
|
|
555
|
+
sessionMeta,
|
|
556
|
+
runtime: recoverableRuntime ?? undefined,
|
|
557
|
+
browserConfig: {
|
|
558
|
+
...browserConfig,
|
|
559
|
+
autoReattachIntervalMs: connectionLostIntervalMs,
|
|
560
|
+
autoReattachDelayMs: browserConfig?.autoReattachDelayMs ?? 0,
|
|
561
|
+
autoReattachTimeoutMs: browserConfig?.autoReattachTimeoutMs ?? browserConfig?.timeoutMs ?? 120_000,
|
|
562
|
+
},
|
|
563
|
+
browserMetadata: currentBrowser,
|
|
564
|
+
runOptions,
|
|
565
|
+
modelForStatus,
|
|
566
|
+
notificationSettings,
|
|
567
|
+
log,
|
|
568
|
+
maxAttempts: configuredIntervalMs > 0 ? undefined : 1,
|
|
569
|
+
signal,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
catch (recoveryError) {
|
|
573
|
+
if (signal?.aborted || recoveryError instanceof BrowserRunCancelledError) {
|
|
574
|
+
return await finalizeBrowserCancellation(new BrowserRunCancelledError());
|
|
575
|
+
}
|
|
576
|
+
throw recoveryError;
|
|
577
|
+
}
|
|
514
578
|
if (success) {
|
|
515
579
|
return;
|
|
516
580
|
}
|
|
@@ -531,6 +595,11 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
531
595
|
};
|
|
532
596
|
const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
|
|
533
597
|
const autoRuntime = runtime ?? currentBrowser?.runtime;
|
|
598
|
+
currentBrowser = {
|
|
599
|
+
...currentBrowser,
|
|
600
|
+
config: browserConfig,
|
|
601
|
+
runtime: autoRuntime,
|
|
602
|
+
};
|
|
534
603
|
const willAutoReattach = autoReattachIntervalMs > 0 && Boolean(autoRuntime);
|
|
535
604
|
if (willAutoReattach) {
|
|
536
605
|
if (modelForStatus) {
|
|
@@ -554,16 +623,26 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
554
623
|
response: timeoutResponse,
|
|
555
624
|
error: timeoutError,
|
|
556
625
|
});
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
626
|
+
let success;
|
|
627
|
+
try {
|
|
628
|
+
success = await autoReattachUntilComplete({
|
|
629
|
+
sessionMeta,
|
|
630
|
+
runtime: autoRuntime,
|
|
631
|
+
browserConfig,
|
|
632
|
+
browserMetadata: currentBrowser,
|
|
633
|
+
runOptions,
|
|
634
|
+
modelForStatus,
|
|
635
|
+
notificationSettings,
|
|
636
|
+
log,
|
|
637
|
+
signal,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
catch (recoveryError) {
|
|
641
|
+
if (signal?.aborted || recoveryError instanceof BrowserRunCancelledError) {
|
|
642
|
+
return await finalizeBrowserCancellation(new BrowserRunCancelledError());
|
|
643
|
+
}
|
|
644
|
+
throw recoveryError;
|
|
645
|
+
}
|
|
567
646
|
if (success) {
|
|
568
647
|
return;
|
|
569
648
|
}
|
|
@@ -976,7 +1055,7 @@ async function writeAssistantOutput(targetPath, content, log) {
|
|
|
976
1055
|
log(dim(`write-output failed (${reason}); session completed anyway.`));
|
|
977
1056
|
}
|
|
978
1057
|
}
|
|
979
|
-
async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, maxAttempts, }) {
|
|
1058
|
+
async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, maxAttempts, signal, }) {
|
|
980
1059
|
if (!runtime || !browserConfig) {
|
|
981
1060
|
log(dim("Auto-reattach disabled: missing runtime or browser config."));
|
|
982
1061
|
return false;
|
|
@@ -991,12 +1070,16 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
991
1070
|
120_000;
|
|
992
1071
|
const maxTotalMs = 2 * 60 * 60 * 1000; // 2h hard cap; avoid infinite polling by default.
|
|
993
1072
|
const maxDeadline = Date.now() + maxTotalMs;
|
|
1073
|
+
const checkCancellation = () => {
|
|
1074
|
+
if (signal?.aborted)
|
|
1075
|
+
throw new BrowserRunCancelledError();
|
|
1076
|
+
};
|
|
994
1077
|
const attemptLimit = typeof maxAttempts === "number" && maxAttempts > 0
|
|
995
1078
|
? Math.floor(maxAttempts)
|
|
996
1079
|
: Number.POSITIVE_INFINITY;
|
|
997
1080
|
if (delayMs > 0) {
|
|
998
1081
|
log(dim(`Auto-reattach starting in ${formatElapsed(delayMs)}...`));
|
|
999
|
-
await
|
|
1082
|
+
await delay(delayMs, signal);
|
|
1000
1083
|
}
|
|
1001
1084
|
if (Number.isFinite(attemptLimit)) {
|
|
1002
1085
|
log(dim(`Auto-reattach will try up to ${attemptLimit} attempt(s).`));
|
|
@@ -1012,6 +1095,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1012
1095
|
logger.verbose = true;
|
|
1013
1096
|
let attempt = 0;
|
|
1014
1097
|
for (;;) {
|
|
1098
|
+
checkCancellation();
|
|
1015
1099
|
const remainingBudgetMs = maxDeadline - Date.now();
|
|
1016
1100
|
if (remainingBudgetMs <= 0) {
|
|
1017
1101
|
log(dim(`Auto-reattach stopped after ${formatElapsed(maxTotalMs)} without capturing an answer.`));
|
|
@@ -1025,9 +1109,11 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1025
1109
|
...browserConfig,
|
|
1026
1110
|
timeoutMs,
|
|
1027
1111
|
};
|
|
1028
|
-
const result = await resumeBrowserSession(runtime, reattachConfig, logger, {
|
|
1112
|
+
const result = await awaitBrowserCancellation(resumeBrowserSession(runtime, reattachConfig, logger, {
|
|
1029
1113
|
promptPreview: sessionMeta.promptPreview,
|
|
1030
|
-
|
|
1114
|
+
signal,
|
|
1115
|
+
}), signal);
|
|
1116
|
+
checkCancellation();
|
|
1031
1117
|
captureSucceeded = true;
|
|
1032
1118
|
const answerText = result.answerMarkdown || result.answerText || "";
|
|
1033
1119
|
const outputTokens = estimateTokenCount(answerText);
|
|
@@ -1040,8 +1126,11 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1040
1126
|
existingArtifacts: sessionMeta.artifacts,
|
|
1041
1127
|
logger,
|
|
1042
1128
|
});
|
|
1129
|
+
checkCancellation();
|
|
1043
1130
|
const paths = await sessionStore.getPaths(sessionMeta.id);
|
|
1131
|
+
checkCancellation();
|
|
1044
1132
|
await fs.appendFile(paths.log, `[auto-reattach] captured assistant response on attempt ${attempt}\nAnswer:\n${answerText}\n`, "utf8");
|
|
1133
|
+
checkCancellation();
|
|
1045
1134
|
if (modelForStatus) {
|
|
1046
1135
|
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
1047
1136
|
status: "completed",
|
|
@@ -1053,8 +1142,10 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1053
1142
|
totalTokens: outputTokens,
|
|
1054
1143
|
},
|
|
1055
1144
|
});
|
|
1145
|
+
checkCancellation();
|
|
1056
1146
|
}
|
|
1057
1147
|
await writeAssistantOutput(runOptions.writeOutputPath, answerText, log);
|
|
1148
|
+
checkCancellation();
|
|
1058
1149
|
await sendSessionNotification({
|
|
1059
1150
|
sessionId: sessionMeta.id,
|
|
1060
1151
|
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
@@ -1066,6 +1157,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1066
1157
|
},
|
|
1067
1158
|
characters: answerText.length,
|
|
1068
1159
|
}, notificationSettings, log, answerText.slice(0, 140));
|
|
1160
|
+
checkCancellation();
|
|
1069
1161
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
1070
1162
|
status: "completed",
|
|
1071
1163
|
completedAt: new Date().toISOString(),
|
|
@@ -1091,6 +1183,9 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1091
1183
|
return true;
|
|
1092
1184
|
}
|
|
1093
1185
|
catch (error) {
|
|
1186
|
+
if (signal?.aborted || error instanceof BrowserRunCancelledError) {
|
|
1187
|
+
throw new BrowserRunCancelledError();
|
|
1188
|
+
}
|
|
1094
1189
|
if (captureSucceeded) {
|
|
1095
1190
|
const message = formatError(error);
|
|
1096
1191
|
if (modelForStatus) {
|
|
@@ -1128,7 +1223,25 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1128
1223
|
log(dim(`Auto-reattach stopped after ${formatElapsed(maxTotalMs)} without capturing an answer.`));
|
|
1129
1224
|
return false;
|
|
1130
1225
|
}
|
|
1131
|
-
await
|
|
1226
|
+
await delay(Math.min(intervalMs, remainingAfterAttemptMs), signal);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
async function awaitBrowserCancellation(task, signal) {
|
|
1230
|
+
if (!signal)
|
|
1231
|
+
return task;
|
|
1232
|
+
if (signal.aborted)
|
|
1233
|
+
throw new BrowserRunCancelledError();
|
|
1234
|
+
let abort;
|
|
1235
|
+
const cancelled = new Promise((_, reject) => {
|
|
1236
|
+
abort = () => reject(new BrowserRunCancelledError());
|
|
1237
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1238
|
+
});
|
|
1239
|
+
try {
|
|
1240
|
+
return await Promise.race([task, cancelled]);
|
|
1241
|
+
}
|
|
1242
|
+
finally {
|
|
1243
|
+
if (abort)
|
|
1244
|
+
signal.removeEventListener("abort", abort);
|
|
1132
1245
|
}
|
|
1133
1246
|
}
|
|
1134
1247
|
export function deriveModelOutputPath(basePath, model) {
|
|
@@ -299,6 +299,7 @@ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputMode
|
|
|
299
299
|
researchMode: browserResearchMode ?? configuredBrowser.researchMode,
|
|
300
300
|
archiveConversations: browserArchive ?? configuredBrowser.archiveConversations,
|
|
301
301
|
desiredModel: desiredModelLabel || mapModelToBrowserLabel(runModel),
|
|
302
|
+
modelIsImplicitDefault: !inputModel && !userConfig.model && !browserModelLabel,
|
|
302
303
|
};
|
|
303
304
|
}
|
|
304
305
|
export function buildConsultDryRunResolved({ resolvedEngine, runOptions, browserConfig, }) {
|
|
@@ -436,13 +437,6 @@ export async function runConsultTool(input, { log: requestLog, launchDetached =
|
|
|
436
437
|
const cwd = process.cwd();
|
|
437
438
|
const sendLog = (text, level = "info") => requestLog(level, { text, bytes: Buffer.byteLength(text, "utf8") }).catch(() => { });
|
|
438
439
|
const resolvedRemote = resolveRemoteServiceConfig({ userConfig, env: process.env });
|
|
439
|
-
const imageOutputPath = runOptions.generateImage ?? runOptions.outputPath;
|
|
440
|
-
if (resolvedEngine === "browser" && resolvedRemote.host && imageOutputPath) {
|
|
441
|
-
return {
|
|
442
|
-
isError: true,
|
|
443
|
-
content: textContent("ChatGPT image output is not supported with a remote browser service: generated files are not transferred back to the MCP caller. Unset ORACLE_REMOTE_HOST to generate images locally, or omit generateImage/outputPath."),
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
440
|
let browserConfig;
|
|
447
441
|
if (resolvedEngine === "browser") {
|
|
448
442
|
browserConfig = buildConsultBrowserConfig({
|
|
@@ -5,11 +5,12 @@ import { pipeline } from "node:stream/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { mkdir, readFile, rename, rm, stat } from "node:fs/promises";
|
|
7
7
|
import { appendArtifacts, computeFileSha256, resolveSessionArtifactsDir, resolveUniqueArtifactPath, sanitizeArtifactFilename, sanitizeArtifactMimeType, validateArtifactFile, } from "../browser/artifacts.js";
|
|
8
|
-
import { MAX_REMOTE_ARTIFACT_BYTES, } from "./types.js";
|
|
8
|
+
import { MAX_REMOTE_ARTIFACT_BYTES, pickRemoteImageMetadata, } from "./types.js";
|
|
9
9
|
import { materializeStagedFallbackBundle } from "../browser/prompt.js";
|
|
10
10
|
import { checkRemoteHealth } from "./health.js";
|
|
11
11
|
import { parseHostPort } from "../bridge/connection.js";
|
|
12
12
|
import { BrowserRunCancelledError } from "../oracle/errors.js";
|
|
13
|
+
import { resolveSiblingImagePath } from "../browser/chatgptImages.js";
|
|
13
14
|
export function createRemoteBrowserExecutor({ host, token }) {
|
|
14
15
|
// Return a drop-in replacement for runBrowserMode so the browser session runner can stay unchanged.
|
|
15
16
|
return async function remoteBrowserExecutor(options) {
|
|
@@ -17,13 +18,20 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
17
18
|
throw new Error("Web Search is a local browser pilot; --remote-host does not negotiate this capability yet. Use local Chrome or --browser-attach-running.");
|
|
18
19
|
}
|
|
19
20
|
const callerSignal = options.signal;
|
|
21
|
+
const imageOutputRequested = Boolean(options.generateImagePath || options.outputPath);
|
|
20
22
|
if (callerSignal?.aborted)
|
|
21
23
|
throw new BrowserRunCancelledError("Browser run cancelled before the request was sent.");
|
|
22
|
-
if (callerSignal) {
|
|
24
|
+
if (callerSignal || imageOutputRequested) {
|
|
23
25
|
const health = await checkRemoteHealth({ host, token, signal: callerSignal });
|
|
24
|
-
if (callerSignal
|
|
26
|
+
if (callerSignal?.aborted)
|
|
25
27
|
throw new BrowserRunCancelledError();
|
|
26
|
-
if (
|
|
28
|
+
if (imageOutputRequested &&
|
|
29
|
+
(!health.ok ||
|
|
30
|
+
health.capabilities?.generatedImages !== true ||
|
|
31
|
+
health.capabilities.artifactProtocolVersion !== 1)) {
|
|
32
|
+
throw new Error("Remote host cannot capture and transfer generated images; upgrade Oracle on the host and retry. The image request was not sent.");
|
|
33
|
+
}
|
|
34
|
+
if (callerSignal && health.capabilities?.runCancellation !== true)
|
|
27
35
|
throw new Error("Remote host does not support run cancellation; upgrade the host before using an AbortSignal.");
|
|
28
36
|
}
|
|
29
37
|
const payload = {
|
|
@@ -37,6 +45,7 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
37
45
|
sessionId: options.sessionId,
|
|
38
46
|
followUpPrompts: options.followUpPrompts,
|
|
39
47
|
cancelOnDisconnect: callerSignal ? true : undefined,
|
|
48
|
+
imageOutputRequested,
|
|
40
49
|
},
|
|
41
50
|
};
|
|
42
51
|
const body = Buffer.from(JSON.stringify(payload));
|
|
@@ -46,10 +55,12 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
46
55
|
reject(new Error("Browser run cancelled before the request was sent."));
|
|
47
56
|
return;
|
|
48
57
|
}
|
|
49
|
-
const
|
|
58
|
+
const transferredArtifacts = [];
|
|
50
59
|
const transferFailures = [];
|
|
51
60
|
const transferPromises = [];
|
|
52
61
|
let artifactTransferQueue = Promise.resolve();
|
|
62
|
+
const preferredImagePath = options.generateImagePath ?? options.outputPath;
|
|
63
|
+
let preferredImageIndex = 0;
|
|
53
64
|
let settled = false;
|
|
54
65
|
let resolved = null;
|
|
55
66
|
const fail = (error) => {
|
|
@@ -95,7 +106,7 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
95
106
|
resolved = result;
|
|
96
107
|
},
|
|
97
108
|
onArtifact: (artifact) => {
|
|
98
|
-
|
|
109
|
+
transferredArtifacts.push(artifact);
|
|
99
110
|
},
|
|
100
111
|
onArtifactFailure: (message) => {
|
|
101
112
|
transferFailures.push(message);
|
|
@@ -105,6 +116,12 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
105
116
|
artifactTransferQueue = queued.catch(() => undefined);
|
|
106
117
|
return queued;
|
|
107
118
|
},
|
|
119
|
+
resolvePreferredImagePath: (descriptor) => {
|
|
120
|
+
if (!preferredImagePath)
|
|
121
|
+
return undefined;
|
|
122
|
+
const extension = path.extname(descriptor.filename).slice(1) || "png";
|
|
123
|
+
return resolveSiblingImagePath(path.resolve(preferredImagePath), preferredImageIndex++, extension);
|
|
124
|
+
},
|
|
108
125
|
onError: fail,
|
|
109
126
|
});
|
|
110
127
|
if (transferPromise) {
|
|
@@ -123,9 +140,18 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
123
140
|
fail(new Error("Remote browser run completed without a result."));
|
|
124
141
|
return;
|
|
125
142
|
}
|
|
143
|
+
if (preferredImagePath) {
|
|
144
|
+
const images = transferredArtifacts.filter((artifact) => artifact.kind === "image");
|
|
145
|
+
if (images.length === 0 ||
|
|
146
|
+
images.length !== preferredImageIndex ||
|
|
147
|
+
resolved.warnings?.some((warning) => warning.code === "remote-image-registration-failed")) {
|
|
148
|
+
fail(new Error("Remote image output was not fully delivered. Inspect the bridge host's generated images and the artifact transfer diagnostics before retrying."));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
126
152
|
settled = true;
|
|
127
153
|
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
128
|
-
resolve(mergeTransferredArtifacts(resolved,
|
|
154
|
+
resolve(mergeTransferredArtifacts(resolved, transferredArtifacts, transferFailures));
|
|
129
155
|
})().catch(fail);
|
|
130
156
|
});
|
|
131
157
|
res.on("error", fail);
|
|
@@ -222,12 +248,16 @@ function handleEvent(params) {
|
|
|
222
248
|
}
|
|
223
249
|
if (event.type === "artifact-ready") {
|
|
224
250
|
const displayFilename = sanitizeArtifactFilename(String(event.artifact?.filename ?? ""), "artifact.bin");
|
|
251
|
+
const preferredPath = event.artifact.kind === "image"
|
|
252
|
+
? params.resolvePreferredImagePath(event.artifact)
|
|
253
|
+
: undefined;
|
|
225
254
|
const transfer = params.enqueueArtifactTransfer(() => transferRemoteArtifact({
|
|
226
255
|
hostname: params.hostname,
|
|
227
256
|
port: params.port,
|
|
228
257
|
token: params.token,
|
|
229
258
|
descriptor: event.artifact,
|
|
230
259
|
sessionId: params.options.sessionId,
|
|
260
|
+
preferredPath,
|
|
231
261
|
signal: params.options.signal,
|
|
232
262
|
log: params.options.log,
|
|
233
263
|
})
|
|
@@ -256,9 +286,11 @@ async function transferRemoteArtifact(params) {
|
|
|
256
286
|
validateRemoteArtifactDescriptor(params.descriptor);
|
|
257
287
|
const sessionId = params.sessionId ?? params.descriptor.runId;
|
|
258
288
|
const artifactsDir = resolveSessionArtifactsDir(sessionId);
|
|
259
|
-
await mkdir(artifactsDir, { recursive: true });
|
|
260
289
|
const filename = sanitizeArtifactFilename(params.descriptor.filename, `artifact-${params.descriptor.artifactId}.bin`);
|
|
261
|
-
const finalPath =
|
|
290
|
+
const finalPath = params.preferredPath
|
|
291
|
+
? path.resolve(params.preferredPath)
|
|
292
|
+
: await resolveUniqueArtifactPath(path.join(artifactsDir, filename));
|
|
293
|
+
await mkdir(path.dirname(finalPath), { recursive: true });
|
|
262
294
|
const partPath = `${finalPath}.part-${params.descriptor.artifactId}`;
|
|
263
295
|
const artifactPath = `/runs/${encodeURIComponent(params.descriptor.runId)}/artifacts/${encodeURIComponent(params.descriptor.artifactId)}`;
|
|
264
296
|
params.log?.(`[browser] Transferring artifact ${filename} from bridge host...`);
|
|
@@ -300,8 +332,7 @@ async function transferRemoteArtifact(params) {
|
|
|
300
332
|
await rename(partPath, finalPath);
|
|
301
333
|
params.log?.(`[browser] Transferred artifact to ${finalPath}`);
|
|
302
334
|
const publishedFilename = path.basename(finalPath);
|
|
303
|
-
|
|
304
|
-
kind: "file",
|
|
335
|
+
const baseArtifact = {
|
|
305
336
|
path: finalPath,
|
|
306
337
|
label: publishedFilename,
|
|
307
338
|
mimeType: sanitizeArtifactMimeType(params.descriptor.mimeType),
|
|
@@ -313,6 +344,17 @@ async function transferRemoteArtifact(params) {
|
|
|
313
344
|
origin: { mode: "bridge" },
|
|
314
345
|
url: "bridge-artifact",
|
|
315
346
|
finalUrl: "bridge-artifact",
|
|
347
|
+
};
|
|
348
|
+
if (params.descriptor.kind === "image") {
|
|
349
|
+
return {
|
|
350
|
+
...baseArtifact,
|
|
351
|
+
kind: "image",
|
|
352
|
+
...pickRemoteImageMetadata(params.descriptor.image),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
...baseArtifact,
|
|
357
|
+
kind: "file",
|
|
316
358
|
filename: publishedFilename,
|
|
317
359
|
};
|
|
318
360
|
}
|
|
@@ -371,7 +413,7 @@ async function downloadArtifactToFile(params) {
|
|
|
371
413
|
function validateRemoteArtifactDescriptor(descriptor) {
|
|
372
414
|
if (!descriptor ||
|
|
373
415
|
typeof descriptor !== "object" ||
|
|
374
|
-
descriptor.kind !== "file" ||
|
|
416
|
+
(descriptor.kind !== "file" && descriptor.kind !== "image") ||
|
|
375
417
|
typeof descriptor.runId !== "string" ||
|
|
376
418
|
!/^[a-zA-Z0-9_-]{1,128}$/.test(descriptor.runId) ||
|
|
377
419
|
typeof descriptor.artifactId !== "string" ||
|
|
@@ -385,9 +427,12 @@ function validateRemoteArtifactDescriptor(descriptor) {
|
|
|
385
427
|
throw new Error("invalid bridge artifact descriptor");
|
|
386
428
|
}
|
|
387
429
|
}
|
|
388
|
-
function mergeTransferredArtifacts(result,
|
|
389
|
-
const
|
|
430
|
+
function mergeTransferredArtifacts(result, transferredArtifacts, transferFailures) {
|
|
431
|
+
const transferredFiles = transferredArtifacts.filter((artifact) => artifact.kind === "file");
|
|
432
|
+
const transferredImages = transferredArtifacts.filter((artifact) => artifact.kind === "image");
|
|
433
|
+
const artifacts = appendArtifacts(result.artifacts, transferredArtifacts);
|
|
390
434
|
const savedFiles = appendSavedFiles(result.savedFiles, transferredFiles);
|
|
435
|
+
const savedImages = appendSavedImages(result.savedImages, transferredImages);
|
|
391
436
|
const warnings = [
|
|
392
437
|
...(result.warnings ?? []),
|
|
393
438
|
...transferFailures.map((message) => ({
|
|
@@ -400,9 +445,21 @@ function mergeTransferredArtifacts(result, transferredFiles, transferFailures) {
|
|
|
400
445
|
...result,
|
|
401
446
|
artifacts,
|
|
402
447
|
savedFiles,
|
|
448
|
+
savedImages,
|
|
403
449
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
404
450
|
};
|
|
405
451
|
}
|
|
452
|
+
function appendSavedImages(existing, additions) {
|
|
453
|
+
const merged = new Map();
|
|
454
|
+
for (const artifact of existing ?? []) {
|
|
455
|
+
merged.set(artifact.path, artifact);
|
|
456
|
+
}
|
|
457
|
+
for (const artifact of additions) {
|
|
458
|
+
merged.set(artifact.path, artifact);
|
|
459
|
+
}
|
|
460
|
+
const values = Array.from(merged.values());
|
|
461
|
+
return values.length > 0 ? values : undefined;
|
|
462
|
+
}
|
|
406
463
|
function appendSavedFiles(existing, additions) {
|
|
407
464
|
const merged = new Map();
|
|
408
465
|
for (const artifact of existing ?? []) {
|
|
@@ -93,6 +93,7 @@ function parseCapabilities(value) {
|
|
|
93
93
|
return {
|
|
94
94
|
...(raw.deferredFallbackBundling === true ? { deferredFallbackBundling: true } : {}),
|
|
95
95
|
...(raw.runCancellation === true ? { runCancellation: true } : {}),
|
|
96
|
+
...(raw.generatedImages === true ? { generatedImages: true } : {}),
|
|
96
97
|
artifactTransfer: true,
|
|
97
98
|
artifactProtocolVersion,
|
|
98
99
|
maxArtifactBytes: Math.min(maxArtifactBytes, MAX_REMOTE_ARTIFACT_BYTES),
|