@steipete/oracle 0.18.0 → 0.20.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 +76 -76
- 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 +170 -45
- package/dist/src/browser/actions/modelSelection.js +91 -4
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +364 -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 +156 -16
- package/dist/src/browser/actions/webSearch.js +96 -0
- 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 +272 -47
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +525 -274
- package/dist/src/browser/liveTabs.js +3 -4
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +83 -15
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +3 -0
- package/dist/src/browser/reattach.js +31 -4
- package/dist/src/browser/recoveryTarget.js +131 -0
- package/dist/src/browser/sessionRunner.js +73 -11
- package/dist/src/browser/tabLeaseRegistry.js +434 -38
- package/dist/src/browser/targetClaim.js +54 -0
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +36 -4
- package/dist/src/cli/browserDefaults.js +10 -0
- package/dist/src/cli/browserTabs.js +15 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -48
- package/dist/src/cli/harvestIntegrity.js +140 -0
- package/dist/src/cli/options.js +15 -0
- package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
- package/dist/src/cli/runOptions.js +19 -4
- package/dist/src/cli/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +13 -7
- package/dist/src/cli/sessionRunner.js +95 -12
- 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 +86 -24
- 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 +8 -1
- package/dist/src/oracle/config.js +14 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/geminiModels.js +1 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/oracle/run.js +27 -4
- package/dist/src/remote/client.js +73 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +346 -71
- 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
package/dist/src/oracle/run.js
CHANGED
|
@@ -36,6 +36,15 @@ const DEFAULT_TIMEOUT_PRO_MS = 60 * 60 * 1000;
|
|
|
36
36
|
const GPT_5_6_API_MODELS = new Set(["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
|
|
37
37
|
const REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh", "max"]);
|
|
38
38
|
const REASONING_MODES = new Set(["standard", "pro"]);
|
|
39
|
+
// Astra has a narrower effort range than the GPT-5.6 API family. Keep this
|
|
40
|
+
// capability separate from GPT_5_6_API_MODELS so adding a model cannot
|
|
41
|
+
// accidentally inherit a family's exact option set.
|
|
42
|
+
const MODEL_REASONING_CAPABILITIES = new Map([
|
|
43
|
+
[
|
|
44
|
+
"gpt-6-astra",
|
|
45
|
+
{ efforts: new Set(["low", "medium", "high", "xhigh", "max"]), modes: REASONING_MODES },
|
|
46
|
+
],
|
|
47
|
+
]);
|
|
39
48
|
const defaultWait = (ms) => new Promise((resolve) => {
|
|
40
49
|
setTimeout(resolve, ms);
|
|
41
50
|
});
|
|
@@ -72,9 +81,13 @@ function runtimeKeySource({ route, providerMode, optionsApiKey, }) {
|
|
|
72
81
|
return "XAI_API_KEY";
|
|
73
82
|
return optionsApiKey ? "apiKey option" : route.keySource;
|
|
74
83
|
}
|
|
75
|
-
function validateReasoningOptions(options, route) {
|
|
84
|
+
function validateReasoningOptions(options, route, modelConfig) {
|
|
76
85
|
const { reasoningEffort, reasoningMode } = options;
|
|
77
|
-
|
|
86
|
+
const capabilities = GPT_5_6_API_MODELS.has(options.model)
|
|
87
|
+
? { efforts: REASONING_EFFORTS, modes: REASONING_MODES }
|
|
88
|
+
: MODEL_REASONING_CAPABILITIES.get(options.model);
|
|
89
|
+
const effectiveEffort = reasoningEffort ?? modelConfig?.reasoning?.effort;
|
|
90
|
+
if (!reasoningEffort && !reasoningMode && !effectiveEffort)
|
|
78
91
|
return;
|
|
79
92
|
if (reasoningEffort && !REASONING_EFFORTS.has(reasoningEffort)) {
|
|
80
93
|
throw new PromptValidationError(`Invalid reasoning effort "${reasoningEffort}". Expected none, low, medium, high, xhigh, or max.`, { model: options.model, reasoningEffort });
|
|
@@ -82,14 +95,20 @@ function validateReasoningOptions(options, route) {
|
|
|
82
95
|
if (reasoningMode && !REASONING_MODES.has(reasoningMode)) {
|
|
83
96
|
throw new PromptValidationError(`Invalid reasoning mode "${reasoningMode}". Expected standard or pro.`, { model: options.model, reasoningMode });
|
|
84
97
|
}
|
|
85
|
-
if (
|
|
98
|
+
if ((reasoningEffort || reasoningMode) && !capabilities) {
|
|
86
99
|
const option = reasoningMode
|
|
87
100
|
? `Reasoning mode "${reasoningMode}"`
|
|
88
101
|
: `Reasoning effort "${reasoningEffort}"`;
|
|
89
102
|
const guidance = reasoningMode
|
|
90
103
|
? `Use --model gpt-5.6-sol --reasoning-mode ${reasoningMode}.`
|
|
91
104
|
: `Use --model gpt-5.6-sol --reasoning-effort ${reasoningEffort}.`;
|
|
92
|
-
throw new PromptValidationError(`${option} is available only for GPT-5.6 API models. ${guidance}`, { model: options.model, reasoningEffort, reasoningMode });
|
|
105
|
+
throw new PromptValidationError(`${option} is available only for GPT-5.6 or GPT-6 Astra API models. ${guidance}`, { model: options.model, reasoningEffort, reasoningMode });
|
|
106
|
+
}
|
|
107
|
+
if (effectiveEffort && capabilities && !capabilities.efforts.has(effectiveEffort)) {
|
|
108
|
+
throw new PromptValidationError(`Reasoning effort "${effectiveEffort}" is not supported for ${options.model}. Expected low, medium, high, xhigh, or max.`, { model: options.model, reasoningEffort: effectiveEffort, reasoningMode });
|
|
109
|
+
}
|
|
110
|
+
if (reasoningMode && capabilities && !capabilities.modes.has(reasoningMode)) {
|
|
111
|
+
throw new PromptValidationError(`Reasoning mode "${reasoningMode}" is not supported for ${options.model}. Expected standard or pro.`, { model: options.model, reasoningEffort, reasoningMode });
|
|
93
112
|
}
|
|
94
113
|
if (reasoningMode &&
|
|
95
114
|
!route.isAzureOpenAI &&
|
|
@@ -166,6 +185,10 @@ export async function runOracle(options, deps = {}) {
|
|
|
166
185
|
openRouterApiKey: resolverOpenRouterApiKey,
|
|
167
186
|
modelOverrides: options.modelOverrides,
|
|
168
187
|
});
|
|
188
|
+
// Validate the resolved default as well as explicit flags. This catches a
|
|
189
|
+
// model override such as Astra + reasoning.effort=none, while allowing an
|
|
190
|
+
// explicit supported effort to replace an invalid bundled/overridden default.
|
|
191
|
+
validateReasoningOptions(options, route, modelConfig);
|
|
169
192
|
const isLongRunningModel = isProTierModel;
|
|
170
193
|
const supportsBackground = modelConfig.supportsBackground !== false;
|
|
171
194
|
const useBackground = supportsBackground ? (options.background ?? isLongRunningModel) : false;
|
|
@@ -6,30 +6,46 @@ 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
8
|
import { MAX_REMOTE_ARTIFACT_BYTES, } from "./types.js";
|
|
9
|
+
import { materializeStagedFallbackBundle } from "../browser/prompt.js";
|
|
10
|
+
import { checkRemoteHealth } from "./health.js";
|
|
9
11
|
import { parseHostPort } from "../bridge/connection.js";
|
|
12
|
+
import { BrowserRunCancelledError } from "../oracle/errors.js";
|
|
10
13
|
export function createRemoteBrowserExecutor({ host, token }) {
|
|
11
14
|
// Return a drop-in replacement for runBrowserMode so the browser session runner can stay unchanged.
|
|
12
15
|
return async function remoteBrowserExecutor(options) {
|
|
16
|
+
if (options.config?.researchMode === "search") {
|
|
17
|
+
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
|
+
const callerSignal = options.signal;
|
|
20
|
+
if (callerSignal?.aborted)
|
|
21
|
+
throw new BrowserRunCancelledError("Browser run cancelled before the request was sent.");
|
|
22
|
+
if (callerSignal) {
|
|
23
|
+
const health = await checkRemoteHealth({ host, token, signal: callerSignal });
|
|
24
|
+
if (callerSignal.aborted)
|
|
25
|
+
throw new BrowserRunCancelledError();
|
|
26
|
+
if (health.capabilities?.runCancellation !== true)
|
|
27
|
+
throw new Error("Remote host does not support run cancellation; upgrade the host before using an AbortSignal.");
|
|
28
|
+
}
|
|
13
29
|
const payload = {
|
|
14
30
|
prompt: options.prompt,
|
|
15
31
|
attachments: await serializeAttachments(options.attachments ?? []),
|
|
16
|
-
fallbackSubmission: options.fallbackSubmission
|
|
17
|
-
? {
|
|
18
|
-
prompt: options.fallbackSubmission.prompt,
|
|
19
|
-
attachments: await serializeAttachments(options.fallbackSubmission.attachments ?? []),
|
|
20
|
-
}
|
|
21
|
-
: undefined,
|
|
32
|
+
fallbackSubmission: await serializeFallback(options.fallbackSubmission, { host, token }),
|
|
22
33
|
browserConfig: options.config ?? {},
|
|
23
34
|
options: {
|
|
24
35
|
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
25
36
|
verbose: options.verbose,
|
|
26
37
|
sessionId: options.sessionId,
|
|
27
38
|
followUpPrompts: options.followUpPrompts,
|
|
39
|
+
cancelOnDisconnect: callerSignal ? true : undefined,
|
|
28
40
|
},
|
|
29
41
|
};
|
|
30
42
|
const body = Buffer.from(JSON.stringify(payload));
|
|
31
43
|
const { hostname, port } = parseHost(host);
|
|
32
44
|
return new Promise((resolve, reject) => {
|
|
45
|
+
if (callerSignal?.aborted) {
|
|
46
|
+
reject(new Error("Browser run cancelled before the request was sent."));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
33
49
|
const transferredFiles = [];
|
|
34
50
|
const transferFailures = [];
|
|
35
51
|
const transferPromises = [];
|
|
@@ -40,6 +56,7 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
40
56
|
if (settled)
|
|
41
57
|
return;
|
|
42
58
|
settled = true;
|
|
59
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
43
60
|
reject(error);
|
|
44
61
|
};
|
|
45
62
|
const req = http.request({
|
|
@@ -107,17 +124,55 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
107
124
|
return;
|
|
108
125
|
}
|
|
109
126
|
settled = true;
|
|
127
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
110
128
|
resolve(mergeTransferredArtifacts(resolved, transferredFiles, transferFailures));
|
|
111
129
|
})().catch(fail);
|
|
112
130
|
});
|
|
113
131
|
res.on("error", fail);
|
|
114
132
|
});
|
|
115
133
|
req.on("error", fail);
|
|
134
|
+
// Destroying the request closes the socket, which is how the service learns
|
|
135
|
+
// to abort: its own disconnect handler fires and releases the slot and the
|
|
136
|
+
// browser tab.
|
|
137
|
+
const onCallerAbort = () => {
|
|
138
|
+
req.destroy();
|
|
139
|
+
fail(new BrowserRunCancelledError("Browser run cancelled: the caller aborted."));
|
|
140
|
+
};
|
|
141
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
116
142
|
req.write(body);
|
|
117
143
|
req.end();
|
|
118
144
|
});
|
|
119
145
|
};
|
|
120
146
|
}
|
|
147
|
+
async function serializeFallback(fallback, remote) {
|
|
148
|
+
if (!fallback)
|
|
149
|
+
return undefined;
|
|
150
|
+
if (fallback.pendingBundle) {
|
|
151
|
+
const health = await checkRemoteHealth(remote);
|
|
152
|
+
if (!health.ok || health.capabilities?.deferredFallbackBundling !== true) {
|
|
153
|
+
// Older hosts ignore bundle metadata, so send a ready-to-upload fallback.
|
|
154
|
+
const prepared = await materializeStagedFallbackBundle({
|
|
155
|
+
composerText: fallback.prompt,
|
|
156
|
+
attachments: fallback.attachments,
|
|
157
|
+
...fallback.pendingBundle,
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
return {
|
|
161
|
+
prompt: prepared.composerText,
|
|
162
|
+
attachments: await serializeAttachments(prepared.attachments),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
await rm(path.dirname(prepared.bundled.bundlePath), { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
prompt: fallback.prompt,
|
|
172
|
+
attachments: await serializeAttachments(fallback.attachments),
|
|
173
|
+
bundle: fallback.pendingBundle,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
121
176
|
async function serializeAttachments(attachments) {
|
|
122
177
|
const serialized = [];
|
|
123
178
|
for (const attachment of attachments) {
|
|
@@ -173,12 +228,17 @@ function handleEvent(params) {
|
|
|
173
228
|
token: params.token,
|
|
174
229
|
descriptor: event.artifact,
|
|
175
230
|
sessionId: params.options.sessionId,
|
|
231
|
+
signal: params.options.signal,
|
|
176
232
|
log: params.options.log,
|
|
177
233
|
})
|
|
178
234
|
.then((artifact) => {
|
|
179
235
|
params.onArtifact(artifact);
|
|
180
236
|
})
|
|
181
237
|
.catch((error) => {
|
|
238
|
+
if (params.options.signal?.aborted) {
|
|
239
|
+
params.onError(new BrowserRunCancelledError());
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
182
242
|
const message = error instanceof Error ? error.message : String(error);
|
|
183
243
|
const fallback = `Oracle captured the browser text response, but bridge artifact transfer failed for ${displayFilename}. Open the ChatGPT browser on the bridge host, download the ZIP/file shown in the current response, and copy it to a cloud-readable path. Reason: ${message}`;
|
|
184
244
|
params.options.log?.(`[browser] ${fallback}`);
|
|
@@ -192,6 +252,7 @@ function handleEvent(params) {
|
|
|
192
252
|
return null;
|
|
193
253
|
}
|
|
194
254
|
async function transferRemoteArtifact(params) {
|
|
255
|
+
params.signal?.throwIfAborted();
|
|
195
256
|
validateRemoteArtifactDescriptor(params.descriptor);
|
|
196
257
|
const sessionId = params.sessionId ?? params.descriptor.runId;
|
|
197
258
|
const artifactsDir = resolveSessionArtifactsDir(sessionId);
|
|
@@ -208,6 +269,7 @@ async function transferRemoteArtifact(params) {
|
|
|
208
269
|
token: params.token,
|
|
209
270
|
targetPath: partPath,
|
|
210
271
|
descriptor: params.descriptor,
|
|
272
|
+
signal: params.signal,
|
|
211
273
|
}).catch(async (error) => {
|
|
212
274
|
await rm(partPath, { force: true }).catch(() => undefined);
|
|
213
275
|
throw error;
|
|
@@ -231,6 +293,10 @@ async function transferRemoteArtifact(params) {
|
|
|
231
293
|
await rm(partPath, { force: true }).catch(() => undefined);
|
|
232
294
|
throw new Error(`${validation.type} validation failed: ${validation.error ?? "invalid"}`);
|
|
233
295
|
}
|
|
296
|
+
if (params.signal?.aborted) {
|
|
297
|
+
await rm(partPath, { force: true });
|
|
298
|
+
throw new BrowserRunCancelledError();
|
|
299
|
+
}
|
|
234
300
|
await rename(partPath, finalPath);
|
|
235
301
|
params.log?.(`[browser] Transferred artifact to ${finalPath}`);
|
|
236
302
|
const publishedFilename = path.basename(finalPath);
|
|
@@ -257,6 +323,7 @@ async function downloadArtifactToFile(params) {
|
|
|
257
323
|
port: params.port,
|
|
258
324
|
path: params.path,
|
|
259
325
|
method: "GET",
|
|
326
|
+
signal: params.signal,
|
|
260
327
|
headers: params.token ? { authorization: `Bearer ${params.token}` } : undefined,
|
|
261
328
|
}, (res) => {
|
|
262
329
|
if (res.statusCode !== 200) {
|
|
@@ -30,7 +30,7 @@ export async function checkTcpConnection(host, timeoutMs = 2000) {
|
|
|
30
30
|
socket.once("timeout", onTimeout);
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
-
export async function checkRemoteHealth({ host, token, timeoutMs = 5000, }) {
|
|
33
|
+
export async function checkRemoteHealth({ host, token, timeoutMs = 5000, signal, }) {
|
|
34
34
|
const { hostname, port } = parseHostPort(host);
|
|
35
35
|
const headers = { accept: "application/json" };
|
|
36
36
|
if (token) {
|
|
@@ -43,6 +43,7 @@ export async function checkRemoteHealth({ host, token, timeoutMs = 5000, }) {
|
|
|
43
43
|
path: "/health",
|
|
44
44
|
headers,
|
|
45
45
|
timeoutMs,
|
|
46
|
+
signal,
|
|
46
47
|
});
|
|
47
48
|
if (response.statusCode === 200 && typeof response.json === "object" && response.json) {
|
|
48
49
|
const ok = response.json.ok === true;
|
|
@@ -90,6 +91,8 @@ function parseCapabilities(value) {
|
|
|
90
91
|
return undefined;
|
|
91
92
|
}
|
|
92
93
|
return {
|
|
94
|
+
...(raw.deferredFallbackBundling === true ? { deferredFallbackBundling: true } : {}),
|
|
95
|
+
...(raw.runCancellation === true ? { runCancellation: true } : {}),
|
|
93
96
|
artifactTransfer: true,
|
|
94
97
|
artifactProtocolVersion,
|
|
95
98
|
maxArtifactBytes: Math.min(maxArtifactBytes, MAX_REMOTE_ARTIFACT_BYTES),
|
|
@@ -105,7 +108,7 @@ function extractErrorMessage(json, bodyText) {
|
|
|
105
108
|
const trimmed = bodyText.trim();
|
|
106
109
|
return trimmed.length ? trimmed : null;
|
|
107
110
|
}
|
|
108
|
-
async function requestJson({ hostname, port, path, headers, timeoutMs, }) {
|
|
111
|
+
async function requestJson({ hostname, port, path, headers, timeoutMs, signal, }) {
|
|
109
112
|
return await new Promise((resolve, reject) => {
|
|
110
113
|
const req = http.request({
|
|
111
114
|
hostname,
|
|
@@ -113,6 +116,7 @@ async function requestJson({ hostname, port, path, headers, timeoutMs, }) {
|
|
|
113
116
|
path,
|
|
114
117
|
method: "GET",
|
|
115
118
|
headers,
|
|
119
|
+
signal,
|
|
116
120
|
}, (res) => {
|
|
117
121
|
res.setEncoding("utf8");
|
|
118
122
|
let body = "";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export class RunQueueFullError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("The browser run queue is full.");
|
|
4
|
+
this.name = "RunQueueFullError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class RunSlots {
|
|
8
|
+
maxConcurrent;
|
|
9
|
+
maxQueued;
|
|
10
|
+
active = 0;
|
|
11
|
+
waiting = [];
|
|
12
|
+
constructor(maxConcurrent, maxQueued) {
|
|
13
|
+
this.maxConcurrent = maxConcurrent;
|
|
14
|
+
this.maxQueued = maxQueued;
|
|
15
|
+
if (!Number.isSafeInteger(maxConcurrent) ||
|
|
16
|
+
maxConcurrent < 1 ||
|
|
17
|
+
!Number.isSafeInteger(maxQueued) ||
|
|
18
|
+
maxQueued < 0) {
|
|
19
|
+
throw new Error("Run capacity must be a positive integer and queue capacity a nonnegative integer.");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
get activeCount() {
|
|
23
|
+
return this.active;
|
|
24
|
+
}
|
|
25
|
+
get queuedCount() {
|
|
26
|
+
return this.waiting.length;
|
|
27
|
+
}
|
|
28
|
+
get capacity() {
|
|
29
|
+
return this.maxConcurrent;
|
|
30
|
+
}
|
|
31
|
+
get queueCapacity() {
|
|
32
|
+
return this.maxQueued;
|
|
33
|
+
}
|
|
34
|
+
get isSaturated() {
|
|
35
|
+
return this.active >= this.maxConcurrent && this.waiting.length >= this.maxQueued;
|
|
36
|
+
}
|
|
37
|
+
positionFor() {
|
|
38
|
+
return this.active < this.maxConcurrent ? 0 : this.waiting.length + 1;
|
|
39
|
+
}
|
|
40
|
+
// Reservation is synchronous, including the queue bound; request-body reads cannot race it.
|
|
41
|
+
acquire(signal) {
|
|
42
|
+
if (signal?.aborted)
|
|
43
|
+
return Promise.reject(new Error("cancelled before a slot was available"));
|
|
44
|
+
if (this.active < this.maxConcurrent) {
|
|
45
|
+
this.active++;
|
|
46
|
+
return Promise.resolve(this.makeRelease());
|
|
47
|
+
}
|
|
48
|
+
if (this.waiting.length >= this.maxQueued)
|
|
49
|
+
return Promise.reject(new RunQueueFullError());
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const onAbort = () => {
|
|
52
|
+
const index = this.waiting.indexOf(entry);
|
|
53
|
+
if (index < 0)
|
|
54
|
+
return;
|
|
55
|
+
this.waiting.splice(index, 1);
|
|
56
|
+
entry.cleanup();
|
|
57
|
+
reject(new Error("cancelled while waiting for a slot"));
|
|
58
|
+
};
|
|
59
|
+
const entry = {
|
|
60
|
+
resolve,
|
|
61
|
+
reject,
|
|
62
|
+
cleanup: () => signal?.removeEventListener("abort", onAbort),
|
|
63
|
+
};
|
|
64
|
+
this.waiting.push(entry);
|
|
65
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
makeRelease() {
|
|
69
|
+
let released = false;
|
|
70
|
+
return () => {
|
|
71
|
+
if (released)
|
|
72
|
+
return;
|
|
73
|
+
released = true;
|
|
74
|
+
const next = this.waiting.shift();
|
|
75
|
+
if (next) {
|
|
76
|
+
next.cleanup();
|
|
77
|
+
next.resolve(this.makeRelease());
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
this.active--;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|