pi-oracle 0.7.7 → 0.7.8
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 +17 -0
- package/README.md +6 -6
- package/docs/ORACLE_DESIGN.md +13 -9
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +18 -17
- package/docs/platform-smoke.md +1 -1
- package/extensions/oracle/index.ts +84 -4
- package/extensions/oracle/lib/auth.ts +4 -4
- package/extensions/oracle/lib/commands.ts +48 -22
- package/extensions/oracle/lib/poller.ts +20 -5
- package/extensions/oracle/lib/runtime.ts +8 -0
- package/extensions/oracle/lib/tools.ts +18 -5
- package/extensions/oracle/shared/browser-profile-helpers.d.mts +15 -0
- package/extensions/oracle/shared/browser-profile-helpers.mjs +37 -13
- package/extensions/oracle/shared/job-observability-helpers.d.mts +3 -1
- package/extensions/oracle/shared/job-observability-helpers.mjs +14 -5
- package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +9 -0
- package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +29 -2
- package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +1 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +52 -13
- package/extensions/oracle/worker/run-job.mjs +179 -53
- package/package.json +3 -6
- package/prompts/oracle-followup.md +2 -2
- package/prompts/oracle.md +13 -5
- package/scripts/oracle-real-smoke.mjs +6 -2
|
@@ -30,12 +30,14 @@ import {
|
|
|
30
30
|
effortSelectionVisible,
|
|
31
31
|
snapshotCanSafelySkipModelConfiguration,
|
|
32
32
|
snapshotHasModelConfigurationUi,
|
|
33
|
+
snapshotHasModelOpener,
|
|
33
34
|
snapshotHasUsableComposerControls,
|
|
34
35
|
snapshotStronglyMatchesRequestedModel,
|
|
35
36
|
snapshotWeaklyMatchesRequestedModel,
|
|
36
37
|
autoSwitchToThinkingSelectionVisible,
|
|
38
|
+
stripChatGptResponseChrome,
|
|
37
39
|
} from "./chatgpt-ui-helpers.mjs";
|
|
38
|
-
import { assistantSnapshotSlice, nextStableValueState, resolveStableConversationUrlCandidate, stripUrlQueryAndHash } from "./chatgpt-flow-helpers.mjs";
|
|
40
|
+
import { assistantSnapshotSlice, nextStableValueState, providerSendAccepted, resolveStableConversationUrlCandidate, stripUrlQueryAndHash } from "./chatgpt-flow-helpers.mjs";
|
|
39
41
|
import { assertNotKnownBrowserUserDataPath, scrubSweetCookieSafeStoragePasswordEnv, sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
|
|
40
42
|
import { createLease, listLeaseMetadata, readLeaseMetadata, releaseLease, withLock } from "./state-locks.mjs";
|
|
41
43
|
|
|
@@ -86,6 +88,7 @@ const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/ag
|
|
|
86
88
|
const CP_BIN = process.env.PI_ORACLE_CP_PATH?.trim() || "cp";
|
|
87
89
|
scrubSweetCookieSafeStoragePasswordEnv();
|
|
88
90
|
|
|
91
|
+
let cpSupportsApfsCloneFlag;
|
|
89
92
|
let currentJob;
|
|
90
93
|
let browserStarted = false;
|
|
91
94
|
let cleaningUpBrowser = false;
|
|
@@ -277,6 +280,14 @@ function spawnCommand(command, args, options = {}) {
|
|
|
277
280
|
});
|
|
278
281
|
}
|
|
279
282
|
|
|
283
|
+
async function cpSupportsApfsClone() {
|
|
284
|
+
if (process.platform !== "darwin") return false;
|
|
285
|
+
if (cpSupportsApfsCloneFlag !== undefined) return cpSupportsApfsCloneFlag;
|
|
286
|
+
const probe = await spawnCommand(CP_BIN, ["-c"], { allowFailure: true, timeoutMs: 5_000 });
|
|
287
|
+
cpSupportsApfsCloneFlag = !/invalid option\s+--\s+['"]?c/i.test(`${probe.stderr}\n${probe.stdout}`);
|
|
288
|
+
return cpSupportsApfsCloneFlag;
|
|
289
|
+
}
|
|
290
|
+
|
|
280
291
|
function parseConversationId(chatUrl) {
|
|
281
292
|
if (!chatUrl) return undefined;
|
|
282
293
|
try {
|
|
@@ -321,7 +332,7 @@ async function cloneSeedProfileToRuntime(job) {
|
|
|
321
332
|
await withLock(ORACLE_STATE_DIR, "auth", "global", { jobId: job.id, processPid: process.pid, action: "cloneSeedProfile" }, async () => {
|
|
322
333
|
await rm(job.runtimeProfileDir, { recursive: true, force: true }).catch(() => undefined);
|
|
323
334
|
await ensurePrivateDir(dirname(job.runtimeProfileDir));
|
|
324
|
-
if (job.config.browser.cloneStrategy === "apfs-clone" &&
|
|
335
|
+
if (job.config.browser.cloneStrategy === "apfs-clone" && await cpSupportsApfsClone()) {
|
|
325
336
|
try {
|
|
326
337
|
await spawnCommand(CP_BIN, ["-cR", seedDir, job.runtimeProfileDir], { timeoutMs: PROFILE_CLONE_TIMEOUT_MS });
|
|
327
338
|
} catch (error) {
|
|
@@ -774,8 +785,8 @@ function snapshotHasCompactIntelligenceMenuControls(snapshot) {
|
|
|
774
785
|
return Boolean(findEntry(snapshot, (candidate) => {
|
|
775
786
|
if (candidate.disabled) return false;
|
|
776
787
|
const label = normalizeSnapshotLabel(candidate.label);
|
|
777
|
-
return (candidate.kind === "menu" && /Intelligence.*Instant.*Medium.*High.*Pro/i.test(label))
|
|
778
|
-
|| (candidate.kind === "menuitemradio" && /^(?:Instant
|
|
788
|
+
return (candidate.kind === "menu" && /(?:Intelligence.*Instant.*Medium.*High.*Pro|^(?:Instant|Medium|High|Extra High|Pro Extended)$)/i.test(label))
|
|
789
|
+
|| (candidate.kind === "menuitemradio" && /^(?:Instant(?:\s+5s)?|Medium(?:\s+5\s*[–-]\s*30s)?|High(?:\s+15\s*[–-]\s*60s)?|Extra High|Pro\s+5\+\s*min|Pro Standard|Pro Extended)$/i.test(label));
|
|
779
790
|
}));
|
|
780
791
|
}
|
|
781
792
|
|
|
@@ -783,9 +794,10 @@ function matchesRequestedModelControl(candidate, selection, options = {}) {
|
|
|
783
794
|
if (!["button", "radio", "menuitemradio"].includes(candidate.kind || "") || typeof candidate.label !== "string" || candidate.disabled) return false;
|
|
784
795
|
if (candidate.kind === "button") {
|
|
785
796
|
if (/\bexpanded=true\b/.test(String(candidate.line || ""))) return false;
|
|
786
|
-
if (options.ignoreCompactTierButtons && /^(?:Instant|Medium|High|Pro)$/i.test(candidate.label)) return false;
|
|
787
|
-
if (options.ignoreCompactOnlyButtons && /^(?:Medium|High)$/i.test(candidate.label)) return false;
|
|
797
|
+
if (options.ignoreCompactTierButtons && /^(?:Instant|Medium|High|Extra High|Pro|Pro Extended)$/i.test(candidate.label)) return false;
|
|
798
|
+
if (options.ignoreCompactOnlyButtons && /^(?:Medium|High|Extra High)$/i.test(candidate.label)) return false;
|
|
788
799
|
}
|
|
800
|
+
if (selection.modelFamily === "pro" && /^Pro(?:\s+Extended)?$/i.test(candidate.label)) return true;
|
|
789
801
|
return matchesRequestedModelControlLabel(candidate.label, selection);
|
|
790
802
|
}
|
|
791
803
|
|
|
@@ -859,7 +871,41 @@ async function maybeClickLabeledEntry(job, label, options = {}) {
|
|
|
859
871
|
}
|
|
860
872
|
|
|
861
873
|
async function openEffortDropdown(job) {
|
|
862
|
-
|
|
874
|
+
let snapshot = await snapshotText(job);
|
|
875
|
+
if (job.selection?.modelFamily === "pro") {
|
|
876
|
+
let proEffortEntry = findEntry(
|
|
877
|
+
snapshot,
|
|
878
|
+
(candidate) => candidate.kind === "menuitem" && candidate.label === "Pro effort options" && !candidate.disabled,
|
|
879
|
+
);
|
|
880
|
+
if (!proEffortEntry) {
|
|
881
|
+
const opener = findEntry(snapshot, matchesModelConfigurationOpener);
|
|
882
|
+
if (opener) {
|
|
883
|
+
await clickRef(job, opener.ref);
|
|
884
|
+
await agentBrowser(job, "wait", "500");
|
|
885
|
+
snapshot = await snapshotText(job);
|
|
886
|
+
proEffortEntry = findEntry(
|
|
887
|
+
snapshot,
|
|
888
|
+
(candidate) => candidate.kind === "menuitem" && candidate.label === "Pro effort options" && !candidate.disabled,
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (proEffortEntry) {
|
|
893
|
+
try {
|
|
894
|
+
await clickRef(job, proEffortEntry.ref);
|
|
895
|
+
return true;
|
|
896
|
+
} catch {
|
|
897
|
+
// Fall through to DOM click. ChatGPT's tiny trailing Pro effort icon can
|
|
898
|
+
// be covered at the accessibility click point by the parent Pro row.
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const clicked = await evalPage(job, toJsonScript(`
|
|
902
|
+
const el = document.querySelector('[aria-label="Pro effort options"], [data-composer-intelligence-pro-effort-action]');
|
|
903
|
+
if (!el) return false;
|
|
904
|
+
el.click();
|
|
905
|
+
return true;
|
|
906
|
+
`));
|
|
907
|
+
if (clicked) return true;
|
|
908
|
+
}
|
|
863
909
|
const effortLabels = new Set(["Light", "Standard", "Extended", "Heavy"]);
|
|
864
910
|
const entry = findEntry(
|
|
865
911
|
snapshot,
|
|
@@ -1157,46 +1203,128 @@ async function waitForSendReady(job) {
|
|
|
1157
1203
|
throw new Error(`Timed out waiting for ${labelsForJob(job).send} to become enabled`);
|
|
1158
1204
|
}
|
|
1159
1205
|
|
|
1160
|
-
async function
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
if (
|
|
1206
|
+
async function activateSendButton(job) {
|
|
1207
|
+
const result = await evalPage(job, toJsonScript(`
|
|
1208
|
+
const labels = ${JSON.stringify(labelsForJob(job))};
|
|
1209
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
1210
|
+
const button = buttons.find((candidate) => {
|
|
1211
|
+
const label = (candidate.getAttribute('aria-label') || candidate.textContent || '').trim();
|
|
1212
|
+
return label === labels.send;
|
|
1213
|
+
});
|
|
1214
|
+
if (!button) return { ok: false, reason: 'send button not found' };
|
|
1215
|
+
if (button.disabled || button.getAttribute('aria-disabled') === 'true') return { ok: false, reason: 'send button disabled' };
|
|
1216
|
+
button.click();
|
|
1217
|
+
return { ok: true };
|
|
1218
|
+
`));
|
|
1219
|
+
return result;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
async function sendAcceptanceState(job, baselineAssistantCount) {
|
|
1223
|
+
const [urlResult, snapshot, messages] = await Promise.all([
|
|
1224
|
+
currentUrl(job).then((url) => ({ url, ok: true })).catch(() => ({ url: "", ok: false })),
|
|
1225
|
+
snapshotText(job).catch(() => ""),
|
|
1226
|
+
assistantMessages(job).catch(() => []),
|
|
1227
|
+
]);
|
|
1228
|
+
return {
|
|
1229
|
+
url: urlResult.url,
|
|
1230
|
+
urlKnown: urlResult.ok,
|
|
1231
|
+
assistantCount: Math.max(baselineAssistantCount, messages.length),
|
|
1232
|
+
stopStreaming: isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming"),
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
async function clickSend(job, baselineAssistantCount) {
|
|
1237
|
+
await waitForSendReady(job);
|
|
1238
|
+
const beforeSend = await sendAcceptanceState(job, baselineAssistantCount);
|
|
1239
|
+
const activation = await activateSendButton(job);
|
|
1240
|
+
if (!activation?.ok) throw new Error(`Could not activate ${labelsForJob(job).send}: ${activation?.reason || "DOM activation failed"}`);
|
|
1241
|
+
await log(`Activated ${labelsForJob(job).send}; waiting for provider acceptance evidence`);
|
|
1242
|
+
if (await waitForSendAccepted(job, beforeSend, { timeoutMs: 20_000 })) return;
|
|
1243
|
+
|
|
1244
|
+
await captureDiagnostics(job, "send-not-accepted");
|
|
1245
|
+
throw new Error(`${isGrokJob(job) ? "Grok" : "ChatGPT"} message did not leave the composer after activating ${labelsForJob(job).send}`);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
async function waitForSendAccepted(job, beforeSend, options = {}) {
|
|
1249
|
+
const timeoutAt = Date.now() + (options.timeoutMs || 15_000);
|
|
1250
|
+
while (Date.now() < timeoutAt) {
|
|
1251
|
+
await heartbeat();
|
|
1252
|
+
const afterSend = await sendAcceptanceState(job, beforeSend.assistantCount || 0);
|
|
1253
|
+
if (providerSendAccepted(beforeSend, afterSend)) return true;
|
|
1254
|
+
await sleep(500);
|
|
1170
1255
|
}
|
|
1171
|
-
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
async function dismissProFeedbackModal(job, snapshot) {
|
|
1260
|
+
const entries = parseSnapshotEntries(snapshot);
|
|
1261
|
+
const hasProFeedback = entries.some((entry) => entry.kind === "heading" && entry.label === "Pro feedback" && !entry.disabled);
|
|
1262
|
+
if (!hasProFeedback) return false;
|
|
1263
|
+
const close = entries.find((entry) => entry.kind === "button" && entry.label === CHATGPT_LABELS.close && !entry.disabled);
|
|
1264
|
+
if (close) {
|
|
1265
|
+
await clickRef(job, close.ref).catch(() => undefined);
|
|
1266
|
+
await agentBrowser(job, "wait", "500");
|
|
1267
|
+
if (!(await pageText(job).catch(() => "")).includes("Pro feedback")) return true;
|
|
1268
|
+
}
|
|
1269
|
+
await agentBrowser(job, "press", "Escape").catch(() => undefined);
|
|
1270
|
+
await agentBrowser(job, "wait", "500");
|
|
1271
|
+
if (!(await pageText(job).catch(() => "")).includes("Pro feedback")) return true;
|
|
1272
|
+
|
|
1273
|
+
const dismissed = await evalPage(job, toJsonScript(`
|
|
1274
|
+
const dialogText = document.body.innerText || '';
|
|
1275
|
+
if (!/Pro feedback/.test(dialogText)) return false;
|
|
1276
|
+
const button = Array.from(document.querySelectorAll('button'))
|
|
1277
|
+
.find((candidate) => (candidate.getAttribute('aria-label') || candidate.textContent || '').trim() === 'Close');
|
|
1278
|
+
if (!button) return false;
|
|
1279
|
+
button.click();
|
|
1280
|
+
return true;
|
|
1281
|
+
`));
|
|
1282
|
+
if (dismissed) await agentBrowser(job, "wait", "500");
|
|
1283
|
+
return Boolean(dismissed);
|
|
1172
1284
|
}
|
|
1173
1285
|
|
|
1174
1286
|
async function openModelConfiguration(job) {
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1287
|
+
const timeoutAt = Date.now() + 15_000;
|
|
1288
|
+
let lastSnapshot = "";
|
|
1177
1289
|
|
|
1178
|
-
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
if (
|
|
1182
|
-
await
|
|
1183
|
-
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1290
|
+
while (Date.now() < timeoutAt) {
|
|
1291
|
+
const initialSnapshot = await snapshotText(job);
|
|
1292
|
+
lastSnapshot = initialSnapshot;
|
|
1293
|
+
if (snapshotHasModelConfigurationUi(initialSnapshot)) return initialSnapshot;
|
|
1294
|
+
if (await dismissProFeedbackModal(job, initialSnapshot)) continue;
|
|
1295
|
+
|
|
1296
|
+
for (const predicate of [matchesModelConfigurationOpener]) {
|
|
1297
|
+
const snapshot = await snapshotText(job);
|
|
1298
|
+
lastSnapshot = snapshot;
|
|
1299
|
+
const entry = findEntry(snapshot, predicate);
|
|
1300
|
+
if (!entry) continue;
|
|
1301
|
+
await clickRef(job, entry.ref);
|
|
1302
|
+
await agentBrowser(job, "wait", "800");
|
|
1303
|
+
const after = await snapshotText(job);
|
|
1304
|
+
lastSnapshot = after;
|
|
1305
|
+
if (snapshotHasModelConfigurationUi(after)) return after;
|
|
1306
|
+
if (canUseOpenModelMenuForSelection(after, job.selection)) return after;
|
|
1307
|
+
|
|
1308
|
+
const configureEntry = findEntry(
|
|
1309
|
+
after,
|
|
1310
|
+
(candidate) => candidate.kind === "menuitem" && candidate.label === CHATGPT_LABELS.configure && !candidate.disabled,
|
|
1311
|
+
);
|
|
1187
1312
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1313
|
+
if (configureEntry) {
|
|
1314
|
+
await clickRef(job, configureEntry.ref);
|
|
1315
|
+
await agentBrowser(job, "wait", "1200");
|
|
1316
|
+
const postConfigure = await snapshotText(job);
|
|
1317
|
+
lastSnapshot = postConfigure;
|
|
1318
|
+
if (snapshotHasModelConfigurationUi(postConfigure)) return postConfigure;
|
|
1319
|
+
if (canUseOpenModelMenuForSelection(postConfigure, job.selection)) return postConfigure;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1192
1322
|
|
|
1193
|
-
if (
|
|
1194
|
-
await
|
|
1195
|
-
|
|
1196
|
-
const postConfigure = await snapshotText(job);
|
|
1197
|
-
if (snapshotHasModelConfigurationUi(postConfigure)) return postConfigure;
|
|
1198
|
-
if (canUseOpenModelMenuForSelection(postConfigure, job.selection)) return postConfigure;
|
|
1323
|
+
if (composerControlsVisible(lastSnapshot, job) && !snapshotHasModelOpener(lastSnapshot)) {
|
|
1324
|
+
await agentBrowser(job, "wait", "1000");
|
|
1325
|
+
continue;
|
|
1199
1326
|
}
|
|
1327
|
+
await agentBrowser(job, "wait", "500");
|
|
1200
1328
|
}
|
|
1201
1329
|
|
|
1202
1330
|
throw new Error("Could not open model configuration UI");
|
|
@@ -1303,7 +1431,11 @@ async function configureModel(job) {
|
|
|
1303
1431
|
throw new Error(`Could not open effort dropdown for requested effort: ${effortLabel}`);
|
|
1304
1432
|
}
|
|
1305
1433
|
await agentBrowser(job, "wait", "300");
|
|
1306
|
-
await
|
|
1434
|
+
if (job.selection.modelFamily === "pro" && await maybeClickLabeledEntry(job, `Pro ${effortLabel}`, { kind: "menuitemradio" })) {
|
|
1435
|
+
// Current ChatGPT exposes Pro effort choices as nested menu radio items.
|
|
1436
|
+
} else {
|
|
1437
|
+
await clickLabeledEntry(job, effortLabel, { kind: "option" });
|
|
1438
|
+
}
|
|
1307
1439
|
await agentBrowser(job, "wait", "400");
|
|
1308
1440
|
const effortSnapshot = await snapshotText(job);
|
|
1309
1441
|
verificationSnapshot = effortSnapshot;
|
|
@@ -2000,15 +2132,8 @@ async function downloadArtifacts(job, responseIndex, responseText = "") {
|
|
|
2000
2132
|
}
|
|
2001
2133
|
}
|
|
2002
2134
|
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
const missedArtifactLabels = suspiciousLabels.filter((label) => !capturedArtifactLabels.has(label) && !capturedArtifactKeys.has(String(label).replace(/\s+/g, "")));
|
|
2006
|
-
if (missedArtifactLabels.length > 0) {
|
|
2007
|
-
await log(`Marking missed artifact signals as unconfirmed: ${missedArtifactLabels.join(", ")}`);
|
|
2008
|
-
for (const label of missedArtifactLabels) {
|
|
2009
|
-
artifacts.push({ displayName: label, unconfirmed: true, error: "Response-local artifact signal was present, but no downloadable artifact was captured." });
|
|
2010
|
-
}
|
|
2011
|
-
await flushArtifactsState(artifacts);
|
|
2135
|
+
if (suspiciousLabels.length > 0) {
|
|
2136
|
+
await log(`Ignoring plain-text artifact-like labels without downloadable controls: ${suspiciousLabels.join(", ")}`);
|
|
2012
2137
|
}
|
|
2013
2138
|
|
|
2014
2139
|
return artifacts;
|
|
@@ -2079,8 +2204,8 @@ async function run() {
|
|
|
2079
2204
|
await setComposerText(currentJob, await readFile(currentJob.promptPath, "utf8"));
|
|
2080
2205
|
const baselineAssistantCount = (await assistantMessages(currentJob)).length;
|
|
2081
2206
|
await log(`Assistant response count before send: ${baselineAssistantCount}`);
|
|
2082
|
-
await clickSend(currentJob);
|
|
2083
|
-
await log(`
|
|
2207
|
+
await clickSend(currentJob, baselineAssistantCount);
|
|
2208
|
+
await log(`Send accepted; waiting ${POST_SEND_SETTLE_MS}ms after send to avoid streaming interruption`);
|
|
2084
2209
|
await sleep(POST_SEND_SETTLE_MS);
|
|
2085
2210
|
|
|
2086
2211
|
const observedChatUrl = isGrokJob(currentJob)
|
|
@@ -2118,14 +2243,15 @@ async function run() {
|
|
|
2118
2243
|
message: "Extracting the completed response body.",
|
|
2119
2244
|
patch: { heartbeatAt: new Date().toISOString() },
|
|
2120
2245
|
}));
|
|
2121
|
-
|
|
2246
|
+
const responseText = isGrokJob(currentJob) ? completion.responseText.trim() : stripChatGptResponseChrome(completion.responseText);
|
|
2247
|
+
await secureWriteText(currentJob.responsePath, `${responseText}\n`);
|
|
2122
2248
|
currentJob = await mutateJob((job) => transitionOracleJobPhase(job, "downloading_artifacts", {
|
|
2123
2249
|
at: new Date().toISOString(),
|
|
2124
2250
|
source: "oracle:worker",
|
|
2125
2251
|
message: "Downloading any response artifacts.",
|
|
2126
2252
|
patch: { heartbeatAt: new Date().toISOString() },
|
|
2127
2253
|
}));
|
|
2128
|
-
const artifacts = await downloadArtifacts(currentJob, completion.responseIndex,
|
|
2254
|
+
const artifacts = await downloadArtifacts(currentJob, completion.responseIndex, responseText);
|
|
2129
2255
|
const artifactFailureCount = artifacts.filter((artifact) => artifact.unconfirmed || artifact.error).length;
|
|
2130
2256
|
const finalPhase = artifactFailureCount > 0 ? "complete_with_artifact_errors" : "complete";
|
|
2131
2257
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-oracle",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"description": "ChatGPT and Grok web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,9 +41,6 @@
|
|
|
41
41
|
"pi": {
|
|
42
42
|
"extensions": [
|
|
43
43
|
"./extensions/oracle/index.ts"
|
|
44
|
-
],
|
|
45
|
-
"prompts": [
|
|
46
|
-
"./prompts"
|
|
47
44
|
]
|
|
48
45
|
},
|
|
49
46
|
"scripts": {
|
|
@@ -83,8 +80,8 @@
|
|
|
83
80
|
"protobufjs": "7.6.1"
|
|
84
81
|
},
|
|
85
82
|
"devDependencies": {
|
|
86
|
-
"@earendil-works/pi-ai": "
|
|
87
|
-
"@earendil-works/pi-coding-agent": "
|
|
83
|
+
"@earendil-works/pi-ai": "0.79.1",
|
|
84
|
+
"@earendil-works/pi-coding-agent": "0.79.1",
|
|
88
85
|
"@types/node": "^22.19.19",
|
|
89
86
|
"esbuild": "^0.28.0",
|
|
90
87
|
"tsx": "^4.22.3",
|
|
@@ -10,7 +10,7 @@ Required workflow:
|
|
|
10
10
|
2. If the request does not include both a prior oracle job id and a follow-up request, stop and report: `Usage: /oracle-followup <job-id> <request>. Find the job id in the earlier oracle response or via /oracle-status.`
|
|
11
11
|
3. Same-thread follow-ups cannot switch providers. If the user asks to move a ChatGPT thread to Grok or a Grok thread to ChatGPT, stop and tell them to start a new `/oracle` job instead.
|
|
12
12
|
4. Call `oracle_preflight` immediately with the parsed `followUpJobId` so readiness checks use the prior job's provider.
|
|
13
|
-
5. If `oracle_preflight` reports `ready: false`, stop before any expensive prep. Do not read files, search the codebase,
|
|
13
|
+
5. If `oracle_preflight` reports `ready: false`, stop before any expensive prep. Do not read files, search the codebase, prepare archive inputs, or call `oracle_auth` automatically. Report the blocking issue plus the suggested next step.
|
|
14
14
|
6. Treat the parsed job id as `followUpJobId` for `oracle_submit`.
|
|
15
15
|
7. Understand whether the follow-up request is explicitly narrow or genuinely broad.
|
|
16
16
|
8. Gather enough repo context to choose archive inputs and write a strong oracle prompt. Bias toward context-rich submissions when they fit within the provider archive ceiling: 250 MB for ChatGPT, 200 MiB for Grok.
|
|
@@ -31,7 +31,7 @@ Oracle provider/model (`oracle_submit`):
|
|
|
31
31
|
|
|
32
32
|
Rules:
|
|
33
33
|
- Use `oracle_preflight` before any expensive `/oracle-followup` preparation so missing persisted-session or local auth/config blockers fail fast.
|
|
34
|
-
- If the immediately preceding oracle run for this follow-up failed because ChatGPT/Grok login is required, the worker said to rerun `/oracle-auth`, or stale auth clearly blocked execution,
|
|
34
|
+
- If the immediately preceding oracle run for this follow-up failed because ChatGPT/Grok login is required, the worker said to rerun `/oracle-auth`, or stale auth clearly blocked execution, stop and tell the user to run `/oracle-auth` (or `/oracle-auth grok` for Grok). Do not call `oracle_auth` automatically from `/oracle-followup`.
|
|
35
35
|
- This prompt exists so normal users can continue the same provider thread without manually constructing `followUpJobId` tool calls.
|
|
36
36
|
- Always include an archive. Do not submit without context files.
|
|
37
37
|
- By default, prefer context-rich archives up to the provider ceiling because more relevant context is usually better than less. The ceiling is 250 MB for ChatGPT and 200 MiB for Grok. For broad or unclear follow-up requests, include the whole repository 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.
|
package/prompts/oracle.md
CHANGED
|
@@ -5,20 +5,28 @@ You are preparing an /oracle job.
|
|
|
5
5
|
|
|
6
6
|
Do not answer the user's request directly yet.
|
|
7
7
|
|
|
8
|
+
Hard requirements:
|
|
9
|
+
- Do not plan instead of submitting. The point of `/oracle` is dispatch.
|
|
10
|
+
- Do not claim preflight, auth, archive prep, or submission happened unless the matching tool call actually happened in this turn.
|
|
11
|
+
- If the user explicitly says ChatGPT Instant or Instant, use provider `chatgpt` and preset `instant`; never switch that request to Grok.
|
|
12
|
+
- If a required tool call is unavailable or fails, stop and report that exact blocker instead of fabricating progress.
|
|
13
|
+
- After a successful or queued `oracle_submit`, your final answer must be only a terse dispatch summary with the job id and response path. Do not ask questions, offer to watch/poll/read, list next steps, or continue working.
|
|
14
|
+
|
|
8
15
|
Required workflow:
|
|
9
|
-
1. Call `oracle_preflight` immediately. If the user says to use Grok, pass `provider: "grok"` to `oracle_preflight`.
|
|
10
|
-
2. If `oracle_preflight` reports `ready: false`, stop before any expensive prep. Do not read files, search the codebase,
|
|
16
|
+
1. Call `oracle_preflight` immediately. If the user says to use Grok, pass `provider: "grok"` to `oracle_preflight`. If the user says to use ChatGPT, pass `provider: "chatgpt"`. If the user says to use ChatGPT Instant, pass `provider: "chatgpt"` and later call `oracle_submit` with `preset: "instant"`.
|
|
17
|
+
2. If `oracle_preflight` reports `ready: false`, stop before any expensive prep. Do not read files, search the codebase, prepare archive inputs, or call `oracle_auth` automatically. Report the blocking issue plus the suggested next step.
|
|
11
18
|
3. Understand the request and decide whether it is explicitly narrow or genuinely broad.
|
|
12
19
|
4. Gather enough repo context to choose archive inputs and write a strong oracle prompt. Bias toward context-rich submissions when they fit within the provider archive ceiling: 250 MB for ChatGPT, 200 MiB for Grok.
|
|
13
20
|
5. If the user scope is explicit and narrow, start from the directly relevant area but still include nearby files, tests, docs, configs, and adjacent modules when they may improve answer quality. Keep the archive tightly minimal only when the user explicitly asks for that, privacy/sensitivity requires it, or size pressure forces it.
|
|
14
21
|
6. If the request is broad, architectural, release-oriented, or otherwise repo-wide, gather broader context and usually archive `.`.
|
|
15
22
|
7. Choose archive inputs for the oracle job.
|
|
16
23
|
8. Craft a concise but complete oracle prompt for the selected web provider.
|
|
17
|
-
9. Call `oracle_submit` with the prompt and exact archive inputs.
|
|
18
|
-
10. Stop immediately after dispatching the oracle job.
|
|
24
|
+
9. Call `oracle_submit` with the prompt and exact archive inputs. Do not ask for confirmation before this submit step unless `oracle_preflight` or `oracle_submit` returns a blocker that requires user action.
|
|
25
|
+
10. Stop immediately after dispatching the oracle job. “Stop” means no follow-up questions, no offers to poll/watch/read, and no extra next-step list.
|
|
19
26
|
|
|
20
27
|
Oracle provider/model (`oracle_submit`):
|
|
21
28
|
- If the user says to use Grok (for example “Use the oracle to Grok about ...”), pass **`provider: "grok"`**. Grok currently supports only **`mode: "heavy"`**; omit `mode` unless the user explicitly says Heavy.
|
|
29
|
+
- If the user says ChatGPT, pass **`provider: "chatgpt"`**. Never route a ChatGPT request to Grok.
|
|
22
30
|
- Otherwise omit **`provider`** to use the configured default provider, or pass **`provider: "chatgpt"`** only when needed for clarity.
|
|
23
31
|
- To choose a specific ChatGPT model, pass **`preset`** with one of the allowed ids from the canonical preset registry.
|
|
24
32
|
- Matching human-readable preset labels and common hyphen/space variants are also accepted and normalized automatically, but prefer canonical ids when readily available.
|
|
@@ -28,7 +36,7 @@ Oracle provider/model (`oracle_submit`):
|
|
|
28
36
|
|
|
29
37
|
Rules:
|
|
30
38
|
- Use `oracle_preflight` before any expensive `/oracle` preparation so missing persisted-session or local auth/config blockers fail fast.
|
|
31
|
-
- If
|
|
39
|
+
- If ChatGPT/Grok login is required, the worker said to rerun `/oracle-auth`, or stale auth clearly blocked execution, stop and tell the user to run `/oracle-auth` (or `/oracle-auth grok` for Grok). Do not call `oracle_auth` automatically from `/oracle`.
|
|
32
40
|
- Always include an archive. Do not submit without context files.
|
|
33
41
|
- By default, prefer context-rich archives up to the provider ceiling because more relevant context is usually better than less. The ceiling is 250 MB for ChatGPT and 200 MiB for Grok. For broad or unclear requests, include the whole repository 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.
|
|
34
42
|
- Only limit file selection if the user explicitly requests a tight archive, if privacy/sensitivity requires it, or if the archive would otherwise exceed the size limit after exclusions/pruning.
|
|
@@ -397,7 +397,9 @@ async function run(mode = "packed") {
|
|
|
397
397
|
const agent1 = join(runDir, "agent1");
|
|
398
398
|
const sessions1 = join(runDir, "sessions1");
|
|
399
399
|
const jobs1 = join(runDir, "jobs1");
|
|
400
|
-
|
|
400
|
+
const authSeed1 = join(agent1, "extensions", "oracle-auth-seed-profile");
|
|
401
|
+
mkdirSync(authSeed1, { recursive: true });
|
|
402
|
+
writeFileSync(join(authSeed1, ".oracle-seed-generation"), "real-smoke-fake-worker\n");
|
|
401
403
|
mkdirSync(sessions1, { recursive: true });
|
|
402
404
|
mkdirSync(jobs1, { recursive: true });
|
|
403
405
|
|
|
@@ -444,7 +446,9 @@ async function run(mode = "packed") {
|
|
|
444
446
|
const sessions2 = join(runDir, "sessions2");
|
|
445
447
|
const jobs2 = join(runDir, "jobs2");
|
|
446
448
|
const outside = join(tmpRoot, "outside");
|
|
447
|
-
|
|
449
|
+
const authSeed2 = join(agent2, "extensions", "oracle-auth-seed-profile");
|
|
450
|
+
mkdirSync(authSeed2, { recursive: true });
|
|
451
|
+
writeFileSync(join(authSeed2, ".oracle-seed-generation"), "real-smoke-fake-worker\n");
|
|
448
452
|
mkdirSync(sessions2, { recursive: true });
|
|
449
453
|
mkdirSync(jobs2, { recursive: true });
|
|
450
454
|
mkdirSync(outside, { recursive: true });
|