@steipete/oracle 0.17.3 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin/oracle-cli.js +75 -64
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/src/browser/actions/archiveConversation.js +31 -7
- package/dist/src/browser/actions/assistantResponse.js +43 -1
- package/dist/src/browser/actions/attachmentContext.js +239 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
- package/dist/src/browser/actions/attachmentEvidence.js +78 -0
- package/dist/src/browser/actions/attachmentProgress.js +41 -0
- package/dist/src/browser/actions/attachmentPrompt.js +130 -0
- package/dist/src/browser/actions/attachments.js +306 -355
- package/dist/src/browser/actions/deepResearch.js +2 -1
- package/dist/src/browser/actions/modelSelection.js +13 -3
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +361 -186
- package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
- package/dist/src/browser/actions/thinkingStatus.js +2 -5
- package/dist/src/browser/actions/thinkingTime.js +267 -15
- package/dist/src/browser/attachRunning.js +46 -2
- package/dist/src/browser/attachmentValidation.js +53 -0
- package/dist/src/browser/cancellation.js +105 -0
- package/dist/src/browser/chatgptImages.js +2 -0
- package/dist/src/browser/chatgptThrottle.js +104 -0
- package/dist/src/browser/chromeLifecycle.js +267 -43
- package/dist/src/browser/config.js +15 -3
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +447 -263
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/policies.js +6 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +88 -17
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
- package/dist/src/browser/reattach.js +14 -4
- package/dist/src/browser/sessionRunner.js +71 -11
- package/dist/src/browser/tabLeaseRegistry.js +414 -38
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +40 -15
- package/dist/src/cli/browserDefaults.js +13 -0
- package/dist/src/cli/browserTabs.js +12 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -47
- package/dist/src/cli/harvestIntegrity.js +140 -0
- package/dist/src/cli/projectSources.js +5 -3
- package/dist/src/cli/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +9 -2
- package/dist/src/cli/sessionRunner.js +91 -7
- package/dist/src/config.js +1 -0
- package/dist/src/gemini-web/client.js +67 -21
- package/dist/src/gemini-web/executor.js +4 -0
- package/dist/src/gemini-web/http.js +15 -0
- package/dist/src/gemini-web/models.js +15 -11
- package/dist/src/mcp/server.js +11 -14
- package/dist/src/mcp/tools/chatgptImage.js +4 -4
- package/dist/src/mcp/tools/consult.js +97 -26
- package/dist/src/mcp/tools/projectSources.js +4 -6
- package/dist/src/mcp/tools/sessionResources.js +1 -1
- package/dist/src/mcp/tools/sessions.js +2 -2
- package/dist/src/mcp/tools/wait.js +182 -0
- package/dist/src/mcp/types.js +7 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/remote/client.js +70 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +351 -72
- package/dist/src/sessionManager.js +36 -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 +25 -23
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -683
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -400
- package/dist/docs-site/browser-mode.html +0 -588
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -436
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -368
- package/dist/docs-site/gemini.html +0 -379
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -595
- package/dist/docs-site/mcp.html +0 -368
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -387
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/profileSync.js +0 -141
- package/dist/src/browser/windowsCookies.js +0 -219
|
@@ -8,7 +8,12 @@ import { randomBytes, randomUUID } from "node:crypto";
|
|
|
8
8
|
import { spawn, spawnSync } from "node:child_process";
|
|
9
9
|
import { mkdtemp, rm, mkdir, writeFile, stat, realpath } from "node:fs/promises";
|
|
10
10
|
import chalk from "chalk";
|
|
11
|
+
import { materializeStagedFallbackBundle } from "../browser/prompt.js";
|
|
11
12
|
import { runBrowserMode } from "../browserMode.js";
|
|
13
|
+
import { resolveBrowserConfig } from "../browser/config.js";
|
|
14
|
+
import { RunSlots } from "./runSlots.js";
|
|
15
|
+
export { RunSlots } from "./runSlots.js";
|
|
16
|
+
import { loadUserConfig } from "../config.js";
|
|
12
17
|
import { MAX_REMOTE_ARTIFACT_BYTES } from "./types.js";
|
|
13
18
|
import { getCookies } from "@steipete/sweet-cookie";
|
|
14
19
|
import { CHATGPT_URL } from "../browser/constants.js";
|
|
@@ -20,6 +25,8 @@ import { computeFileSha256, sanitizeArtifactFilename, sanitizeArtifactMimeType,
|
|
|
20
25
|
const ARTIFACT_PROTOCOL_VERSION = 1;
|
|
21
26
|
const REMOTE_ARTIFACT_TTL_MS = 30 * 60 * 1000;
|
|
22
27
|
const ARTIFACT_CAPABILITIES = {
|
|
28
|
+
runCancellation: true,
|
|
29
|
+
deferredFallbackBundling: true,
|
|
23
30
|
artifactTransfer: true,
|
|
24
31
|
artifactProtocolVersion: ARTIFACT_PROTOCOL_VERSION,
|
|
25
32
|
maxArtifactBytes: MAX_REMOTE_ARTIFACT_BYTES,
|
|
@@ -40,8 +47,29 @@ async function findAvailablePort() {
|
|
|
40
47
|
});
|
|
41
48
|
});
|
|
42
49
|
}
|
|
50
|
+
function validateAdmissionOptions(options) {
|
|
51
|
+
if (options.maxConcurrentRuns === undefined && options.maxQueuedRuns !== undefined)
|
|
52
|
+
throw new Error("--max-queued-runs requires --max-concurrent-runs.");
|
|
53
|
+
if (options.maxConcurrentRuns !== undefined &&
|
|
54
|
+
(!Number.isSafeInteger(options.maxConcurrentRuns) || options.maxConcurrentRuns < 1))
|
|
55
|
+
throw new Error("--max-concurrent-runs must be a positive integer.");
|
|
56
|
+
if (options.maxQueuedRuns !== undefined &&
|
|
57
|
+
(!Number.isSafeInteger(options.maxQueuedRuns) || options.maxQueuedRuns < 0))
|
|
58
|
+
throw new Error("--max-queued-runs must be a nonnegative integer.");
|
|
59
|
+
}
|
|
43
60
|
export async function createRemoteServer(options = {}, deps = {}) {
|
|
44
61
|
const runBrowser = deps.runBrowser ?? runBrowserMode;
|
|
62
|
+
const attachedBrowser = usesHostBrowserAttachment(options.browserConfig);
|
|
63
|
+
const manualLoginDefault = !attachedBrowser && options.manualLoginDefault;
|
|
64
|
+
const hostBrowserConfig = options.browserConfig
|
|
65
|
+
? {
|
|
66
|
+
attachRunning: options.browserConfig?.attachRunning,
|
|
67
|
+
remoteChrome: options.browserConfig?.remoteChrome
|
|
68
|
+
? { ...options.browserConfig.remoteChrome }
|
|
69
|
+
: undefined,
|
|
70
|
+
approvalWaitMs: options.browserConfig?.approvalWaitMs,
|
|
71
|
+
}
|
|
72
|
+
: {};
|
|
45
73
|
const server = http.createServer();
|
|
46
74
|
const logger = options.logger ?? console.log;
|
|
47
75
|
const authToken = options.token ?? randomBytes(16).toString("hex");
|
|
@@ -50,8 +78,20 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
50
78
|
const color = process.stdout.isTTY
|
|
51
79
|
? (formatter, msg) => formatter(msg)
|
|
52
80
|
: (_formatter, msg) => msg;
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
validateAdmissionOptions(options);
|
|
82
|
+
const admissionEnabled = options.maxConcurrentRuns !== undefined;
|
|
83
|
+
const hostConfig = admissionEnabled ? (await loadUserConfig()).config.browser : undefined;
|
|
84
|
+
const browserTabCap = admissionEnabled
|
|
85
|
+
? resolveBrowserConfig({ maxConcurrentTabs: hostConfig?.maxConcurrentTabs }).maxConcurrentTabs
|
|
86
|
+
: undefined;
|
|
87
|
+
const requestedConcurrency = options.maxConcurrentRuns ?? 1;
|
|
88
|
+
const effectiveConcurrency = Math.min(requestedConcurrency, browserTabCap ?? 1);
|
|
89
|
+
if (effectiveConcurrency < requestedConcurrency) {
|
|
90
|
+
logger(`[serve] Admitting ${effectiveConcurrency} concurrent run(s): the host tab cap is lower than the requested ${requestedConcurrency}.`);
|
|
91
|
+
}
|
|
92
|
+
const slots = new RunSlots(effectiveConcurrency, admissionEnabled ? (options.maxQueuedRuns ?? 8) : 0);
|
|
93
|
+
let legacyBusy = false;
|
|
94
|
+
const controllers = new Set();
|
|
55
95
|
const artifactRegistry = new Map();
|
|
56
96
|
if (!process.listenerCount("unhandledRejection")) {
|
|
57
97
|
process.on("unhandledRejection", (reason) => {
|
|
@@ -81,6 +121,13 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
81
121
|
version: getCliVersion(),
|
|
82
122
|
uptimeSeconds: Math.round((Date.now() - startedAt) / 1000),
|
|
83
123
|
capabilities: ARTIFACT_CAPABILITIES,
|
|
124
|
+
// So a caller can decide whether to send work now or later, instead of
|
|
125
|
+
// discovering the answer by being queued.
|
|
126
|
+
activeRuns: slots.activeCount,
|
|
127
|
+
queuedRuns: slots.queuedCount,
|
|
128
|
+
maxConcurrentRuns: slots.capacity,
|
|
129
|
+
maxQueuedRuns: slots.queueCapacity,
|
|
130
|
+
admissionMode: admissionEnabled ? "queue" : "legacy",
|
|
84
131
|
}));
|
|
85
132
|
return;
|
|
86
133
|
}
|
|
@@ -112,75 +159,146 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
112
159
|
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
113
160
|
return;
|
|
114
161
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
res.end(JSON.stringify({ error: "busy" }));
|
|
162
|
+
const refuse = () => {
|
|
163
|
+
res.writeHead(admissionEnabled ? 503 : 409, {
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
...(admissionEnabled ? { "Retry-After": "60" } : {}),
|
|
166
|
+
});
|
|
167
|
+
res.end(JSON.stringify({ error: admissionEnabled ? "queue_full" : "busy" }));
|
|
168
|
+
};
|
|
169
|
+
if (admissionEnabled ? slots.isSaturated : legacyBusy) {
|
|
170
|
+
refuse();
|
|
121
171
|
return;
|
|
122
172
|
}
|
|
123
|
-
|
|
173
|
+
if (!admissionEnabled)
|
|
174
|
+
legacyBusy = true;
|
|
124
175
|
const runStartedAt = Date.now();
|
|
125
|
-
|
|
176
|
+
const controller = new AbortController();
|
|
177
|
+
let signal = admissionEnabled ? controller.signal : undefined;
|
|
178
|
+
const onClientGone = () => controller.abort();
|
|
179
|
+
res.on("close", onClientGone);
|
|
180
|
+
req.on("aborted", onClientGone);
|
|
181
|
+
controllers.add(controller);
|
|
182
|
+
const detachController = () => {
|
|
183
|
+
res.off("close", onClientGone);
|
|
184
|
+
req.off("aborted", onClientGone);
|
|
185
|
+
controllers.delete(controller);
|
|
186
|
+
};
|
|
187
|
+
let queuePosition = 0;
|
|
188
|
+
let slotReady = false;
|
|
189
|
+
let pendingSlot;
|
|
190
|
+
let release;
|
|
191
|
+
const reserve = () => {
|
|
192
|
+
queuePosition = slots.positionFor();
|
|
193
|
+
pendingSlot = slots.acquire(signal).then((releaseSlot) => {
|
|
194
|
+
slotReady = true;
|
|
195
|
+
return releaseSlot;
|
|
196
|
+
});
|
|
197
|
+
// A queued request can disconnect before its body finishes parsing.
|
|
198
|
+
void pendingSlot.catch(() => undefined);
|
|
199
|
+
};
|
|
200
|
+
const abandon = async () => {
|
|
201
|
+
controller.abort();
|
|
202
|
+
if (pendingSlot)
|
|
203
|
+
await pendingSlot.then((releaseSlot) => releaseSlot(), () => { });
|
|
204
|
+
detachController();
|
|
205
|
+
legacyBusy = false;
|
|
206
|
+
};
|
|
207
|
+
// Opt-in admission includes body reception, bounding buffered requests and
|
|
208
|
+
// preserving arrival order even when a later caller uploads faster.
|
|
209
|
+
if (admissionEnabled)
|
|
210
|
+
reserve();
|
|
211
|
+
let payload;
|
|
126
212
|
try {
|
|
127
|
-
|
|
128
|
-
payload
|
|
129
|
-
|
|
213
|
+
payload = JSON.parse(await readRequestBody(req, controller.signal));
|
|
214
|
+
if (!payload || typeof payload.prompt !== "string")
|
|
215
|
+
throw new Error("Missing prompt");
|
|
216
|
+
payload.options ??= {};
|
|
217
|
+
if (payload.browserConfig)
|
|
130
218
|
payload.browserConfig.url = normalizeChatgptUrl(payload.browserConfig.url, CHATGPT_URL);
|
|
131
|
-
}
|
|
132
219
|
}
|
|
133
220
|
catch {
|
|
134
|
-
|
|
135
|
-
res.
|
|
136
|
-
|
|
221
|
+
await abandon();
|
|
222
|
+
if (!res.destroyed) {
|
|
223
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
224
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
225
|
+
}
|
|
137
226
|
return;
|
|
138
227
|
}
|
|
228
|
+
if (res.destroyed || req.aborted) {
|
|
229
|
+
await abandon();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!admissionEnabled) {
|
|
233
|
+
signal = payload.options.cancelOnDisconnect === true ? controller.signal : undefined;
|
|
234
|
+
reserve();
|
|
235
|
+
}
|
|
139
236
|
res.writeHead(200, { "Content-Type": "application/x-ndjson" });
|
|
140
|
-
const runId = randomUUID();
|
|
141
|
-
logger(`[serve] Accepted run ${runId} from ${formatSocket(req)} (prompt ${payload?.prompt?.length ?? 0} chars)`);
|
|
142
|
-
// Each run gets an isolated temp dir so attachments/logs don't collide.
|
|
143
|
-
const runDir = await mkdtemp(path.join(os.tmpdir(), `oracle-serve-${runId}-`));
|
|
144
|
-
const attachmentDir = path.join(runDir, "attachments");
|
|
145
|
-
await mkdir(attachmentDir, { recursive: true });
|
|
146
237
|
const sendEvent = (event) => {
|
|
147
|
-
res.
|
|
238
|
+
if (!res.destroyed && !res.writableEnded)
|
|
239
|
+
res.write(`${JSON.stringify(event)}\n`);
|
|
148
240
|
};
|
|
149
|
-
|
|
241
|
+
if (queuePosition > 0 && !slotReady)
|
|
242
|
+
sendEvent({
|
|
243
|
+
type: "log",
|
|
244
|
+
message: `[serve] Waiting for a browser slot (position ${queuePosition}; ${slots.activeCount} active).`,
|
|
245
|
+
});
|
|
246
|
+
try {
|
|
247
|
+
release = await pendingSlot;
|
|
248
|
+
signal?.throwIfAborted();
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
release?.();
|
|
252
|
+
legacyBusy = false;
|
|
253
|
+
sendEvent({ type: "error", message: error instanceof Error ? error.message : String(error) });
|
|
254
|
+
detachController();
|
|
255
|
+
if (!res.destroyed)
|
|
256
|
+
res.end();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const runId = randomUUID();
|
|
260
|
+
let runDir = "";
|
|
150
261
|
let fallbackSubmission;
|
|
151
262
|
try {
|
|
263
|
+
signal?.throwIfAborted();
|
|
264
|
+
logger(`[serve] Accepted run ${runId} from ${formatSocket(req)} (prompt ${payload.prompt.length} chars)`);
|
|
265
|
+
runDir = await mkdtemp(path.join(os.tmpdir(), `oracle-serve-${runId}-`));
|
|
266
|
+
const attachmentDir = path.join(runDir, "attachments");
|
|
267
|
+
signal?.throwIfAborted();
|
|
152
268
|
const attachmentsPayload = Array.isArray(payload.attachments) ? payload.attachments : [];
|
|
153
|
-
|
|
154
|
-
const safeName = sanitizeName(attachment.fileName ?? `attachment-${index + 1}`);
|
|
155
|
-
const filePath = path.join(attachmentDir, safeName);
|
|
156
|
-
await writeFile(filePath, Buffer.from(attachment.contentBase64, "base64"));
|
|
157
|
-
attachments.push({
|
|
158
|
-
path: filePath,
|
|
159
|
-
displayPath: attachment.displayPath,
|
|
160
|
-
sizeBytes: attachment.sizeBytes,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
269
|
+
const attachments = await stageRemoteAttachments(attachmentsPayload, attachmentDir, "attachment");
|
|
163
270
|
if (payload.fallbackSubmission) {
|
|
164
271
|
const fallbackAttachmentDir = path.join(runDir, "fallback-attachments");
|
|
165
|
-
await mkdir(fallbackAttachmentDir, { recursive: true });
|
|
166
|
-
const fallbackAttachments = [];
|
|
167
272
|
const fallbackPayload = Array.isArray(payload.fallbackSubmission.attachments)
|
|
168
273
|
? payload.fallbackSubmission.attachments
|
|
169
274
|
: [];
|
|
170
|
-
|
|
171
|
-
const safeName = sanitizeName(attachment.fileName ?? `fallback-attachment-${index + 1}`);
|
|
172
|
-
const filePath = path.join(fallbackAttachmentDir, safeName);
|
|
173
|
-
await writeFile(filePath, Buffer.from(attachment.contentBase64, "base64"));
|
|
174
|
-
fallbackAttachments.push({
|
|
175
|
-
path: filePath,
|
|
176
|
-
displayPath: attachment.displayPath,
|
|
177
|
-
sizeBytes: attachment.sizeBytes,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
275
|
+
const fallbackAttachments = await stageRemoteAttachments(fallbackPayload, fallbackAttachmentDir, "fallback-attachment");
|
|
180
276
|
fallbackSubmission = {
|
|
181
277
|
prompt: payload.fallbackSubmission.prompt,
|
|
182
278
|
attachments: fallbackAttachments,
|
|
183
279
|
};
|
|
280
|
+
const pendingBundle = payload.fallbackSubmission.bundle;
|
|
281
|
+
if (pendingBundle) {
|
|
282
|
+
if (!["text", "zip"].includes(pendingBundle.format) ||
|
|
283
|
+
!["all", "text-only"].includes(pendingBundle.scope)) {
|
|
284
|
+
throw new Error("Invalid fallback bundle format or scope.");
|
|
285
|
+
}
|
|
286
|
+
let preparation;
|
|
287
|
+
const prepare = async () => {
|
|
288
|
+
if (!fallbackSubmission)
|
|
289
|
+
return;
|
|
290
|
+
const prepared = await materializeStagedFallbackBundle({
|
|
291
|
+
composerText: fallbackSubmission.prompt,
|
|
292
|
+
attachments: fallbackSubmission.attachments,
|
|
293
|
+
format: pendingBundle.format,
|
|
294
|
+
scope: pendingBundle.scope,
|
|
295
|
+
bundleParentDir: runDir,
|
|
296
|
+
});
|
|
297
|
+
fallbackSubmission.prompt = prepared.composerText;
|
|
298
|
+
fallbackSubmission.attachments = prepared.attachments;
|
|
299
|
+
};
|
|
300
|
+
fallbackSubmission.prepare = () => (preparation ??= prepare());
|
|
301
|
+
}
|
|
184
302
|
}
|
|
185
303
|
// Reuse the existing browser logger surface so clients see the same log stream.
|
|
186
304
|
const automationLogger = ((message) => {
|
|
@@ -192,17 +310,35 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
192
310
|
// Preserve an explicit request to leave the completed conversation tab
|
|
193
311
|
// open before the service forces `keepBrowser` for process lifetime.
|
|
194
312
|
const clientRequestedKeepBrowser = payload.browserConfig?.keepBrowser === true;
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
313
|
+
// A client may describe the conversation it wants; it may not describe this
|
|
314
|
+
// machine. Rebuilding the config from an allowlist rather than deleting
|
|
315
|
+
// known-bad keys makes that the default: a field added to
|
|
316
|
+
// BrowserSessionConfig later is host-owned until someone decides otherwise,
|
|
317
|
+
// instead of reaching Chrome the moment it exists.
|
|
318
|
+
//
|
|
319
|
+
// The distinction is not stylistic. `chromePath` names an executable this
|
|
320
|
+
// process spawns, `remoteChrome` names a debugger to attach to,
|
|
321
|
+
// `copyProfileSource` names a directory to copy credentials out of, and
|
|
322
|
+
// `browserTabRef`/`attachRunning` select a tab that may belong to somebody
|
|
323
|
+
// else's run. With those reachable, a bridge token is not a permission to
|
|
324
|
+
// ask ChatGPT a question — it is a permission to run code here.
|
|
325
|
+
payload.browserConfig = {
|
|
326
|
+
...pickClientBrowserConfig(payload.browserConfig),
|
|
327
|
+
...hostBrowserConfig,
|
|
328
|
+
};
|
|
329
|
+
// Remote runs rely on the host's authentication policy; never accept cookie payloads from clients.
|
|
330
|
+
payload.browserConfig.inlineCookies = null;
|
|
331
|
+
payload.browserConfig.inlineCookiesSource = null;
|
|
332
|
+
payload.browserConfig.cookieSync = !attachedBrowser && options.cookieSyncDefault === true;
|
|
333
|
+
const clientSession = typeof payload.options.sessionId === "string"
|
|
334
|
+
? payload.options.sessionId.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 32)
|
|
335
|
+
: "remote";
|
|
336
|
+
payload.options.sessionId = `${clientSession || "remote"}-${runId}`;
|
|
337
|
+
if (browserTabCap !== undefined)
|
|
338
|
+
payload.browserConfig.maxConcurrentTabs = browserTabCap;
|
|
339
|
+
signal?.throwIfAborted();
|
|
204
340
|
// Enforce manual-login profile when cookie sync is unavailable (e.g., Windows/WSL).
|
|
205
|
-
if (
|
|
341
|
+
if (manualLoginDefault) {
|
|
206
342
|
payload.browserConfig.manualLogin = true;
|
|
207
343
|
payload.browserConfig.manualLoginProfileDir = options.manualLoginProfileDir;
|
|
208
344
|
payload.browserConfig.keepBrowser = true;
|
|
@@ -215,17 +351,20 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
215
351
|
attachments,
|
|
216
352
|
fallbackSubmission,
|
|
217
353
|
config: payload.browserConfig,
|
|
354
|
+
signal,
|
|
218
355
|
// `keepBrowser` above preserves the authenticated shared Chrome
|
|
219
356
|
// process. This separate service policy closes only a successfully
|
|
220
357
|
// captured tab owned by this run, preventing one renderer leak per
|
|
221
358
|
// request while incomplete/reattachable tabs remain untouched.
|
|
222
|
-
closeOwnedTabOnComplete: Boolean(
|
|
359
|
+
closeOwnedTabOnComplete: Boolean(manualLoginDefault && !clientRequestedKeepBrowser),
|
|
360
|
+
closeOwnedTabOnCancel: !clientRequestedKeepBrowser,
|
|
223
361
|
log: automationLogger,
|
|
224
362
|
heartbeatIntervalMs: payload.options.heartbeatIntervalMs,
|
|
225
363
|
verbose: payload.options.verbose,
|
|
226
364
|
sessionId: payload.options.sessionId,
|
|
227
365
|
followUpPrompts: payload.options.followUpPrompts,
|
|
228
366
|
});
|
|
367
|
+
signal?.throwIfAborted();
|
|
229
368
|
const artifactRegistration = await registerRemoteArtifacts({
|
|
230
369
|
runId,
|
|
231
370
|
result,
|
|
@@ -252,15 +391,24 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
252
391
|
: ""}`);
|
|
253
392
|
}
|
|
254
393
|
catch (error) {
|
|
255
|
-
const
|
|
394
|
+
const cancelled = signal?.aborted === true;
|
|
395
|
+
const message = cancelled
|
|
396
|
+
? "Browser run cancelled."
|
|
397
|
+
: error instanceof Error
|
|
398
|
+
? error.message
|
|
399
|
+
: String(error);
|
|
256
400
|
sendEvent({ type: "error", message });
|
|
257
|
-
logger(`[serve] Run ${runId} failed after ${Date.now() - runStartedAt}ms: ${message}`);
|
|
401
|
+
logger(`[serve] Run ${runId} ${cancelled ? "cancelled" : "failed"} after ${Date.now() - runStartedAt}ms: ${message}`);
|
|
258
402
|
}
|
|
259
403
|
finally {
|
|
260
|
-
|
|
261
|
-
|
|
404
|
+
detachController();
|
|
405
|
+
legacyBusy = false;
|
|
406
|
+
release();
|
|
407
|
+
if (!res.destroyed)
|
|
408
|
+
res.end();
|
|
262
409
|
try {
|
|
263
|
-
|
|
410
|
+
if (runDir)
|
|
411
|
+
await rm(runDir, { recursive: true, force: true });
|
|
264
412
|
}
|
|
265
413
|
catch {
|
|
266
414
|
// ignore cleanup errors
|
|
@@ -285,6 +433,8 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
285
433
|
port: address.port,
|
|
286
434
|
token: authToken,
|
|
287
435
|
async close() {
|
|
436
|
+
for (const controller of controllers)
|
|
437
|
+
controller.abort();
|
|
288
438
|
await new Promise((resolve, reject) => {
|
|
289
439
|
server.close((err) => (err ? reject(err) : resolve()));
|
|
290
440
|
});
|
|
@@ -292,8 +442,12 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
292
442
|
};
|
|
293
443
|
}
|
|
294
444
|
export async function serveRemote(options = {}) {
|
|
445
|
+
validateAdmissionOptions(options);
|
|
295
446
|
const manualProfileDir = options.manualLoginProfileDir ?? path.join(os.homedir(), ".oracle", "browser-profile");
|
|
296
|
-
const preferManualLogin = options.manualLoginDefault ||
|
|
447
|
+
const preferManualLogin = options.manualLoginDefault ||
|
|
448
|
+
options.cookieSyncDefault !== true ||
|
|
449
|
+
process.platform === "win32" ||
|
|
450
|
+
isWsl();
|
|
297
451
|
let cookies = null;
|
|
298
452
|
let opened = false;
|
|
299
453
|
if (isWsl() && process.env.ORACLE_ALLOW_WSL_SERVE !== "1") {
|
|
@@ -302,7 +456,13 @@ export async function serveRemote(options = {}) {
|
|
|
302
456
|
console.log("Alternatively, start Windows Chrome with --remote-debugging-port=9222 and use `--remote-chrome <windows-ip>:9222`.");
|
|
303
457
|
return;
|
|
304
458
|
}
|
|
459
|
+
if (usesHostBrowserAttachment(options.browserConfig)) {
|
|
460
|
+
console.log("Using the host browser attachment; skipping local Chrome login/bootstrap.");
|
|
461
|
+
await runRemoteServer(options);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
305
464
|
if (!preferManualLogin) {
|
|
465
|
+
console.log("Warning: Chrome cookie copying can invalidate an active ChatGPT session when tokens rotate. Prefer the default dedicated manual-login profile when possible.");
|
|
306
466
|
// Warm-up: ensure this host has a ChatGPT login before accepting runs.
|
|
307
467
|
const result = await loadLocalChatgptCookies(console.log, CHATGPT_URL);
|
|
308
468
|
cookies = result.cookies;
|
|
@@ -344,11 +504,17 @@ export async function serveRemote(options = {}) {
|
|
|
344
504
|
else {
|
|
345
505
|
console.log(`Detected ${cookies.length} ChatGPT cookies on this host; runs will reuse this session.`);
|
|
346
506
|
}
|
|
347
|
-
|
|
507
|
+
await runRemoteServer({
|
|
348
508
|
...options,
|
|
349
509
|
manualLoginDefault: preferManualLogin,
|
|
350
510
|
manualLoginProfileDir: manualProfileDir,
|
|
351
511
|
});
|
|
512
|
+
}
|
|
513
|
+
function usesHostBrowserAttachment(config) {
|
|
514
|
+
return config?.attachRunning === true || Boolean(config?.remoteChrome);
|
|
515
|
+
}
|
|
516
|
+
async function runRemoteServer(options) {
|
|
517
|
+
const server = await createRemoteServer(options);
|
|
352
518
|
await new Promise((resolve) => {
|
|
353
519
|
const shutdown = () => {
|
|
354
520
|
console.log("Shutting down remote service...");
|
|
@@ -545,16 +711,99 @@ function classifySourceUrlKind(sourceUrl) {
|
|
|
545
711
|
}
|
|
546
712
|
return "chatgpt-file-endpoint";
|
|
547
713
|
}
|
|
548
|
-
async function readRequestBody(req) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
714
|
+
async function readRequestBody(req, signal) {
|
|
715
|
+
if (signal?.aborted) {
|
|
716
|
+
req.destroy();
|
|
717
|
+
signal.throwIfAborted();
|
|
718
|
+
}
|
|
719
|
+
const onAbort = () => req.destroy();
|
|
720
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
721
|
+
try {
|
|
722
|
+
const chunks = [];
|
|
723
|
+
for await (const chunk of req) {
|
|
724
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
725
|
+
}
|
|
726
|
+
signal?.throwIfAborted();
|
|
727
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
728
|
+
}
|
|
729
|
+
finally {
|
|
730
|
+
signal?.removeEventListener("abort", onAbort);
|
|
552
731
|
}
|
|
553
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
554
732
|
}
|
|
555
|
-
|
|
556
|
-
|
|
733
|
+
/**
|
|
734
|
+
* Fields a remote caller may set: they describe the conversation and its time
|
|
735
|
+
* budgets. Everything else on BrowserSessionConfig — executable paths, profile
|
|
736
|
+
* directories, debugger endpoints, tab selection, window mode, cookie policy,
|
|
737
|
+
* and the shared-profile concurrency limits — is the host's to decide.
|
|
738
|
+
*/
|
|
739
|
+
const CLIENT_BROWSER_CONFIG_FIELDS = [
|
|
740
|
+
"chatgptUrl",
|
|
741
|
+
"url",
|
|
742
|
+
"desiredModel",
|
|
743
|
+
"modelStrategy",
|
|
744
|
+
"thinkingTime",
|
|
745
|
+
"researchMode",
|
|
746
|
+
"archiveConversations",
|
|
747
|
+
"resumeConversationUrl",
|
|
748
|
+
"timeoutMs",
|
|
749
|
+
"inputTimeoutMs",
|
|
750
|
+
"attachmentTimeoutMs",
|
|
751
|
+
"assistantRecheckDelayMs",
|
|
752
|
+
"assistantRecheckTimeoutMs",
|
|
753
|
+
"autoReattachDelayMs",
|
|
754
|
+
"autoReattachIntervalMs",
|
|
755
|
+
"autoReattachTimeoutMs",
|
|
756
|
+
"keepBrowser",
|
|
757
|
+
"debug",
|
|
758
|
+
];
|
|
759
|
+
export function pickClientBrowserConfig(requested) {
|
|
760
|
+
const accepted = {};
|
|
761
|
+
if (!requested) {
|
|
762
|
+
return accepted;
|
|
763
|
+
}
|
|
764
|
+
for (const field of CLIENT_BROWSER_CONFIG_FIELDS) {
|
|
765
|
+
const value = requested[field];
|
|
766
|
+
if (value !== undefined) {
|
|
767
|
+
accepted[field] = value;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return accepted;
|
|
557
771
|
}
|
|
772
|
+
async function stageRemoteAttachments(payload, directory, defaultPrefix) {
|
|
773
|
+
await mkdir(directory, { recursive: true });
|
|
774
|
+
const names = payload.map((attachment, index) => {
|
|
775
|
+
const fallback = `${defaultPrefix}-${index + 1}`;
|
|
776
|
+
const sanitized = (attachment.fileName ?? fallback).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
777
|
+
return !sanitized || sanitized === "." || sanitized === ".." ? fallback : sanitized;
|
|
778
|
+
});
|
|
779
|
+
// Reserve future names too, and honor case-insensitive host filesystems.
|
|
780
|
+
const reserved = new Set(names.map((name) => name.toLowerCase()));
|
|
781
|
+
const used = new Set();
|
|
782
|
+
const attachments = [];
|
|
783
|
+
for (const [index, attachment] of payload.entries()) {
|
|
784
|
+
const original = names[index];
|
|
785
|
+
let name = original;
|
|
786
|
+
if (used.has(name.toLowerCase())) {
|
|
787
|
+
const extension = path.extname(original);
|
|
788
|
+
const stem = original.slice(0, original.length - extension.length);
|
|
789
|
+
let suffix = 2;
|
|
790
|
+
do {
|
|
791
|
+
name = `${stem}-${suffix++}${extension}`;
|
|
792
|
+
} while (reserved.has(name.toLowerCase()));
|
|
793
|
+
}
|
|
794
|
+
used.add(name.toLowerCase());
|
|
795
|
+
reserved.add(name.toLowerCase());
|
|
796
|
+
const filePath = path.join(directory, name);
|
|
797
|
+
await writeFile(filePath, Buffer.from(attachment.contentBase64, "base64"), { flag: "wx" });
|
|
798
|
+
attachments.push({
|
|
799
|
+
path: filePath,
|
|
800
|
+
displayPath: attachment.displayPath,
|
|
801
|
+
sizeBytes: attachment.sizeBytes,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
return attachments;
|
|
805
|
+
}
|
|
806
|
+
// Return conversation identity and observed selection; keep process/profile details on the host.
|
|
558
807
|
function sanitizeResult(result, warnings = []) {
|
|
559
808
|
return {
|
|
560
809
|
answerText: result.answerText,
|
|
@@ -563,6 +812,12 @@ function sanitizeResult(result, warnings = []) {
|
|
|
563
812
|
tookMs: result.tookMs,
|
|
564
813
|
answerTokens: result.answerTokens,
|
|
565
814
|
answerChars: result.answerChars,
|
|
815
|
+
modelSelection: result.modelSelection,
|
|
816
|
+
thinkingSelection: result.thinkingSelection,
|
|
817
|
+
archive: result.archive,
|
|
818
|
+
tabUrl: result.tabUrl,
|
|
819
|
+
conversationId: result.conversationId,
|
|
820
|
+
promptSubmitted: result.promptSubmitted,
|
|
566
821
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
567
822
|
chromePid: undefined,
|
|
568
823
|
chromePort: undefined,
|
|
@@ -575,7 +830,31 @@ function formatSocket(req) {
|
|
|
575
830
|
const port = socket.remotePort ?? "0";
|
|
576
831
|
return `${host}:${port}`;
|
|
577
832
|
}
|
|
833
|
+
/**
|
|
834
|
+
* Addresses a client could actually reach this service on.
|
|
835
|
+
*
|
|
836
|
+
* A loopback bind is reachable only from this machine, so listing the host's LAN
|
|
837
|
+
* and tailnet addresses there is not merely noisy — it tells an operator the
|
|
838
|
+
* service is exposed when it is not, which is the wrong direction for a mistake
|
|
839
|
+
* about a token that grants browser automation.
|
|
840
|
+
*/
|
|
578
841
|
function formatReachableAddresses(bindAddress, port) {
|
|
842
|
+
if (isLoopbackAddress(bindAddress)) {
|
|
843
|
+
return [`${formatHostPort(bindAddress, port)}`];
|
|
844
|
+
}
|
|
845
|
+
return formatAllInterfaceAddresses(bindAddress, port);
|
|
846
|
+
}
|
|
847
|
+
function isLoopbackAddress(address) {
|
|
848
|
+
const normalized = address
|
|
849
|
+
.trim()
|
|
850
|
+
.toLowerCase()
|
|
851
|
+
.replace(/^\[|\]$/g, "");
|
|
852
|
+
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
|
853
|
+
}
|
|
854
|
+
function formatHostPort(address, port) {
|
|
855
|
+
return address.includes(":") ? `[${address}]:${port}` : `${address}:${port}`;
|
|
856
|
+
}
|
|
857
|
+
function formatAllInterfaceAddresses(bindAddress, port) {
|
|
579
858
|
const ipv4 = [];
|
|
580
859
|
const ipv6 = [];
|
|
581
860
|
if (bindAddress && bindAddress !== "::" && bindAddress !== "0.0.0.0") {
|
|
@@ -141,12 +141,38 @@ async function writeSessionMetadataFile(sessionId, metadata) {
|
|
|
141
141
|
encoding: "utf8",
|
|
142
142
|
mode: 0o600,
|
|
143
143
|
});
|
|
144
|
-
await
|
|
144
|
+
await renameSessionMetadataFile(temporaryPath, targetPath);
|
|
145
145
|
}
|
|
146
146
|
finally {
|
|
147
147
|
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
+
const METADATA_RENAME_RETRY_DELAYS_MS = [10, 25, 50, 100, 200, 400, 800];
|
|
151
|
+
const RETRIABLE_METADATA_RENAME_CODES = new Set(["EACCES", "EBUSY", "EPERM"]);
|
|
152
|
+
function isRetriableMetadataRenameError(error) {
|
|
153
|
+
return (typeof error === "object" &&
|
|
154
|
+
error !== null &&
|
|
155
|
+
"code" in error &&
|
|
156
|
+
typeof error.code === "string" &&
|
|
157
|
+
RETRIABLE_METADATA_RENAME_CODES.has(error.code));
|
|
158
|
+
}
|
|
159
|
+
async function renameSessionMetadataFile(temporaryPath, targetPath) {
|
|
160
|
+
for (const delayMs of [0, ...METADATA_RENAME_RETRY_DELAYS_MS]) {
|
|
161
|
+
if (delayMs > 0) {
|
|
162
|
+
await wait(delayMs);
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
await fs.rename(temporaryPath, targetPath);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (!isRetriableMetadataRenameError(error) ||
|
|
170
|
+
delayMs === METADATA_RENAME_RETRY_DELAYS_MS.at(-1)) {
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
150
176
|
function requestPath(id) {
|
|
151
177
|
return path.join(sessionDir(id), LEGACY_REQUEST_FILENAME);
|
|
152
178
|
}
|
|
@@ -317,6 +343,7 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
|
|
|
317
343
|
zombieTimeoutMs: options.zombieTimeoutMs,
|
|
318
344
|
zombieUseLastActivity: options.zombieUseLastActivity,
|
|
319
345
|
writeOutputPath: options.writeOutputPath,
|
|
346
|
+
writeArtifacts: options.writeArtifacts,
|
|
320
347
|
partialMode: options.partialMode,
|
|
321
348
|
waitPreference: options.waitPreference,
|
|
322
349
|
youtube: options.youtube,
|
|
@@ -327,6 +354,7 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
|
|
|
327
354
|
browserResumeConversationUrl: options.browserResumeConversationUrl,
|
|
328
355
|
aspectRatio: options.aspectRatio,
|
|
329
356
|
geminiShowThoughts: options.geminiShowThoughts,
|
|
357
|
+
geminiAllowModelFallback: options.geminiAllowModelFallback,
|
|
330
358
|
},
|
|
331
359
|
};
|
|
332
360
|
await ensureDir(modelsDir(sessionId));
|
|
@@ -640,6 +668,13 @@ async function markDeadBrowser(meta) {
|
|
|
640
668
|
const host = runtime.chromeHost ?? "127.0.0.1";
|
|
641
669
|
signals.push(await isPortOpen(host, runtime.chromePort));
|
|
642
670
|
}
|
|
671
|
+
// controllerPid: the foreground process that launched this browser run.
|
|
672
|
+
// When neither chromePid nor chromePort are recorded (common on Linux),
|
|
673
|
+
// signals[] is empty and the early-return below would skip the reap.
|
|
674
|
+
// Use the same isProcessAlive() primitive to fill that gap.
|
|
675
|
+
if (signals.length === 0 && runtime.controllerPid) {
|
|
676
|
+
signals.push(isProcessAlive(runtime.controllerPid));
|
|
677
|
+
}
|
|
643
678
|
if (signals.length === 0 || signals.some(Boolean)) {
|
|
644
679
|
return meta;
|
|
645
680
|
}
|
|
Binary file
|