pi-oracle 0.1.8 → 0.1.9

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.
@@ -7,6 +7,9 @@ import type { OracleConfig } from "./config.js";
7
7
  import { createLease, listLeaseMetadata, readLeaseMetadata, releaseLease, withAuthLock } from "./locks.js";
8
8
 
9
9
  const SEED_GENERATION_FILE = ".oracle-seed-generation";
10
+ const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/agent-browser", "/usr/local/bin/agent-browser"].find(
11
+ (candidate) => typeof candidate === "string" && candidate && existsSync(candidate),
12
+ ) || "agent-browser";
10
13
 
11
14
  export interface OracleRuntimeLeaseMetadata {
12
15
  jobId: string;
@@ -181,7 +184,7 @@ export interface OracleCleanupReport {
181
184
 
182
185
  async function closeRuntimeBrowserSession(runtimeSessionName: string): Promise<string | undefined> {
183
186
  return new Promise<string | undefined>((resolve) => {
184
- const child = spawn("agent-browser", ["--session", runtimeSessionName, "close"], { stdio: "ignore" });
187
+ const child = spawn(AGENT_BROWSER_BIN, ["--session", runtimeSessionName, "close"], { stdio: "ignore" });
185
188
  let settled = false;
186
189
  let timeout: NodeJS.Timeout | undefined;
187
190
  let timedOut = false;
@@ -36,6 +36,9 @@ const REAL_CHROME_USER_DATA_DIR = resolve(homedir(), "Library", "Application Sup
36
36
  const ORACLE_STATE_DIR = "/tmp/pi-oracle-state";
37
37
  const LOCKS_DIR = join(ORACLE_STATE_DIR, "locks");
38
38
  const STALE_STAGING_PROFILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
39
+ const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/agent-browser", "/usr/local/bin/agent-browser"].find(
40
+ (candidate) => typeof candidate === "string" && candidate && existsSync(candidate),
41
+ ) || "agent-browser";
39
42
 
40
43
  let runtimeProfileDir = config.browser.authSeedProfileDir;
41
44
 
@@ -165,7 +168,7 @@ function targetBrowserBaseArgs(options = {}) {
165
168
 
166
169
  async function closeTargetBrowser() {
167
170
  await log(`Closing target browser session ${authSessionName()} if present`);
168
- const result = await spawnCommand("agent-browser", [...targetBrowserBaseArgs(), "close"], { allowFailure: true });
171
+ const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "close"], { allowFailure: true });
169
172
  await log(`close result: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
170
173
  }
171
174
 
@@ -182,7 +185,7 @@ async function ensureNotSymlink(path, label) {
182
185
  }
183
186
 
184
187
  async function isAuthBrowserConnected() {
185
- const result = await spawnCommand("agent-browser", [...targetBrowserBaseArgs(), "--json", "stream", "status"], { allowFailure: true });
188
+ const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "--json", "stream", "status"], { allowFailure: true });
186
189
  try {
187
190
  const parsed = JSON.parse(result.stdout || "{}");
188
191
  return parsed?.data?.connected === true;
@@ -274,7 +277,7 @@ async function launchTargetBrowser() {
274
277
  await closeTargetBrowser();
275
278
  const args = [...targetBrowserBaseArgs({ withLaunchOptions: true, mode: "headed" }), "open", "about:blank"];
276
279
  await log(`Launching isolated browser: agent-browser ${JSON.stringify(args)}`);
277
- const result = await spawnCommand("agent-browser", args, { allowFailure: true });
280
+ const result = await spawnCommand(AGENT_BROWSER_BIN, args, { allowFailure: true });
278
281
  await log(`launch result: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
279
282
  if (result.code !== 0) {
280
283
  throw new Error(result.stderr || result.stdout || "Failed to launch isolated oracle browser");
@@ -282,7 +285,7 @@ async function launchTargetBrowser() {
282
285
  }
283
286
 
284
287
  async function streamStatus() {
285
- const result = await spawnCommand("agent-browser", [...targetBrowserBaseArgs(), "--json", "stream", "status"], { allowFailure: true });
288
+ const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "--json", "stream", "status"], { allowFailure: true });
286
289
  await log(`stream status: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
287
290
  try {
288
291
  const parsed = JSON.parse(result.stdout || "{}");
@@ -311,7 +314,7 @@ async function targetCommand(...args) {
311
314
  options = args.pop();
312
315
  }
313
316
  await ensureBrowserConnected();
314
- const result = await spawnCommand("agent-browser", [...targetBrowserBaseArgs(), ...args], options);
317
+ const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), ...args], options);
315
318
  const label = options?.logLabel || `agent-browser ${args.join(" ")}`;
316
319
  await log(`${label}: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
317
320
  return result;
@@ -37,6 +37,9 @@ const ARTIFACT_DOWNLOAD_HEARTBEAT_MS = 10_000;
37
37
  const ARTIFACT_DOWNLOAD_TIMEOUT_MS = 90_000;
38
38
  const ARTIFACT_DOWNLOAD_MAX_ATTEMPTS = 2;
39
39
  const AGENT_BROWSER_CLOSE_TIMEOUT_MS = 10_000;
40
+ const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/agent-browser", "/usr/local/bin/agent-browser"].find(
41
+ (candidate) => typeof candidate === "string" && candidate && existsSync(candidate),
42
+ ) || "agent-browser";
40
43
 
41
44
  let currentJob;
42
45
  let browserStarted = false;
@@ -321,7 +324,7 @@ async function closeBrowser(job) {
321
324
  if (cleaningUpBrowser) return;
322
325
  cleaningUpBrowser = true;
323
326
  try {
324
- const result = await spawnCommand("agent-browser", [...browserBaseArgs(job), "close"], {
327
+ const result = await spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job), "close"], {
325
328
  allowFailure: true,
326
329
  timeoutMs: AGENT_BROWSER_CLOSE_TIMEOUT_MS,
327
330
  });
@@ -337,12 +340,12 @@ async function closeBrowser(job) {
337
340
  async function launchBrowser(job, url) {
338
341
  await closeBrowser(job);
339
342
  const mode = job.config.browser.runMode;
340
- await spawnCommand("agent-browser", [...browserBaseArgs(job, { withLaunchOptions: true, mode }), "open", url]);
343
+ await spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job, { withLaunchOptions: true, mode }), "open", url]);
341
344
  browserStarted = true;
342
345
  }
343
346
 
344
347
  async function streamStatus(job) {
345
- const { stdout } = await spawnCommand("agent-browser", [...browserBaseArgs(job), "--json", "stream", "status"], { allowFailure: true });
348
+ const { stdout } = await spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job), "--json", "stream", "status"], { allowFailure: true });
346
349
  try {
347
350
  const parsed = JSON.parse(stdout || "{}");
348
351
  return parsed?.data || {};
@@ -374,7 +377,7 @@ async function agentBrowser(job, ...args) {
374
377
  options = args.pop();
375
378
  }
376
379
  await ensureBrowserConnected(job);
377
- return spawnCommand("agent-browser", [...browserBaseArgs(job), ...args], options);
380
+ return spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job), ...args], options);
378
381
  }
379
382
 
380
383
  function parseEvalResult(stdout) {
@@ -829,6 +832,17 @@ function detectUploadErrorText(text) {
829
832
  return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
830
833
  }
831
834
 
835
+ function detectResponseFailureText(text) {
836
+ const patterns = [
837
+ "Message delivery timed out",
838
+ "A network error occurred",
839
+ "An error occurred while connecting to the websocket",
840
+ "There was an error generating a response",
841
+ "Something went wrong while generating the response",
842
+ ];
843
+ return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
844
+ }
845
+
832
846
  function composerSnapshotSlice(snapshot) {
833
847
  const lines = snapshot.split("\n");
834
848
  let composerIndex = -1;
@@ -1120,17 +1134,39 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
1120
1134
  const timeoutAt = Date.now() + job.config.worker.completionTimeoutMs;
1121
1135
  let lastText = "";
1122
1136
  let stableCount = 0;
1137
+ let retriedAfterFailure = false;
1123
1138
 
1124
1139
  while (Date.now() < timeoutAt) {
1125
1140
  await heartbeat();
1126
- const snapshot = await snapshotText(job);
1141
+ const [snapshot, body] = await Promise.all([snapshotText(job), pageText(job).catch(() => "")]);
1127
1142
  const hasStopStreaming = snapshot.includes("Stop streaming");
1143
+ const hasRetryButton = snapshot.includes('button "Retry"');
1128
1144
  const copyResponseCount = (snapshot.match(/Copy response/g) || []).length;
1145
+ const responseFailureText = detectResponseFailureText(`${snapshot}\n${body}`);
1129
1146
  const messages = await assistantMessages(job);
1130
1147
  const targetMessage = messages[baselineAssistantCount];
1131
1148
  const targetText = targetMessage?.text || "";
1132
1149
  const hasTargetCopyResponse = copyResponseCount > baselineAssistantCount;
1133
1150
 
1151
+ if (!hasStopStreaming && hasRetryButton && responseFailureText) {
1152
+ if (!retriedAfterFailure) {
1153
+ const retryEntry = findEntry(
1154
+ snapshot,
1155
+ (candidate) => candidate.kind === "button" && candidate.label === "Retry" && !candidate.disabled,
1156
+ );
1157
+ if (retryEntry) {
1158
+ retriedAfterFailure = true;
1159
+ lastText = "";
1160
+ stableCount = 0;
1161
+ await log(`Response delivery failed (${responseFailureText}); clicking Retry once`);
1162
+ await clickRef(job, retryEntry.ref);
1163
+ await agentBrowser(job, "wait", "1000").catch(() => undefined);
1164
+ continue;
1165
+ }
1166
+ }
1167
+ throw new Error(`ChatGPT response failed: ${responseFailureText}`);
1168
+ }
1169
+
1134
1170
  if (!hasStopStreaming && hasTargetCopyResponse && targetText) {
1135
1171
  if (targetText === lastText) stableCount += 1;
1136
1172
  else stableCount = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "ChatGPT web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
5
5
  "private": false,
6
6
  "license": "MIT",