@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.
Files changed (169) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +76 -76
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/archiveConversation.js +31 -7
  5. package/dist/src/browser/actions/assistantResponse.js +43 -1
  6. package/dist/src/browser/actions/attachmentContext.js +239 -0
  7. package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
  8. package/dist/src/browser/actions/attachmentEvidence.js +78 -0
  9. package/dist/src/browser/actions/attachmentProgress.js +41 -0
  10. package/dist/src/browser/actions/attachmentPrompt.js +130 -0
  11. package/dist/src/browser/actions/attachments.js +306 -355
  12. package/dist/src/browser/actions/deepResearch.js +170 -45
  13. package/dist/src/browser/actions/modelSelection.js +91 -4
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +364 -186
  16. package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
  17. package/dist/src/browser/actions/thinkingStatus.js +2 -5
  18. package/dist/src/browser/actions/thinkingTime.js +156 -16
  19. package/dist/src/browser/actions/webSearch.js +96 -0
  20. package/dist/src/browser/attachRunning.js +46 -2
  21. package/dist/src/browser/attachmentValidation.js +53 -0
  22. package/dist/src/browser/cancellation.js +105 -0
  23. package/dist/src/browser/chatgptImages.js +2 -0
  24. package/dist/src/browser/chatgptThrottle.js +104 -0
  25. package/dist/src/browser/chromeLifecycle.js +272 -47
  26. package/dist/src/browser/config.js +14 -1
  27. package/dist/src/browser/detect.js +1 -1
  28. package/dist/src/browser/index.js +525 -274
  29. package/dist/src/browser/liveTabs.js +3 -4
  30. package/dist/src/browser/modelDisplay.js +9 -0
  31. package/dist/src/browser/pageActions.js +2 -2
  32. package/dist/src/browser/profileState.js +114 -9
  33. package/dist/src/browser/projectSourcesRunner.js +83 -15
  34. package/dist/src/browser/prompt.js +272 -113
  35. package/dist/src/browser/providers/chatgptDomProvider.js +3 -0
  36. package/dist/src/browser/reattach.js +31 -4
  37. package/dist/src/browser/recoveryTarget.js +131 -0
  38. package/dist/src/browser/sessionRunner.js +73 -11
  39. package/dist/src/browser/tabLeaseRegistry.js +434 -38
  40. package/dist/src/browser/targetClaim.js +54 -0
  41. package/dist/src/browser/utils.js +20 -2
  42. package/dist/src/cli/browserConfig.js +36 -4
  43. package/dist/src/cli/browserDefaults.js +10 -0
  44. package/dist/src/cli/browserTabs.js +15 -29
  45. package/dist/src/cli/detachedSession.js +69 -0
  46. package/dist/src/cli/dryRun.js +58 -48
  47. package/dist/src/cli/harvestIntegrity.js +140 -0
  48. package/dist/src/cli/options.js +15 -0
  49. package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
  50. package/dist/src/cli/runOptions.js +19 -4
  51. package/dist/src/cli/serveBrowserConfig.js +24 -0
  52. package/dist/src/cli/sessionDisplay.js +13 -7
  53. package/dist/src/cli/sessionRunner.js +95 -12
  54. package/dist/src/config.js +1 -0
  55. package/dist/src/gemini-web/client.js +67 -21
  56. package/dist/src/gemini-web/executor.js +4 -0
  57. package/dist/src/gemini-web/http.js +15 -0
  58. package/dist/src/gemini-web/models.js +15 -11
  59. package/dist/src/mcp/server.js +11 -14
  60. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  61. package/dist/src/mcp/tools/consult.js +86 -24
  62. package/dist/src/mcp/tools/projectSources.js +4 -6
  63. package/dist/src/mcp/tools/sessionResources.js +1 -1
  64. package/dist/src/mcp/tools/sessions.js +2 -2
  65. package/dist/src/mcp/tools/wait.js +182 -0
  66. package/dist/src/mcp/types.js +8 -1
  67. package/dist/src/oracle/config.js +14 -0
  68. package/dist/src/oracle/errors.js +11 -0
  69. package/dist/src/oracle/geminiModels.js +1 -0
  70. package/dist/src/oracle/providerRoutePlan.js +3 -2
  71. package/dist/src/oracle/run.js +27 -4
  72. package/dist/src/remote/client.js +73 -6
  73. package/dist/src/remote/health.js +6 -2
  74. package/dist/src/remote/runSlots.js +83 -0
  75. package/dist/src/remote/server.js +346 -71
  76. package/dist/src/sessionManager.js +36 -1
  77. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  78. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  79. package/package.json +25 -23
  80. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  81. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  82. package/dist/bin/oracle.js +0 -683
  83. package/dist/docs-site/.nojekyll +0 -0
  84. package/dist/docs-site/CNAME +0 -1
  85. package/dist/docs-site/RELEASING.html +0 -410
  86. package/dist/docs-site/agents.html +0 -374
  87. package/dist/docs-site/anthropic.html +0 -368
  88. package/dist/docs-site/bridge.html +0 -400
  89. package/dist/docs-site/browser-mode.html +0 -588
  90. package/dist/docs-site/chromium-forks.html +0 -347
  91. package/dist/docs-site/cli-reference.html +0 -346
  92. package/dist/docs-site/configuration.html +0 -436
  93. package/dist/docs-site/favicon.svg +0 -14
  94. package/dist/docs-site/followup.html +0 -368
  95. package/dist/docs-site/gemini.html +0 -379
  96. package/dist/docs-site/grok.html +0 -325
  97. package/dist/docs-site/index.html +0 -360
  98. package/dist/docs-site/install.html +0 -335
  99. package/dist/docs-site/linux.html +0 -321
  100. package/dist/docs-site/llms.txt +0 -43
  101. package/dist/docs-site/manual-tests.html +0 -595
  102. package/dist/docs-site/mcp.html +0 -368
  103. package/dist/docs-site/multimodel.html +0 -364
  104. package/dist/docs-site/mythical-pro-agents.html +0 -360
  105. package/dist/docs-site/notifier.html +0 -338
  106. package/dist/docs-site/openai-endpoints.html +0 -387
  107. package/dist/docs-site/openrouter.html +0 -344
  108. package/dist/docs-site/quickstart.html +0 -369
  109. package/dist/docs-site/refactor/ux.html +0 -532
  110. package/dist/docs-site/sessions.html +0 -388
  111. package/dist/docs-site/social-card.png +0 -0
  112. package/dist/docs-site/social-card.svg +0 -79
  113. package/dist/docs-site/spec.html +0 -363
  114. package/dist/docs-site/testing.html +0 -320
  115. package/dist/docs-site/tui-debug.html +0 -326
  116. package/dist/docs-site/windows-work.html +0 -323
  117. package/dist/docs-site/windows.html +0 -320
  118. package/dist/markdansi/types/index.js +0 -4
  119. package/dist/oracle/bin/oracle-cli.js +0 -472
  120. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  121. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  122. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  123. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  124. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  125. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  126. package/dist/oracle/src/browser/config.js +0 -33
  127. package/dist/oracle/src/browser/constants.js +0 -40
  128. package/dist/oracle/src/browser/cookies.js +0 -210
  129. package/dist/oracle/src/browser/domDebug.js +0 -36
  130. package/dist/oracle/src/browser/index.js +0 -331
  131. package/dist/oracle/src/browser/pageActions.js +0 -5
  132. package/dist/oracle/src/browser/prompt.js +0 -88
  133. package/dist/oracle/src/browser/promptSummary.js +0 -20
  134. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  135. package/dist/oracle/src/browser/types.js +0 -1
  136. package/dist/oracle/src/browser/utils.js +0 -62
  137. package/dist/oracle/src/browserMode.js +0 -1
  138. package/dist/oracle/src/cli/browserConfig.js +0 -44
  139. package/dist/oracle/src/cli/dryRun.js +0 -59
  140. package/dist/oracle/src/cli/engine.js +0 -17
  141. package/dist/oracle/src/cli/errorUtils.js +0 -9
  142. package/dist/oracle/src/cli/help.js +0 -70
  143. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  144. package/dist/oracle/src/cli/options.js +0 -103
  145. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  146. package/dist/oracle/src/cli/rootAlias.js +0 -30
  147. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  148. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  149. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  150. package/dist/oracle/src/heartbeat.js +0 -43
  151. package/dist/oracle/src/oracle/client.js +0 -48
  152. package/dist/oracle/src/oracle/config.js +0 -29
  153. package/dist/oracle/src/oracle/errors.js +0 -101
  154. package/dist/oracle/src/oracle/files.js +0 -220
  155. package/dist/oracle/src/oracle/format.js +0 -33
  156. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  157. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  158. package/dist/oracle/src/oracle/request.js +0 -48
  159. package/dist/oracle/src/oracle/run.js +0 -444
  160. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  161. package/dist/oracle/src/oracle/types.js +0 -1
  162. package/dist/oracle/src/oracle.js +0 -9
  163. package/dist/oracle/src/sessionManager.js +0 -205
  164. package/dist/oracle/src/version.js +0 -39
  165. package/dist/scripts/chrome/browser-tools.js +0 -295
  166. package/dist/src/browser/chromeCookies.js +0 -312
  167. package/dist/src/browser/keytarShim.js +0 -56
  168. package/dist/src/browser/profileSync.js +0 -141
  169. package/dist/src/browser/windowsCookies.js +0 -219
@@ -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
- if (!reasoningEffort && !reasoningMode)
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 (!GPT_5_6_API_MODELS.has(options.model)) {
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
+ }