wtagent 0.3.0 → 1.0.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/README.md +163 -9
- package/package.json +9 -8
- package/src/artifacts/artifact-store.js +154 -0
- package/src/audio/music-artifact.js +66 -0
- package/src/audio/native-music-receiver.js +91 -0
- package/src/browser/base-web-adapter.js +2418 -201
- package/src/browser/cdp-browser.js +223 -25
- package/src/browser/cdp-state.js +8 -5
- package/src/browser/chatgpt-dom.js +179 -0
- package/src/browser/chatgpt-web-adapter.js +831 -69
- package/src/browser/claude-web-adapter.js +13 -0
- package/src/browser/fake-web-model-adapter.js +152 -3
- package/src/browser/gemini-web-adapter.js +71 -20
- package/src/browser/glm-web-adapter.js +2 -2
- package/src/browser/grok-web-adapter.js +35 -3
- package/src/browser/rendered-text.js +11 -0
- package/src/cli/i18n.js +50 -20
- package/src/cli/main.js +124 -40
- package/src/cli/prompt-input.js +68 -0
- package/src/image/browser-image-driver.js +238 -0
- package/src/image/browser-image-session-pool.js +96 -0
- package/src/image/image-generation-service.js +101 -0
- package/src/image/image-provider-driver.js +22 -0
- package/src/image/native-image-receiver.js +66 -0
- package/src/image/provider-registry.js +23 -0
- package/src/image/providers/chatgpt-image-driver.js +211 -0
- package/src/image/providers/gemini-image-driver.js +100 -0
- package/src/image/providers/grok-image-driver.js +193 -0
- package/src/platform/command-launcher.js +1 -1
- package/src/platform/paths.js +19 -0
- package/src/platform/windows-diagnostics.js +9 -16
- package/src/policy/path-guard.js +5 -1
- package/src/policy/policy-engine.js +10 -5
- package/src/protocol/envelope.js +74 -0
- package/src/protocol/markers.js +16 -0
- package/src/protocol/prompt-builder.js +12 -9
- package/src/protocol/xml-protocol.js +36 -18
- package/src/runtime/agent-runtime.js +1383 -238
- package/src/session/agent-session.js +1047 -121
- package/src/tools/default-tools.js +1 -1
- package/src/tools/image-tools.js +92 -0
- package/src/tools/registry.js +13 -1
- package/docs/technical-design.md +0 -866
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
BaseWebAdapter,
|
|
4
|
+
firstVisible,
|
|
5
|
+
hasCompleteAgentEnvelope,
|
|
6
|
+
isValidOutboundCorrelationId,
|
|
7
|
+
renderedMessageContainsOutboundMarker,
|
|
8
|
+
} from "./base-web-adapter.js";
|
|
2
9
|
import { BrowserAdapterError } from "../shared/errors.js";
|
|
3
10
|
import { isUsageLimitNotice } from "../shared/usage-limit.js";
|
|
4
11
|
|
|
@@ -7,11 +14,68 @@ import { isUsageLimitNotice } from "../shared/usage-limit.js";
|
|
|
7
14
|
// base module.
|
|
8
15
|
export { isConnectionLostError } from "./base-web-adapter.js";
|
|
9
16
|
|
|
17
|
+
import {
|
|
18
|
+
CHATGPT_DOM,
|
|
19
|
+
readChatGPTDom,
|
|
20
|
+
summarizeChatGPTDom,
|
|
21
|
+
readChatGPTMessageIdentity,
|
|
22
|
+
readChatGPTAssistantText,
|
|
23
|
+
} from "./chatgpt-dom.js";
|
|
24
|
+
|
|
10
25
|
const CHATGPT_URL = "https://chatgpt.com/";
|
|
11
26
|
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
27
|
+
function normalizedConversationUrl(value) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = value instanceof URL ? value : new URL(value);
|
|
30
|
+
const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
|
|
31
|
+
return `${parsed.origin}${pathname}`;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validatedOrderedSnapshot(snapshot) {
|
|
38
|
+
const url = normalizedConversationUrl(snapshot?.url);
|
|
39
|
+
if (!url || !Array.isArray(snapshot?.entries)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const ids = new Set();
|
|
43
|
+
const turns = new Set();
|
|
44
|
+
const entries = [];
|
|
45
|
+
let previousDomIndex = -1;
|
|
46
|
+
let previousTurn = -1;
|
|
47
|
+
for (const entry of snapshot.entries) {
|
|
48
|
+
const id = typeof entry?.id === "string" ? entry.id.trim() : "";
|
|
49
|
+
if (
|
|
50
|
+
!["user", "assistant"].includes(entry?.role)
|
|
51
|
+
|| !id
|
|
52
|
+
|| !Number.isSafeInteger(entry.turn)
|
|
53
|
+
|| entry.turn < 0
|
|
54
|
+
|| !Number.isSafeInteger(entry.domIndex)
|
|
55
|
+
|| entry.domIndex <= previousDomIndex
|
|
56
|
+
|| entry.turn <= previousTurn
|
|
57
|
+
|| ids.has(id)
|
|
58
|
+
|| turns.has(entry.turn)
|
|
59
|
+
) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
ids.add(id);
|
|
63
|
+
turns.add(entry.turn);
|
|
64
|
+
previousDomIndex = entry.domIndex;
|
|
65
|
+
previousTurn = entry.turn;
|
|
66
|
+
entries.push({
|
|
67
|
+
domIndex: entry.domIndex,
|
|
68
|
+
role: entry.role,
|
|
69
|
+
id,
|
|
70
|
+
turn: entry.turn,
|
|
71
|
+
renderedText: String(entry.renderedText ?? ""),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return { url, entries };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sameIdentity(entry, { id, turn }) {
|
|
78
|
+
return entry?.id === id && entry?.turn === turn;
|
|
15
79
|
}
|
|
16
80
|
|
|
17
81
|
// ChatGPT-specific implementation of the WebModelAdapter contract. Only the
|
|
@@ -34,26 +98,77 @@ export class ChatGPTWebAdapter extends BaseWebAdapter {
|
|
|
34
98
|
return /^\/c\//;
|
|
35
99
|
}
|
|
36
100
|
|
|
37
|
-
|
|
101
|
+
classifyConversationUrl(value) {
|
|
102
|
+
const kind = super.classifyConversationUrl(value);
|
|
103
|
+
if (kind !== "restorable") {
|
|
104
|
+
return kind;
|
|
105
|
+
}
|
|
106
|
+
const parsed = value instanceof URL ? value : new URL(value);
|
|
107
|
+
return /^\/c\/(?:WEB:|local-chatgpt(?::|%3[aA]))/.test(parsed.pathname)
|
|
108
|
+
? "provisional"
|
|
109
|
+
: "restorable";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async startConversation(...args) {
|
|
113
|
+
try {
|
|
114
|
+
const result = await super.startConversation(...args);
|
|
115
|
+
await this.collectStructuralDiagnostics();
|
|
116
|
+
return result;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const diagnostic = await this.writeDiagnostics("conversation-start-failed", { stage: "conversation-start" }).catch(() => null);
|
|
119
|
+
if (diagnostic?.filename) {
|
|
120
|
+
error.details = { ...error.details, diagnosticFile: diagnostic.filename, dom: diagnostic.report.dom };
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
hasStructuralDiagnostics() {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
recordConversationNavigation(url) {
|
|
131
|
+
if (url === this.lastDiagnosticUrl) return;
|
|
132
|
+
this.lastDiagnosticUrl = url;
|
|
133
|
+
this.diagnosticRoutes ??= [];
|
|
134
|
+
this.diagnosticRoutes.push({ kind: this.classifyConversationUrl(url), at: Date.now() });
|
|
135
|
+
this.diagnosticRoutes = this.diagnosticRoutes.slice(-12);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async structuralDiagnostics() {
|
|
139
|
+
const snapshot = await this.page.evaluate(readChatGPTDom, {
|
|
140
|
+
selector: CHATGPT_DOM.messages, registry: CHATGPT_DOM,
|
|
141
|
+
});
|
|
142
|
+
const report = summarizeChatGPTDom(snapshot);
|
|
143
|
+
this.lastDomReport = report;
|
|
144
|
+
return report;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
pendingAttachmentLocators() {
|
|
38
148
|
return [
|
|
39
|
-
this.page.locator(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
149
|
+
this.page.locator(
|
|
150
|
+
'form [data-testid$="-file-thumbnail"], '
|
|
151
|
+
+ 'form [data-testid^="file-thumbnail"], '
|
|
152
|
+
+ 'form button[aria-label*="remove file" i], '
|
|
153
|
+
+ 'form button[aria-label*="删除文件"]',
|
|
154
|
+
),
|
|
44
155
|
];
|
|
45
156
|
}
|
|
46
157
|
|
|
158
|
+
composerLocators() {
|
|
159
|
+
return CHATGPT_DOM.composers.map(({ selector }) => this.page.locator(selector));
|
|
160
|
+
}
|
|
161
|
+
|
|
47
162
|
sendButtonLocators() {
|
|
48
163
|
return [
|
|
49
|
-
this.page.locator(
|
|
164
|
+
this.page.locator(CHATGPT_DOM.send),
|
|
50
165
|
this.page.getByRole("button", { name: /send|发送/i }),
|
|
51
166
|
];
|
|
52
167
|
}
|
|
53
168
|
|
|
54
169
|
stopButtonLocators() {
|
|
55
170
|
return [
|
|
56
|
-
this.page.locator(
|
|
171
|
+
this.page.locator(CHATGPT_DOM.stop),
|
|
57
172
|
this.page.getByRole("button", {
|
|
58
173
|
name: /stop generating|stop|停止生成|停止/i,
|
|
59
174
|
}),
|
|
@@ -99,38 +214,709 @@ export class ChatGPTWebAdapter extends BaseWebAdapter {
|
|
|
99
214
|
// keeps a failed request from being read as an envelope-less reply and
|
|
100
215
|
// burned through the protocol-error retry loop; the turn instead sees "no
|
|
101
216
|
// reply" and recovers with the continuation nudge.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
217
|
+
const legacy = this.page.locator(CHATGPT_DOM.assistant.legacy);
|
|
218
|
+
return legacy.or ? legacy.or(this.page.locator(CHATGPT_DOM.assistant.modern)) : legacy;
|
|
105
219
|
}
|
|
106
220
|
|
|
107
221
|
userMessages() {
|
|
108
|
-
|
|
222
|
+
const legacy = this.page.locator(CHATGPT_DOM.user.legacy);
|
|
223
|
+
return legacy.or ? legacy.or(this.page.locator(CHATGPT_DOM.user.modern)) : legacy;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async userMessageSnapshot() {
|
|
227
|
+
const messages = this.userMessages();
|
|
228
|
+
if (typeof messages.evaluateAll !== "function") {
|
|
229
|
+
return super.userMessageSnapshot();
|
|
230
|
+
}
|
|
231
|
+
// ChatGPT unmounts older turns as a long thread advances. count() followed
|
|
232
|
+
// by nth() can then wait for a vanished index until the send deadline, or
|
|
233
|
+
// combine one bubble's identity with another bubble's text.
|
|
234
|
+
return await messages.evaluateAll(readChatGPTDom).then((snapshot) => (
|
|
235
|
+
snapshot.entries.every((entry) => entry.role === "user" && entry.id && Number.isSafeInteger(entry.turn))
|
|
236
|
+
? snapshot.entries : null
|
|
237
|
+
)).catch(() => null);
|
|
109
238
|
}
|
|
110
239
|
|
|
111
240
|
conversationMessages() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
241
|
+
const legacy = this.page.locator(CHATGPT_DOM.conversationLegacy);
|
|
242
|
+
return legacy.or ? legacy.or(this.page.locator(`${CHATGPT_DOM.user.modern}, ${CHATGPT_DOM.assistant.modern}`)) : legacy;
|
|
115
243
|
}
|
|
116
244
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
message.getAttribute("data-message-id").catch(() => null),
|
|
120
|
-
this.#messageTurn(message),
|
|
121
|
-
]);
|
|
122
|
-
return { id, turn };
|
|
245
|
+
requiresOrderedConversationSnapshotForFreshSend() {
|
|
246
|
+
return true;
|
|
123
247
|
}
|
|
124
248
|
|
|
125
|
-
async
|
|
126
|
-
if (typeof
|
|
249
|
+
async orderedConversationSnapshot() {
|
|
250
|
+
if (typeof this.page?.evaluate !== "function") {
|
|
127
251
|
return null;
|
|
128
252
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
253
|
+
return await this.page.evaluate(readChatGPTDom,
|
|
254
|
+
CHATGPT_DOM.messages,
|
|
255
|
+
).catch(() => null);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async assertRecoveredTurnTopology({
|
|
259
|
+
assistantPresent = false,
|
|
260
|
+
assistantMessageId = null,
|
|
261
|
+
assistantTurn = null,
|
|
262
|
+
} = {}) {
|
|
263
|
+
if (!this.recoveredTurnTopologyRequired) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const snapshot = validatedOrderedSnapshot(
|
|
267
|
+
await this.orderedConversationSnapshot(),
|
|
268
|
+
);
|
|
269
|
+
const currentUrl = normalizedConversationUrl(this.page?.url?.());
|
|
270
|
+
if (!snapshot || !currentUrl || snapshot.url !== currentUrl) {
|
|
271
|
+
throw this.#pendingOutboundUncertain(
|
|
272
|
+
"ChatGPT could not atomically revalidate the recovered conversation boundary.",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
typeof this.lastUserMessageId !== "string"
|
|
277
|
+
|| !this.lastUserMessageId
|
|
278
|
+
|| !Number.isSafeInteger(this.sentUserTurn)
|
|
279
|
+
) {
|
|
280
|
+
throw this.#pendingOutboundUncertain(
|
|
281
|
+
"The recovered ChatGPT user-turn identity is incomplete.",
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const matchingUsers = snapshot.entries.filter((entry) => (
|
|
286
|
+
entry.role === "user"
|
|
287
|
+
&& sameIdentity(entry, {
|
|
288
|
+
id: this.lastUserMessageId,
|
|
289
|
+
turn: this.sentUserTurn,
|
|
290
|
+
})
|
|
291
|
+
));
|
|
292
|
+
if (matchingUsers.length !== 1) {
|
|
293
|
+
throw this.#pendingOutboundUncertain(
|
|
294
|
+
"The correlated ChatGPT user turn disappeared or changed after recovery.",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const user = matchingUsers[0];
|
|
298
|
+
const suffix = snapshot.entries.slice(snapshot.entries.indexOf(user) + 1);
|
|
299
|
+
if (suffix.some((entry) => entry.role === "user")) {
|
|
300
|
+
throw this.#pendingOutboundUncertain(
|
|
301
|
+
"A later ChatGPT user turn invalidated the recovered assistant boundary.",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (suffix.length > 1 || suffix.some((entry) => entry.role !== "assistant")) {
|
|
305
|
+
throw this.#pendingOutboundUncertain(
|
|
306
|
+
"ChatGPT assistant chronology changed after recovery.",
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const assistant = suffix[0] ?? null;
|
|
311
|
+
if (assistant && assistant.turn !== user.turn + 1) {
|
|
312
|
+
throw this.#pendingOutboundUncertain(
|
|
313
|
+
"The recovered ChatGPT assistant successor is no longer adjacent.",
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
if (
|
|
317
|
+
(this.expectedAssistantCandidateId != null
|
|
318
|
+
|| this.expectedAssistantCandidateTurn != null)
|
|
319
|
+
&& (
|
|
320
|
+
!assistant
|
|
321
|
+
|| (
|
|
322
|
+
this.expectedAssistantCandidateId != null
|
|
323
|
+
&& assistant.id !== this.expectedAssistantCandidateId
|
|
324
|
+
)
|
|
325
|
+
|| (
|
|
326
|
+
this.expectedAssistantCandidateTurn != null
|
|
327
|
+
&& assistant.turn !== this.expectedAssistantCandidateTurn
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
) {
|
|
331
|
+
throw this.#pendingOutboundUncertain(
|
|
332
|
+
"The persisted ChatGPT assistant successor changed after recovery.",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (
|
|
336
|
+
assistantPresent
|
|
337
|
+
&& (
|
|
338
|
+
!assistant
|
|
339
|
+
|| assistant.id !== assistantMessageId
|
|
340
|
+
|| assistant.turn !== assistantTurn
|
|
341
|
+
)
|
|
342
|
+
) {
|
|
343
|
+
throw this.#pendingOutboundUncertain(
|
|
344
|
+
"The ChatGPT assistant node being processed is outside the recovered boundary.",
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
supportsPendingOutboundRecovery() {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#pendingOutboundUncertain(message, details = {}) {
|
|
354
|
+
return new BrowserAdapterError(message, {
|
|
355
|
+
code: "OUTBOUND_COMMIT_UNCERTAIN",
|
|
356
|
+
recoverable: false,
|
|
357
|
+
details,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#assertRecoveryTarget({ page, mainFrame, targetId, initialUrl, navigated }) {
|
|
362
|
+
if (
|
|
363
|
+
navigated
|
|
364
|
+
|| this.page !== page
|
|
365
|
+
|| (typeof page.mainFrame === "function" && page.mainFrame() !== mainFrame)
|
|
366
|
+
|| !this.hasExactRecoveryTarget(targetId)
|
|
367
|
+
|| normalizedConversationUrl(page.url()) !== initialUrl
|
|
368
|
+
) {
|
|
369
|
+
throw this.#pendingOutboundUncertain(
|
|
370
|
+
"The exact ChatGPT target changed while reconciling the pending outbound.",
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#recoveryDeadlineError() {
|
|
376
|
+
return this.#pendingOutboundUncertain(
|
|
377
|
+
"ChatGPT could not reconcile the pending outbound before the recovery deadline.",
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async #awaitWithinRecoveryDeadline(operation, deadline) {
|
|
382
|
+
const remaining = deadline - this.monotonicNow();
|
|
383
|
+
if (remaining <= 0) {
|
|
384
|
+
throw this.#recoveryDeadlineError();
|
|
385
|
+
}
|
|
386
|
+
let timer;
|
|
387
|
+
try {
|
|
388
|
+
const result = await Promise.race([
|
|
389
|
+
Promise.resolve().then(operation),
|
|
390
|
+
new Promise((_, reject) => {
|
|
391
|
+
timer = setTimeout(
|
|
392
|
+
() => reject(this.#recoveryDeadlineError()),
|
|
393
|
+
remaining,
|
|
394
|
+
);
|
|
395
|
+
}),
|
|
396
|
+
]);
|
|
397
|
+
if (this.monotonicNow() >= deadline) {
|
|
398
|
+
throw this.#recoveryDeadlineError();
|
|
399
|
+
}
|
|
400
|
+
return result;
|
|
401
|
+
} finally {
|
|
402
|
+
clearTimeout(timer);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async #waitRecoveryPoll(deadline) {
|
|
407
|
+
const remaining = deadline - this.monotonicNow();
|
|
408
|
+
await this.#awaitWithinRecoveryDeadline(
|
|
409
|
+
() => this.waitForPoll(Math.min(100, Math.max(1, remaining))),
|
|
410
|
+
deadline,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#recoveryBoundary(snapshot, {
|
|
415
|
+
initialUrl,
|
|
416
|
+
outboundId,
|
|
417
|
+
observedUser,
|
|
418
|
+
expectedUser,
|
|
419
|
+
requiresEmptyPrefix,
|
|
420
|
+
expectedPreOutboundIds,
|
|
421
|
+
expectedWaitingAssistant,
|
|
422
|
+
}) {
|
|
423
|
+
if (snapshot.url !== initialUrl) {
|
|
424
|
+
throw this.#pendingOutboundUncertain(
|
|
425
|
+
"ChatGPT returned a mixed-URL conversation snapshot during recovery.",
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const matchingEntries = snapshot.entries.filter((entry) => (
|
|
429
|
+
renderedMessageContainsOutboundMarker(entry.renderedText, outboundId)
|
|
430
|
+
));
|
|
431
|
+
if (
|
|
432
|
+
matchingEntries.length !== 1
|
|
433
|
+
|| matchingEntries[0].role !== "user"
|
|
434
|
+
) {
|
|
435
|
+
throw this.#pendingOutboundUncertain(
|
|
436
|
+
"The pending outbound marker is not unique to one ChatGPT user turn.",
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const user = matchingEntries[0];
|
|
441
|
+
if (
|
|
442
|
+
(observedUser && !sameIdentity(user, observedUser))
|
|
443
|
+
|| (expectedUser && !sameIdentity(user, expectedUser))
|
|
444
|
+
) {
|
|
445
|
+
throw this.#pendingOutboundUncertain(
|
|
446
|
+
"The correlated ChatGPT user-turn identity changed during recovery.",
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
const userIndex = snapshot.entries.indexOf(user);
|
|
450
|
+
const prefix = snapshot.entries.slice(0, userIndex);
|
|
451
|
+
const suffix = snapshot.entries.slice(userIndex + 1);
|
|
452
|
+
if (suffix.some((entry) => entry.role === "user")) {
|
|
453
|
+
throw this.#pendingOutboundUncertain(
|
|
454
|
+
"A later ChatGPT user turn exists after the pending outbound.",
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (requiresEmptyPrefix) {
|
|
458
|
+
if (prefix.length !== 0 || user.turn !== 1) {
|
|
459
|
+
throw this.#pendingOutboundUncertain(
|
|
460
|
+
"Fresh pending outbound recovery found unexpected conversation history.",
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
} else if (
|
|
464
|
+
expectedPreOutboundIds.size === 0
|
|
465
|
+
|| !prefix.some((entry) => expectedPreOutboundIds.has(entry.id))
|
|
466
|
+
) {
|
|
467
|
+
throw this.#pendingOutboundUncertain(
|
|
468
|
+
"Existing-conversation recovery could not verify a pre-outbound marker.",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const successors = suffix.filter((entry) => entry.role === "assistant");
|
|
473
|
+
if (successors.length > 1 || successors.length !== suffix.length) {
|
|
474
|
+
throw this.#pendingOutboundUncertain(
|
|
475
|
+
"ChatGPT assistant chronology is ambiguous after the pending outbound.",
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
const assistant = successors[0] ?? null;
|
|
479
|
+
if (assistant && assistant.turn !== user.turn + 1) {
|
|
480
|
+
throw this.#pendingOutboundUncertain(
|
|
481
|
+
"Pending outbound recovery found a discontinuous assistant turn.",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (
|
|
485
|
+
expectedWaitingAssistant
|
|
486
|
+
&& assistant
|
|
487
|
+
&& !sameIdentity(assistant, expectedWaitingAssistant)
|
|
488
|
+
) {
|
|
489
|
+
throw this.#pendingOutboundUncertain(
|
|
490
|
+
"The assistant successor changed after the recovery handoff.",
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const assistantBaselineEntries = prefix.filter(
|
|
495
|
+
(entry) => entry.role === "assistant",
|
|
496
|
+
);
|
|
497
|
+
const preOutboundMarkerIds = prefix
|
|
498
|
+
.filter((entry) => expectedPreOutboundIds.has(entry.id))
|
|
499
|
+
.map((entry) => entry.id);
|
|
500
|
+
return {
|
|
501
|
+
user,
|
|
502
|
+
assistant,
|
|
503
|
+
assistantBaselineEntries,
|
|
504
|
+
preOutboundMarkerIds,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async #assistantLocatorFor(entry) {
|
|
509
|
+
const messages = this.assistantMessages();
|
|
510
|
+
const count = await messages.count().catch(() => 0);
|
|
511
|
+
let found = null;
|
|
512
|
+
for (let index = 0; index < count; index += 1) {
|
|
513
|
+
const message = messages.nth(index);
|
|
514
|
+
const identity = await this.messageIdentity(message);
|
|
515
|
+
if (!sameIdentity(entry, identity)) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (found) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
found = message;
|
|
522
|
+
}
|
|
523
|
+
return found;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async reconcilePendingOutbound(options = {}) {
|
|
527
|
+
try {
|
|
528
|
+
return await this.#reconcilePendingOutbound(options);
|
|
529
|
+
} catch (error) {
|
|
530
|
+
const diagnostic = await this.writeDiagnostics("pending-outbound-recovery-failed", { stage: "recovery" }).catch(() => null);
|
|
531
|
+
if (diagnostic?.filename) {
|
|
532
|
+
error.details = { ...error.details, diagnosticFile: diagnostic.filename, dom: diagnostic.report.dom };
|
|
533
|
+
}
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async #reconcilePendingOutbound({
|
|
539
|
+
pendingOutbound = null,
|
|
540
|
+
conversationTargetId = null,
|
|
541
|
+
lastUserMessageId = null,
|
|
542
|
+
lastAssistantMessageId = null,
|
|
543
|
+
priorHandoff = null,
|
|
544
|
+
timeoutMs = this.sendConfirmationTimeoutMs(),
|
|
545
|
+
stableWindowMs = this.restorationCorrelationWindowMs(),
|
|
546
|
+
} = {}) {
|
|
547
|
+
this.requirePage();
|
|
548
|
+
const outboundId = pendingOutbound?.outboundId
|
|
549
|
+
?? priorHandoff?.sourceOutboundId
|
|
550
|
+
?? null;
|
|
551
|
+
if (!isValidOutboundCorrelationId(outboundId)) {
|
|
552
|
+
throw this.#pendingOutboundUncertain(
|
|
553
|
+
"The pending outbound has no valid transport correlation ID.",
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
if (
|
|
557
|
+
typeof conversationTargetId !== "string"
|
|
558
|
+
|| conversationTargetId.length === 0
|
|
559
|
+
|| !this.hasExactRecoveryTarget(conversationTargetId)
|
|
560
|
+
) {
|
|
561
|
+
throw new BrowserAdapterError(
|
|
562
|
+
"The exact saved ChatGPT target is unavailable for pending outbound recovery.",
|
|
563
|
+
{ code: "RECOVERY_TARGET_UNAVAILABLE", recoverable: false },
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const page = this.page;
|
|
568
|
+
const mainFrame = typeof page.mainFrame === "function"
|
|
569
|
+
? page.mainFrame()
|
|
570
|
+
: null;
|
|
571
|
+
const initialUrl = normalizedConversationUrl(page.url());
|
|
572
|
+
const initialKind = initialUrl
|
|
573
|
+
? this.classifyConversationUrl(initialUrl)
|
|
574
|
+
: "invalid";
|
|
575
|
+
if (!["restorable", "provisional"].includes(initialKind)) {
|
|
576
|
+
throw this.#pendingOutboundUncertain(
|
|
577
|
+
"The exact ChatGPT target is not on a recognized conversation route.",
|
|
578
|
+
{ currentUrl: initialUrl },
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
let navigated = false;
|
|
583
|
+
const navigationListener = (frame) => {
|
|
584
|
+
if (
|
|
585
|
+
!mainFrame
|
|
586
|
+
|| frame === mainFrame
|
|
587
|
+
|| (typeof page.mainFrame === "function" && frame === page.mainFrame())
|
|
588
|
+
) {
|
|
589
|
+
navigated = true;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
page.on?.("framenavigated", navigationListener);
|
|
593
|
+
|
|
594
|
+
const outboundKind = pendingOutbound?.kind
|
|
595
|
+
?? priorHandoff?.outboundKind
|
|
596
|
+
?? "runtime_message";
|
|
597
|
+
const requiresEmptyPrefix = ["bootstrap", "fresh_rebuild"]
|
|
598
|
+
.includes(outboundKind);
|
|
599
|
+
const expectedPreOutboundIds = new Set([
|
|
600
|
+
lastUserMessageId,
|
|
601
|
+
lastAssistantMessageId,
|
|
602
|
+
...(priorHandoff?.preOutboundMarkerIds ?? []),
|
|
603
|
+
...(priorHandoff?.assistantBaseline?.ids ?? []),
|
|
604
|
+
].filter(Boolean));
|
|
605
|
+
const expectedUser = priorHandoff
|
|
606
|
+
? {
|
|
607
|
+
id: priorHandoff.userMessageId ?? null,
|
|
608
|
+
turn: priorHandoff.userTurn ?? null,
|
|
609
|
+
}
|
|
610
|
+
: null;
|
|
611
|
+
const expectedAssistant = priorHandoff?.status === "complete"
|
|
612
|
+
? {
|
|
613
|
+
id: priorHandoff.assistantMessageId ?? null,
|
|
614
|
+
turn: priorHandoff.assistantTurn ?? null,
|
|
615
|
+
responseHash: priorHandoff.responseHash ?? null,
|
|
616
|
+
}
|
|
617
|
+
: null;
|
|
618
|
+
const expectedWaitingAssistant = priorHandoff?.status === "waiting"
|
|
619
|
+
&& priorHandoff.assistantCandidateMessageId
|
|
620
|
+
? {
|
|
621
|
+
id: priorHandoff.assistantCandidateMessageId,
|
|
622
|
+
turn: priorHandoff.assistantCandidateTurn ?? null,
|
|
623
|
+
}
|
|
624
|
+
: null;
|
|
625
|
+
const deadline = this.monotonicNow() + timeoutMs;
|
|
626
|
+
let stableSignature = null;
|
|
627
|
+
let stableSince = 0;
|
|
628
|
+
let observedUser = null;
|
|
629
|
+
let observedAssistant = expectedWaitingAssistant ?? expectedAssistant ?? null;
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
while (this.monotonicNow() < deadline) {
|
|
633
|
+
this.#assertRecoveryTarget({
|
|
634
|
+
page,
|
|
635
|
+
mainFrame,
|
|
636
|
+
targetId: conversationTargetId,
|
|
637
|
+
initialUrl,
|
|
638
|
+
navigated,
|
|
639
|
+
});
|
|
640
|
+
const snapshot = validatedOrderedSnapshot(
|
|
641
|
+
await this.#awaitWithinRecoveryDeadline(
|
|
642
|
+
() => this.orderedConversationSnapshot().catch(() => null),
|
|
643
|
+
deadline,
|
|
644
|
+
),
|
|
645
|
+
);
|
|
646
|
+
if (!snapshot) {
|
|
647
|
+
stableSignature = null;
|
|
648
|
+
stableSince = 0;
|
|
649
|
+
await this.#waitRecoveryPoll(deadline);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
this.#assertRecoveryTarget({
|
|
653
|
+
page,
|
|
654
|
+
mainFrame,
|
|
655
|
+
targetId: conversationTargetId,
|
|
656
|
+
initialUrl,
|
|
657
|
+
navigated,
|
|
658
|
+
});
|
|
659
|
+
const hasOutboundMarker = snapshot.entries.some((entry) => (
|
|
660
|
+
renderedMessageContainsOutboundMarker(entry.renderedText, outboundId)
|
|
661
|
+
));
|
|
662
|
+
if (!hasOutboundMarker) {
|
|
663
|
+
stableSignature = null;
|
|
664
|
+
stableSince = 0;
|
|
665
|
+
await this.#waitRecoveryPoll(deadline);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
const {
|
|
669
|
+
user,
|
|
670
|
+
assistant,
|
|
671
|
+
assistantBaselineEntries,
|
|
672
|
+
preOutboundMarkerIds,
|
|
673
|
+
} = this.#recoveryBoundary(snapshot, {
|
|
674
|
+
initialUrl,
|
|
675
|
+
outboundId,
|
|
676
|
+
observedUser,
|
|
677
|
+
expectedUser,
|
|
678
|
+
requiresEmptyPrefix,
|
|
679
|
+
expectedPreOutboundIds,
|
|
680
|
+
expectedWaitingAssistant,
|
|
681
|
+
});
|
|
682
|
+
observedUser = { id: user.id, turn: user.turn };
|
|
683
|
+
if (
|
|
684
|
+
assistant
|
|
685
|
+
&& observedAssistant
|
|
686
|
+
&& !sameIdentity(assistant, observedAssistant)
|
|
687
|
+
) {
|
|
688
|
+
throw this.#pendingOutboundUncertain(
|
|
689
|
+
"The ChatGPT assistant successor changed during recovery.",
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
if (assistant && !observedAssistant) {
|
|
693
|
+
observedAssistant = { id: assistant.id, turn: assistant.turn };
|
|
694
|
+
}
|
|
695
|
+
if (observedAssistant && !assistant) {
|
|
696
|
+
stableSignature = null;
|
|
697
|
+
stableSince = 0;
|
|
698
|
+
await this.#waitRecoveryPoll(deadline);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
let status = "waiting";
|
|
702
|
+
let rawResponse = null;
|
|
703
|
+
let responseHash = null;
|
|
704
|
+
if (assistant) {
|
|
705
|
+
const locator = await this.#awaitWithinRecoveryDeadline(
|
|
706
|
+
() => this.#assistantLocatorFor(assistant),
|
|
707
|
+
deadline,
|
|
708
|
+
);
|
|
709
|
+
if (!locator) {
|
|
710
|
+
stableSignature = null;
|
|
711
|
+
stableSince = 0;
|
|
712
|
+
await this.#waitRecoveryPoll(deadline);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const stopVisible = Boolean(await this.#awaitWithinRecoveryDeadline(
|
|
716
|
+
() => firstVisible(this.stopButtonLocators()),
|
|
717
|
+
deadline,
|
|
718
|
+
));
|
|
719
|
+
const generating = stopVisible || await this.#awaitWithinRecoveryDeadline(
|
|
720
|
+
() => this.isAssistantGenerating(locator),
|
|
721
|
+
deadline,
|
|
722
|
+
);
|
|
723
|
+
if (!generating) {
|
|
724
|
+
const [generationError, usageLimit] = await this.#awaitWithinRecoveryDeadline(
|
|
725
|
+
() => Promise.all([
|
|
726
|
+
this.findGenerationErrorMarker(locator),
|
|
727
|
+
this.findUsageLimitMarker(locator),
|
|
728
|
+
]),
|
|
729
|
+
deadline,
|
|
730
|
+
);
|
|
731
|
+
if (!generationError && !usageLimit) {
|
|
732
|
+
rawResponse = assistant.renderedText;
|
|
733
|
+
if (rawResponse.trim() || priorHandoff?.nativeImages?.length > 0) {
|
|
734
|
+
responseHash = createHash("sha256")
|
|
735
|
+
.update(rawResponse)
|
|
736
|
+
.digest("hex");
|
|
737
|
+
status = "complete";
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (expectedAssistant) {
|
|
743
|
+
if (
|
|
744
|
+
status !== "complete"
|
|
745
|
+
|| !sameIdentity(assistant, expectedAssistant)
|
|
746
|
+
|| responseHash !== expectedAssistant.responseHash
|
|
747
|
+
) {
|
|
748
|
+
throw this.#pendingOutboundUncertain(
|
|
749
|
+
"The completed assistant handoff no longer matches the browser reply.",
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const signature = [
|
|
755
|
+
initialUrl,
|
|
756
|
+
conversationTargetId,
|
|
757
|
+
user.id,
|
|
758
|
+
user.turn,
|
|
759
|
+
preOutboundMarkerIds.join(","),
|
|
760
|
+
assistant?.id ?? "",
|
|
761
|
+
assistant?.turn ?? "",
|
|
762
|
+
status,
|
|
763
|
+
responseHash ?? "",
|
|
764
|
+
].join(":");
|
|
765
|
+
const observedAt = this.monotonicNow();
|
|
766
|
+
if (signature !== stableSignature) {
|
|
767
|
+
stableSignature = signature;
|
|
768
|
+
stableSince = observedAt;
|
|
769
|
+
}
|
|
770
|
+
if (observedAt - stableSince < stableWindowMs) {
|
|
771
|
+
await this.#waitRecoveryPoll(deadline);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (this.monotonicNow() >= deadline) {
|
|
775
|
+
throw this.#recoveryDeadlineError();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const finalSnapshot = validatedOrderedSnapshot(
|
|
779
|
+
await this.#awaitWithinRecoveryDeadline(
|
|
780
|
+
() => this.orderedConversationSnapshot().catch(() => null),
|
|
781
|
+
deadline,
|
|
782
|
+
),
|
|
783
|
+
);
|
|
784
|
+
if (
|
|
785
|
+
!finalSnapshot
|
|
786
|
+
|| !finalSnapshot.entries.some((entry) => (
|
|
787
|
+
renderedMessageContainsOutboundMarker(entry.renderedText, outboundId)
|
|
788
|
+
))
|
|
789
|
+
) {
|
|
790
|
+
stableSignature = null;
|
|
791
|
+
stableSince = 0;
|
|
792
|
+
await this.#waitRecoveryPoll(deadline);
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
this.#assertRecoveryTarget({
|
|
796
|
+
page,
|
|
797
|
+
mainFrame,
|
|
798
|
+
targetId: conversationTargetId,
|
|
799
|
+
initialUrl,
|
|
800
|
+
navigated,
|
|
801
|
+
});
|
|
802
|
+
const finalBoundary = this.#recoveryBoundary(finalSnapshot, {
|
|
803
|
+
initialUrl,
|
|
804
|
+
outboundId,
|
|
805
|
+
observedUser,
|
|
806
|
+
expectedUser,
|
|
807
|
+
requiresEmptyPrefix,
|
|
808
|
+
expectedPreOutboundIds,
|
|
809
|
+
expectedWaitingAssistant,
|
|
810
|
+
});
|
|
811
|
+
if (expectedWaitingAssistant && !finalBoundary.assistant) {
|
|
812
|
+
stableSignature = null;
|
|
813
|
+
stableSince = 0;
|
|
814
|
+
await this.#waitRecoveryPoll(deadline);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (
|
|
818
|
+
Boolean(finalBoundary.assistant) !== Boolean(assistant)
|
|
819
|
+
|| (
|
|
820
|
+
assistant
|
|
821
|
+
&& !sameIdentity(finalBoundary.assistant, assistant)
|
|
822
|
+
)
|
|
823
|
+
) {
|
|
824
|
+
if (!assistant && finalBoundary.assistant) {
|
|
825
|
+
stableSignature = null;
|
|
826
|
+
stableSince = 0;
|
|
827
|
+
await this.#waitRecoveryPoll(deadline);
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
throw this.#pendingOutboundUncertain(
|
|
831
|
+
"The ChatGPT assistant boundary changed during recovery verification.",
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
if (
|
|
835
|
+
finalBoundary.preOutboundMarkerIds.join(",")
|
|
836
|
+
!== preOutboundMarkerIds.join(",")
|
|
837
|
+
) {
|
|
838
|
+
stableSignature = null;
|
|
839
|
+
stableSince = 0;
|
|
840
|
+
await this.#waitRecoveryPoll(deadline);
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
if (status === "complete") {
|
|
844
|
+
const finalRawResponse = finalBoundary.assistant?.renderedText ?? "";
|
|
845
|
+
const finalResponseHash = createHash("sha256")
|
|
846
|
+
.update(finalRawResponse)
|
|
847
|
+
.digest("hex");
|
|
848
|
+
if (finalResponseHash !== responseHash) {
|
|
849
|
+
stableSignature = null;
|
|
850
|
+
stableSince = 0;
|
|
851
|
+
await this.#waitRecoveryPoll(deadline);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
rawResponse = finalRawResponse;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const assistantBaseline = {
|
|
858
|
+
ids: assistantBaselineEntries.map((entry) => entry.id),
|
|
859
|
+
count: assistantBaselineEntries.length,
|
|
860
|
+
maxTurn: assistantBaselineEntries.at(-1)?.turn ?? null,
|
|
861
|
+
lastText: assistantBaselineEntries.at(-1)?.renderedText ?? "",
|
|
862
|
+
};
|
|
863
|
+
this.acceptRecoveredOutboundAnchor({
|
|
864
|
+
conversationUrl: initialUrl,
|
|
865
|
+
conversationTargetId,
|
|
866
|
+
userMessageId: user.id,
|
|
867
|
+
userTurn: user.turn,
|
|
868
|
+
assistantBaseline,
|
|
869
|
+
assistantCandidateMessageId: assistant?.id ?? null,
|
|
870
|
+
assistantCandidateTurn: assistant?.turn ?? null,
|
|
871
|
+
});
|
|
872
|
+
if (status === "complete") {
|
|
873
|
+
this.lastAssistantMessageId = assistant.id;
|
|
874
|
+
this.lastAssistantTurn = assistant.turn;
|
|
875
|
+
}
|
|
876
|
+
if (this.monotonicNow() >= deadline) {
|
|
877
|
+
throw this.#recoveryDeadlineError();
|
|
878
|
+
}
|
|
879
|
+
this.#assertRecoveryTarget({
|
|
880
|
+
page,
|
|
881
|
+
mainFrame,
|
|
882
|
+
targetId: conversationTargetId,
|
|
883
|
+
initialUrl,
|
|
884
|
+
navigated,
|
|
885
|
+
});
|
|
886
|
+
return {
|
|
887
|
+
status,
|
|
888
|
+
conversationUrl: initialUrl,
|
|
889
|
+
conversationTargetId,
|
|
890
|
+
userMessageId: user.id,
|
|
891
|
+
userTurn: user.turn,
|
|
892
|
+
preOutboundMarkerIds,
|
|
893
|
+
assistantBaseline,
|
|
894
|
+
...(assistant
|
|
895
|
+
? {
|
|
896
|
+
assistantCandidateMessageId: assistant.id,
|
|
897
|
+
assistantCandidateTurn: assistant.turn,
|
|
898
|
+
}
|
|
899
|
+
: {}),
|
|
900
|
+
...(status === "complete"
|
|
901
|
+
? {
|
|
902
|
+
assistantMessageId: assistant.id,
|
|
903
|
+
assistantTurn: assistant.turn,
|
|
904
|
+
rawResponse,
|
|
905
|
+
responseHash,
|
|
906
|
+
}
|
|
907
|
+
: {}),
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
throw this.#pendingOutboundUncertain(
|
|
911
|
+
"ChatGPT could not stably reconcile the pending outbound on its exact target.",
|
|
912
|
+
);
|
|
913
|
+
} finally {
|
|
914
|
+
page.off?.("framenavigated", navigationListener);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async messageIdentity(message) {
|
|
919
|
+
return readChatGPTMessageIdentity(message);
|
|
134
920
|
}
|
|
135
921
|
|
|
136
922
|
// The redesigned UI still exposes the stop button ([data-testid="stop-button"],
|
|
@@ -154,45 +940,21 @@ export class ChatGPTWebAdapter extends BaseWebAdapter {
|
|
|
154
940
|
if (super.isNewAssistantIdentity({ id, turn })) {
|
|
155
941
|
return true;
|
|
156
942
|
}
|
|
943
|
+
if (
|
|
944
|
+
!this.allowInPlaceAssistantContinuation
|
|
945
|
+
|| !this.inPlaceAssistantGenerationObserved
|
|
946
|
+
|| this.sentUserTurn == null
|
|
947
|
+
|| !id
|
|
948
|
+
|| !this.assistantIdsBeforeSend.has(id)
|
|
949
|
+
) {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
157
952
|
const previous = String(this.lastAssistantTextBeforeSend ?? "");
|
|
158
953
|
return previous !== "" && String(text ?? "") !== previous;
|
|
159
954
|
}
|
|
160
955
|
|
|
161
956
|
async assistantText(message) {
|
|
162
|
-
|
|
163
|
-
// Markdown fences inside CDATA; ChatGPT then splits the rendered response
|
|
164
|
-
// into many <pre><code> nodes and the first node contains the opening tag
|
|
165
|
-
// but not the closing tag. The parent innerText keeps the complete envelope
|
|
166
|
-
// and avoids one CDP round trip per nested code block on every poll.
|
|
167
|
-
const fullText = await message.innerText().catch(() => "");
|
|
168
|
-
if (fullText.includes("<agent_response")) {
|
|
169
|
-
return fullText;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Rare fallback for alternate renderers where the parent text omits code
|
|
173
|
-
// contents: prefer a complete code-block envelope, but retain a partial
|
|
174
|
-
// one so streaming progress remains visible until its closing tag arrives.
|
|
175
|
-
const codeBlocks = message.locator("pre code");
|
|
176
|
-
const codeBlockCount = await codeBlocks.count();
|
|
177
|
-
let partialEnvelope = "";
|
|
178
|
-
for (let index = 0; index < codeBlockCount; index += 1) {
|
|
179
|
-
const code = await codeBlocks.nth(index).innerText().catch(() => "");
|
|
180
|
-
if (hasCompleteAgentEnvelope(code)) {
|
|
181
|
-
return code;
|
|
182
|
-
}
|
|
183
|
-
if (!partialEnvelope && code.includes("<agent_response")) {
|
|
184
|
-
partialEnvelope = code;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
if (partialEnvelope) {
|
|
188
|
-
return partialEnvelope;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const markdown = message.locator(".markdown");
|
|
192
|
-
if (await markdown.count()) {
|
|
193
|
-
return await markdown.last().innerText().catch(() => "");
|
|
194
|
-
}
|
|
195
|
-
return fullText;
|
|
957
|
+
return readChatGPTAssistantText(message, hasCompleteAgentEnvelope);
|
|
196
958
|
}
|
|
197
959
|
|
|
198
960
|
// A plan/usage limit renders as an error card: error-tinted token classes
|
|
@@ -312,8 +1074,8 @@ export class ChatGPTWebAdapter extends BaseWebAdapter {
|
|
|
312
1074
|
// Scrolls the thread container to the bottom so the latest replies mount.
|
|
313
1075
|
// Best-effort — any failure is swallowed.
|
|
314
1076
|
async scrollConversationToBottom() {
|
|
315
|
-
await this.page.evaluate?.(() => {
|
|
316
|
-
const message = document.querySelector(
|
|
1077
|
+
await this.page.evaluate?.((selector) => {
|
|
1078
|
+
const message = document.querySelector(selector);
|
|
317
1079
|
if (!message) {
|
|
318
1080
|
return;
|
|
319
1081
|
}
|
|
@@ -328,7 +1090,7 @@ export class ChatGPTWebAdapter extends BaseWebAdapter {
|
|
|
328
1090
|
return;
|
|
329
1091
|
}
|
|
330
1092
|
}
|
|
331
|
-
}).catch(() => null);
|
|
1093
|
+
}, CHATGPT_DOM.messages).catch(() => null);
|
|
332
1094
|
}
|
|
333
1095
|
|
|
334
1096
|
}
|