pi-oracle 0.3.4 → 0.5.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 +38 -0
- package/README.md +27 -8
- package/docs/ORACLE_DESIGN.md +14 -8
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +276 -0
- package/extensions/oracle/index.ts +8 -1
- package/extensions/oracle/lib/commands.ts +25 -29
- package/extensions/oracle/lib/config.ts +56 -2
- package/extensions/oracle/lib/jobs.ts +134 -219
- package/extensions/oracle/lib/locks.ts +41 -209
- package/extensions/oracle/lib/poller.ts +38 -52
- package/extensions/oracle/lib/queue.ts +75 -112
- package/extensions/oracle/lib/runtime.ts +102 -19
- package/extensions/oracle/lib/tools.ts +663 -294
- package/extensions/oracle/shared/job-coordination-helpers.d.mts +84 -0
- package/extensions/oracle/shared/job-coordination-helpers.mjs +168 -0
- package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +131 -0
- package/extensions/oracle/shared/job-lifecycle-helpers.mjs +390 -0
- package/extensions/oracle/shared/job-observability-helpers.d.mts +60 -0
- package/extensions/oracle/shared/job-observability-helpers.mjs +161 -0
- package/extensions/oracle/shared/process-helpers.d.mts +20 -0
- package/extensions/oracle/shared/process-helpers.mjs +128 -0
- package/extensions/oracle/shared/state-coordination-helpers.d.mts +43 -0
- package/extensions/oracle/shared/state-coordination-helpers.mjs +381 -0
- package/extensions/oracle/worker/artifact-heuristics.mjs +5 -0
- package/extensions/oracle/worker/auth-bootstrap.mjs +125 -134
- package/extensions/oracle/worker/auth-cookie-policy.mjs +5 -0
- package/extensions/oracle/worker/auth-flow-helpers.d.mts +41 -0
- package/extensions/oracle/worker/auth-flow-helpers.mjs +165 -0
- package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +13 -0
- package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +85 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +93 -9
- package/extensions/oracle/worker/run-job.mjs +166 -274
- package/extensions/oracle/worker/state-locks.mjs +31 -216
- package/package.json +4 -3
- package/prompts/oracle.md +16 -10
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Purpose: Provide pure ChatGPT conversation-state helpers used by the oracle worker.
|
|
2
|
+
// Responsibilities: Slice assistant snapshot regions, normalize URLs, and track stable conversation URL observations.
|
|
3
|
+
// Scope: Pure worker flow logic only; browser I/O and polling loops stay in run-job.mjs.
|
|
4
|
+
// Usage: Imported by run-job.mjs and sanity tests to validate conversation-state heuristics without driving a browser.
|
|
5
|
+
// Invariants/Assumptions: Snapshot text comes from agent-browser `snapshot -i`; URL inputs may be malformed and must fail safely.
|
|
6
|
+
|
|
7
|
+
/** @typedef {import("./chatgpt-flow-helpers.d.mts").OracleStableValueState} OracleStableValueState */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} snapshot
|
|
11
|
+
* @param {string} composerLabel
|
|
12
|
+
* @param {number} responseIndex
|
|
13
|
+
* @returns {string | undefined}
|
|
14
|
+
*/
|
|
15
|
+
export function assistantSnapshotSlice(snapshot, composerLabel, responseIndex) {
|
|
16
|
+
const lines = snapshot.split("\n");
|
|
17
|
+
const assistantHeadingIndices = lines.flatMap((line, index) => (line.includes('heading "ChatGPT said:"') ? [index] : []));
|
|
18
|
+
const startIndex = assistantHeadingIndices[responseIndex];
|
|
19
|
+
if (startIndex === undefined) return undefined;
|
|
20
|
+
|
|
21
|
+
const endCandidates = [];
|
|
22
|
+
const nextAssistantIndex = assistantHeadingIndices[responseIndex + 1];
|
|
23
|
+
if (nextAssistantIndex !== undefined) endCandidates.push(nextAssistantIndex);
|
|
24
|
+
|
|
25
|
+
const composerIndex = lines.findIndex(
|
|
26
|
+
(line, index) => index > startIndex && line.includes(`textbox "${composerLabel}"`),
|
|
27
|
+
);
|
|
28
|
+
if (composerIndex !== -1) endCandidates.push(composerIndex);
|
|
29
|
+
|
|
30
|
+
const endIndex = endCandidates.length > 0 ? Math.min(...endCandidates) : undefined;
|
|
31
|
+
return lines.slice(startIndex, endIndex).join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string | undefined} url
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function stripUrlQueryAndHash(url) {
|
|
39
|
+
if (typeof url !== "string") return "";
|
|
40
|
+
try {
|
|
41
|
+
const parsed = new URL(url);
|
|
42
|
+
parsed.hash = "";
|
|
43
|
+
parsed.search = "";
|
|
44
|
+
return parsed.toString();
|
|
45
|
+
} catch {
|
|
46
|
+
return url;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} url
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
export function isConversationPathUrl(url) {
|
|
55
|
+
try {
|
|
56
|
+
return /\/c\/[A-Za-z0-9-]+$/i.test(new URL(url).pathname);
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} url
|
|
64
|
+
* @param {string | undefined} previousChatUrl
|
|
65
|
+
* @returns {string | undefined}
|
|
66
|
+
*/
|
|
67
|
+
export function resolveStableConversationUrlCandidate(url, previousChatUrl) {
|
|
68
|
+
const normalizedUrl = stripUrlQueryAndHash(url);
|
|
69
|
+
if (!normalizedUrl) return undefined;
|
|
70
|
+
if (isConversationPathUrl(normalizedUrl)) return normalizedUrl;
|
|
71
|
+
const normalizedPrevious = stripUrlQueryAndHash(previousChatUrl);
|
|
72
|
+
return normalizedPrevious && normalizedPrevious === normalizedUrl ? normalizedUrl : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {Partial<OracleStableValueState> | undefined} state
|
|
77
|
+
* @param {string} nextValue
|
|
78
|
+
* @returns {OracleStableValueState}
|
|
79
|
+
*/
|
|
80
|
+
export function nextStableValueState(state, nextValue) {
|
|
81
|
+
return {
|
|
82
|
+
lastValue: nextValue,
|
|
83
|
+
stableCount: state?.lastValue === nextValue ? (state?.stableCount ?? 0) + 1 : 1,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
+
// Purpose: Provide pure ChatGPT UI interpretation helpers shared by oracle worker/auth flows.
|
|
2
|
+
// Responsibilities: Normalize allowed origins, interpret model-selection snapshots, and derive assistant-completion signatures.
|
|
3
|
+
// Scope: Pure snapshot/text heuristics only; browser I/O and retry loops stay in the worker/auth entrypoints.
|
|
4
|
+
// Usage: Imported by worker/auth runtime code and sanity tests to keep browser-driven logic behaviorally testable.
|
|
5
|
+
// Invariants/Assumptions: Snapshot text comes from agent-browser `snapshot -i`; helper outputs must stay deterministic and side-effect free.
|
|
6
|
+
|
|
1
7
|
import { parseSnapshotEntries } from "./artifact-heuristics.mjs";
|
|
2
8
|
|
|
9
|
+
/** @typedef {import("./chatgpt-ui-helpers.d.mts").OracleUiModelFamily} OracleUiModelFamily */
|
|
10
|
+
/** @typedef {import("./chatgpt-ui-helpers.d.mts").OracleUiSelection} OracleUiSelection */
|
|
11
|
+
/** @typedef {import("./artifact-heuristics.d.mts").SnapshotEntry} SnapshotEntry */
|
|
12
|
+
|
|
13
|
+
/** @typedef {{ responseText: string; artifactLabels?: string[]; suspiciousArtifactLabels?: string[] }} CompletionSignatureArgs */
|
|
14
|
+
/** @typedef {{ hasStopStreaming: boolean; hasTargetCopyResponse: boolean; responseText: string; artifactLabels?: string[]; suspiciousArtifactLabels?: string[] }} DerivedCompletionSignatureArgs */
|
|
15
|
+
|
|
3
16
|
export const CHATGPT_CANONICAL_APP_ORIGINS = Object.freeze([
|
|
4
17
|
"https://chatgpt.com",
|
|
5
18
|
"https://chat.openai.com",
|
|
6
19
|
]);
|
|
7
20
|
|
|
21
|
+
/** @type {Record<OracleUiModelFamily, string>} */
|
|
8
22
|
const MODEL_FAMILY_PREFIX = {
|
|
9
23
|
instant: "Instant ",
|
|
10
24
|
thinking: "Thinking ",
|
|
@@ -13,6 +27,10 @@ const MODEL_FAMILY_PREFIX = {
|
|
|
13
27
|
|
|
14
28
|
const AUTO_SWITCH_LABEL = "Auto-switch to Thinking";
|
|
15
29
|
|
|
30
|
+
/**
|
|
31
|
+
* @param {string | undefined} url
|
|
32
|
+
* @returns {string | undefined}
|
|
33
|
+
*/
|
|
16
34
|
function originFromUrl(url) {
|
|
17
35
|
if (typeof url !== "string" || !url.trim()) return undefined;
|
|
18
36
|
try {
|
|
@@ -22,18 +40,35 @@ function originFromUrl(url) {
|
|
|
22
40
|
}
|
|
23
41
|
}
|
|
24
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @param {Array<string | undefined>} values
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
25
47
|
function uniqueStrings(values) {
|
|
26
48
|
return [...new Set(values.filter((value) => typeof value === "string" && value))];
|
|
27
49
|
}
|
|
28
50
|
|
|
51
|
+
/**
|
|
52
|
+
* @param {string | undefined} value
|
|
53
|
+
* @returns {string | undefined}
|
|
54
|
+
*/
|
|
29
55
|
function titleCase(value) {
|
|
30
56
|
return value ? `${value[0].toUpperCase()}${value.slice(1)}` : value;
|
|
31
57
|
}
|
|
32
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @param {string | undefined} value
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
33
63
|
function normalizeText(value) {
|
|
34
64
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
35
65
|
}
|
|
36
66
|
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} chatUrl
|
|
69
|
+
* @param {string | undefined} authUrl
|
|
70
|
+
* @returns {string[]}
|
|
71
|
+
*/
|
|
37
72
|
export function buildAllowedChatGptOrigins(chatUrl, authUrl) {
|
|
38
73
|
return uniqueStrings([
|
|
39
74
|
...CHATGPT_CANONICAL_APP_ORIGINS,
|
|
@@ -43,6 +78,11 @@ export function buildAllowedChatGptOrigins(chatUrl, authUrl) {
|
|
|
43
78
|
]);
|
|
44
79
|
}
|
|
45
80
|
|
|
81
|
+
/**
|
|
82
|
+
* @param {string | undefined} label
|
|
83
|
+
* @param {OracleUiModelFamily} family
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
46
86
|
export function matchesModelFamilyLabel(label, family) {
|
|
47
87
|
const normalized = String(label || "");
|
|
48
88
|
const prefix = MODEL_FAMILY_PREFIX[family];
|
|
@@ -50,12 +90,22 @@ export function matchesModelFamilyLabel(label, family) {
|
|
|
50
90
|
return normalized === exact || normalized.startsWith(prefix) || normalized.startsWith(`${exact},`);
|
|
51
91
|
}
|
|
52
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @param {OracleUiSelection} selection
|
|
95
|
+
* @returns {string | undefined}
|
|
96
|
+
*/
|
|
53
97
|
export function requestedEffortLabel(selection) {
|
|
54
98
|
return selection?.effort ? titleCase(selection.effort) : undefined;
|
|
55
99
|
}
|
|
56
100
|
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} snapshot
|
|
103
|
+
* @param {string | undefined} effortLabel
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
57
106
|
export function effortSelectionVisible(snapshot, effortLabel) {
|
|
58
107
|
if (!effortLabel) return true;
|
|
108
|
+
/** @type {SnapshotEntry[]} */
|
|
59
109
|
const entries = parseSnapshotEntries(snapshot);
|
|
60
110
|
return entries.some((entry) => {
|
|
61
111
|
if (entry.disabled) return false;
|
|
@@ -72,19 +122,27 @@ export function effortSelectionVisible(snapshot, effortLabel) {
|
|
|
72
122
|
});
|
|
73
123
|
}
|
|
74
124
|
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} snapshot
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
75
129
|
export function thinkingChipVisible(snapshot) {
|
|
76
130
|
return /button "(?:Light|Standard|Extended|Heavy)(?: thinking)?(?:, click to remove)?"/i.test(snapshot);
|
|
77
131
|
}
|
|
78
132
|
|
|
133
|
+
/**
|
|
134
|
+
* @param {string} snapshot
|
|
135
|
+
* @returns {boolean}
|
|
136
|
+
*/
|
|
79
137
|
export function snapshotHasModelConfigurationUi(snapshot) {
|
|
138
|
+
/** @type {SnapshotEntry[]} */
|
|
80
139
|
const entries = parseSnapshotEntries(snapshot);
|
|
81
140
|
const visibleFamilies = new Set(
|
|
82
141
|
entries
|
|
83
142
|
.filter((entry) => entry.kind === "button" && typeof entry.label === "string")
|
|
84
143
|
.flatMap((entry) =>
|
|
85
|
-
|
|
86
|
-
.filter((family) => matchesModelFamilyLabel(entry.label, family))
|
|
87
|
-
.map((family) => family),
|
|
144
|
+
/** @type {OracleUiModelFamily[]} */ (["instant", "thinking", "pro"])
|
|
145
|
+
.filter((family) => matchesModelFamilyLabel(entry.label, family)),
|
|
88
146
|
),
|
|
89
147
|
);
|
|
90
148
|
const hasCloseButton = entries.some((entry) => entry.kind === "button" && entry.label === "Close" && !entry.disabled);
|
|
@@ -94,7 +152,12 @@ export function snapshotHasModelConfigurationUi(snapshot) {
|
|
|
94
152
|
return visibleFamilies.size >= 2 || hasCloseButton || hasEffortCombobox;
|
|
95
153
|
}
|
|
96
154
|
|
|
155
|
+
/**
|
|
156
|
+
* @param {string} snapshot
|
|
157
|
+
* @returns {boolean | undefined}
|
|
158
|
+
*/
|
|
97
159
|
export function autoSwitchToThinkingSelectionVisible(snapshot) {
|
|
160
|
+
/** @type {SnapshotEntry[]} */
|
|
98
161
|
const entries = parseSnapshotEntries(snapshot);
|
|
99
162
|
let foundControl = false;
|
|
100
163
|
|
|
@@ -111,6 +174,11 @@ export function autoSwitchToThinkingSelectionVisible(snapshot) {
|
|
|
111
174
|
return foundControl ? false : undefined;
|
|
112
175
|
}
|
|
113
176
|
|
|
177
|
+
/**
|
|
178
|
+
* @param {string} snapshot
|
|
179
|
+
* @param {OracleUiSelection} selection
|
|
180
|
+
* @returns {boolean}
|
|
181
|
+
*/
|
|
114
182
|
export function snapshotCanSafelySkipModelConfiguration(snapshot, selection) {
|
|
115
183
|
if (!snapshotStronglyMatchesRequestedModel(snapshot, selection)) return false;
|
|
116
184
|
|
|
@@ -126,11 +194,15 @@ export function snapshotCanSafelySkipModelConfiguration(snapshot, selection) {
|
|
|
126
194
|
return true;
|
|
127
195
|
}
|
|
128
196
|
|
|
197
|
+
/**
|
|
198
|
+
* @param {string} snapshot
|
|
199
|
+
* @param {OracleUiSelection} selection
|
|
200
|
+
* @returns {boolean}
|
|
201
|
+
*/
|
|
129
202
|
export function snapshotStronglyMatchesRequestedModel(snapshot, selection) {
|
|
203
|
+
/** @type {SnapshotEntry[]} */
|
|
130
204
|
const entries = parseSnapshotEntries(snapshot);
|
|
131
|
-
const familyMatched = entries.some((entry) =>
|
|
132
|
-
return !entry.disabled && matchesModelFamilyLabel(entry.label, selection.modelFamily);
|
|
133
|
-
});
|
|
205
|
+
const familyMatched = entries.some((entry) => !entry.disabled && matchesModelFamilyLabel(entry.label, selection.modelFamily));
|
|
134
206
|
if (!familyMatched) return false;
|
|
135
207
|
|
|
136
208
|
const configurationUiVisible = snapshotHasModelConfigurationUi(snapshot);
|
|
@@ -153,11 +225,15 @@ export function snapshotStronglyMatchesRequestedModel(snapshot, selection) {
|
|
|
153
225
|
return false;
|
|
154
226
|
}
|
|
155
227
|
|
|
228
|
+
/**
|
|
229
|
+
* @param {string} snapshot
|
|
230
|
+
* @param {OracleUiSelection} selection
|
|
231
|
+
* @returns {boolean}
|
|
232
|
+
*/
|
|
156
233
|
export function snapshotWeaklyMatchesRequestedModel(snapshot, selection) {
|
|
234
|
+
/** @type {SnapshotEntry[]} */
|
|
157
235
|
const entries = parseSnapshotEntries(snapshot);
|
|
158
|
-
const familyMatched = entries.some((entry) =>
|
|
159
|
-
return !entry.disabled && matchesModelFamilyLabel(entry.label, selection.modelFamily);
|
|
160
|
-
});
|
|
236
|
+
const familyMatched = entries.some((entry) => !entry.disabled && matchesModelFamilyLabel(entry.label, selection.modelFamily));
|
|
161
237
|
|
|
162
238
|
if (selection.modelFamily === "thinking") {
|
|
163
239
|
return familyMatched || effortSelectionVisible(snapshot, requestedEffortLabel(selection));
|
|
@@ -177,6 +253,10 @@ export function snapshotWeaklyMatchesRequestedModel(snapshot, selection) {
|
|
|
177
253
|
return false;
|
|
178
254
|
}
|
|
179
255
|
|
|
256
|
+
/**
|
|
257
|
+
* @param {CompletionSignatureArgs} args
|
|
258
|
+
* @returns {string | undefined}
|
|
259
|
+
*/
|
|
180
260
|
export function buildAssistantCompletionSignature({ responseText, artifactLabels = [], suspiciousArtifactLabels = [] }) {
|
|
181
261
|
const normalizedResponse = normalizeText(responseText);
|
|
182
262
|
if (normalizedResponse) return `text:${normalizedResponse}`;
|
|
@@ -187,6 +267,10 @@ export function buildAssistantCompletionSignature({ responseText, artifactLabels
|
|
|
187
267
|
return undefined;
|
|
188
268
|
}
|
|
189
269
|
|
|
270
|
+
/**
|
|
271
|
+
* @param {DerivedCompletionSignatureArgs} args
|
|
272
|
+
* @returns {string | undefined}
|
|
273
|
+
*/
|
|
190
274
|
export function deriveAssistantCompletionSignature({
|
|
191
275
|
hasStopStreaming,
|
|
192
276
|
hasTargetCopyResponse,
|