wtagent 0.3.0 → 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.
- package/README.md +2 -2
- package/docs/technical-design.md +18 -11
- package/package.json +5 -5
- package/src/browser/base-web-adapter.js +2081 -168
- package/src/browser/cdp-browser.js +223 -25
- package/src/browser/cdp-state.js +8 -5
- package/src/browser/chatgpt-web-adapter.js +799 -1
- package/src/browser/claude-web-adapter.js +13 -0
- package/src/browser/fake-web-model-adapter.js +137 -3
- package/src/browser/gemini-web-adapter.js +10 -0
- package/src/browser/glm-web-adapter.js +2 -2
- package/src/cli/i18n.js +10 -10
- package/src/cli/main.js +94 -39
- 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/runtime/agent-runtime.js +1056 -191
- package/src/session/agent-session.js +975 -120
- package/src/tools/default-tools.js +1 -1
|
@@ -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
|
|
|
@@ -14,6 +21,60 @@ function parseConversationTurn(value) {
|
|
|
14
21
|
return match ? Number.parseInt(match[1], 10) : null;
|
|
15
22
|
}
|
|
16
23
|
|
|
24
|
+
function normalizedConversationUrl(value) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = value instanceof URL ? value : new URL(value);
|
|
27
|
+
const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
|
|
28
|
+
return `${parsed.origin}${pathname}`;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validatedOrderedSnapshot(snapshot) {
|
|
35
|
+
const url = normalizedConversationUrl(snapshot?.url);
|
|
36
|
+
if (!url || !Array.isArray(snapshot?.entries)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const ids = new Set();
|
|
40
|
+
const turns = new Set();
|
|
41
|
+
const entries = [];
|
|
42
|
+
let previousDomIndex = -1;
|
|
43
|
+
let previousTurn = -1;
|
|
44
|
+
for (const entry of snapshot.entries) {
|
|
45
|
+
const id = typeof entry?.id === "string" ? entry.id.trim() : "";
|
|
46
|
+
if (
|
|
47
|
+
!["user", "assistant"].includes(entry?.role)
|
|
48
|
+
|| !id
|
|
49
|
+
|| !Number.isSafeInteger(entry.turn)
|
|
50
|
+
|| entry.turn < 0
|
|
51
|
+
|| !Number.isSafeInteger(entry.domIndex)
|
|
52
|
+
|| entry.domIndex <= previousDomIndex
|
|
53
|
+
|| entry.turn <= previousTurn
|
|
54
|
+
|| ids.has(id)
|
|
55
|
+
|| turns.has(entry.turn)
|
|
56
|
+
) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
ids.add(id);
|
|
60
|
+
turns.add(entry.turn);
|
|
61
|
+
previousDomIndex = entry.domIndex;
|
|
62
|
+
previousTurn = entry.turn;
|
|
63
|
+
entries.push({
|
|
64
|
+
domIndex: entry.domIndex,
|
|
65
|
+
role: entry.role,
|
|
66
|
+
id,
|
|
67
|
+
turn: entry.turn,
|
|
68
|
+
renderedText: String(entry.renderedText ?? ""),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return { url, entries };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sameIdentity(entry, { id, turn }) {
|
|
75
|
+
return entry?.id === id && entry?.turn === turn;
|
|
76
|
+
}
|
|
77
|
+
|
|
17
78
|
// ChatGPT-specific implementation of the WebModelAdapter contract. Only the
|
|
18
79
|
// provider primitives (DOM locators, message identity, upload, overlay
|
|
19
80
|
// handling, usage-limit card) live here; all orchestration — launch/auth/send/
|
|
@@ -34,6 +95,28 @@ export class ChatGPTWebAdapter extends BaseWebAdapter {
|
|
|
34
95
|
return /^\/c\//;
|
|
35
96
|
}
|
|
36
97
|
|
|
98
|
+
classifyConversationUrl(value) {
|
|
99
|
+
const kind = super.classifyConversationUrl(value);
|
|
100
|
+
if (kind !== "restorable") {
|
|
101
|
+
return kind;
|
|
102
|
+
}
|
|
103
|
+
const parsed = value instanceof URL ? value : new URL(value);
|
|
104
|
+
return /^\/c\/WEB:/.test(parsed.pathname)
|
|
105
|
+
? "provisional"
|
|
106
|
+
: "restorable";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pendingAttachmentLocators() {
|
|
110
|
+
return [
|
|
111
|
+
this.page.locator(
|
|
112
|
+
'form [data-testid$="-file-thumbnail"], '
|
|
113
|
+
+ 'form [data-testid^="file-thumbnail"], '
|
|
114
|
+
+ 'form button[aria-label*="remove file" i], '
|
|
115
|
+
+ 'form button[aria-label*="删除文件"]',
|
|
116
|
+
),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
37
120
|
composerLocators() {
|
|
38
121
|
return [
|
|
39
122
|
this.page.locator("#prompt-textarea"),
|
|
@@ -114,6 +197,712 @@ export class ChatGPTWebAdapter extends BaseWebAdapter {
|
|
|
114
197
|
);
|
|
115
198
|
}
|
|
116
199
|
|
|
200
|
+
requiresOrderedConversationSnapshotForFreshSend() {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async orderedConversationSnapshot() {
|
|
205
|
+
if (typeof this.page?.evaluate !== "function") {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return await this.page.evaluate(() => {
|
|
209
|
+
const entries = [
|
|
210
|
+
...document.querySelectorAll(
|
|
211
|
+
'[data-message-author-role="user"], [data-message-author-role="assistant"]',
|
|
212
|
+
),
|
|
213
|
+
].filter((element) => {
|
|
214
|
+
const role = element.getAttribute("data-message-author-role");
|
|
215
|
+
return role === "user"
|
|
216
|
+
|| (role === "assistant" && !element.id.startsWith("request-placeholder-"));
|
|
217
|
+
}).map((element, domIndex) => {
|
|
218
|
+
const role = element.getAttribute("data-message-author-role");
|
|
219
|
+
const wrapper = element.closest('[data-testid^="conversation-turn-"]');
|
|
220
|
+
const turnMatch = wrapper?.getAttribute("data-testid")
|
|
221
|
+
?.match(/^conversation-turn-(\d+)$/);
|
|
222
|
+
let renderedText = element.innerText ?? element.textContent ?? "";
|
|
223
|
+
if (role === "assistant" && !renderedText.includes("<agent_response")) {
|
|
224
|
+
const codeTexts = [...element.querySelectorAll("pre code")]
|
|
225
|
+
.map((code) => code.innerText ?? code.textContent ?? "");
|
|
226
|
+
const completeEnvelope = codeTexts.find((text) => {
|
|
227
|
+
const start = text.trim().indexOf("<agent_response");
|
|
228
|
+
const end = text.trim().lastIndexOf("</agent_response>");
|
|
229
|
+
return start >= 0 && end >= start;
|
|
230
|
+
});
|
|
231
|
+
const partialEnvelope = codeTexts.find(
|
|
232
|
+
(text) => text.includes("<agent_response"),
|
|
233
|
+
);
|
|
234
|
+
if (completeEnvelope) {
|
|
235
|
+
renderedText = completeEnvelope;
|
|
236
|
+
} else if (partialEnvelope) {
|
|
237
|
+
renderedText = partialEnvelope;
|
|
238
|
+
} else {
|
|
239
|
+
const markdown = [...element.querySelectorAll(".markdown")];
|
|
240
|
+
if (markdown.length > 0) {
|
|
241
|
+
const last = markdown.at(-1);
|
|
242
|
+
renderedText = last.innerText ?? last.textContent ?? "";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
domIndex,
|
|
248
|
+
role,
|
|
249
|
+
id: element.getAttribute("data-message-id"),
|
|
250
|
+
turn: turnMatch ? Number.parseInt(turnMatch[1], 10) : null,
|
|
251
|
+
renderedText,
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
return { url: window.location.href, entries };
|
|
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({
|
|
527
|
+
pendingOutbound = null,
|
|
528
|
+
conversationTargetId = null,
|
|
529
|
+
lastUserMessageId = null,
|
|
530
|
+
lastAssistantMessageId = null,
|
|
531
|
+
priorHandoff = null,
|
|
532
|
+
timeoutMs = this.sendConfirmationTimeoutMs(),
|
|
533
|
+
stableWindowMs = this.restorationCorrelationWindowMs(),
|
|
534
|
+
} = {}) {
|
|
535
|
+
this.requirePage();
|
|
536
|
+
const outboundId = pendingOutbound?.outboundId
|
|
537
|
+
?? priorHandoff?.sourceOutboundId
|
|
538
|
+
?? null;
|
|
539
|
+
if (!isValidOutboundCorrelationId(outboundId)) {
|
|
540
|
+
throw this.#pendingOutboundUncertain(
|
|
541
|
+
"The pending outbound has no valid transport correlation ID.",
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (
|
|
545
|
+
typeof conversationTargetId !== "string"
|
|
546
|
+
|| conversationTargetId.length === 0
|
|
547
|
+
|| !this.hasExactRecoveryTarget(conversationTargetId)
|
|
548
|
+
) {
|
|
549
|
+
throw new BrowserAdapterError(
|
|
550
|
+
"The exact saved ChatGPT target is unavailable for pending outbound recovery.",
|
|
551
|
+
{ code: "RECOVERY_TARGET_UNAVAILABLE", recoverable: false },
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const page = this.page;
|
|
556
|
+
const mainFrame = typeof page.mainFrame === "function"
|
|
557
|
+
? page.mainFrame()
|
|
558
|
+
: null;
|
|
559
|
+
const initialUrl = normalizedConversationUrl(page.url());
|
|
560
|
+
const initialKind = initialUrl
|
|
561
|
+
? this.classifyConversationUrl(initialUrl)
|
|
562
|
+
: "invalid";
|
|
563
|
+
if (!["restorable", "provisional"].includes(initialKind)) {
|
|
564
|
+
throw this.#pendingOutboundUncertain(
|
|
565
|
+
"The exact ChatGPT target is not on a recognized conversation route.",
|
|
566
|
+
{ currentUrl: initialUrl },
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let navigated = false;
|
|
571
|
+
const navigationListener = (frame) => {
|
|
572
|
+
if (
|
|
573
|
+
!mainFrame
|
|
574
|
+
|| frame === mainFrame
|
|
575
|
+
|| (typeof page.mainFrame === "function" && frame === page.mainFrame())
|
|
576
|
+
) {
|
|
577
|
+
navigated = true;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
page.on?.("framenavigated", navigationListener);
|
|
581
|
+
|
|
582
|
+
const outboundKind = pendingOutbound?.kind
|
|
583
|
+
?? priorHandoff?.outboundKind
|
|
584
|
+
?? "runtime_message";
|
|
585
|
+
const requiresEmptyPrefix = ["bootstrap", "fresh_rebuild"]
|
|
586
|
+
.includes(outboundKind);
|
|
587
|
+
const expectedPreOutboundIds = new Set([
|
|
588
|
+
lastUserMessageId,
|
|
589
|
+
lastAssistantMessageId,
|
|
590
|
+
...(priorHandoff?.preOutboundMarkerIds ?? []),
|
|
591
|
+
...(priorHandoff?.assistantBaseline?.ids ?? []),
|
|
592
|
+
].filter(Boolean));
|
|
593
|
+
const expectedUser = priorHandoff
|
|
594
|
+
? {
|
|
595
|
+
id: priorHandoff.userMessageId ?? null,
|
|
596
|
+
turn: priorHandoff.userTurn ?? null,
|
|
597
|
+
}
|
|
598
|
+
: null;
|
|
599
|
+
const expectedAssistant = priorHandoff?.status === "complete"
|
|
600
|
+
? {
|
|
601
|
+
id: priorHandoff.assistantMessageId ?? null,
|
|
602
|
+
turn: priorHandoff.assistantTurn ?? null,
|
|
603
|
+
responseHash: priorHandoff.responseHash ?? null,
|
|
604
|
+
}
|
|
605
|
+
: null;
|
|
606
|
+
const expectedWaitingAssistant = priorHandoff?.status === "waiting"
|
|
607
|
+
&& priorHandoff.assistantCandidateMessageId
|
|
608
|
+
? {
|
|
609
|
+
id: priorHandoff.assistantCandidateMessageId,
|
|
610
|
+
turn: priorHandoff.assistantCandidateTurn ?? null,
|
|
611
|
+
}
|
|
612
|
+
: null;
|
|
613
|
+
const deadline = this.monotonicNow() + timeoutMs;
|
|
614
|
+
let stableSignature = null;
|
|
615
|
+
let stableSince = 0;
|
|
616
|
+
let observedUser = null;
|
|
617
|
+
let observedAssistant = expectedWaitingAssistant ?? expectedAssistant ?? null;
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
while (this.monotonicNow() < deadline) {
|
|
621
|
+
this.#assertRecoveryTarget({
|
|
622
|
+
page,
|
|
623
|
+
mainFrame,
|
|
624
|
+
targetId: conversationTargetId,
|
|
625
|
+
initialUrl,
|
|
626
|
+
navigated,
|
|
627
|
+
});
|
|
628
|
+
const snapshot = validatedOrderedSnapshot(
|
|
629
|
+
await this.#awaitWithinRecoveryDeadline(
|
|
630
|
+
() => this.orderedConversationSnapshot().catch(() => null),
|
|
631
|
+
deadline,
|
|
632
|
+
),
|
|
633
|
+
);
|
|
634
|
+
if (!snapshot) {
|
|
635
|
+
stableSignature = null;
|
|
636
|
+
stableSince = 0;
|
|
637
|
+
await this.#waitRecoveryPoll(deadline);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
this.#assertRecoveryTarget({
|
|
641
|
+
page,
|
|
642
|
+
mainFrame,
|
|
643
|
+
targetId: conversationTargetId,
|
|
644
|
+
initialUrl,
|
|
645
|
+
navigated,
|
|
646
|
+
});
|
|
647
|
+
const hasOutboundMarker = snapshot.entries.some((entry) => (
|
|
648
|
+
renderedMessageContainsOutboundMarker(entry.renderedText, outboundId)
|
|
649
|
+
));
|
|
650
|
+
if (!hasOutboundMarker) {
|
|
651
|
+
stableSignature = null;
|
|
652
|
+
stableSince = 0;
|
|
653
|
+
await this.#waitRecoveryPoll(deadline);
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const {
|
|
657
|
+
user,
|
|
658
|
+
assistant,
|
|
659
|
+
assistantBaselineEntries,
|
|
660
|
+
preOutboundMarkerIds,
|
|
661
|
+
} = this.#recoveryBoundary(snapshot, {
|
|
662
|
+
initialUrl,
|
|
663
|
+
outboundId,
|
|
664
|
+
observedUser,
|
|
665
|
+
expectedUser,
|
|
666
|
+
requiresEmptyPrefix,
|
|
667
|
+
expectedPreOutboundIds,
|
|
668
|
+
expectedWaitingAssistant,
|
|
669
|
+
});
|
|
670
|
+
observedUser = { id: user.id, turn: user.turn };
|
|
671
|
+
if (
|
|
672
|
+
assistant
|
|
673
|
+
&& observedAssistant
|
|
674
|
+
&& !sameIdentity(assistant, observedAssistant)
|
|
675
|
+
) {
|
|
676
|
+
throw this.#pendingOutboundUncertain(
|
|
677
|
+
"The ChatGPT assistant successor changed during recovery.",
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (assistant && !observedAssistant) {
|
|
681
|
+
observedAssistant = { id: assistant.id, turn: assistant.turn };
|
|
682
|
+
}
|
|
683
|
+
if (observedAssistant && !assistant) {
|
|
684
|
+
stableSignature = null;
|
|
685
|
+
stableSince = 0;
|
|
686
|
+
await this.#waitRecoveryPoll(deadline);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
let status = "waiting";
|
|
690
|
+
let rawResponse = null;
|
|
691
|
+
let responseHash = null;
|
|
692
|
+
if (assistant) {
|
|
693
|
+
const locator = await this.#awaitWithinRecoveryDeadline(
|
|
694
|
+
() => this.#assistantLocatorFor(assistant),
|
|
695
|
+
deadline,
|
|
696
|
+
);
|
|
697
|
+
if (!locator) {
|
|
698
|
+
stableSignature = null;
|
|
699
|
+
stableSince = 0;
|
|
700
|
+
await this.#waitRecoveryPoll(deadline);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
const stopVisible = Boolean(await this.#awaitWithinRecoveryDeadline(
|
|
704
|
+
() => firstVisible(this.stopButtonLocators()),
|
|
705
|
+
deadline,
|
|
706
|
+
));
|
|
707
|
+
const generating = stopVisible || await this.#awaitWithinRecoveryDeadline(
|
|
708
|
+
() => this.isAssistantGenerating(locator),
|
|
709
|
+
deadline,
|
|
710
|
+
);
|
|
711
|
+
if (!generating) {
|
|
712
|
+
const [generationError, usageLimit] = await this.#awaitWithinRecoveryDeadline(
|
|
713
|
+
() => Promise.all([
|
|
714
|
+
this.findGenerationErrorMarker(locator),
|
|
715
|
+
this.findUsageLimitMarker(locator),
|
|
716
|
+
]),
|
|
717
|
+
deadline,
|
|
718
|
+
);
|
|
719
|
+
if (!generationError && !usageLimit) {
|
|
720
|
+
rawResponse = assistant.renderedText;
|
|
721
|
+
if (rawResponse.trim()) {
|
|
722
|
+
responseHash = createHash("sha256")
|
|
723
|
+
.update(rawResponse)
|
|
724
|
+
.digest("hex");
|
|
725
|
+
status = "complete";
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (expectedAssistant) {
|
|
731
|
+
if (
|
|
732
|
+
status !== "complete"
|
|
733
|
+
|| !sameIdentity(assistant, expectedAssistant)
|
|
734
|
+
|| responseHash !== expectedAssistant.responseHash
|
|
735
|
+
) {
|
|
736
|
+
throw this.#pendingOutboundUncertain(
|
|
737
|
+
"The completed assistant handoff no longer matches the browser reply.",
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const signature = [
|
|
743
|
+
initialUrl,
|
|
744
|
+
conversationTargetId,
|
|
745
|
+
user.id,
|
|
746
|
+
user.turn,
|
|
747
|
+
preOutboundMarkerIds.join(","),
|
|
748
|
+
assistant?.id ?? "",
|
|
749
|
+
assistant?.turn ?? "",
|
|
750
|
+
status,
|
|
751
|
+
responseHash ?? "",
|
|
752
|
+
].join(":");
|
|
753
|
+
const observedAt = this.monotonicNow();
|
|
754
|
+
if (signature !== stableSignature) {
|
|
755
|
+
stableSignature = signature;
|
|
756
|
+
stableSince = observedAt;
|
|
757
|
+
}
|
|
758
|
+
if (observedAt - stableSince < stableWindowMs) {
|
|
759
|
+
await this.#waitRecoveryPoll(deadline);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
if (this.monotonicNow() >= deadline) {
|
|
763
|
+
throw this.#recoveryDeadlineError();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const finalSnapshot = validatedOrderedSnapshot(
|
|
767
|
+
await this.#awaitWithinRecoveryDeadline(
|
|
768
|
+
() => this.orderedConversationSnapshot().catch(() => null),
|
|
769
|
+
deadline,
|
|
770
|
+
),
|
|
771
|
+
);
|
|
772
|
+
if (
|
|
773
|
+
!finalSnapshot
|
|
774
|
+
|| !finalSnapshot.entries.some((entry) => (
|
|
775
|
+
renderedMessageContainsOutboundMarker(entry.renderedText, outboundId)
|
|
776
|
+
))
|
|
777
|
+
) {
|
|
778
|
+
stableSignature = null;
|
|
779
|
+
stableSince = 0;
|
|
780
|
+
await this.#waitRecoveryPoll(deadline);
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
this.#assertRecoveryTarget({
|
|
784
|
+
page,
|
|
785
|
+
mainFrame,
|
|
786
|
+
targetId: conversationTargetId,
|
|
787
|
+
initialUrl,
|
|
788
|
+
navigated,
|
|
789
|
+
});
|
|
790
|
+
const finalBoundary = this.#recoveryBoundary(finalSnapshot, {
|
|
791
|
+
initialUrl,
|
|
792
|
+
outboundId,
|
|
793
|
+
observedUser,
|
|
794
|
+
expectedUser,
|
|
795
|
+
requiresEmptyPrefix,
|
|
796
|
+
expectedPreOutboundIds,
|
|
797
|
+
expectedWaitingAssistant,
|
|
798
|
+
});
|
|
799
|
+
if (expectedWaitingAssistant && !finalBoundary.assistant) {
|
|
800
|
+
stableSignature = null;
|
|
801
|
+
stableSince = 0;
|
|
802
|
+
await this.#waitRecoveryPoll(deadline);
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (
|
|
806
|
+
Boolean(finalBoundary.assistant) !== Boolean(assistant)
|
|
807
|
+
|| (
|
|
808
|
+
assistant
|
|
809
|
+
&& !sameIdentity(finalBoundary.assistant, assistant)
|
|
810
|
+
)
|
|
811
|
+
) {
|
|
812
|
+
if (!assistant && finalBoundary.assistant) {
|
|
813
|
+
stableSignature = null;
|
|
814
|
+
stableSince = 0;
|
|
815
|
+
await this.#waitRecoveryPoll(deadline);
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
throw this.#pendingOutboundUncertain(
|
|
819
|
+
"The ChatGPT assistant boundary changed during recovery verification.",
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
if (
|
|
823
|
+
finalBoundary.preOutboundMarkerIds.join(",")
|
|
824
|
+
!== preOutboundMarkerIds.join(",")
|
|
825
|
+
) {
|
|
826
|
+
stableSignature = null;
|
|
827
|
+
stableSince = 0;
|
|
828
|
+
await this.#waitRecoveryPoll(deadline);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (status === "complete") {
|
|
832
|
+
const finalRawResponse = finalBoundary.assistant?.renderedText ?? "";
|
|
833
|
+
const finalResponseHash = createHash("sha256")
|
|
834
|
+
.update(finalRawResponse)
|
|
835
|
+
.digest("hex");
|
|
836
|
+
if (finalResponseHash !== responseHash) {
|
|
837
|
+
stableSignature = null;
|
|
838
|
+
stableSince = 0;
|
|
839
|
+
await this.#waitRecoveryPoll(deadline);
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
rawResponse = finalRawResponse;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const assistantBaseline = {
|
|
846
|
+
ids: assistantBaselineEntries.map((entry) => entry.id),
|
|
847
|
+
count: assistantBaselineEntries.length,
|
|
848
|
+
maxTurn: assistantBaselineEntries.at(-1)?.turn ?? null,
|
|
849
|
+
lastText: assistantBaselineEntries.at(-1)?.renderedText ?? "",
|
|
850
|
+
};
|
|
851
|
+
this.acceptRecoveredOutboundAnchor({
|
|
852
|
+
conversationUrl: initialUrl,
|
|
853
|
+
conversationTargetId,
|
|
854
|
+
userMessageId: user.id,
|
|
855
|
+
userTurn: user.turn,
|
|
856
|
+
assistantBaseline,
|
|
857
|
+
assistantCandidateMessageId: assistant?.id ?? null,
|
|
858
|
+
assistantCandidateTurn: assistant?.turn ?? null,
|
|
859
|
+
});
|
|
860
|
+
if (status === "complete") {
|
|
861
|
+
this.lastAssistantMessageId = assistant.id;
|
|
862
|
+
this.lastAssistantTurn = assistant.turn;
|
|
863
|
+
}
|
|
864
|
+
if (this.monotonicNow() >= deadline) {
|
|
865
|
+
throw this.#recoveryDeadlineError();
|
|
866
|
+
}
|
|
867
|
+
this.#assertRecoveryTarget({
|
|
868
|
+
page,
|
|
869
|
+
mainFrame,
|
|
870
|
+
targetId: conversationTargetId,
|
|
871
|
+
initialUrl,
|
|
872
|
+
navigated,
|
|
873
|
+
});
|
|
874
|
+
return {
|
|
875
|
+
status,
|
|
876
|
+
conversationUrl: initialUrl,
|
|
877
|
+
conversationTargetId,
|
|
878
|
+
userMessageId: user.id,
|
|
879
|
+
userTurn: user.turn,
|
|
880
|
+
preOutboundMarkerIds,
|
|
881
|
+
assistantBaseline,
|
|
882
|
+
...(assistant
|
|
883
|
+
? {
|
|
884
|
+
assistantCandidateMessageId: assistant.id,
|
|
885
|
+
assistantCandidateTurn: assistant.turn,
|
|
886
|
+
}
|
|
887
|
+
: {}),
|
|
888
|
+
...(status === "complete"
|
|
889
|
+
? {
|
|
890
|
+
assistantMessageId: assistant.id,
|
|
891
|
+
assistantTurn: assistant.turn,
|
|
892
|
+
rawResponse,
|
|
893
|
+
responseHash,
|
|
894
|
+
}
|
|
895
|
+
: {}),
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
throw this.#pendingOutboundUncertain(
|
|
899
|
+
"ChatGPT could not stably reconcile the pending outbound on its exact target.",
|
|
900
|
+
);
|
|
901
|
+
} finally {
|
|
902
|
+
page.off?.("framenavigated", navigationListener);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
117
906
|
async messageIdentity(message) {
|
|
118
907
|
const [id, turn] = await Promise.all([
|
|
119
908
|
message.getAttribute("data-message-id").catch(() => null),
|
|
@@ -154,6 +943,15 @@ export class ChatGPTWebAdapter extends BaseWebAdapter {
|
|
|
154
943
|
if (super.isNewAssistantIdentity({ id, turn })) {
|
|
155
944
|
return true;
|
|
156
945
|
}
|
|
946
|
+
if (
|
|
947
|
+
!this.allowInPlaceAssistantContinuation
|
|
948
|
+
|| !this.inPlaceAssistantGenerationObserved
|
|
949
|
+
|| this.sentUserTurn == null
|
|
950
|
+
|| !id
|
|
951
|
+
|| !this.assistantIdsBeforeSend.has(id)
|
|
952
|
+
) {
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
157
955
|
const previous = String(this.lastAssistantTextBeforeSend ?? "");
|
|
158
956
|
return previous !== "" && String(text ?? "") !== previous;
|
|
159
957
|
}
|