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.
- package/CHANGELOG.md +16 -0
- package/README.md +21 -7
- package/docs/ORACLE_DESIGN.md +25 -15
- package/extensions/oracle/index.ts +1 -1
- package/extensions/oracle/lib/auth.ts +50 -0
- package/extensions/oracle/lib/commands.ts +51 -50
- package/extensions/oracle/lib/config.ts +3 -1
- package/extensions/oracle/lib/jobs.ts +14 -3
- package/extensions/oracle/lib/poller.ts +8 -2
- package/extensions/oracle/lib/runtime.ts +129 -2
- package/extensions/oracle/lib/tools.ts +126 -21
- package/extensions/oracle/shared/job-observability-helpers.mjs +2 -2
- package/extensions/oracle/worker/auth-flow-helpers.mjs +1 -1
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +106 -41
- package/extensions/oracle/worker/run-job.mjs +17 -13
- package/package.json +6 -2
- package/prompts/oracle-followup.md +48 -0
- package/prompts/oracle.md +7 -6
|
@@ -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
|
-
|
|
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({
|
|
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: "
|
|
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: [{
|
|
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:
|
|
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:
|
|
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
|
-
"
|
|
952
|
-
"
|
|
953
|
-
"
|
|
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
|
|
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(
|
|
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,
|
|
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(
|
|
1077
|
+
resolveArchiveInputs(projectCwd, params.files);
|
|
973
1078
|
await assertOracleSubmitPrerequisites(config);
|
|
974
1079
|
try {
|
|
975
|
-
await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_submit", cwd:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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)(
|
|
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,
|