pi-oracle 0.3.4 → 0.4.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 (33) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +2 -0
  3. package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +249 -0
  4. package/extensions/oracle/index.ts +8 -1
  5. package/extensions/oracle/lib/commands.ts +11 -24
  6. package/extensions/oracle/lib/config.ts +5 -0
  7. package/extensions/oracle/lib/jobs.ts +117 -217
  8. package/extensions/oracle/lib/locks.ts +41 -209
  9. package/extensions/oracle/lib/poller.ts +14 -51
  10. package/extensions/oracle/lib/queue.ts +75 -112
  11. package/extensions/oracle/lib/runtime.ts +60 -14
  12. package/extensions/oracle/lib/tools.ts +66 -65
  13. package/extensions/oracle/shared/job-coordination-helpers.d.mts +84 -0
  14. package/extensions/oracle/shared/job-coordination-helpers.mjs +168 -0
  15. package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +130 -0
  16. package/extensions/oracle/shared/job-lifecycle-helpers.mjs +377 -0
  17. package/extensions/oracle/shared/job-observability-helpers.d.mts +59 -0
  18. package/extensions/oracle/shared/job-observability-helpers.mjs +143 -0
  19. package/extensions/oracle/shared/process-helpers.d.mts +20 -0
  20. package/extensions/oracle/shared/process-helpers.mjs +128 -0
  21. package/extensions/oracle/shared/state-coordination-helpers.d.mts +43 -0
  22. package/extensions/oracle/shared/state-coordination-helpers.mjs +381 -0
  23. package/extensions/oracle/worker/artifact-heuristics.mjs +5 -0
  24. package/extensions/oracle/worker/auth-bootstrap.mjs +76 -130
  25. package/extensions/oracle/worker/auth-cookie-policy.mjs +5 -0
  26. package/extensions/oracle/worker/auth-flow-helpers.d.mts +41 -0
  27. package/extensions/oracle/worker/auth-flow-helpers.mjs +165 -0
  28. package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +13 -0
  29. package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +85 -0
  30. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +93 -9
  31. package/extensions/oracle/worker/run-job.mjs +166 -274
  32. package/extensions/oracle/worker/state-locks.mjs +31 -216
  33. package/package.json +4 -3
@@ -0,0 +1,41 @@
1
+ export interface OracleAuthLoginProbe {
2
+ ok: boolean;
3
+ status: number;
4
+ pageUrl?: string;
5
+ domLoginCta?: boolean;
6
+ onAuthPage?: boolean;
7
+ error?: string;
8
+ bodyKeys?: string[];
9
+ bodyHasId?: boolean;
10
+ bodyHasEmail?: boolean;
11
+ name?: string;
12
+ responsePreview?: string;
13
+ }
14
+
15
+ export type OracleAuthPageState =
16
+ | "challenge_blocking"
17
+ | "login_required"
18
+ | "transient_outage_error"
19
+ | "auth_transitioning"
20
+ | "authenticated_and_ready"
21
+ | "unknown";
22
+
23
+ export interface OracleAuthPageClassification {
24
+ state: OracleAuthPageState;
25
+ message: string;
26
+ }
27
+
28
+ export declare function normalizeLoginProbeResult(result: unknown): OracleAuthLoginProbe;
29
+ export declare function buildAccountChooserCandidateLabels(name?: string): string[];
30
+ export declare function classifyChatAuthPage(args: {
31
+ url: string;
32
+ snapshot: string;
33
+ body: string;
34
+ probe?: OracleAuthLoginProbe;
35
+ allowedOrigins: readonly string[];
36
+ cookieSourceLabel: string;
37
+ runtimeProfileDir: string;
38
+ logPath: string;
39
+ composerLabel?: string;
40
+ addFilesLabel?: string;
41
+ }): OracleAuthPageClassification;
@@ -0,0 +1,165 @@
1
+ // Purpose: Provide pure auth-bootstrap classification helpers for ChatGPT browser state handling.
2
+ // Responsibilities: Normalize login-probe payloads, classify ChatGPT auth page states, and derive account chooser candidate labels.
3
+ // Scope: Pure worker/auth decision logic only; browser I/O, logging, and side effects stay in auth-bootstrap.mjs.
4
+ // Usage: Imported by auth-bootstrap.mjs and sanity tests to exercise auth classification behavior without driving a browser.
5
+ // Invariants/Assumptions: Inputs are already captured snapshots/probe results from the live browser session; outputs are deterministic and side-effect free.
6
+
7
+ /** @typedef {import("./auth-flow-helpers.d.mts").OracleAuthLoginProbe} OracleAuthLoginProbe */
8
+ /** @typedef {import("./auth-flow-helpers.d.mts").OracleAuthPageClassification} OracleAuthPageClassification */
9
+
10
+ const DEFAULT_COMPOSER_LABEL = "Chat with ChatGPT";
11
+ const DEFAULT_ADD_FILES_LABEL = "Add files and more";
12
+
13
+ /**
14
+ * @param {unknown} result
15
+ * @returns {OracleAuthLoginProbe}
16
+ */
17
+ export function normalizeLoginProbeResult(result) {
18
+ if (!result || typeof result !== "object") {
19
+ return { ok: false, status: 0, error: "invalid-probe-result" };
20
+ }
21
+ const value = /** @type {Record<string, unknown>} */ (result);
22
+ return {
23
+ ok: value.ok === true,
24
+ status: typeof value.status === "number" ? value.status : 0,
25
+ pageUrl: typeof value.pageUrl === "string" ? value.pageUrl : undefined,
26
+ domLoginCta: value.domLoginCta === true,
27
+ onAuthPage: value.onAuthPage === true,
28
+ error: typeof value.error === "string" ? value.error : undefined,
29
+ bodyKeys: Array.isArray(value.bodyKeys) ? value.bodyKeys.filter((entry) => typeof entry === "string") : [],
30
+ bodyHasId: value.bodyHasId === true,
31
+ bodyHasEmail: value.bodyHasEmail === true,
32
+ name: typeof value.name === "string" ? value.name : undefined,
33
+ responsePreview: typeof value.responsePreview === "string" ? value.responsePreview : undefined,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * @param {string | undefined} name
39
+ * @returns {string[]}
40
+ */
41
+ export function buildAccountChooserCandidateLabels(name) {
42
+ const normalized = typeof name === "string" ? name.trim() : "";
43
+ if (!normalized) return [];
44
+ const firstToken = normalized.split(/\s+/)[0] || "";
45
+ return firstToken && firstToken !== normalized ? [normalized, firstToken] : [normalized];
46
+ }
47
+
48
+ /**
49
+ * @param {{
50
+ * url: string;
51
+ * snapshot: string;
52
+ * body: string;
53
+ * probe?: OracleAuthLoginProbe;
54
+ * allowedOrigins: readonly string[];
55
+ * cookieSourceLabel: string;
56
+ * runtimeProfileDir: string;
57
+ * logPath: string;
58
+ * composerLabel?: string;
59
+ * addFilesLabel?: string;
60
+ * }} args
61
+ * @returns {OracleAuthPageClassification}
62
+ */
63
+ export function classifyChatAuthPage(args) {
64
+ const text = `${args.snapshot}\n${args.body}`;
65
+ const composerLabel = args.composerLabel || DEFAULT_COMPOSER_LABEL;
66
+ const addFilesLabel = args.addFilesLabel || DEFAULT_ADD_FILES_LABEL;
67
+ const onAllowedOrigin = args.allowedOrigins.some((origin) => args.url.startsWith(origin));
68
+ const hasComposer = args.snapshot.includes(`textbox "${composerLabel}"`);
69
+ const hasAddFiles = args.snapshot.includes(`button "${addFilesLabel}"`);
70
+ const hasModelControl =
71
+ args.snapshot.includes('button "Model selector"') ||
72
+ /button "(Instant|Thinking|Pro)(?: [^"]*)?"/.test(args.snapshot);
73
+
74
+ const challengePatterns = [
75
+ /just a moment/i,
76
+ /verify you are human/i,
77
+ /cloudflare/i,
78
+ /captcha|turnstile|hcaptcha/i,
79
+ /unusual activity detected/i,
80
+ /we detect suspicious activity/i,
81
+ ];
82
+ if (challengePatterns.some((pattern) => pattern.test(text))) {
83
+ return {
84
+ state: "challenge_blocking",
85
+ message:
86
+ `ChatGPT challenge detected after syncing cookies from ${args.cookieSourceLabel}. ` +
87
+ `The isolated oracle browser was left open on profile ${args.runtimeProfileDir}; complete the challenge there, then rerun /oracle-auth. Logs: ${args.logPath}`,
88
+ };
89
+ }
90
+
91
+ if (/http error 431|request header or cookie too large/i.test(text)) {
92
+ return {
93
+ state: "login_required",
94
+ message:
95
+ `Imported auth hit HTTP 431 during ChatGPT auth resolution, which usually means the imported cookie set is too large or stale. ` +
96
+ `Inspect ${args.logPath}.`,
97
+ };
98
+ }
99
+
100
+ const outagePatterns = [
101
+ /something went wrong/i,
102
+ /a network error occurred/i,
103
+ /an error occurred while connecting to the websocket/i,
104
+ /try again later/i,
105
+ ];
106
+ if (outagePatterns.some((pattern) => pattern.test(text))) {
107
+ return { state: "transient_outage_error", message: `ChatGPT is showing a transient outage/error page. Logs: ${args.logPath}` };
108
+ }
109
+
110
+ if (args.probe?.status === 401 || args.probe?.status === 403) {
111
+ return {
112
+ state: "login_required",
113
+ message:
114
+ `Synced cookies from ${args.cookieSourceLabel}, but ChatGPT still rejected the session ` +
115
+ `(status=${args.probe?.status ?? 0}). Check auth.chromeProfile/auth.chromeCookiePath and inspect ${args.logPath}.`,
116
+ };
117
+ }
118
+
119
+ if (args.probe?.onAuthPage) {
120
+ if (args.probe?.bodyHasId || args.probe?.bodyHasEmail) {
121
+ return {
122
+ state: "auth_transitioning",
123
+ message:
124
+ `ChatGPT is on /auth/login, but /backend-api/me returned a partial authenticated session. ` +
125
+ `Trying to drive the login resolution flow. Logs: ${args.logPath}`,
126
+ };
127
+ }
128
+ return {
129
+ state: "login_required",
130
+ message:
131
+ `Synced cookies from ${args.cookieSourceLabel}, but ChatGPT still rejected the session ` +
132
+ `(status=${args.probe?.status ?? 0}). Check auth.chromeProfile/auth.chromeCookiePath and inspect ${args.logPath}.`,
133
+ };
134
+ }
135
+
136
+ if (onAllowedOrigin && args.probe?.status === 200 && hasComposer && hasAddFiles && hasModelControl) {
137
+ if (!args.probe?.domLoginCta) {
138
+ return {
139
+ state: "authenticated_and_ready",
140
+ message: `Imported ChatGPT auth from ${args.cookieSourceLabel} into the isolated oracle profile. Logs: ${args.logPath}`,
141
+ };
142
+ }
143
+
144
+ return {
145
+ state: "auth_transitioning",
146
+ message:
147
+ args.probe?.bodyHasId || args.probe?.bodyHasEmail
148
+ ? `ChatGPT backend session is authenticated but the shell still shows public CTA chrome. Logs: ${args.logPath}`
149
+ : `ChatGPT accepted cookies but is still hydrating/auth-selecting. Logs: ${args.logPath}`,
150
+ };
151
+ }
152
+
153
+ if (onAllowedOrigin && args.probe?.ok && hasComposer && hasAddFiles && hasModelControl) {
154
+ return {
155
+ state: "authenticated_and_ready",
156
+ message: `Imported ChatGPT auth from ${args.cookieSourceLabel} into the isolated oracle profile. Logs: ${args.logPath}`,
157
+ };
158
+ }
159
+
160
+ if (args.url && !onAllowedOrigin) {
161
+ return { state: "login_required", message: `Imported auth redirected away from the expected ChatGPT origin. Logs: ${args.logPath}` };
162
+ }
163
+
164
+ return { state: "unknown", message: `ChatGPT page state is not yet ready. Logs: ${args.logPath}` };
165
+ }
@@ -0,0 +1,13 @@
1
+ export interface OracleStableValueState {
2
+ lastValue: string;
3
+ stableCount: number;
4
+ }
5
+
6
+ export declare function assistantSnapshotSlice(snapshot: string, composerLabel: string, responseIndex: number): string | undefined;
7
+ export declare function stripUrlQueryAndHash(url: string | undefined): string;
8
+ export declare function isConversationPathUrl(url: string): boolean;
9
+ export declare function resolveStableConversationUrlCandidate(url: string, previousChatUrl?: string): string | undefined;
10
+ export declare function nextStableValueState(
11
+ state: Partial<OracleStableValueState> | undefined,
12
+ nextValue: string,
13
+ ): OracleStableValueState;
@@ -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,