pi-oracle 0.7.7 → 0.7.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.
- package/CHANGELOG.md +27 -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 +322 -60
- 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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Invariants/Assumptions: Poller scans are serialized per session key, wake-up delivery is best-effort, and terminal-job notifications always re-read durable job state before send.
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { buildOracleStatusText, buildOracleWakeupNotificationContent } from "../shared/job-observability-helpers.mjs";
|
|
8
|
+
import { buildOracleStatusText, buildOracleWakeupNotificationContent, type OracleReadinessStatus } from "../shared/job-observability-helpers.mjs";
|
|
9
9
|
import { isProcessAlive, readProcessStartedAt } from "../shared/process-helpers.mjs";
|
|
10
10
|
import { isLockTimeoutError, listLeaseMetadata, releaseLease, withGlobalReconcileLock, writeLeaseMetadata } from "./locks.js";
|
|
11
11
|
import {
|
|
@@ -46,6 +46,7 @@ interface OraclePollerLifecycle {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const activePollers = new Map<string, OracleActivePoller>();
|
|
49
|
+
const readinessBySession = new Map<string, OracleReadinessStatus>();
|
|
49
50
|
const scansInFlight = new Set<string>();
|
|
50
51
|
const POLLER_LOCK_TIMEOUT_MS = 50;
|
|
51
52
|
const WAKEUP_TARGET_LEASE_KIND = "wakeup-target";
|
|
@@ -167,19 +168,31 @@ function getJobCountsForSession(sessionFile: string | undefined, cwd: string): {
|
|
|
167
168
|
function refreshOracleStatusSnapshot(snapshot: OraclePollerContextSnapshot): void {
|
|
168
169
|
if (!snapshot.hasUI) return;
|
|
169
170
|
if (!snapshot.sessionFile) {
|
|
170
|
-
snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("
|
|
171
|
+
snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("error", "oracle: unavailable"));
|
|
171
172
|
return;
|
|
172
173
|
}
|
|
173
174
|
const counts = getJobCountsForSession(snapshot.sessionFile, snapshot.cwd);
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
175
|
+
const readiness = readinessBySession.get(getPollerSessionKey(snapshot.sessionFile, snapshot.cwd)) ?? "loaded";
|
|
176
|
+
const statusText = buildOracleStatusText(counts, readiness);
|
|
177
|
+
if (counts.active > 0) {
|
|
178
|
+
snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("success", statusText));
|
|
179
|
+
} else if (readiness === "auth_needed" || readiness === "config_error") {
|
|
180
|
+
snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("error", statusText));
|
|
181
|
+
} else {
|
|
182
|
+
snapshot.ui.setStatus("oracle", statusText);
|
|
183
|
+
}
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
export function refreshOracleStatus(ctx: ExtensionContext): void {
|
|
180
187
|
refreshOracleStatusSnapshot(snapshotPollerContext(ctx));
|
|
181
188
|
}
|
|
182
189
|
|
|
190
|
+
export function setOracleReadiness(ctx: ExtensionContext, readiness: OracleReadinessStatus): void {
|
|
191
|
+
const snapshot = snapshotPollerContext(ctx);
|
|
192
|
+
if (snapshot.sessionFile) readinessBySession.set(getPollerSessionKey(snapshot.sessionFile, snapshot.cwd), readiness);
|
|
193
|
+
refreshOracleStatusSnapshot(snapshot);
|
|
194
|
+
}
|
|
195
|
+
|
|
183
196
|
function requestWakeupTurn(pi: ExtensionAPI, job: OraclePollerJob): void {
|
|
184
197
|
pi.sendMessage(
|
|
185
198
|
{
|
|
@@ -412,6 +425,7 @@ export function stopPollerForSession(sessionFile: string | undefined, cwd: strin
|
|
|
412
425
|
if (handle.timer) clearInterval(handle.timer);
|
|
413
426
|
activePollers.delete(sessionKey);
|
|
414
427
|
}
|
|
428
|
+
readinessBySession.delete(sessionKey);
|
|
415
429
|
const wakeupTargetLeaseKey = getWakeupTargetLeaseKey(sessionKey);
|
|
416
430
|
void releaseLease(WAKEUP_TARGET_LEASE_KIND, wakeupTargetLeaseKey).catch(() => undefined);
|
|
417
431
|
}
|
|
@@ -423,6 +437,7 @@ export async function stopAllPollers(): Promise<void> {
|
|
|
423
437
|
if (handle.timer) clearInterval(handle.timer);
|
|
424
438
|
}
|
|
425
439
|
activePollers.clear();
|
|
440
|
+
readinessBySession.clear();
|
|
426
441
|
await Promise.all(handles.map(async (handle) => {
|
|
427
442
|
const wakeupTargetLeaseKey = getWakeupTargetLeaseKey(handle.sessionKey);
|
|
428
443
|
await releaseLease(WAKEUP_TARGET_LEASE_KIND, wakeupTargetLeaseKey).catch(() => undefined);
|
|
@@ -169,6 +169,10 @@ function unreadableAuthSeedProfileMessage(seedDir: string): string {
|
|
|
169
169
|
return `Oracle auth seed profile is not readable: ${seedDir}. Fix its permissions or rerun /oracle-auth.`;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
function unauthenticatedAuthSeedProfileMessage(seedDir: string): string {
|
|
173
|
+
return `Oracle auth seed profile exists but is not authenticated: ${seedDir}. Run /oracle-auth to create a verified auth seed before submitting oracle jobs.`;
|
|
174
|
+
}
|
|
175
|
+
|
|
172
176
|
function missingBrowserExecutableMessage(executablePath: string): string {
|
|
173
177
|
return `Configured oracle browser executable does not exist: ${executablePath}. Fix browser.executablePath or install Chrome there.`;
|
|
174
178
|
}
|
|
@@ -315,6 +319,10 @@ export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Pr
|
|
|
315
319
|
} catch {
|
|
316
320
|
throw new Error(unreadableAuthSeedProfileMessage(seedDir));
|
|
317
321
|
}
|
|
322
|
+
|
|
323
|
+
if (!getSeedGeneration(config)) {
|
|
324
|
+
throw new Error(unauthenticatedAuthSeedProfileMessage(seedDir));
|
|
325
|
+
}
|
|
318
326
|
}
|
|
319
327
|
|
|
320
328
|
export async function assertOracleSubmitPrerequisites(config: OracleConfig): Promise<void> {
|
|
@@ -914,6 +914,15 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
|
|
|
914
914
|
};
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
+
if (message.startsWith("Oracle auth seed profile exists but is not authenticated: ")) {
|
|
918
|
+
return {
|
|
919
|
+
code: "auth_seed_profile_unauthenticated",
|
|
920
|
+
message,
|
|
921
|
+
rejectedValue: message.replace(/^Oracle auth seed profile exists but is not authenticated: /, "").replace(/\. Run \/oracle-auth.*$/, ""),
|
|
922
|
+
suggestedNextStep: "Call oracle_auth or run /oracle-auth once to create a verified auth seed, then retry the oracle tool call.",
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
917
926
|
if (message.startsWith("Failed to parse oracle config ") || message.startsWith("Invalid oracle config:") || message.startsWith("Invalid oracle project config:")) {
|
|
918
927
|
return {
|
|
919
928
|
code: "oracle_config_invalid",
|
|
@@ -1151,6 +1160,10 @@ function formatOraclePreflightResponse(details: OraclePreflightDetails): string
|
|
|
1151
1160
|
].filter(Boolean).join("\n");
|
|
1152
1161
|
}
|
|
1153
1162
|
|
|
1163
|
+
function isProjectTrusted(ctx: ExtensionContext): boolean {
|
|
1164
|
+
return (ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() ?? true;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1154
1167
|
async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: unknown; followUpJobId?: unknown } = {}): Promise<OraclePreflightDetails> {
|
|
1155
1168
|
const sessionFile = getSessionFile(ctx);
|
|
1156
1169
|
if (!hasPersistedSessionFile(sessionFile)) {
|
|
@@ -1174,7 +1187,7 @@ async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: un
|
|
|
1174
1187
|
if (followUpJobId !== undefined && typeof followUpJobId !== "string") {
|
|
1175
1188
|
throw new Error("oracle_preflight followUpJobId must be a string");
|
|
1176
1189
|
}
|
|
1177
|
-
const baseConfig = loadOracleConfig(ctx.cwd);
|
|
1190
|
+
const baseConfig = loadOracleConfig(ctx.cwd, { projectConfigTrusted: isProjectTrusted(ctx) });
|
|
1178
1191
|
const followUp = resolveFollowUp(followUpJobId, ctx.cwd);
|
|
1179
1192
|
provider = normalizeOracleProvider(params.provider, followUp.provider ?? baseConfig.defaults.provider, "oracle_preflight");
|
|
1180
1193
|
if (followUp.provider && provider !== followUp.provider) {
|
|
@@ -1202,7 +1215,7 @@ async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: un
|
|
|
1202
1215
|
session: { persisted: true, sessionFile },
|
|
1203
1216
|
config: { ready: true },
|
|
1204
1217
|
auth: {
|
|
1205
|
-
ready: !["auth_seed_profile_missing", "auth_seed_profile_unreadable", "auth_seed_profile_invalid_type"].includes(errorDetails.code),
|
|
1218
|
+
ready: !["auth_seed_profile_missing", "auth_seed_profile_unreadable", "auth_seed_profile_invalid_type", "auth_seed_profile_unauthenticated"].includes(errorDetails.code),
|
|
1206
1219
|
seedProfileDir: config.browser.authSeedProfileDir,
|
|
1207
1220
|
},
|
|
1208
1221
|
error: errorDetails,
|
|
@@ -1264,9 +1277,9 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
|
|
|
1264
1277
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1265
1278
|
try {
|
|
1266
1279
|
const projectCwd = getProjectId(ctx.cwd);
|
|
1267
|
-
const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd });
|
|
1280
|
+
const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted: isProjectTrusted(ctx) });
|
|
1268
1281
|
const provider = normalizeOracleProvider(params.provider, baseConfig.defaults.provider, "oracle_auth");
|
|
1269
|
-
const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd, provider);
|
|
1282
|
+
const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd, provider, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted: isProjectTrusted(ctx) });
|
|
1270
1283
|
return {
|
|
1271
1284
|
content: [{ type: "text" as const, text: message }],
|
|
1272
1285
|
details: {
|
|
@@ -1311,7 +1324,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
|
|
|
1311
1324
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1312
1325
|
try {
|
|
1313
1326
|
const projectCwd = getProjectId(ctx.cwd);
|
|
1314
|
-
const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd });
|
|
1327
|
+
const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted: isProjectTrusted(ctx) });
|
|
1315
1328
|
const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
|
|
1316
1329
|
const projectId = getProjectId(projectCwd);
|
|
1317
1330
|
const sessionId = getSessionId(originSessionFile, projectId);
|
|
@@ -10,6 +10,12 @@ export interface ExecutableSearchOptions {
|
|
|
10
10
|
pathDelimiter?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export interface KnownBrowserUserDataPathMatchDetails {
|
|
14
|
+
root: string;
|
|
15
|
+
source: "knownBrowserUserDataDir" | "auth.chromeCookiePath" | "auth.chromeProfile" | "extraProtectedPath";
|
|
16
|
+
configuredPath?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
export const SWEET_COOKIE_SAFE_STORAGE_PASSWORD_ENV_NAMES: readonly [
|
|
14
20
|
"SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD",
|
|
15
21
|
"SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD",
|
|
@@ -40,6 +46,15 @@ export function knownBrowserUserDataPathMatch(
|
|
|
40
46
|
cookieSources?: { chromeProfile?: string; chromeCookiePath?: string };
|
|
41
47
|
},
|
|
42
48
|
): string | undefined;
|
|
49
|
+
export function knownBrowserUserDataPathMatchDetails(
|
|
50
|
+
pathValue: string,
|
|
51
|
+
options?: BrowserPathOptions & {
|
|
52
|
+
platform?: OraclePlatform;
|
|
53
|
+
includeUnsupported?: boolean;
|
|
54
|
+
extraProtectedPaths?: string[];
|
|
55
|
+
cookieSources?: { chromeProfile?: string; chromeCookiePath?: string };
|
|
56
|
+
},
|
|
57
|
+
): KnownBrowserUserDataPathMatchDetails | undefined;
|
|
43
58
|
export function assertNotKnownBrowserUserDataPath(
|
|
44
59
|
pathValue: string,
|
|
45
60
|
label: string,
|
|
@@ -224,23 +224,40 @@ function protectedPathsForCookieDb(cookiePath) {
|
|
|
224
224
|
* @returns {string[]}
|
|
225
225
|
*/
|
|
226
226
|
export function protectedCookieSourcePaths(cookieSources) {
|
|
227
|
+
return protectedCookieSourcePathEntries(cookieSources).map((entry) => entry.path);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function protectedCookieSourcePathEntries(cookieSources) {
|
|
227
231
|
if (!cookieSources) return [];
|
|
228
232
|
const roots = [];
|
|
229
233
|
const cookiePath = typeof cookieSources.chromeCookiePath === "string" && cookieSources.chromeCookiePath.trim()
|
|
230
234
|
? cookieSources.chromeCookiePath.trim()
|
|
231
235
|
: undefined;
|
|
232
|
-
if (cookiePath)
|
|
236
|
+
if (cookiePath) {
|
|
237
|
+
roots.push(...protectedPathsForCookieDb(cookiePath).map((path) => ({ path, source: "auth.chromeCookiePath", configuredPath: cookiePath })));
|
|
238
|
+
}
|
|
233
239
|
|
|
234
240
|
const profile = typeof cookieSources.chromeProfile === "string" && cookieSources.chromeProfile.trim()
|
|
235
241
|
? cookieSources.chromeProfile.trim()
|
|
236
242
|
: undefined;
|
|
237
243
|
if (profile && looksLikeFilesystemPath(profile)) {
|
|
238
244
|
const normalizedProfile = normalizedAbsolutePath(profile);
|
|
239
|
-
if (isCookiesDbPath(normalizedProfile))
|
|
240
|
-
|
|
245
|
+
if (isCookiesDbPath(normalizedProfile)) {
|
|
246
|
+
roots.push(...protectedPathsForCookieDb(normalizedProfile).map((path) => ({ path, source: "auth.chromeProfile", configuredPath: profile })));
|
|
247
|
+
} else {
|
|
248
|
+
roots.push({ path: normalizedProfile, source: "auth.chromeProfile", configuredPath: profile }, { path: dirname(normalizedProfile), source: "auth.chromeProfile", configuredPath: profile });
|
|
249
|
+
}
|
|
241
250
|
}
|
|
242
251
|
|
|
243
|
-
|
|
252
|
+
const seen = new Set();
|
|
253
|
+
return roots
|
|
254
|
+
.map((root) => ({ ...root, path: normalize(root.path) }))
|
|
255
|
+
.filter((root) => {
|
|
256
|
+
const key = `${root.path}\0${root.source}\0${root.configuredPath}`;
|
|
257
|
+
if (seen.has(key)) return false;
|
|
258
|
+
seen.add(key);
|
|
259
|
+
return true;
|
|
260
|
+
});
|
|
244
261
|
}
|
|
245
262
|
|
|
246
263
|
/**
|
|
@@ -249,19 +266,24 @@ export function protectedCookieSourcePaths(cookieSources) {
|
|
|
249
266
|
* @returns {string | undefined}
|
|
250
267
|
*/
|
|
251
268
|
export function knownBrowserUserDataPathMatch(pathValue, options = {}) {
|
|
269
|
+
return knownBrowserUserDataPathMatchDetails(pathValue, options)?.root;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function knownBrowserUserDataPathMatchDetails(pathValue, options = {}) {
|
|
252
273
|
const platform = options.platform ?? process.platform;
|
|
253
274
|
const normalizedPath = normalizedAbsolutePath(pathValue, options);
|
|
254
275
|
const resolvedPath = resolvePathThroughExistingAncestorsSync(normalizedPath);
|
|
255
276
|
const roots = [
|
|
256
|
-
...browserUserDataDirsForPlatform(platform, { ...options, includeUnsupported: options.includeUnsupported ?? true })
|
|
257
|
-
|
|
258
|
-
...(
|
|
277
|
+
...browserUserDataDirsForPlatform(platform, { ...options, includeUnsupported: options.includeUnsupported ?? true })
|
|
278
|
+
.map((path) => ({ path, source: "knownBrowserUserDataDir" })),
|
|
279
|
+
...protectedCookieSourcePathEntries(options.cookieSources),
|
|
280
|
+
...((options.extraProtectedPaths ?? [])).map((path) => ({ path, source: "extraProtectedPath" })),
|
|
259
281
|
];
|
|
260
282
|
for (const root of roots) {
|
|
261
|
-
const normalizedRoot = normalizedAbsolutePath(root, options);
|
|
262
|
-
if (pathInsideOrEqual(normalizedPath, normalizedRoot)) return normalizedRoot;
|
|
283
|
+
const normalizedRoot = normalizedAbsolutePath(root.path, options);
|
|
284
|
+
if (pathInsideOrEqual(normalizedPath, normalizedRoot)) return { root: normalizedRoot, source: root.source, configuredPath: root.configuredPath };
|
|
263
285
|
const resolvedRoot = resolvePathThroughExistingAncestorsSync(normalizedRoot) ?? normalizedRoot;
|
|
264
|
-
if (resolvedPath && pathInsideOrEqual(resolvedPath, resolvedRoot)) return resolvedRoot;
|
|
286
|
+
if (resolvedPath && pathInsideOrEqual(resolvedPath, resolvedRoot)) return { root: resolvedRoot, source: root.source, configuredPath: root.configuredPath };
|
|
265
287
|
}
|
|
266
288
|
return undefined;
|
|
267
289
|
}
|
|
@@ -273,10 +295,12 @@ export function knownBrowserUserDataPathMatch(pathValue, options = {}) {
|
|
|
273
295
|
* @returns {void}
|
|
274
296
|
*/
|
|
275
297
|
export function assertNotKnownBrowserUserDataPath(pathValue, label, options = {}) {
|
|
276
|
-
const match =
|
|
277
|
-
if (match)
|
|
278
|
-
|
|
298
|
+
const match = knownBrowserUserDataPathMatchDetails(pathValue, options);
|
|
299
|
+
if (!match) return;
|
|
300
|
+
if (match.source === "auth.chromeCookiePath" || match.source === "auth.chromeProfile") {
|
|
301
|
+
throw new Error(`${label} is inside the browser profile root inferred from ${match.source} (${match.configuredPath} -> ${match.root}): ${pathValue}`);
|
|
279
302
|
}
|
|
303
|
+
throw new Error(`${label} must not point into a real browser user-data directory (${match.root}): ${pathValue}`);
|
|
280
304
|
}
|
|
281
305
|
|
|
282
306
|
/**
|
|
@@ -62,6 +62,8 @@ export interface OracleStatusCounts {
|
|
|
62
62
|
queued: number;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export type OracleReadinessStatus = "unavailable" | "loaded" | "auth_needed" | "config_error" | "ready";
|
|
66
|
+
|
|
65
67
|
export declare function formatBytes(bytes: number): string;
|
|
66
68
|
export declare function formatOracleLifecycleEvent(event: OracleJobLifecycleEvent | undefined): string | undefined;
|
|
67
69
|
export declare function formatOracleCancelOutcome(job: { id: string; status: string }): string;
|
|
@@ -71,4 +73,4 @@ export declare function buildOracleWakeupNotificationContent(
|
|
|
71
73
|
options?: { responsePath?: string; responseAvailable?: boolean; artifactsPath?: string },
|
|
72
74
|
): string;
|
|
73
75
|
export declare function formatOracleSubmitResponse(job: OracleJobSummaryLike & { promptPath: string; archivePath: string }, options: OracleSubmitResponseOptions): string;
|
|
74
|
-
export declare function buildOracleStatusText(counts: OracleStatusCounts): string;
|
|
76
|
+
export declare function buildOracleStatusText(counts: OracleStatusCounts, readiness?: OracleReadinessStatus): string;
|
|
@@ -254,19 +254,28 @@ export function formatOracleSubmitResponse(job, options) {
|
|
|
254
254
|
|
|
255
255
|
/**
|
|
256
256
|
* @param {OracleStatusCounts} counts
|
|
257
|
+
* @param {"unavailable" | "loaded" | "auth_needed" | "config_error" | "ready"} [readiness]
|
|
257
258
|
* @returns {string}
|
|
258
259
|
*/
|
|
259
|
-
export function buildOracleStatusText(counts) {
|
|
260
|
+
export function buildOracleStatusText(counts, readiness = "loaded") {
|
|
261
|
+
const readinessText = {
|
|
262
|
+
unavailable: "unavailable",
|
|
263
|
+
loaded: "loaded",
|
|
264
|
+
auth_needed: "auth needed",
|
|
265
|
+
config_error: "config error",
|
|
266
|
+
ready: "ready",
|
|
267
|
+
}[readiness] ?? "loaded";
|
|
268
|
+
const readinessSuffix = readiness === "ready" ? "" : `, ${readinessText}`;
|
|
260
269
|
if (counts.active > 0 && counts.queued > 0) {
|
|
261
|
-
return `oracle: running (${counts.active}), queued (${counts.queued})`;
|
|
270
|
+
return `oracle: running (${counts.active}), queued (${counts.queued})${readinessSuffix}`;
|
|
262
271
|
}
|
|
263
272
|
if (counts.active > 0) {
|
|
264
273
|
const suffix = counts.active > 1 ? ` (${counts.active})` : "";
|
|
265
|
-
return `oracle: running${suffix}`;
|
|
274
|
+
return `oracle: running${suffix}${readinessSuffix}`;
|
|
266
275
|
}
|
|
267
276
|
if (counts.queued > 0) {
|
|
268
277
|
const suffix = counts.queued > 1 ? ` (${counts.queued})` : "";
|
|
269
|
-
return `oracle: queued${suffix}`;
|
|
278
|
+
return `oracle: queued${suffix}${readinessSuffix}`;
|
|
270
279
|
}
|
|
271
|
-
return
|
|
280
|
+
return `oracle: ${readinessText}`;
|
|
272
281
|
}
|
|
@@ -3,9 +3,18 @@ export interface OracleStableValueState {
|
|
|
3
3
|
stableCount: number;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
export interface OracleSendAcceptanceState {
|
|
7
|
+
url?: string;
|
|
8
|
+
urlKnown?: boolean;
|
|
9
|
+
assistantCount?: number;
|
|
10
|
+
stopStreaming?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export declare function assistantSnapshotSlice(snapshot: string, composerLabel: string, responseIndex: number): string | undefined;
|
|
7
14
|
export declare function stripUrlQueryAndHash(url: string | undefined): string;
|
|
8
15
|
export declare function isConversationPathUrl(url: string): boolean;
|
|
16
|
+
export declare function conversationIdFromUrl(url: string | undefined): string | undefined;
|
|
17
|
+
export declare function providerSendAccepted(before: OracleSendAcceptanceState, after: OracleSendAcceptanceState): boolean;
|
|
9
18
|
export declare function resolveStableConversationUrlCandidate(url: string, previousChatUrl?: string): string | undefined;
|
|
10
19
|
export declare function nextStableValueState(
|
|
11
20
|
state: Partial<OracleStableValueState> | undefined,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// Invariants/Assumptions: Snapshot text comes from agent-browser `snapshot -i`; URL inputs may be malformed and must fail safely.
|
|
6
6
|
|
|
7
7
|
/** @typedef {import("./chatgpt-flow-helpers.d.mts").OracleStableValueState} OracleStableValueState */
|
|
8
|
+
/** @typedef {import("./chatgpt-flow-helpers.d.mts").OracleSendAcceptanceState} OracleSendAcceptanceState */
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @param {string} snapshot
|
|
@@ -52,13 +53,39 @@ export function stripUrlQueryAndHash(url) {
|
|
|
52
53
|
* @returns {boolean}
|
|
53
54
|
*/
|
|
54
55
|
export function isConversationPathUrl(url) {
|
|
56
|
+
return Boolean(conversationIdFromUrl(url));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string | undefined} url
|
|
61
|
+
* @returns {string | undefined}
|
|
62
|
+
*/
|
|
63
|
+
export function conversationIdFromUrl(url) {
|
|
64
|
+
if (typeof url !== "string" || !url.trim()) return undefined;
|
|
55
65
|
try {
|
|
56
|
-
|
|
66
|
+
const match = new URL(url).pathname.match(/\/(?:c|chat)\/([A-Za-z0-9-]+)$/i);
|
|
67
|
+
return match?.[1];
|
|
57
68
|
} catch {
|
|
58
|
-
return
|
|
69
|
+
return undefined;
|
|
59
70
|
}
|
|
60
71
|
}
|
|
61
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @param {OracleSendAcceptanceState} before
|
|
75
|
+
* @param {OracleSendAcceptanceState} after
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
export function providerSendAccepted(before, after) {
|
|
79
|
+
const beforeUrlKnown = before.urlKnown !== false;
|
|
80
|
+
const afterUrlKnown = after.urlKnown !== false;
|
|
81
|
+
const beforeConversationId = beforeUrlKnown ? conversationIdFromUrl(before.url) : undefined;
|
|
82
|
+
const afterConversationId = afterUrlKnown ? conversationIdFromUrl(after.url) : undefined;
|
|
83
|
+
if (beforeUrlKnown && afterUrlKnown && afterConversationId && afterConversationId !== beforeConversationId) return true;
|
|
84
|
+
if ((after.assistantCount ?? 0) > (before.assistantCount ?? 0)) return true;
|
|
85
|
+
if (after.stopStreaming === true && before.stopStreaming !== true) return true;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
/**
|
|
63
90
|
* @param {string} url
|
|
64
91
|
* @param {string | undefined} previousChatUrl
|
|
@@ -10,6 +10,7 @@ export interface OracleUiSelection {
|
|
|
10
10
|
export declare const CHATGPT_CANONICAL_APP_ORIGINS: readonly string[];
|
|
11
11
|
|
|
12
12
|
export declare function buildAllowedChatGptOrigins(chatUrl: string, authUrl?: string): string[];
|
|
13
|
+
export declare function stripChatGptResponseChrome(value: string | undefined): string;
|
|
13
14
|
export declare function matchesModelFamilyLabel(label: string | undefined, family: OracleUiModelFamily): boolean;
|
|
14
15
|
export declare function matchesRequestedModelControlLabel(label: string | undefined, selection: OracleUiSelection): boolean;
|
|
15
16
|
export declare function matchesCompactIntelligenceOpenerLabel(label: string | undefined): boolean;
|
|
@@ -29,15 +29,19 @@ const AUTO_SWITCH_LABEL = "Auto-switch to Thinking";
|
|
|
29
29
|
const THINKING_EFFORT_COMBOBOX_LABEL = "Thinking effort";
|
|
30
30
|
const PRO_THINKING_EFFORT_COMBOBOX_LABEL = "Pro thinking effort";
|
|
31
31
|
const EFFORT_LABELS = new Set(["Light", "Standard", "Extended", "Heavy"]);
|
|
32
|
-
const COMPACT_INTELLIGENCE_MENU_PATTERN = /Intelligence.*Instant.*Medium.*High.*Pro/i;
|
|
33
|
-
const COMPACT_INTELLIGENCE_CONTROL_PATTERN = /^(?:Instant(?:\s+5s)?|Medium(?:\s+5\s*[–-]\s*30s)?|High(?:\s+15\s*[–-]\s*60s)?|Pro
|
|
34
|
-
const COMPACT_INTELLIGENCE_OPENER_PATTERN = /^(?:Instant|Medium|High|Pro)$/i;
|
|
32
|
+
const COMPACT_INTELLIGENCE_MENU_PATTERN = /(?:Intelligence.*Instant.*Medium.*High.*Pro|^(?:Instant|Medium|High|Extra High|Pro Standard|Pro Extended)$)/i;
|
|
33
|
+
const COMPACT_INTELLIGENCE_CONTROL_PATTERN = /^(?: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;
|
|
34
|
+
const COMPACT_INTELLIGENCE_OPENER_PATTERN = /^(?:Instant|Medium|High|Extra High|Pro|Pro Standard|Pro Extended)$/i;
|
|
35
35
|
const BARE_EFFORT_PATTERN = /^(light|standard|extended|heavy)(?:, click to remove)?$/i;
|
|
36
36
|
const INSTANT_CHIP_PATTERN = /^instant(?:, click to remove)?$/i;
|
|
37
37
|
const THINKING_CHIP_PATTERN = /^(?:(light|standard|extended|heavy)\s+)?thinking(?:, click to remove)?$/i;
|
|
38
38
|
const PRO_CHIP_PATTERN = /^(?:(light|standard|extended|heavy)\s+)?pro(?:, click to remove)?$/i;
|
|
39
39
|
const MODEL_FAMILY_CONTROL_KINDS = new Set(["button", "radio", "menuitemradio"]);
|
|
40
40
|
const COMPACT_INTELLIGENCE_CONTROL_KINDS = new Set(["menuitemradio"]);
|
|
41
|
+
const CHATGPT_RESPONSE_CHROME_LINE_PATTERNS = Object.freeze([
|
|
42
|
+
/^Stopped thinking$/i,
|
|
43
|
+
/^Do you like this personality\?$/i,
|
|
44
|
+
]);
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
47
|
* @param {string | undefined} url
|
|
@@ -90,6 +94,18 @@ export function buildAllowedChatGptOrigins(chatUrl, authUrl) {
|
|
|
90
94
|
]);
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
/**
|
|
98
|
+
* @param {string | undefined} value
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
export function stripChatGptResponseChrome(value) {
|
|
102
|
+
return String(value || "")
|
|
103
|
+
.split("\n")
|
|
104
|
+
.filter((line) => !CHATGPT_RESPONSE_CHROME_LINE_PATTERNS.some((pattern) => pattern.test(line.trim())))
|
|
105
|
+
.join("\n")
|
|
106
|
+
.trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
93
109
|
/**
|
|
94
110
|
* @param {string | undefined} label
|
|
95
111
|
* @param {OracleUiModelFamily} family
|
|
@@ -144,6 +160,14 @@ function parseComposerChipSelection(label) {
|
|
|
144
160
|
};
|
|
145
161
|
}
|
|
146
162
|
|
|
163
|
+
const proPrefixedEffortMatch = normalized.match(/^pro\s+(standard|extended)$/i);
|
|
164
|
+
if (proPrefixedEffortMatch) {
|
|
165
|
+
return {
|
|
166
|
+
modelFamily: /** @type {OracleUiModelFamily} */ ("pro"),
|
|
167
|
+
effort: /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiEffort} */ (proPrefixedEffortMatch[1].toLowerCase()),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
147
171
|
const proMatch = normalized.match(PRO_CHIP_PATTERN);
|
|
148
172
|
if (proMatch) {
|
|
149
173
|
return {
|
|
@@ -180,7 +204,22 @@ function parseCompactIntelligenceSelection(label) {
|
|
|
180
204
|
compactTier: "high",
|
|
181
205
|
};
|
|
182
206
|
}
|
|
183
|
-
if (/^
|
|
207
|
+
if (/^Extra High$/i.test(normalized)) {
|
|
208
|
+
return {
|
|
209
|
+
modelFamily: /** @type {OracleUiModelFamily} */ ("thinking"),
|
|
210
|
+
effort: /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiEffort} */ ("heavy"),
|
|
211
|
+
compactTier: "extra-high",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const proEffortMatch = normalized.match(/^Pro\s+(Standard|Extended)$/i);
|
|
215
|
+
if (proEffortMatch) {
|
|
216
|
+
return {
|
|
217
|
+
modelFamily: /** @type {OracleUiModelFamily} */ ("pro"),
|
|
218
|
+
effort: /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiEffort} */ (proEffortMatch[1].toLowerCase()),
|
|
219
|
+
compactTier: "pro",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (/^Pro\s+5\+\s*min$/i.test(normalized)) {
|
|
184
223
|
return {
|
|
185
224
|
modelFamily: /** @type {OracleUiModelFamily} */ ("pro"),
|
|
186
225
|
compactTier: "pro",
|
|
@@ -198,7 +237,7 @@ function hasRemovableComposerModelChip(entries) {
|
|
|
198
237
|
|
|
199
238
|
function hasCompactIntelligenceMenuContext(entries) {
|
|
200
239
|
return entries.some((entry) => !entry.disabled && entry.kind === "menu" && COMPACT_INTELLIGENCE_MENU_PATTERN.test(normalizeText(entry.label)))
|
|
201
|
-
|| entries.some((entry) => !entry.disabled && entry.kind === "menuitemradio" && checkedState(entry) === true &&
|
|
240
|
+
|| entries.some((entry) => !entry.disabled && entry.kind === "menuitemradio" && checkedState(entry) === true && parseCompactIntelligenceSelection(entry.label));
|
|
202
241
|
}
|
|
203
242
|
|
|
204
243
|
function hasLegacyEffortCombobox(entries) {
|
|
@@ -211,7 +250,6 @@ function hasLegacyEffortCombobox(entries) {
|
|
|
211
250
|
|
|
212
251
|
function compactSelectionFromEntry(entry, _entries, _options = {}) {
|
|
213
252
|
if (entry.disabled || !COMPACT_INTELLIGENCE_CONTROL_KINDS.has(entry.kind || "")) return undefined;
|
|
214
|
-
if (!/\d/.test(String(entry.label || ""))) return undefined;
|
|
215
253
|
return parseCompactIntelligenceSelection(entry.label);
|
|
216
254
|
}
|
|
217
255
|
|
|
@@ -225,14 +263,16 @@ function compactSelectionMatchesRequested(selection, compactSelection) {
|
|
|
225
263
|
}
|
|
226
264
|
|
|
227
265
|
if (selection.modelFamily === "pro") {
|
|
228
|
-
|
|
229
|
-
|
|
266
|
+
if (compactSelection.compactTier !== "pro") return false;
|
|
267
|
+
if (!compactSelection.effort) return true;
|
|
268
|
+
return compactSelection.effort === (selection.effort || "standard");
|
|
230
269
|
}
|
|
231
270
|
|
|
232
271
|
if (selection.modelFamily === "thinking") {
|
|
233
272
|
const requestedEffort = selection.effort || "standard";
|
|
234
273
|
if (compactSelection.compactTier === "medium") return requestedEffort === "light" || requestedEffort === "standard";
|
|
235
|
-
if (compactSelection.compactTier === "high") return requestedEffort === "extended"
|
|
274
|
+
if (compactSelection.compactTier === "high") return requestedEffort === "extended";
|
|
275
|
+
if (compactSelection.compactTier === "extra-high") return requestedEffort === "heavy";
|
|
236
276
|
}
|
|
237
277
|
|
|
238
278
|
return false;
|
|
@@ -353,7 +393,7 @@ export function effortSelectionVisible(snapshot, effortLabel) {
|
|
|
353
393
|
if (compactSelection?.modelFamily === "thinking") {
|
|
354
394
|
return compactSelectionMatchesRequested({ modelFamily: "thinking", effort: /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiEffort} */ (normalizedEffort), autoSwitchToThinking: false }, compactSelection);
|
|
355
395
|
}
|
|
356
|
-
if (compactSelection?.modelFamily === "pro") return
|
|
396
|
+
if (compactSelection?.modelFamily === "pro") return !compactSelection.effort || compactSelection.effort === normalizedEffort;
|
|
357
397
|
if (entry.kind === "combobox" && normalizeText(entry.value).toLowerCase() === normalizedEffort) return true;
|
|
358
398
|
const chipSelection = entry.kind === "button" ? parseComposerChipSelection(entry.label) : undefined;
|
|
359
399
|
if (chipSelection?.effort === normalizedEffort) return true;
|
|
@@ -395,17 +435,16 @@ export function snapshotHasModelConfigurationUi(snapshot) {
|
|
|
395
435
|
),
|
|
396
436
|
);
|
|
397
437
|
const visibleCompactControls = entries.filter(
|
|
398
|
-
(entry) => !entry.disabled && entry.kind === "menuitemradio" &&
|
|
438
|
+
(entry) => !entry.disabled && entry.kind === "menuitemradio" && parseCompactIntelligenceSelection(entry.label),
|
|
399
439
|
);
|
|
400
440
|
const hasCompactIntelligenceMenu = entries.some(
|
|
401
441
|
(entry) => !entry.disabled && entry.kind === "menu" && COMPACT_INTELLIGENCE_MENU_PATTERN.test(normalizeText(entry.label)),
|
|
402
442
|
);
|
|
403
|
-
const hasCloseButton = entries.some((entry) => entry.kind === "button" && entry.label === "Close" && !entry.disabled);
|
|
404
443
|
const hasIntelligenceHeading = entries.some((entry) => entry.kind === "heading" && normalizeText(entry.label) === "Intelligence" && !entry.disabled);
|
|
405
444
|
const hasEffortCombobox = entries.some(
|
|
406
445
|
(entry) => entry.kind === "combobox" && EFFORT_LABELS.has(entry.value || "") && !entry.disabled,
|
|
407
446
|
);
|
|
408
|
-
return visibleFamilies.size >= 2 || visibleRadioFamilies.size >= 2 || visibleCompactControls.length >= 2 || hasCompactIntelligenceMenu ||
|
|
447
|
+
return visibleFamilies.size >= 2 || visibleRadioFamilies.size >= 2 || visibleCompactControls.length >= 2 || hasCompactIntelligenceMenu || hasIntelligenceHeading || hasEffortCombobox;
|
|
409
448
|
}
|
|
410
449
|
|
|
411
450
|
/**
|