@steipete/oracle 0.9.0 → 0.10.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 (177) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +61 -48
  3. package/dist/bin/oracle-cli.js +455 -402
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/assistantResponse.js +149 -101
  18. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  19. package/dist/src/browser/actions/attachments.js +246 -150
  20. package/dist/src/browser/actions/domEvents.js +2 -2
  21. package/dist/src/browser/actions/modelSelection.js +275 -117
  22. package/dist/src/browser/actions/navigation.js +161 -137
  23. package/dist/src/browser/actions/promptComposer.js +100 -64
  24. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  25. package/dist/src/browser/actions/thinkingTime.js +207 -110
  26. package/dist/src/browser/chromeLifecycle.js +62 -60
  27. package/dist/src/browser/config.js +34 -15
  28. package/dist/src/browser/constants.js +17 -12
  29. package/dist/src/browser/cookies.js +19 -19
  30. package/dist/src/browser/detect.js +62 -62
  31. package/dist/src/browser/domDebug.js +1 -1
  32. package/dist/src/browser/index.js +390 -295
  33. package/dist/src/browser/modelStrategy.js +1 -1
  34. package/dist/src/browser/pageActions.js +5 -5
  35. package/dist/src/browser/policies.js +16 -13
  36. package/dist/src/browser/profileState.js +44 -39
  37. package/dist/src/browser/prompt.js +72 -42
  38. package/dist/src/browser/promptSummary.js +5 -5
  39. package/dist/src/browser/providerDomFlow.js +1 -1
  40. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  41. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  42. package/dist/src/browser/providers/index.js +2 -2
  43. package/dist/src/browser/reattach.js +67 -34
  44. package/dist/src/browser/reattachHelpers.js +31 -26
  45. package/dist/src/browser/sessionRunner.js +37 -25
  46. package/dist/src/browser/utils.js +9 -9
  47. package/dist/src/browserMode.js +1 -1
  48. package/dist/src/cli/bridge/claudeConfig.js +16 -16
  49. package/dist/src/cli/bridge/client.js +28 -20
  50. package/dist/src/cli/bridge/codexConfig.js +16 -16
  51. package/dist/src/cli/bridge/doctor.js +47 -39
  52. package/dist/src/cli/bridge/host.js +58 -56
  53. package/dist/src/cli/browserConfig.js +62 -48
  54. package/dist/src/cli/browserDefaults.js +27 -26
  55. package/dist/src/cli/bundleWarnings.js +1 -1
  56. package/dist/src/cli/clipboard.js +11 -2
  57. package/dist/src/cli/detach.js +2 -2
  58. package/dist/src/cli/dryRun.js +29 -25
  59. package/dist/src/cli/duplicatePromptGuard.js +3 -3
  60. package/dist/src/cli/engine.js +9 -9
  61. package/dist/src/cli/errorUtils.js +1 -1
  62. package/dist/src/cli/fileSize.js +3 -3
  63. package/dist/src/cli/format.js +2 -2
  64. package/dist/src/cli/help.js +28 -28
  65. package/dist/src/cli/hiddenAliases.js +3 -3
  66. package/dist/src/cli/markdownBundle.js +7 -7
  67. package/dist/src/cli/markdownRenderer.js +15 -15
  68. package/dist/src/cli/notifier.js +77 -67
  69. package/dist/src/cli/options.js +127 -106
  70. package/dist/src/cli/oscUtils.js +1 -1
  71. package/dist/src/cli/promptRequirement.js +2 -2
  72. package/dist/src/cli/renderOutput.js +1 -1
  73. package/dist/src/cli/rootAlias.js +1 -1
  74. package/dist/src/cli/runOptions.js +32 -28
  75. package/dist/src/cli/sessionCommand.js +31 -21
  76. package/dist/src/cli/sessionDisplay.js +95 -81
  77. package/dist/src/cli/sessionLineage.js +6 -2
  78. package/dist/src/cli/sessionRunner.js +103 -93
  79. package/dist/src/cli/sessionTable.js +26 -23
  80. package/dist/src/cli/stdin.js +22 -0
  81. package/dist/src/cli/tagline.js +121 -124
  82. package/dist/src/cli/tui/index.js +139 -128
  83. package/dist/src/cli/writeOutputPath.js +5 -5
  84. package/dist/src/config.js +7 -7
  85. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  86. package/dist/src/gemini-web/client.js +76 -70
  87. package/dist/src/gemini-web/executionMode.js +6 -8
  88. package/dist/src/gemini-web/executor.js +98 -93
  89. package/dist/src/gemini-web/index.js +1 -1
  90. package/dist/src/mcp/server.js +16 -12
  91. package/dist/src/mcp/tools/consult.js +51 -47
  92. package/dist/src/mcp/tools/sessionResources.js +12 -12
  93. package/dist/src/mcp/tools/sessions.js +26 -17
  94. package/dist/src/mcp/types.js +5 -5
  95. package/dist/src/mcp/utils.js +15 -7
  96. package/dist/src/oracle/background.js +15 -15
  97. package/dist/src/oracle/claude.js +53 -25
  98. package/dist/src/oracle/client.js +50 -41
  99. package/dist/src/oracle/config.js +96 -66
  100. package/dist/src/oracle/errors.js +38 -38
  101. package/dist/src/oracle/files.js +55 -46
  102. package/dist/src/oracle/finishLine.js +10 -8
  103. package/dist/src/oracle/format.js +3 -3
  104. package/dist/src/oracle/gemini.js +37 -33
  105. package/dist/src/oracle/logging.js +7 -7
  106. package/dist/src/oracle/markdown.js +28 -28
  107. package/dist/src/oracle/modelResolver.js +16 -16
  108. package/dist/src/oracle/multiModelRunner.js +12 -12
  109. package/dist/src/oracle/oscProgress.js +8 -8
  110. package/dist/src/oracle/promptAssembly.js +6 -3
  111. package/dist/src/oracle/request.js +16 -13
  112. package/dist/src/oracle/run.js +156 -134
  113. package/dist/src/oracle/runUtils.js +8 -5
  114. package/dist/src/oracle/tokenEstimate.js +6 -6
  115. package/dist/src/oracle/tokenStats.js +5 -5
  116. package/dist/src/oracle/tokenStringifier.js +5 -5
  117. package/dist/src/oracle.js +12 -12
  118. package/dist/src/oracleHome.js +3 -3
  119. package/dist/src/remote/client.js +25 -25
  120. package/dist/src/remote/health.js +20 -20
  121. package/dist/src/remote/remoteServiceConfig.js +9 -9
  122. package/dist/src/remote/server.js +129 -118
  123. package/dist/src/sessionManager.js +77 -75
  124. package/dist/src/sessionStore.js +3 -3
  125. package/dist/src/version.js +10 -10
  126. package/dist/vendor/oracle-notifier/README.md +2 -0
  127. package/package.json +66 -62
  128. package/vendor/oracle-notifier/README.md +2 -0
  129. package/dist/markdansi/types/index.js +0 -4
  130. package/dist/oracle/bin/oracle-cli.js +0 -472
  131. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  132. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  133. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  134. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  135. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  136. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  137. package/dist/oracle/src/browser/config.js +0 -33
  138. package/dist/oracle/src/browser/constants.js +0 -40
  139. package/dist/oracle/src/browser/cookies.js +0 -210
  140. package/dist/oracle/src/browser/domDebug.js +0 -36
  141. package/dist/oracle/src/browser/index.js +0 -331
  142. package/dist/oracle/src/browser/pageActions.js +0 -5
  143. package/dist/oracle/src/browser/prompt.js +0 -88
  144. package/dist/oracle/src/browser/promptSummary.js +0 -20
  145. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  146. package/dist/oracle/src/browser/types.js +0 -1
  147. package/dist/oracle/src/browser/utils.js +0 -62
  148. package/dist/oracle/src/browserMode.js +0 -1
  149. package/dist/oracle/src/cli/browserConfig.js +0 -44
  150. package/dist/oracle/src/cli/dryRun.js +0 -59
  151. package/dist/oracle/src/cli/engine.js +0 -17
  152. package/dist/oracle/src/cli/errorUtils.js +0 -9
  153. package/dist/oracle/src/cli/help.js +0 -70
  154. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  155. package/dist/oracle/src/cli/options.js +0 -103
  156. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  157. package/dist/oracle/src/cli/rootAlias.js +0 -30
  158. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  159. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  160. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  161. package/dist/oracle/src/heartbeat.js +0 -43
  162. package/dist/oracle/src/oracle/client.js +0 -48
  163. package/dist/oracle/src/oracle/config.js +0 -29
  164. package/dist/oracle/src/oracle/errors.js +0 -101
  165. package/dist/oracle/src/oracle/files.js +0 -220
  166. package/dist/oracle/src/oracle/format.js +0 -33
  167. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  168. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  169. package/dist/oracle/src/oracle/request.js +0 -48
  170. package/dist/oracle/src/oracle/run.js +0 -444
  171. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  172. package/dist/oracle/src/oracle/types.js +0 -1
  173. package/dist/oracle/src/oracle.js +0 -9
  174. package/dist/oracle/src/sessionManager.js +0 -205
  175. package/dist/oracle/src/version.js +0 -39
  176. package/dist/scripts/chrome/browser-tools.js +0 -295
  177. package/dist/src/browser/profileSync.js +0 -141
@@ -1,46 +1,51 @@
1
- import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, COPY_BUTTON_SELECTOR, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, } from '../constants.js';
2
- import { delay } from '../utils.js';
3
- import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression } from '../domDebug.js';
4
- import { buildClickDispatcher } from './domEvents.js';
5
- const ASSISTANT_POLL_TIMEOUT_ERROR = 'assistant-response-watchdog-timeout';
1
+ import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, COPY_BUTTON_SELECTOR, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, } from "../constants.js";
2
+ import { delay } from "../utils.js";
3
+ import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression, } from "../domDebug.js";
4
+ import { buildClickDispatcher } from "./domEvents.js";
5
+ const ASSISTANT_POLL_TIMEOUT_ERROR = "assistant-response-watchdog-timeout";
6
6
  function isAnswerNowPlaceholderText(normalized) {
7
7
  const text = normalized.trim();
8
8
  if (!text)
9
9
  return false;
10
10
  // Learned: "Pro thinking" shows a placeholder turn that contains "Answer now".
11
11
  // That is not the final answer and must be ignored in browser automation.
12
- if (text === 'chatgpt said:' || text === 'chatgpt said')
12
+ if (text === "chatgpt said:" || text === "chatgpt said")
13
13
  return true;
14
- if (text.includes('file upload request') && (text.includes('pro thinking') || text.includes('chatgpt said'))) {
14
+ if (text.includes("file upload request") &&
15
+ (text.includes("pro thinking") || text.includes("chatgpt said"))) {
15
16
  return true;
16
17
  }
17
- return text.includes('answer now') && (text.includes('pro thinking') || text.includes('chatgpt said'));
18
+ return (text.includes("answer now") && (text.includes("pro thinking") || text.includes("chatgpt said")));
18
19
  }
19
- export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex) {
20
+ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId) {
20
21
  const start = Date.now();
21
- logger('Waiting for ChatGPT response');
22
+ logger("Waiting for ChatGPT response");
22
23
  // Learned: two paths are needed:
23
24
  // 1) DOM observer (fast when mutations fire),
24
25
  // 2) snapshot poller (fallback when observers miss or JS stalls).
25
- const expression = buildResponseObserverExpression(timeoutMs, minTurnIndex);
26
- const evaluationPromise = Runtime.evaluate({ expression, awaitPromise: true, returnByValue: true });
27
- const raceReadyEvaluation = evaluationPromise.then((value) => ({ kind: 'evaluation', value }), (error) => {
28
- throw { source: 'evaluation', error };
26
+ const expression = buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConversationId);
27
+ const evaluationPromise = Runtime.evaluate({
28
+ expression,
29
+ awaitPromise: true,
30
+ returnByValue: true,
31
+ });
32
+ const raceReadyEvaluation = evaluationPromise.then((value) => ({ kind: "evaluation", value }), (error) => {
33
+ throw { source: "evaluation", error };
29
34
  });
30
35
  // Use AbortController to stop the poller when the evaluation wins the race,
31
36
  // preventing abandoned polling loops from consuming resources.
32
37
  const pollerAbort = new AbortController();
33
- const pollerPromise = pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, pollerAbort.signal).then((value) => ({ kind: 'poll', value }), (error) => {
34
- throw { source: 'poll', error };
38
+ const pollerPromise = pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expectedConversationId, pollerAbort.signal).then((value) => ({ kind: "poll", value }), (error) => {
39
+ throw { source: "poll", error };
35
40
  });
36
41
  let evaluation = null;
37
42
  try {
38
43
  const winner = await Promise.race([raceReadyEvaluation, pollerPromise]);
39
- if (winner.kind === 'poll') {
44
+ if (winner.kind === "poll") {
40
45
  if (!winner.value) {
41
- throw { source: 'poll', error: new Error(ASSISTANT_POLL_TIMEOUT_ERROR) };
46
+ throw { source: "poll", error: new Error(ASSISTANT_POLL_TIMEOUT_ERROR) };
42
47
  }
43
- logger('Captured assistant response via snapshot watchdog');
48
+ logger("Captured assistant response via snapshot watchdog");
44
49
  evaluationPromise.catch(() => undefined);
45
50
  await terminateRuntimeExecution(Runtime);
46
51
  return winner.value;
@@ -50,21 +55,26 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
50
55
  evaluation = winner.value;
51
56
  }
52
57
  catch (wrappedError) {
53
- if (wrappedError && typeof wrappedError === 'object' && 'source' in wrappedError && 'error' in wrappedError) {
58
+ if (wrappedError &&
59
+ typeof wrappedError === "object" &&
60
+ "source" in wrappedError &&
61
+ "error" in wrappedError) {
54
62
  const { source, error } = wrappedError;
55
- if (source === 'poll' && error instanceof Error && error.message === ASSISTANT_POLL_TIMEOUT_ERROR) {
63
+ if (source === "poll" &&
64
+ error instanceof Error &&
65
+ error.message === ASSISTANT_POLL_TIMEOUT_ERROR) {
56
66
  evaluation = await evaluationPromise;
57
67
  }
58
- else if (source === 'poll') {
68
+ else if (source === "poll") {
59
69
  throw error;
60
70
  }
61
- else if (source === 'evaluation') {
62
- const recovered = await recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex);
71
+ else if (source === "evaluation") {
72
+ const recovered = await recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId);
63
73
  if (recovered) {
64
74
  return recovered;
65
75
  }
66
- await logDomFailure(Runtime, logger, 'assistant-response');
67
- throw error ?? new Error('Failed to capture assistant response');
76
+ await logDomFailure(Runtime, logger, "assistant-response");
77
+ throw error ?? new Error("Failed to capture assistant response");
68
78
  }
69
79
  }
70
80
  else {
@@ -72,14 +82,14 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
72
82
  }
73
83
  }
74
84
  if (!evaluation) {
75
- await logDomFailure(Runtime, logger, 'assistant-response');
76
- throw new Error('Failed to capture assistant response');
85
+ await logDomFailure(Runtime, logger, "assistant-response");
86
+ throw new Error("Failed to capture assistant response");
77
87
  }
78
88
  const parsed = await parseAssistantEvaluationResult(Runtime, evaluation, logger);
79
89
  if (!parsed) {
80
90
  let remainingMs = Math.max(0, timeoutMs - (Date.now() - start));
81
91
  if (remainingMs > 0) {
82
- const recovered = await recoverAssistantResponse(Runtime, remainingMs, logger, minTurnIndex);
92
+ const recovered = await recoverAssistantResponse(Runtime, remainingMs, logger, minTurnIndex, expectedConversationId);
83
93
  if (recovered) {
84
94
  return recovered;
85
95
  }
@@ -89,15 +99,15 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
89
99
  pollerPromise.catch(() => null),
90
100
  delay(remainingMs).then(() => null),
91
101
  ]);
92
- if (polled && polled.kind === 'poll' && polled.value) {
102
+ if (polled && polled.kind === "poll" && polled.value) {
93
103
  return polled.value;
94
104
  }
95
105
  }
96
106
  }
97
- await logDomFailure(Runtime, logger, 'assistant-response');
98
- throw new Error('Unable to capture assistant response');
107
+ await logDomFailure(Runtime, logger, "assistant-response");
108
+ throw new Error("Unable to capture assistant response");
99
109
  }
100
- const refreshed = await refreshAssistantSnapshot(Runtime, parsed, logger, minTurnIndex);
110
+ const refreshed = await refreshAssistantSnapshot(Runtime, parsed, logger, minTurnIndex, expectedConversationId);
101
111
  const candidate = refreshed ?? parsed;
102
112
  // The evaluation path can race ahead of completion. If ChatGPT is still streaming, wait for the watchdog poller.
103
113
  const elapsedMs = Date.now() - start;
@@ -108,8 +118,8 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
108
118
  isCompletionVisible(Runtime),
109
119
  ]);
110
120
  if (stopVisible) {
111
- logger('Assistant still generating; waiting for completion');
112
- const completed = await pollAssistantCompletion(Runtime, remainingMs, minTurnIndex);
121
+ logger("Assistant still generating; waiting for completion");
122
+ const completed = await pollAssistantCompletion(Runtime, remainingMs, minTurnIndex, expectedConversationId);
113
123
  if (completed) {
114
124
  return completed;
115
125
  }
@@ -120,16 +130,16 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
120
130
  }
121
131
  return candidate;
122
132
  }
123
- export async function readAssistantSnapshot(Runtime, minTurnIndex) {
133
+ export async function readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId) {
124
134
  const { result } = await Runtime.evaluate({
125
- expression: buildAssistantSnapshotExpression(minTurnIndex),
135
+ expression: buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId),
126
136
  returnByValue: true,
127
137
  });
128
138
  const value = result?.value;
129
- if (value && typeof value === 'object') {
139
+ if (value && typeof value === "object") {
130
140
  const snapshot = value;
131
- if (typeof minTurnIndex === 'number' && Number.isFinite(minTurnIndex)) {
132
- const turnIndex = typeof snapshot.turnIndex === 'number' ? snapshot.turnIndex : null;
141
+ if (typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex)) {
142
+ const turnIndex = typeof snapshot.turnIndex === "number" ? snapshot.turnIndex : null;
133
143
  if (turnIndex === null) {
134
144
  return snapshot;
135
145
  }
@@ -147,42 +157,45 @@ export async function captureAssistantMarkdown(Runtime, meta, logger) {
147
157
  returnByValue: true,
148
158
  awaitPromise: true,
149
159
  });
150
- if (result?.value?.success && typeof result.value.markdown === 'string') {
160
+ if (result?.value?.success && typeof result.value.markdown === "string") {
151
161
  return result.value.markdown;
152
162
  }
153
163
  const status = result?.value?.status;
154
- if (status && status !== 'missing-button') {
164
+ if (status && status !== "missing-button") {
155
165
  logger(`Copy button fallback status: ${status}`);
156
- await logDomFailure(Runtime, logger, 'copy-markdown');
166
+ await logDomFailure(Runtime, logger, "copy-markdown");
157
167
  }
158
168
  if (!status) {
159
- await logDomFailure(Runtime, logger, 'copy-markdown');
169
+ await logDomFailure(Runtime, logger, "copy-markdown");
160
170
  }
161
171
  return null;
162
172
  }
163
173
  export function buildAssistantExtractorForTest(name) {
164
174
  return buildAssistantExtractor(name);
165
175
  }
176
+ export function buildAssistantSnapshotExpressionForTest(minTurnIndex, expectedConversationId) {
177
+ return buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId);
178
+ }
166
179
  export function buildConversationDebugExpressionForTest() {
167
180
  return buildConversationDebugExpression();
168
181
  }
169
- export function buildMarkdownFallbackExtractorForTest(minTurnLiteral = '0') {
182
+ export function buildMarkdownFallbackExtractorForTest(minTurnLiteral = "0") {
170
183
  return buildMarkdownFallbackExtractor(minTurnLiteral);
171
184
  }
172
185
  export function buildCopyExpressionForTest(meta = {}) {
173
186
  return buildCopyExpression(meta);
174
187
  }
175
- async function recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex) {
188
+ async function recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId) {
176
189
  const recoveryTimeoutMs = Math.max(0, timeoutMs);
177
190
  if (recoveryTimeoutMs === 0) {
178
191
  return null;
179
192
  }
180
193
  const recovered = await waitForCondition(async () => {
181
- const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
194
+ const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId);
182
195
  return normalizeAssistantSnapshot(snapshot);
183
196
  }, recoveryTimeoutMs, 400);
184
197
  if (recovered) {
185
- logger('Recovered assistant response via polling fallback');
198
+ logger("Recovered assistant response via polling fallback");
186
199
  return recovered;
187
200
  }
188
201
  await logConversationSnapshot(Runtime, logger).catch(() => undefined);
@@ -190,24 +203,27 @@ async function recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex
190
203
  }
191
204
  async function parseAssistantEvaluationResult(_Runtime, evaluation, _logger) {
192
205
  const { result } = evaluation;
193
- if (result.type === 'object' && result.value && typeof result.value === 'object' && 'text' in result.value) {
194
- const html = typeof result.value.html === 'string'
206
+ if (result.type === "object" &&
207
+ result.value &&
208
+ typeof result.value === "object" &&
209
+ "text" in result.value) {
210
+ const html = typeof result.value.html === "string"
195
211
  ? (result.value.html ?? undefined)
196
212
  : undefined;
197
- const turnId = typeof result.value.turnId === 'string'
213
+ const turnId = typeof result.value.turnId === "string"
198
214
  ? (result.value.turnId ?? undefined)
199
215
  : undefined;
200
- const messageId = typeof result.value.messageId === 'string'
216
+ const messageId = typeof result.value.messageId === "string"
201
217
  ? (result.value.messageId ?? undefined)
202
218
  : undefined;
203
- const text = cleanAssistantText(String(result.value.text ?? ''));
219
+ const text = cleanAssistantText(String(result.value.text ?? ""));
204
220
  const normalized = text.toLowerCase();
205
221
  if (isAnswerNowPlaceholderText(normalized)) {
206
222
  return null;
207
223
  }
208
224
  return { text, html, meta: { turnId, messageId } };
209
225
  }
210
- const fallbackText = typeof result.value === 'string' ? cleanAssistantText(result.value) : '';
226
+ const fallbackText = typeof result.value === "string" ? cleanAssistantText(result.value) : "";
211
227
  if (!fallbackText) {
212
228
  return null;
213
229
  }
@@ -216,14 +232,14 @@ async function parseAssistantEvaluationResult(_Runtime, evaluation, _logger) {
216
232
  }
217
233
  return { text: fallbackText, html: undefined, meta: {} };
218
234
  }
219
- async function refreshAssistantSnapshot(Runtime, current, logger, minTurnIndex) {
235
+ async function refreshAssistantSnapshot(Runtime, current, logger, minTurnIndex, expectedConversationId) {
220
236
  const deadline = Date.now() + 5_000;
221
237
  let best = null;
222
238
  let stableCycles = 0;
223
239
  const stableTarget = 3;
224
240
  while (Date.now() < deadline) {
225
241
  // Learned: short/fast answers can race; poll a few extra cycles to pick up messageId + full text.
226
- const latestSnapshot = await readAssistantSnapshot(Runtime, minTurnIndex).catch(() => null);
242
+ const latestSnapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId).catch(() => null);
227
243
  const latest = normalizeAssistantSnapshot(latestSnapshot);
228
244
  if (latest) {
229
245
  if (!best ||
@@ -250,13 +266,13 @@ async function refreshAssistantSnapshot(Runtime, current, logger, minTurnIndex)
250
266
  const isLonger = latestLength > currentLength;
251
267
  const hasDifferentText = best.text.trim() !== current.text.trim();
252
268
  if (isLonger || hasBetterId || hasDifferentText) {
253
- logger('Refreshed assistant response via latest snapshot');
269
+ logger("Refreshed assistant response via latest snapshot");
254
270
  return best;
255
271
  }
256
272
  return null;
257
273
  }
258
274
  async function terminateRuntimeExecution(Runtime) {
259
- if (typeof Runtime.terminateExecution !== 'function') {
275
+ if (typeof Runtime.terminateExecution !== "function") {
260
276
  return;
261
277
  }
262
278
  try {
@@ -266,7 +282,7 @@ async function terminateRuntimeExecution(Runtime) {
266
282
  // ignore termination failures
267
283
  }
268
284
  }
269
- async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, abortSignal) {
285
+ async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expectedConversationId, abortSignal) {
270
286
  const watchdogDeadline = Date.now() + timeoutMs;
271
287
  let previousLength = 0;
272
288
  let stableCycles = 0;
@@ -276,7 +292,7 @@ async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, abortSi
276
292
  if (abortSignal?.aborted) {
277
293
  return null;
278
294
  }
279
- const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
295
+ const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId);
280
296
  const normalized = normalizeAssistantSnapshot(snapshot);
281
297
  if (normalized) {
282
298
  const currentLength = normalized.text.length;
@@ -377,7 +393,7 @@ async function isCompletionVisible(Runtime) {
377
393
  }
378
394
  }
379
395
  function normalizeAssistantSnapshot(snapshot) {
380
- const text = snapshot?.text ? cleanAssistantText(snapshot.text) : '';
396
+ const text = snapshot?.text ? cleanAssistantText(snapshot.text) : "";
381
397
  if (!text.trim()) {
382
398
  return null;
383
399
  }
@@ -388,7 +404,7 @@ function normalizeAssistantSnapshot(snapshot) {
388
404
  return null;
389
405
  }
390
406
  // Ignore user echo turns that can show up in project view fallbacks.
391
- if (normalized.startsWith('you said')) {
407
+ if (normalized.startsWith("you said")) {
392
408
  return null;
393
409
  }
394
410
  return {
@@ -408,14 +424,27 @@ async function waitForCondition(getter, timeoutMs, pollIntervalMs = 400) {
408
424
  }
409
425
  return null;
410
426
  }
411
- function buildAssistantSnapshotExpression(minTurnIndex) {
412
- const minTurnLiteral = typeof minTurnIndex === 'number' && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
427
+ function buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId) {
428
+ const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
413
429
  ? Math.floor(minTurnIndex)
414
430
  : -1;
431
+ const expectedConversationLiteral = typeof expectedConversationId === "string" && expectedConversationId.trim().length > 0
432
+ ? JSON.stringify(expectedConversationId.trim())
433
+ : "null";
415
434
  return `(() => {
416
435
  const MIN_TURN_INDEX = ${minTurnLiteral};
436
+ const EXPECTED_CONVERSATION_ID = ${expectedConversationLiteral};
437
+ const currentHref = typeof location === 'object' && location.href ? location.href : '';
438
+ const currentConversationId = currentHref.match(/\\/c\\/([a-zA-Z0-9-]+)/)?.[1] ?? null;
439
+ if (
440
+ EXPECTED_CONVERSATION_ID &&
441
+ currentConversationId &&
442
+ currentConversationId !== EXPECTED_CONVERSATION_ID
443
+ ) {
444
+ return null;
445
+ }
417
446
  // Learned: the default turn DOM misses project view; keep a fallback extractor.
418
- ${buildAssistantExtractor('extractAssistantTurn')}
447
+ ${buildAssistantExtractor("extractAssistantTurn")}
419
448
  const extracted = extractAssistantTurn();
420
449
  const isPlaceholder = (snapshot) => {
421
450
  const normalized = String(snapshot?.text ?? '').toLowerCase().trim();
@@ -429,17 +458,20 @@ function buildAssistantSnapshotExpression(minTurnIndex) {
429
458
  return extracted;
430
459
  }
431
460
  // Fallback for ChatGPT project view: answers can live outside conversation turns.
432
- const fallback = ${buildMarkdownFallbackExtractor('MIN_TURN_INDEX')};
461
+ const fallback = ${buildMarkdownFallbackExtractor("MIN_TURN_INDEX")};
433
462
  return fallback ?? extracted;
434
463
  })()`;
435
464
  }
436
- function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
465
+ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConversationId) {
437
466
  const selectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
438
467
  const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
439
468
  const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
440
- const minTurnLiteral = typeof minTurnIndex === 'number' && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
469
+ const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
441
470
  ? Math.floor(minTurnIndex)
442
471
  : -1;
472
+ const expectedConversationLiteral = typeof expectedConversationId === "string" && expectedConversationId.trim().length > 0
473
+ ? JSON.stringify(expectedConversationId.trim())
474
+ : "null";
443
475
  return `(() => {
444
476
  ${buildClickDispatcher()}
445
477
  const SELECTORS = ${selectorsLiteral};
@@ -447,8 +479,18 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
447
479
  const FINISHED_SELECTOR = '${FINISHED_ACTIONS_SELECTOR}';
448
480
  const CONVERSATION_SELECTOR = ${conversationLiteral};
449
481
  const ASSISTANT_SELECTOR = ${assistantLiteral};
482
+ const EXPECTED_CONVERSATION_ID = ${expectedConversationLiteral};
450
483
  // Learned: settling avoids capturing mid-stream HTML; keep short.
451
484
  const settleDelayMs = 800;
485
+ const currentConversationId = () => {
486
+ const href = typeof location === 'object' && location.href ? location.href : '';
487
+ return href.match(/\\/c\\/([a-zA-Z0-9-]+)/)?.[1] ?? null;
488
+ };
489
+ const matchesExpectedConversation = () => {
490
+ if (!EXPECTED_CONVERSATION_ID) return true;
491
+ const currentId = currentConversationId();
492
+ return !currentId || currentId === EXPECTED_CONVERSATION_ID;
493
+ };
452
494
  const isAnswerNowPlaceholder = (snapshot) => {
453
495
  const normalized = String(snapshot?.text ?? '').toLowerCase().trim();
454
496
  if (normalized === 'chatgpt said:' || normalized === 'chatgpt said') return true;
@@ -471,12 +513,13 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
471
513
  };
472
514
 
473
515
  const MIN_TURN_INDEX = ${minTurnLiteral};
474
- ${buildAssistantExtractor('extractFromTurns')}
516
+ ${buildAssistantExtractor("extractFromTurns")}
475
517
  // Learned: some layouts (project view) render markdown without assistant turn wrappers.
476
- const extractFromMarkdownFallback = ${buildMarkdownFallbackExtractor('MIN_TURN_INDEX')};
518
+ const extractFromMarkdownFallback = ${buildMarkdownFallbackExtractor("MIN_TURN_INDEX")};
477
519
 
478
520
  const acceptSnapshot = (snapshot) => {
479
521
  if (!snapshot) return null;
522
+ if (!matchesExpectedConversation()) return null;
480
523
  const index = typeof snapshot.turnIndex === 'number' ? snapshot.turnIndex : -1;
481
524
  if (MIN_TURN_INDEX >= 0) {
482
525
  if (index < 0 || index < MIN_TURN_INDEX) {
@@ -719,7 +762,9 @@ function buildAssistantExtractor(functionName) {
719
762
  };`;
720
763
  }
721
764
  function buildMarkdownFallbackExtractor(minTurnLiteral) {
722
- const turnIndexValue = minTurnLiteral ? `(${minTurnLiteral} >= 0 ? ${minTurnLiteral} : null)` : 'null';
765
+ const turnIndexValue = minTurnLiteral
766
+ ? `(${minTurnLiteral} >= 0 ? ${minTurnLiteral} : null)`
767
+ : "null";
723
768
  return `(() => {
724
769
  const __minTurn = ${turnIndexValue};
725
770
  const roots = [
@@ -1025,37 +1070,37 @@ function buildCopyExpression(meta) {
1025
1070
  })()`;
1026
1071
  }
1027
1072
  const LANGUAGE_TAGS = new Set([
1028
- 'copy code',
1029
- 'markdown',
1030
- 'bash',
1031
- 'sh',
1032
- 'shell',
1033
- 'javascript',
1034
- 'typescript',
1035
- 'ts',
1036
- 'js',
1037
- 'yaml',
1038
- 'json',
1039
- 'python',
1040
- 'py',
1041
- 'go',
1042
- 'java',
1043
- 'c',
1044
- 'c++',
1045
- 'cpp',
1046
- 'c#',
1047
- 'php',
1048
- 'ruby',
1049
- 'rust',
1050
- 'swift',
1051
- 'kotlin',
1052
- 'html',
1053
- 'css',
1054
- 'sql',
1055
- 'text',
1073
+ "copy code",
1074
+ "markdown",
1075
+ "bash",
1076
+ "sh",
1077
+ "shell",
1078
+ "javascript",
1079
+ "typescript",
1080
+ "ts",
1081
+ "js",
1082
+ "yaml",
1083
+ "json",
1084
+ "python",
1085
+ "py",
1086
+ "go",
1087
+ "java",
1088
+ "c",
1089
+ "c++",
1090
+ "cpp",
1091
+ "c#",
1092
+ "php",
1093
+ "ruby",
1094
+ "rust",
1095
+ "swift",
1096
+ "kotlin",
1097
+ "html",
1098
+ "css",
1099
+ "sql",
1100
+ "text",
1056
1101
  ].map((token) => token.toLowerCase()));
1057
1102
  function cleanAssistantText(text) {
1058
- const normalized = text.replace(/\u00a0/g, ' ');
1103
+ const normalized = text.replace(/\u00a0/g, " ");
1059
1104
  const lines = normalized.split(/\r?\n/);
1060
1105
  const filtered = lines.filter((line) => {
1061
1106
  const trimmed = line.trim().toLowerCase();
@@ -1063,5 +1108,8 @@ function cleanAssistantText(text) {
1063
1108
  return false;
1064
1109
  return true;
1065
1110
  });
1066
- return filtered.join('\n').replace(/\n{3,}/g, '\n\n').trim();
1111
+ return filtered
1112
+ .join("\n")
1113
+ .replace(/\n{3,}/g, "\n\n")
1114
+ .trim();
1067
1115
  }
@@ -1,12 +1,12 @@
1
- import { readFile } from 'node:fs/promises';
2
- import path from 'node:path';
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
3
  const MAX_DATA_TRANSFER_BYTES = 20 * 1024 * 1024;
4
4
  export async function transferAttachmentViaDataTransfer(runtime, attachment, selector) {
5
5
  const fileContent = await readFile(attachment.path);
6
6
  if (fileContent.length > MAX_DATA_TRANSFER_BYTES) {
7
7
  throw new Error(`Attachment ${path.basename(attachment.path)} is too large for data transfer (${fileContent.length} bytes). Maximum size is ${MAX_DATA_TRANSFER_BYTES} bytes.`);
8
8
  }
9
- const base64Content = fileContent.toString('base64');
9
+ const base64Content = fileContent.toString("base64");
10
10
  const fileName = path.basename(attachment.path);
11
11
  const mimeType = guessMimeType(fileName);
12
12
  const expression = `(() => {
@@ -77,62 +77,64 @@ export async function transferAttachmentViaDataTransfer(runtime, attachment, sel
77
77
  })()`;
78
78
  const evalResult = await runtime.evaluate({ expression, returnByValue: true });
79
79
  if (evalResult.exceptionDetails) {
80
- const description = evalResult.exceptionDetails.text ?? 'JS evaluation failed';
80
+ const description = evalResult.exceptionDetails.text ?? "JS evaluation failed";
81
81
  throw new Error(`Failed to transfer file to browser: ${description}`);
82
82
  }
83
- if (!evalResult.result || typeof evalResult.result.value !== 'object' || evalResult.result.value == null) {
84
- throw new Error('Failed to transfer file to browser: unexpected evaluation result');
83
+ if (!evalResult.result ||
84
+ typeof evalResult.result.value !== "object" ||
85
+ evalResult.result.value == null) {
86
+ throw new Error("Failed to transfer file to browser: unexpected evaluation result");
85
87
  }
86
88
  const uploadResult = evalResult.result.value;
87
89
  if (!uploadResult.success) {
88
- throw new Error(`Failed to transfer file to browser: ${uploadResult.error || 'Unknown error'}`);
90
+ throw new Error(`Failed to transfer file to browser: ${uploadResult.error || "Unknown error"}`);
89
91
  }
90
92
  return {
91
93
  fileName: uploadResult.fileName ?? fileName,
92
- size: typeof uploadResult.size === 'number' ? uploadResult.size : fileContent.length,
94
+ size: typeof uploadResult.size === "number" ? uploadResult.size : fileContent.length,
93
95
  };
94
96
  }
95
97
  export function guessMimeType(fileName) {
96
98
  const ext = path.extname(fileName).toLowerCase();
97
99
  const mimeTypes = {
98
- '.txt': 'text/plain',
99
- '.md': 'text/markdown',
100
- '.csv': 'text/csv',
101
- '.json': 'application/json',
102
- '.js': 'text/javascript',
103
- '.ts': 'text/typescript',
104
- '.jsx': 'text/javascript',
105
- '.tsx': 'text/typescript',
106
- '.py': 'text/x-python',
107
- '.java': 'text/x-java',
108
- '.c': 'text/x-c',
109
- '.cpp': 'text/x-c++',
110
- '.h': 'text/x-c',
111
- '.hpp': 'text/x-c++',
112
- '.sh': 'text/x-sh',
113
- '.bash': 'text/x-sh',
114
- '.html': 'text/html',
115
- '.css': 'text/css',
116
- '.xml': 'text/xml',
117
- '.yaml': 'text/yaml',
118
- '.yml': 'text/yaml',
119
- '.pdf': 'application/pdf',
120
- '.doc': 'application/msword',
121
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
122
- '.xls': 'application/vnd.ms-excel',
123
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
124
- '.ppt': 'application/vnd.ms-powerpoint',
125
- '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
126
- '.png': 'image/png',
127
- '.jpg': 'image/jpeg',
128
- '.jpeg': 'image/jpeg',
129
- '.gif': 'image/gif',
130
- '.svg': 'image/svg+xml',
131
- '.webp': 'image/webp',
132
- '.zip': 'application/zip',
133
- '.tar': 'application/x-tar',
134
- '.gz': 'application/gzip',
135
- '.7z': 'application/x-7z-compressed',
100
+ ".txt": "text/plain",
101
+ ".md": "text/markdown",
102
+ ".csv": "text/csv",
103
+ ".json": "application/json",
104
+ ".js": "text/javascript",
105
+ ".ts": "text/typescript",
106
+ ".jsx": "text/javascript",
107
+ ".tsx": "text/typescript",
108
+ ".py": "text/x-python",
109
+ ".java": "text/x-java",
110
+ ".c": "text/x-c",
111
+ ".cpp": "text/x-c++",
112
+ ".h": "text/x-c",
113
+ ".hpp": "text/x-c++",
114
+ ".sh": "text/x-sh",
115
+ ".bash": "text/x-sh",
116
+ ".html": "text/html",
117
+ ".css": "text/css",
118
+ ".xml": "text/xml",
119
+ ".yaml": "text/yaml",
120
+ ".yml": "text/yaml",
121
+ ".pdf": "application/pdf",
122
+ ".doc": "application/msword",
123
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
124
+ ".xls": "application/vnd.ms-excel",
125
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
126
+ ".ppt": "application/vnd.ms-powerpoint",
127
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
128
+ ".png": "image/png",
129
+ ".jpg": "image/jpeg",
130
+ ".jpeg": "image/jpeg",
131
+ ".gif": "image/gif",
132
+ ".svg": "image/svg+xml",
133
+ ".webp": "image/webp",
134
+ ".zip": "application/zip",
135
+ ".tar": "application/x-tar",
136
+ ".gz": "application/gzip",
137
+ ".7z": "application/x-7z-compressed",
136
138
  };
137
- return mimeTypes[ext] || 'application/octet-stream';
139
+ return mimeTypes[ext] || "application/octet-stream";
138
140
  }