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.
Files changed (35) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +27 -8
  3. package/docs/ORACLE_DESIGN.md +14 -8
  4. package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +276 -0
  5. package/extensions/oracle/index.ts +8 -1
  6. package/extensions/oracle/lib/commands.ts +25 -29
  7. package/extensions/oracle/lib/config.ts +56 -2
  8. package/extensions/oracle/lib/jobs.ts +134 -219
  9. package/extensions/oracle/lib/locks.ts +41 -209
  10. package/extensions/oracle/lib/poller.ts +38 -52
  11. package/extensions/oracle/lib/queue.ts +75 -112
  12. package/extensions/oracle/lib/runtime.ts +102 -19
  13. package/extensions/oracle/lib/tools.ts +663 -294
  14. package/extensions/oracle/shared/job-coordination-helpers.d.mts +84 -0
  15. package/extensions/oracle/shared/job-coordination-helpers.mjs +168 -0
  16. package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +131 -0
  17. package/extensions/oracle/shared/job-lifecycle-helpers.mjs +390 -0
  18. package/extensions/oracle/shared/job-observability-helpers.d.mts +60 -0
  19. package/extensions/oracle/shared/job-observability-helpers.mjs +161 -0
  20. package/extensions/oracle/shared/process-helpers.d.mts +20 -0
  21. package/extensions/oracle/shared/process-helpers.mjs +128 -0
  22. package/extensions/oracle/shared/state-coordination-helpers.d.mts +43 -0
  23. package/extensions/oracle/shared/state-coordination-helpers.mjs +381 -0
  24. package/extensions/oracle/worker/artifact-heuristics.mjs +5 -0
  25. package/extensions/oracle/worker/auth-bootstrap.mjs +125 -134
  26. package/extensions/oracle/worker/auth-cookie-policy.mjs +5 -0
  27. package/extensions/oracle/worker/auth-flow-helpers.d.mts +41 -0
  28. package/extensions/oracle/worker/auth-flow-helpers.mjs +165 -0
  29. package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +13 -0
  30. package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +85 -0
  31. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +93 -9
  32. package/extensions/oracle/worker/run-job.mjs +166 -274
  33. package/extensions/oracle/worker/state-locks.mjs +31 -216
  34. package/package.json +4 -3
  35. 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
- Object.keys(MODEL_FAMILY_PREFIX)
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,