pi-oracle 0.5.0 → 0.6.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.
@@ -7,7 +7,7 @@ import { randomUUID } from "node:crypto";
7
7
  import { spawn } from "node:child_process";
8
8
  import { constants as fsConstants, existsSync, realpathSync, readFileSync } from "node:fs";
9
9
  import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
10
- import { dirname, join } from "node:path";
10
+ import { delimiter, dirname, join } from "node:path";
11
11
  import { jobBlocksAdmission } from "../shared/job-coordination-helpers.mjs";
12
12
  import { isTrackedProcessAlive } from "../shared/process-helpers.mjs";
13
13
  import type { OracleConfig } from "./config.js";
@@ -21,6 +21,14 @@ const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/ag
21
21
  ) || "agent-browser";
22
22
  const PROFILE_CLONE_TIMEOUT_MS = 120_000;
23
23
  const ORACLE_SUBPROCESS_KILL_GRACE_MS = 2_000;
24
+ const WORKSPACE_ROOT_MARKERS = [
25
+ ".pi/extensions/oracle.json",
26
+ ] as const;
27
+ const REQUIRED_ORACLE_DEPENDENCIES = [
28
+ { name: "agent-browser", command: AGENT_BROWSER_BIN },
29
+ { name: "tar", command: "tar" },
30
+ { name: "zstd", command: "zstd" },
31
+ ] as const;
24
32
 
25
33
  export interface OracleRuntimeLeaseMetadata {
26
34
  jobId: string;
@@ -51,7 +59,7 @@ export interface OracleConversationLeaseAttempt {
51
59
  blocker?: OracleConversationLeaseMetadata;
52
60
  }
53
61
 
54
- export function getProjectId(cwd: string): string {
62
+ function resolveRealCwd(cwd: string): string {
55
63
  try {
56
64
  return realpathSync(cwd);
57
65
  } catch {
@@ -59,6 +67,26 @@ export function getProjectId(cwd: string): string {
59
67
  }
60
68
  }
61
69
 
70
+ function hasWorkspaceRootMarker(path: string): boolean {
71
+ return WORKSPACE_ROOT_MARKERS.some((marker) => existsSync(join(path, marker)));
72
+ }
73
+
74
+ function resolveWorkspaceRoot(realCwd: string): string {
75
+ let current = realCwd;
76
+ let nearestMarkerRoot: string | undefined;
77
+ while (true) {
78
+ if (existsSync(join(current, ".git"))) return current;
79
+ if (!nearestMarkerRoot && hasWorkspaceRootMarker(current)) nearestMarkerRoot = current;
80
+ const parent = dirname(current);
81
+ if (parent === current) return nearestMarkerRoot ?? realCwd;
82
+ current = parent;
83
+ }
84
+ }
85
+
86
+ export function getProjectId(cwd: string): string {
87
+ return resolveWorkspaceRoot(resolveRealCwd(cwd));
88
+ }
89
+
62
90
  export function hasPersistedSessionFile(originSessionFile: string | undefined): originSessionFile is string {
63
91
  return Boolean(originSessionFile);
64
92
  }
@@ -110,6 +138,99 @@ function unreadableAuthSeedProfileMessage(seedDir: string): string {
110
138
  return `Oracle auth seed profile is not readable: ${seedDir}. Fix its permissions or rerun /oracle-auth.`;
111
139
  }
112
140
 
141
+ function missingBrowserExecutableMessage(executablePath: string): string {
142
+ return `Configured oracle browser executable does not exist: ${executablePath}. Fix browser.executablePath or install Chrome there.`;
143
+ }
144
+
145
+ function nonExecutableBrowserMessage(executablePath: string): string {
146
+ return `Configured oracle browser executable is not executable: ${executablePath}. Fix browser.executablePath permissions or point it at a runnable Chrome binary.`;
147
+ }
148
+
149
+ function missingLocalDependencyMessage(name: string): string {
150
+ return `Oracle prerequisite not found on PATH: ${name}. Install ${name} and retry.`;
151
+ }
152
+
153
+ function unwritableOracleDirectoryMessage(label: "runtime profiles" | "jobs", path: string): string {
154
+ return `Oracle ${label} directory is not writable: ${path}. Fix its permissions or configure a writable path, then retry.`;
155
+ }
156
+
157
+ async function resolveExecutableOnPath(command: string): Promise<string | undefined> {
158
+ if (!command) return undefined;
159
+ if (command.includes("/")) {
160
+ try {
161
+ await access(command, fsConstants.X_OK);
162
+ return command;
163
+ } catch {
164
+ return undefined;
165
+ }
166
+ }
167
+
168
+ const pathValue = process.env.PATH ?? "";
169
+ for (const segment of pathValue.split(delimiter)) {
170
+ if (!segment) continue;
171
+ const candidate = join(segment, command);
172
+ try {
173
+ await access(candidate, fsConstants.X_OK);
174
+ return candidate;
175
+ } catch {
176
+ continue;
177
+ }
178
+ }
179
+ return undefined;
180
+ }
181
+
182
+ async function assertConfiguredBrowserExecutableReady(executablePath: string | undefined): Promise<void> {
183
+ if (!executablePath) return;
184
+ let executableStats;
185
+ try {
186
+ executableStats = await stat(executablePath);
187
+ } catch (error) {
188
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
189
+ if (code === "ENOENT") throw new Error(missingBrowserExecutableMessage(executablePath));
190
+ if (code === "EACCES" || code === "EPERM") throw new Error(nonExecutableBrowserMessage(executablePath));
191
+ throw new Error(`Failed to inspect configured oracle browser executable ${executablePath}: ${error instanceof Error ? error.message : String(error)}`);
192
+ }
193
+
194
+ if (!executableStats.isFile()) {
195
+ throw new Error(nonExecutableBrowserMessage(executablePath));
196
+ }
197
+
198
+ try {
199
+ await access(executablePath, fsConstants.X_OK);
200
+ } catch {
201
+ throw new Error(nonExecutableBrowserMessage(executablePath));
202
+ }
203
+ }
204
+
205
+ async function assertRequiredLocalDependencyReady(name: string, command: string): Promise<void> {
206
+ const resolved = await resolveExecutableOnPath(command);
207
+ if (!resolved) throw new Error(missingLocalDependencyMessage(name));
208
+ }
209
+
210
+ async function assertWritableDirectory(path: string, label: "runtime profiles" | "jobs"): Promise<void> {
211
+ try {
212
+ await mkdir(path, { recursive: true, mode: 0o700 });
213
+ } catch {
214
+ throw new Error(unwritableOracleDirectoryMessage(label, path));
215
+ }
216
+
217
+ let directoryStats;
218
+ try {
219
+ directoryStats = await stat(path);
220
+ } catch {
221
+ throw new Error(unwritableOracleDirectoryMessage(label, path));
222
+ }
223
+ if (!directoryStats.isDirectory()) {
224
+ throw new Error(unwritableOracleDirectoryMessage(label, path));
225
+ }
226
+
227
+ try {
228
+ await access(path, fsConstants.W_OK | fsConstants.X_OK);
229
+ } catch {
230
+ throw new Error(unwritableOracleDirectoryMessage(label, path));
231
+ }
232
+ }
233
+
113
234
  export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Promise<void> {
114
235
  const seedDir = config.browser.authSeedProfileDir;
115
236
  let seedStats;
@@ -135,6 +256,12 @@ export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Pr
135
256
 
136
257
  export async function assertOracleSubmitPrerequisites(config: OracleConfig): Promise<void> {
137
258
  await assertOracleAuthSeedProfileReady(config);
259
+ await assertConfiguredBrowserExecutableReady(config.browser.executablePath);
260
+ for (const dependency of REQUIRED_ORACLE_DEPENDENCIES) {
261
+ await assertRequiredLocalDependencyReady(dependency.name, dependency.command);
262
+ }
263
+ await assertWritableDirectory(config.browser.runtimeProfilesDir, "runtime profiles");
264
+ await assertWritableDirectory(ORACLE_JOBS_DIR, "jobs");
138
265
  }
139
266
 
140
267
  export function getSeedGeneration(config: OracleConfig): string | undefined {
@@ -7,6 +7,7 @@ import { randomUUID } from "node:crypto";
7
7
  import { lstat, mkdtemp, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
8
8
  import { tmpdir } from "node:os";
9
9
  import { basename, join, posix } from "node:path";
10
+ import { runOracleAuthBootstrap } from "./auth.js";
10
11
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
11
12
  import { Type } from "@sinclair/typebox";
12
13
  import { formatOracleJobSummary, formatOracleSubmitResponse } from "../shared/job-observability-helpers.mjs";
@@ -58,7 +59,11 @@ import {
58
59
 
59
60
  const ORACLE_SUBMIT_PARAMS = Type.Object({
60
61
  prompt: Type.String({ description: "Prompt text to send to ChatGPT web." }),
61
- files: Type.Array(Type.String({ description: "Project-relative file or directory path to include in the archive." }), {
62
+ files: Type.Array(Type.String({
63
+ description: "Project-relative file or directory path to include in the archive.",
64
+ minLength: 1,
65
+ pattern: ".*\\S.*",
66
+ }), {
62
67
  description: "Exact project-relative files/directories to include in the oracle archive.",
63
68
  minItems: 1,
64
69
  }),
@@ -568,7 +573,7 @@ function resolveFollowUp(previousJobId: string | undefined, cwd: string): {
568
573
  };
569
574
  }
570
575
 
571
- type OracleToolName = "oracle_submit" | "oracle_read" | "oracle_cancel";
576
+ type OracleToolName = "oracle_auth" | "oracle_submit" | "oracle_read" | "oracle_cancel";
572
577
  type OracleToolErrorSource = OracleToolName | "oracle_preflight";
573
578
  type OracleQueueSnapshot = { queued: boolean; position?: number; depth?: number };
574
579
  type OracleToolErrorDetails = {
@@ -587,7 +592,7 @@ type OracleToolJobDetailsOptions = {
587
592
  responseAvailable?: boolean;
588
593
  };
589
594
 
590
- const ORACLE_TOOL_NAMES = new Set<OracleToolName>(["oracle_submit", "oracle_read", "oracle_cancel"]);
595
+ const ORACLE_TOOL_NAMES = new Set<OracleToolName>(["oracle_auth", "oracle_submit", "oracle_read", "oracle_cancel"]);
591
596
 
592
597
  function asRecord(value: unknown): Record<string, unknown> | undefined {
593
598
  return typeof value === "object" && value !== null && !Array.isArray(value)
@@ -682,7 +687,7 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
682
687
  code: "auth_seed_profile_missing",
683
688
  message,
684
689
  rejectedValue: message.replace(/^Oracle auth seed profile not found: /, "").replace(/\. Run \/oracle-auth first\.$/, ""),
685
- suggestedNextStep: "Run /oracle-auth first, then retry the oracle tool call.",
690
+ suggestedNextStep: "Call oracle_auth or run /oracle-auth once, then retry the oracle tool call.",
686
691
  };
687
692
  }
688
693
 
@@ -691,7 +696,7 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
691
696
  code: "auth_seed_profile_unreadable",
692
697
  message,
693
698
  rejectedValue: message.replace(/^Oracle auth seed profile is not readable: /, "").replace(/\. Fix its permissions or rerun \/oracle-auth\.$/, ""),
694
- suggestedNextStep: "Fix the auth seed profile permissions or rerun /oracle-auth, then retry.",
699
+ suggestedNextStep: "Fix the auth seed profile permissions or call oracle_auth / rerun /oracle-auth once, then retry.",
695
700
  };
696
701
  }
697
702
 
@@ -700,7 +705,7 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
700
705
  code: "auth_seed_profile_invalid_type",
701
706
  message,
702
707
  rejectedValue: message.replace(/^Oracle auth seed profile is not a directory: /, "").replace(/\. Remove the invalid path or rerun \/oracle-auth\.$/, ""),
703
- suggestedNextStep: "Remove the invalid auth seed path or rerun /oracle-auth to recreate it.",
708
+ suggestedNextStep: "Remove the invalid auth seed path or call oracle_auth / rerun /oracle-auth once to recreate it.",
704
709
  };
705
710
  }
706
711
 
@@ -712,6 +717,52 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
712
717
  };
713
718
  }
714
719
 
720
+ if (message.startsWith("Configured oracle browser executable does not exist: ")) {
721
+ return {
722
+ code: "browser_executable_missing",
723
+ message,
724
+ rejectedValue: message.replace(/^Configured oracle browser executable does not exist: /, "").replace(/\. Fix browser\.executablePath or install Chrome there\.$/, ""),
725
+ suggestedNextStep: "Fix browser.executablePath or install Chrome at that path, then retry.",
726
+ };
727
+ }
728
+
729
+ if (message.startsWith("Configured oracle browser executable is not executable: ")) {
730
+ return {
731
+ code: "browser_executable_not_executable",
732
+ message,
733
+ rejectedValue: message.replace(/^Configured oracle browser executable is not executable: /, "").replace(/\. Fix browser\.executablePath permissions or point it at a runnable Chrome binary\.$/, ""),
734
+ suggestedNextStep: "Fix browser.executablePath permissions or point it at a runnable Chrome binary, then retry.",
735
+ };
736
+ }
737
+
738
+ if (message.startsWith("Oracle prerequisite not found on PATH: ")) {
739
+ const rejectedValue = message.replace(/^Oracle prerequisite not found on PATH: /, "").replace(/\. Install .*$/, "");
740
+ return {
741
+ code: "local_dependency_missing",
742
+ message,
743
+ rejectedValue,
744
+ suggestedNextStep: `Install ${rejectedValue || "the missing dependency"} and retry.`,
745
+ };
746
+ }
747
+
748
+ if (message.startsWith("Oracle runtime profiles directory is not writable: ")) {
749
+ return {
750
+ code: "runtime_profiles_dir_unwritable",
751
+ message,
752
+ rejectedValue: message.replace(/^Oracle runtime profiles directory is not writable: /, "").replace(/\. Fix its permissions or configure a writable path, then retry\.$/, ""),
753
+ suggestedNextStep: "Fix browser.runtimeProfilesDir permissions or configure a writable directory, then retry.",
754
+ };
755
+ }
756
+
757
+ if (message.startsWith("Oracle jobs directory is not writable: ")) {
758
+ return {
759
+ code: "jobs_dir_unwritable",
760
+ message,
761
+ rejectedValue: message.replace(/^Oracle jobs directory is not writable: /, "").replace(/\. Fix its permissions or configure a writable path, then retry\.$/, ""),
762
+ suggestedNextStep: "Fix PI_ORACLE_JOBS_DIR permissions or point it at a writable directory, then retry.",
763
+ };
764
+ }
765
+
715
766
  if (toolName === "oracle_submit" && message === "oracle_submit requires at least one file or directory to archive") {
716
767
  return {
717
768
  code: "archive_input_required",
@@ -720,6 +771,22 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
720
771
  };
721
772
  }
722
773
 
774
+ if (toolName === "oracle_submit" && message === "Archive input must be a non-empty project-relative path") {
775
+ return {
776
+ code: "archive_input_blank",
777
+ message,
778
+ suggestedNextStep: "Retry with a non-empty project-relative file or directory path. Use '.' only when you intentionally want a whole-repo archive.",
779
+ };
780
+ }
781
+
782
+ if (toolName === "oracle_submit" && message === "Archive input must use '.' exactly for a whole-repo archive") {
783
+ return {
784
+ code: "archive_input_whole_repo_sentinel_invalid",
785
+ message,
786
+ suggestedNextStep: "If you want a whole-repo archive, pass '.' exactly. Otherwise pass an exact project-relative path without extra padding.",
787
+ };
788
+ }
789
+
723
790
  if (toolName === "oracle_submit" && message.startsWith("Archive input does not exist: ")) {
724
791
  return {
725
792
  code: "archive_input_missing",
@@ -815,7 +882,13 @@ function buildOracleToolErrorResult(
815
882
  ) {
816
883
  const errorDetails = buildOracleToolErrorDetails(toolName, error, params);
817
884
  return {
818
- content: [{ type: "text" as const, text: errorDetails.message }],
885
+ content: [{
886
+ type: "text" as const,
887
+ text: [
888
+ errorDetails.message,
889
+ errorDetails.suggestedNextStep ? `Suggested next step: ${errorDetails.suggestedNextStep}` : undefined,
890
+ ].filter(Boolean).join("\n"),
891
+ }],
819
892
  details: {
820
893
  job: options?.job ? redactJobDetails(options.job, options.jobDetails) : undefined,
821
894
  error: errorDetails,
@@ -887,15 +960,16 @@ async function runOraclePreflight(ctx: ExtensionContext): Promise<OraclePrefligh
887
960
  try {
888
961
  await assertOracleSubmitPrerequisites(config);
889
962
  } catch (error) {
963
+ const errorDetails = buildOracleToolErrorDetails("oracle_preflight", error, {});
890
964
  return {
891
965
  ready: false,
892
966
  session: { persisted: true, sessionFile },
893
967
  config: { ready: true },
894
968
  auth: {
895
- ready: false,
969
+ ready: !["auth_seed_profile_missing", "auth_seed_profile_unreadable", "auth_seed_profile_invalid_type"].includes(errorDetails.code),
896
970
  seedProfileDir: config.browser.authSeedProfileDir,
897
971
  },
898
- error: buildOracleToolErrorDetails("oracle_preflight", error, {}),
972
+ error: errorDetails,
899
973
  };
900
974
  }
901
975
 
@@ -910,7 +984,7 @@ async function runOraclePreflight(ctx: ExtensionContext): Promise<OraclePrefligh
910
984
  };
911
985
  }
912
986
 
913
- export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void {
987
+ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWorkerPath = workerPath): void {
914
988
  pi.on("tool_result", async (event) => {
915
989
  if (!ORACLE_TOOL_NAMES.has(event.toolName as OracleToolName)) return;
916
990
  if (event.isError) return;
@@ -939,6 +1013,34 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
939
1013
  },
940
1014
  });
941
1015
 
1016
+ pi.registerTool({
1017
+ name: "oracle_auth",
1018
+ label: "Oracle Auth",
1019
+ description: "Refresh the shared oracle auth seed profile by importing ChatGPT cookies from your configured real Chrome profile.",
1020
+ promptSnippet: "Refresh oracle auth before retrying a login-required oracle run.",
1021
+ promptGuidelines: [
1022
+ "Call oracle_auth when an oracle run failed because ChatGPT login is required, the worker said to rerun /oracle-auth, or stale auth appears to be blocking submission execution.",
1023
+ "At most once per user request, refresh auth and then retry the blocked oracle submission.",
1024
+ "If oracle_auth itself fails, stop and report the failure instead of looping.",
1025
+ ],
1026
+ parameters: Type.Object({}),
1027
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1028
+ try {
1029
+ const projectCwd = getProjectId(ctx.cwd);
1030
+ const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd);
1031
+ return {
1032
+ content: [{ type: "text" as const, text: message }],
1033
+ details: {
1034
+ refreshed: true,
1035
+ authSeedProfileDir: loadOracleConfig(projectCwd).browser.authSeedProfileDir,
1036
+ },
1037
+ };
1038
+ } catch (error) {
1039
+ return buildOracleToolErrorResult("oracle_auth", error, {});
1040
+ }
1041
+ },
1042
+ });
1043
+
942
1044
  pi.registerTool({
943
1045
  name: "oracle_submit",
944
1046
  label: "Oracle Submit",
@@ -948,9 +1050,11 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
948
1050
  promptSnippet: "Dispatch a background ChatGPT web oracle job after gathering repo context.",
949
1051
  promptGuidelines: [
950
1052
  "Gather context before calling oracle_submit.",
951
- "By default, archive the whole repo by passing '.'; default archive exclusions apply automatically, including common bulky outputs and obvious credentials/private data like .env files, key material, credential dotfiles, local database files, and nested secrets directories anywhere in the repo.",
952
- "Only narrow file selection when the user explicitly asks, the task is clearly scoped smaller, or privacy/sensitivity requires it.",
953
- "For very targeted asks like a single function or stack trace, a smaller archive is preferable.",
1053
+ "If the immediately preceding oracle run failed because ChatGPT login is required or the worker explicitly said to rerun /oracle-auth, call oracle_auth once before retrying the submission. Do not loop auth refreshes.",
1054
+ "Prefer context-rich archives up to the 250 MB ceiling because more relevant surrounding context is usually better than less.",
1055
+ "By default, archive the whole repo by passing '.' for broad or unclear requests; default archive exclusions apply automatically, including common bulky outputs and obvious credentials/private data like .env files, key material, credential dotfiles, local database files, and nested secrets directories anywhere in the repo.",
1056
+ "For narrower asks, still include nearby tests, docs, configs, and adjacent modules when they may improve answer quality. Only narrow aggressively when the user explicitly asks, privacy/sensitivity requires it, or size pressure forces it.",
1057
+ "Do not default to a one-file archive for a single function, file, or stack trace if the relevant surrounding context still fits comfortably within the limit.",
954
1058
  "When files='.' and the post-exclusion archive is still too large, submit automatically prunes the largest nested directories matching generic generated-output names like build/, dist/, out/, coverage/, and tmp/ outside obvious source roots like src/ and lib/ until the archive fits or no candidate remains; successful submissions report what was pruned.",
955
1059
  "If a submitted oracle job later fails because upload is rejected, retry smaller: remove the largest obviously irrelevant/generated content first, then narrow to modified files plus adjacent files plus directly relevant subtrees, then explain the cut or ask the user if still needed.",
956
1060
  "If oracle_submit itself fails because the local archive still exceeds the upload limit after default exclusions and automatic generic generated-output-dir pruning, or for any other submit-time error, stop and report the error instead of retrying automatically.",
@@ -961,18 +1065,19 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
961
1065
  parameters: ORACLE_SUBMIT_PARAMS,
962
1066
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
963
1067
  try {
964
- const config = loadOracleConfig(ctx.cwd);
1068
+ const projectCwd = getProjectId(ctx.cwd);
1069
+ const config = loadOracleConfig(projectCwd);
965
1070
  const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
966
- const projectId = getProjectId(ctx.cwd);
1071
+ const projectId = getProjectId(projectCwd);
967
1072
  const sessionId = getSessionId(originSessionFile, projectId);
968
1073
  const presetId = typeof params.preset === "string" ? coerceOracleSubmitPresetId(params.preset) : config.defaults.preset;
969
1074
  const selection = resolveOracleSubmitPreset(presetId);
970
- const followUp = resolveFollowUp(params.followUpJobId, ctx.cwd);
1075
+ const followUp = resolveFollowUp(params.followUpJobId, projectCwd);
971
1076
  // Validate caller-specified archive paths before surfacing unrelated local setup failures such as a missing auth seed profile.
972
- resolveArchiveInputs(ctx.cwd, params.files);
1077
+ resolveArchiveInputs(projectCwd, params.files);
973
1078
  await assertOracleSubmitPrerequisites(config);
974
1079
  try {
975
- await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_submit", cwd: ctx.cwd }, async () => {
1080
+ await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_submit", cwd: projectCwd }, async () => {
976
1081
  await reconcileStaleOracleJobs();
977
1082
  await pruneTerminalOracleJobs();
978
1083
  });
@@ -993,7 +1098,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
993
1098
  let spawnedWorker: Awaited<ReturnType<typeof spawnWorker>> | undefined;
994
1099
 
995
1100
  try {
996
- archive = await createArchive(ctx.cwd, params.files, tempArchivePath);
1101
+ archive = await createArchive(projectCwd, params.files, tempArchivePath);
997
1102
  const currentArchive = archive;
998
1103
  await withLock("admission", "global", { jobId, processPid: process.pid }, async () => {
999
1104
  await promoteQueuedJobsWithinAdmissionLock({ workerPath, source: "oracle_submit" });
@@ -1036,7 +1141,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
1036
1141
  chatUrl: followUp.chatUrl,
1037
1142
  requestSource: "tool",
1038
1143
  },
1039
- ctx.cwd,
1144
+ projectCwd,
1040
1145
  originSessionFile,
1041
1146
  config,
1042
1147
  runtime,
@@ -1079,7 +1184,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
1079
1184
  chatUrl: followUp.chatUrl,
1080
1185
  requestSource: "tool",
1081
1186
  },
1082
- ctx.cwd,
1187
+ projectCwd,
1083
1188
  originSessionFile,
1084
1189
  config,
1085
1190
  runtime,
@@ -111,11 +111,11 @@ export function buildOracleWakeupNotificationContent(job, options = {}) {
111
111
  const artifactsPath = options.artifactsPath ?? `artifacts unavailable for ${job.id}`;
112
112
  return [
113
113
  `Oracle job ${job.id} is ${job.status}.`,
114
- `Use oracle_read with jobId ${job.id} to open the response and settle wake-up retries.`,
114
+ `Use /oracle-read ${job.id} to inspect the saved response preview. /oracle-status ${job.id} still shows saved job metadata. Agent callers can use oracle_read({ jobId: "${job.id}" }) if they need tool output in the current turn.`,
115
115
  responseLine,
116
116
  `Artifacts: ${artifactsPath}`,
117
117
  formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job)) ? `Last event: ${formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job))}` : undefined,
118
- job.error ? `Error: ${job.error}` : "After oracle_read, continue from the oracle output.",
118
+ job.error ? `Error: ${job.error}` : "After opening the saved result, continue from the oracle output.",
119
119
  ].filter(Boolean).join("\n");
120
120
  }
121
121
 
@@ -69,7 +69,7 @@ export function classifyChatAuthPage(args) {
69
69
  const hasAddFiles = args.snapshot.includes(`button "${addFilesLabel}"`);
70
70
  const hasModelControl =
71
71
  args.snapshot.includes('button "Model selector"') ||
72
- /button "(Instant|Thinking|Pro)(?: [^"]*)?"/.test(args.snapshot);
72
+ /button "(?:Instant|(?:(?:Light|Standard|Extended|Heavy) )?Thinking|(?:(?:Light|Standard|Extended|Heavy) )?Pro)(?:, click to remove)?"/i.test(args.snapshot);
73
73
 
74
74
  const challengePatterns = [
75
75
  /just a moment/i,