@andrewting19/oracle 0.9.1 → 0.9.3

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.
@@ -12,6 +12,7 @@ import { CHATGPT_URL } from "../browser/constants.js";
12
12
  import { getCliVersion } from "../version.js";
13
13
  import { cleanupStaleProfileState, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "../browser/profileState.js";
14
14
  import { normalizeChatgptUrl } from "../browser/utils.js";
15
+ import { launchChrome } from "../browser/chromeLifecycle.js";
15
16
  async function findAvailablePort() {
16
17
  return await new Promise((resolve, reject) => {
17
18
  const srv = net.createServer();
@@ -38,8 +39,10 @@ export async function createRemoteServer(options = {}, deps = {}) {
38
39
  const color = process.stdout.isTTY
39
40
  ? (formatter, msg) => formatter(msg)
40
41
  : (_formatter, msg) => msg;
41
- // Single-flight guard: remote Chrome can only host one run at a time, so we serialize requests.
42
- let busy = false;
42
+ // Concurrency control: allow parallel browser runs (each gets its own Chrome tab).
43
+ // Set ORACLE_SERVE_MAX_CONCURRENT to limit simultaneous runs (default: unlimited).
44
+ let activeRuns = 0;
45
+ const maxConcurrent = parseInt(process.env.ORACLE_SERVE_MAX_CONCURRENT ?? "0", 10) || Infinity;
43
46
  if (!process.listenerCount("unhandledRejection")) {
44
47
  process.on("unhandledRejection", (reason) => {
45
48
  logger(`Unhandled promise rejection in remote server: ${reason instanceof Error ? reason.message : String(reason)}`);
@@ -67,6 +70,8 @@ export async function createRemoteServer(options = {}, deps = {}) {
67
70
  ok: true,
68
71
  version: getCliVersion(),
69
72
  uptimeSeconds: Math.round((Date.now() - startedAt) / 1000),
73
+ activeRuns,
74
+ maxConcurrent: maxConcurrent === Infinity ? null : maxConcurrent,
70
75
  }));
71
76
  return;
72
77
  }
@@ -84,15 +89,15 @@ export async function createRemoteServer(options = {}, deps = {}) {
84
89
  res.end(JSON.stringify({ error: "unauthorized" }));
85
90
  return;
86
91
  }
87
- if (busy) {
92
+ if (activeRuns >= maxConcurrent) {
88
93
  if (verbose) {
89
- logger(`[serve] Busy: rejecting new run from ${formatSocket(req)} while another run is active`);
94
+ logger(`[serve] At capacity (${activeRuns}/${maxConcurrent}): rejecting run from ${formatSocket(req)}`);
90
95
  }
91
- res.writeHead(409, { "Content-Type": "application/json" });
92
- res.end(JSON.stringify({ error: "busy" }));
96
+ res.writeHead(429, { "Content-Type": "application/json" });
97
+ res.end(JSON.stringify({ error: "at_capacity", activeRuns, maxConcurrent }));
93
98
  return;
94
99
  }
95
- busy = true;
100
+ activeRuns++;
96
101
  const runStartedAt = Date.now();
97
102
  let payload = null;
98
103
  try {
@@ -103,7 +108,7 @@ export async function createRemoteServer(options = {}, deps = {}) {
103
108
  }
104
109
  }
105
110
  catch {
106
- busy = false;
111
+ activeRuns--;
107
112
  res.writeHead(400, { "Content-Type": "application/json" });
108
113
  res.end(JSON.stringify({ error: "invalid_request" }));
109
114
  return;
@@ -147,6 +152,10 @@ export async function createRemoteServer(options = {}, deps = {}) {
147
152
  else {
148
153
  payload.browserConfig = {};
149
154
  }
155
+ // Route through shared Chrome instance (each run gets its own isolated tab).
156
+ if (options.sharedChrome) {
157
+ payload.browserConfig.remoteChrome = options.sharedChrome;
158
+ }
150
159
  // Enforce manual-login profile when cookie sync is unavailable (e.g., Windows/WSL).
151
160
  if (options.manualLoginDefault) {
152
161
  payload.browserConfig.manualLogin = true;
@@ -173,7 +182,7 @@ export async function createRemoteServer(options = {}, deps = {}) {
173
182
  logger(`[serve] Run ${runId} failed after ${Date.now() - runStartedAt}ms: ${message}`);
174
183
  }
175
184
  finally {
176
- busy = false;
185
+ activeRuns--;
177
186
  res.end();
178
187
  try {
179
188
  await rm(runDir, { recursive: true, force: true });
@@ -260,10 +269,38 @@ export async function serveRemote(options = {}) {
260
269
  else {
261
270
  console.log(`Detected ${cookies.length} ChatGPT cookies on this host; runs will reuse this session.`);
262
271
  }
272
+ // Launch a shared Chrome instance for concurrent tab-based runs.
273
+ let sharedChrome;
274
+ if (!preferManualLogin) {
275
+ try {
276
+ const userDataDir = await mkdtemp(path.join(os.tmpdir(), "oracle-serve-chrome-"));
277
+ const chrome = await launchChrome({ headless: false, hideWindow: true }, userDataDir, console.log);
278
+ sharedChrome = { host: "127.0.0.1", port: chrome.port };
279
+ console.log(`Shared Chrome launched (pid ${chrome.pid}, port ${chrome.port}). Concurrent runs will use isolated tabs.`);
280
+ // Clean up Chrome on process exit
281
+ const killChrome = async () => {
282
+ try {
283
+ await chrome.kill();
284
+ }
285
+ catch { }
286
+ try {
287
+ await rm(userDataDir, { recursive: true, force: true });
288
+ }
289
+ catch { }
290
+ };
291
+ process.on("SIGINT", () => void killChrome());
292
+ process.on("SIGTERM", () => void killChrome());
293
+ }
294
+ catch (error) {
295
+ const message = error instanceof Error ? error.message : String(error);
296
+ console.log(`Warning: failed to launch shared Chrome (${message}). Runs will each launch their own Chrome.`);
297
+ }
298
+ }
263
299
  const server = await createRemoteServer({
264
300
  ...options,
265
301
  manualLoginDefault: preferManualLogin,
266
302
  manualLoginProfileDir: manualProfileDir,
303
+ sharedChrome,
267
304
  });
268
305
  await new Promise((resolve) => {
269
306
  const shutdown = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrewting19/oracle",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "CLI wrapper around OpenAI Responses API with GPT-5.4 Pro, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/steipete/oracle#readme",