@wlv-zedd/dsh-chatgpt-web 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/LICENSE +21 -0
- package/README.md +140 -0
- package/assets/demo.gif +0 -0
- package/assets/hero-demo.png +0 -0
- package/assets/promo-dshmarket-official.png +0 -0
- package/cordis.patch.yml +4 -0
- package/lib/cli.js +239642 -0
- package/lib/plugin.js +195 -0
- package/package.json +88 -0
- package/screenshots.json +5 -0
- package/src/adapters/base.ts +16 -0
- package/src/adapters/chatgpt-web/adapter-error.ts +59 -0
- package/src/adapters/chatgpt-web/browser-helper-main.ts +513 -0
- package/src/adapters/chatgpt-web/browser-helper-prompt-selection.ts +27 -0
- package/src/adapters/chatgpt-web/browser-worker.ts +4944 -0
- package/src/adapters/chatgpt-web/codex-rollout-environment.ts +628 -0
- package/src/adapters/chatgpt-web/compaction-handoff.ts +533 -0
- package/src/adapters/chatgpt-web/compaction-transaction.ts +142 -0
- package/src/adapters/chatgpt-web/concurrency.ts +6 -0
- package/src/adapters/chatgpt-web/conversation-key.ts +58 -0
- package/src/adapters/chatgpt-web/environment.ts +669 -0
- package/src/adapters/chatgpt-web/index.ts +1544 -0
- package/src/adapters/chatgpt-web/input-tokens.ts +74 -0
- package/src/adapters/chatgpt-web/launcher-helper-client.ts +695 -0
- package/src/adapters/chatgpt-web/markdown.ts +418 -0
- package/src/adapters/chatgpt-web/mcp-main.ts +25 -0
- package/src/adapters/chatgpt-web/mcp-server.ts +933 -0
- package/src/adapters/chatgpt-web/model.ts +70 -0
- package/src/adapters/chatgpt-web/native-compaction-control.ts +74 -0
- package/src/adapters/chatgpt-web/output-validation.ts +62 -0
- package/src/adapters/chatgpt-web/process-line-writer.ts +46 -0
- package/src/adapters/chatgpt-web/prompt.ts +702 -0
- package/src/adapters/chatgpt-web/retry-policy.ts +73 -0
- package/src/adapters/chatgpt-web/rolling-checkpoint.ts +384 -0
- package/src/adapters/chatgpt-web/thread-environment.ts +238 -0
- package/src/adapters/chatgpt-web/tool-stream-parser.ts +601 -0
- package/src/adapters/chatgpt-web/turn-broker.ts +1481 -0
- package/src/adapters/chatgpt-web/turn-execution.ts +816 -0
- package/src/adapters/chatgpt-web/turn-progress.ts +292 -0
- package/src/adapters/chatgpt-web/usage.ts +121 -0
- package/src/adapters/image.ts +9 -0
- package/src/bridge.ts +1083 -0
- package/src/browser-login.ts +521 -0
- package/src/chatgpt-session.ts +240 -0
- package/src/chatgpt-web-models.ts +400 -0
- package/src/cli.ts +568 -0
- package/src/codex-integration-document.ts +824 -0
- package/src/codex-integration-journal.ts +212 -0
- package/src/codex-integration-route.ts +515 -0
- package/src/codex-integration-shared.ts +332 -0
- package/src/codex-integration.ts +529 -0
- package/src/codex-interrupt-hook.ts +158 -0
- package/src/config.ts +616 -0
- package/src/dev-chat/cli.ts +432 -0
- package/src/dev-chat/constants.ts +3 -0
- package/src/dev-chat/driver.ts +655 -0
- package/src/dev-chat/profile.ts +223 -0
- package/src/dev-chat/session.ts +287 -0
- package/src/dev-chat/transport.ts +54 -0
- package/src/doctor.ts +237 -0
- package/src/event-queue.ts +45 -0
- package/src/http-body.ts +30 -0
- package/src/launcher-browser-host.ts +695 -0
- package/src/lib/errors.ts +281 -0
- package/src/lib/token-estimate.ts +42 -0
- package/src/login-helper.cjs +140 -0
- package/src/model-catalog.ts +197 -0
- package/src/native-passthrough.ts +261 -0
- package/src/plugin.ts +191 -0
- package/src/process.ts +45 -0
- package/src/responses/compaction.ts +199 -0
- package/src/responses/parser.ts +633 -0
- package/src/responses/reasoning-envelope.ts +49 -0
- package/src/responses/schema.ts +172 -0
- package/src/responses/state.ts +230 -0
- package/src/server.ts +1111 -0
- package/src/service.ts +315 -0
- package/src/setup.ts +671 -0
- package/src/stall-timeout.ts +23 -0
- package/src/tunnel-service.ts +160 -0
- package/src/tunnel.ts +417 -0
- package/src/turndown-plugin-gfm.d.ts +5 -0
- package/src/types.ts +307 -0
- package/src/usage/totals.ts +12 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,1544 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { isChatGptWebZeroRiskBackendModel } from "../../chatgpt-web-models";
|
|
4
|
+
import { defaultBrokerEndpoint, expandUserPath, resolveBrokerEndpoint } from "../../config";
|
|
5
|
+
import {
|
|
6
|
+
cancelLauncherManualTurn,
|
|
7
|
+
endLauncherManualTurn,
|
|
8
|
+
LauncherBrowserTurnCancelledError,
|
|
9
|
+
LauncherManualTurnFailedError,
|
|
10
|
+
LauncherManualTurnTimedOutError,
|
|
11
|
+
markLauncherManualTurnStarted,
|
|
12
|
+
releaseLauncherRetainedConversation,
|
|
13
|
+
startLauncherManualTurn,
|
|
14
|
+
waitForLauncherManualSent,
|
|
15
|
+
waitForLauncherManualTerminal,
|
|
16
|
+
type LauncherManualTurnEnd,
|
|
17
|
+
type LauncherManualTurnOwner,
|
|
18
|
+
type LauncherManualTurnStart,
|
|
19
|
+
} from "../../launcher-browser-host";
|
|
20
|
+
import { namespacedToolName, type AdapterEvent, type CodexContentPart, type CodexParsedRequest, type CodexProviderConfig, type CodexToolResultMessage, type CodexUsage } from "../../types";
|
|
21
|
+
import type { ProviderAdapter } from "../base";
|
|
22
|
+
import { parseDataUrl } from "../image";
|
|
23
|
+
import { ChatGptWebAdapterError } from "./adapter-error";
|
|
24
|
+
import { ChatGptBrowserWorker } from "./browser-worker";
|
|
25
|
+
import { extractChatGptTurnEnvironment, extractChatGptTurnIdentity, priorChatGptAbortedTurnIds } from "./environment";
|
|
26
|
+
import { CHATGPT_WEB_LUNA_MODEL_ID, resolveChatGptWebModelMode, type ChatGptWebCapabilities } from "./model";
|
|
27
|
+
import { chatGptReadOnlyContextWarning, compileChatGptWebPrompt } from "./prompt";
|
|
28
|
+
import { createChatGptStructuredOutputValidator } from "./output-validation";
|
|
29
|
+
import { ChatGptToolStreamParser, type ParsedToolCall } from "./tool-stream-parser";
|
|
30
|
+
import { chatGptWebTurnRetryPolicy } from "./retry-policy";
|
|
31
|
+
import { TurnBroker, type BrokerToolRequest, type BrokerToolResult, type TurnBrokerOwner } from "./turn-broker";
|
|
32
|
+
import { ChatGptTextFeed, ChatGptTraceFeed, chatGptCompactionSourceExecutionKey, chatGptThreadOwnershipKey, chatGptTurnExecutionKey, chatGptTurnRetryKey, chatGptTurnRoundKey, chatGptTurnSessions, type ChatGptBrowserOutcome, type ChatGptTraceEvent, type ChatGptTurnRuntime, type ChatGptTurnSession } from "./turn-execution";
|
|
33
|
+
import { estimateChatGptWebUsage, resolveBiggerContextMultipartParts } from "./usage";
|
|
34
|
+
import { ChatGptThreadEnvironmentStore } from "./thread-environment";
|
|
35
|
+
import {
|
|
36
|
+
ChatGptLunaCheckpointStore,
|
|
37
|
+
type CapturedChatGptLunaCheckpoint,
|
|
38
|
+
} from "./rolling-checkpoint";
|
|
39
|
+
import { ChatGptExternalTurnProgress } from "./turn-progress";
|
|
40
|
+
import {
|
|
41
|
+
canonicalizeCompactionHandoff,
|
|
42
|
+
existingStructuredCompactionRun,
|
|
43
|
+
MAX_COMPACTION_HANDOFF_TIMEOUT_MS,
|
|
44
|
+
requestRetainedCompactionHandoff,
|
|
45
|
+
runStructuredCompactionOnce,
|
|
46
|
+
settleActiveCompactionSource,
|
|
47
|
+
settleActiveZeroRiskCompactionSource,
|
|
48
|
+
} from "./compaction-handoff";
|
|
49
|
+
import {
|
|
50
|
+
chatGptConversationKey,
|
|
51
|
+
retainedConversationResumeRequest,
|
|
52
|
+
} from "./conversation-key";
|
|
53
|
+
|
|
54
|
+
function extractLatestUserPrompt(parsed: CodexParsedRequest): string {
|
|
55
|
+
const messages = parsed.context.messages ?? [];
|
|
56
|
+
const userTexts: string[] = [];
|
|
57
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
58
|
+
const msg = messages[i];
|
|
59
|
+
if (msg && msg.role === "user") {
|
|
60
|
+
let text = "";
|
|
61
|
+
if (typeof msg.content === "string") {
|
|
62
|
+
text = msg.content;
|
|
63
|
+
} else if (Array.isArray(msg.content)) {
|
|
64
|
+
text = msg.content
|
|
65
|
+
.map(part => {
|
|
66
|
+
if (typeof part === "string") return part;
|
|
67
|
+
if (part && "text" in part && typeof (part as { text?: string }).text === "string") return (part as { text: string }).text;
|
|
68
|
+
return "";
|
|
69
|
+
})
|
|
70
|
+
.join(" ");
|
|
71
|
+
}
|
|
72
|
+
text = text.trim();
|
|
73
|
+
if (!text) continue;
|
|
74
|
+
// Skip operational or system-injected messages
|
|
75
|
+
if (
|
|
76
|
+
text.startsWith("Time sampled") ||
|
|
77
|
+
text.includes("<environment_context>") ||
|
|
78
|
+
text.includes("<system-reminder>") ||
|
|
79
|
+
text.includes("Context injection") ||
|
|
80
|
+
text.startsWith("Turn checkpoint")
|
|
81
|
+
) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
userTexts.push(text);
|
|
85
|
+
if (userTexts.length >= 5) break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return userTexts.join(" ");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function brokerSocketPath(provider: CodexProviderConfig): string {
|
|
92
|
+
const configured = provider.chatgptWeb?.brokerSocketPath?.trim();
|
|
93
|
+
return resolveBrokerEndpoint(configured || defaultBrokerEndpoint());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void; reject: (error: Error) => void } {
|
|
97
|
+
let resolvePromise!: (value: T) => void;
|
|
98
|
+
let rejectPromise!: (error: Error) => void;
|
|
99
|
+
const promise = new Promise<T>((resolveDeferred, rejectDeferred) => {
|
|
100
|
+
resolvePromise = resolveDeferred;
|
|
101
|
+
rejectPromise = rejectDeferred;
|
|
102
|
+
});
|
|
103
|
+
return { promise, resolve: resolvePromise, reject: rejectPromise };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function abortError(signal?: AbortSignal): Error {
|
|
107
|
+
if (signal?.reason instanceof ChatGptWebAdapterError) return signal.reason;
|
|
108
|
+
return new DOMException("ChatGPT web turn aborted", "AbortError");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function withAbort<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
|
|
112
|
+
if (!signal) return promise;
|
|
113
|
+
if (signal.aborted) return Promise.reject(abortError(signal));
|
|
114
|
+
return new Promise<T>((resolveWait, rejectWait) => {
|
|
115
|
+
const onAbort = () => rejectWait(abortError(signal));
|
|
116
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
117
|
+
promise.then(
|
|
118
|
+
value => {
|
|
119
|
+
signal.removeEventListener("abort", onAbort);
|
|
120
|
+
resolveWait(value);
|
|
121
|
+
},
|
|
122
|
+
error => {
|
|
123
|
+
signal.removeEventListener("abort", onAbort);
|
|
124
|
+
rejectWait(error);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function cancellableBrowserTurn(
|
|
131
|
+
run: Promise<string>,
|
|
132
|
+
controller: AbortController,
|
|
133
|
+
): { browser: Promise<string>; physicalSettlement: Promise<void>; cancel: (reason?: Error) => void } {
|
|
134
|
+
let rejectCancellation!: (error: Error) => void;
|
|
135
|
+
const cancellation = new Promise<never>((_resolve, reject) => {
|
|
136
|
+
rejectCancellation = reject;
|
|
137
|
+
});
|
|
138
|
+
let cancellationRejected = false;
|
|
139
|
+
return {
|
|
140
|
+
// Cancellation wins immediately even while the detached Playwright helper is still unwinding.
|
|
141
|
+
// The helper keeps the same abort signal and remains responsible for its normal end/cleanup
|
|
142
|
+
// handshake, but the Codex Responses turn no longer waits on that process cleanup.
|
|
143
|
+
browser: Promise.race([run, cancellation]),
|
|
144
|
+
// `browser` is the fast client-facing result. Replacement ownership must wait for the actual
|
|
145
|
+
// worker promise, whose finally block completes the launcher /turn/end handshake.
|
|
146
|
+
physicalSettlement: run.then(() => undefined, () => undefined),
|
|
147
|
+
cancel(reason?: Error) {
|
|
148
|
+
if (!controller.signal.aborted) controller.abort(reason);
|
|
149
|
+
// Explicit targeted cancellation ends the Codex Responses turn immediately. Generic
|
|
150
|
+
// retirement (client disconnect or compaction replacement) still waits for the helper's
|
|
151
|
+
// cleanup handshake before a replacement browser may start.
|
|
152
|
+
if (reason && !cancellationRejected) {
|
|
153
|
+
cancellationRejected = true;
|
|
154
|
+
rejectCancellation(reason);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface ChatGptZeroRiskManualControl {
|
|
161
|
+
start(descriptorPath: string, activity: LauncherManualTurnStart): Promise<unknown>;
|
|
162
|
+
waitSent(
|
|
163
|
+
descriptorPath: string,
|
|
164
|
+
owner: LauncherManualTurnOwner,
|
|
165
|
+
options?: { abortSignal?: AbortSignal; timeoutMs?: number },
|
|
166
|
+
): Promise<unknown>;
|
|
167
|
+
waitTerminal(
|
|
168
|
+
descriptorPath: string,
|
|
169
|
+
owner: LauncherManualTurnOwner,
|
|
170
|
+
options?: { abortSignal?: AbortSignal; timeoutMs?: number },
|
|
171
|
+
): Promise<{ status: "cancelled" | "failed" }>;
|
|
172
|
+
markStarted(descriptorPath: string, owner: LauncherManualTurnOwner): Promise<void>;
|
|
173
|
+
end(descriptorPath: string, activity: LauncherManualTurnEnd): Promise<unknown>;
|
|
174
|
+
cancel(descriptorPath: string, owner: LauncherManualTurnOwner): Promise<void>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const launcherZeroRiskManualControl: ChatGptZeroRiskManualControl = {
|
|
178
|
+
start: startLauncherManualTurn,
|
|
179
|
+
waitSent: waitForLauncherManualSent,
|
|
180
|
+
waitTerminal: waitForLauncherManualTerminal,
|
|
181
|
+
markStarted: markLauncherManualTurnStarted,
|
|
182
|
+
end: endLauncherManualTurn,
|
|
183
|
+
cancel: cancelLauncherManualTurn,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
function safeManualAdapterError(error: unknown): Error {
|
|
187
|
+
if (error instanceof DOMException && error.name === "AbortError") return error;
|
|
188
|
+
if (error instanceof ChatGptWebAdapterError) return error;
|
|
189
|
+
if (error instanceof LauncherManualTurnTimedOutError) {
|
|
190
|
+
return new ChatGptWebAdapterError(error.message, {
|
|
191
|
+
status: 408,
|
|
192
|
+
errorType: "invalid_request_error",
|
|
193
|
+
code: "manual_handoff_timeout",
|
|
194
|
+
retryable: false,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (error instanceof LauncherBrowserTurnCancelledError) {
|
|
198
|
+
return new ChatGptWebAdapterError(error.message, {
|
|
199
|
+
status: 409,
|
|
200
|
+
errorType: "invalid_request_error",
|
|
201
|
+
code: "manual_turn_cancelled",
|
|
202
|
+
retryable: false,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (error instanceof LauncherManualTurnFailedError) {
|
|
206
|
+
return new ChatGptWebAdapterError(error.message, {
|
|
207
|
+
status: 502,
|
|
208
|
+
errorType: "server_error",
|
|
209
|
+
code: "manual_launcher_failed",
|
|
210
|
+
retryable: false,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function safeManualTerminalError(status: "cancelled" | "failed"): ChatGptWebAdapterError {
|
|
217
|
+
if (status === "cancelled") {
|
|
218
|
+
return new ChatGptWebAdapterError("The Zero Risk browser turn was cancelled in the Launcher", {
|
|
219
|
+
status: 409,
|
|
220
|
+
errorType: "invalid_request_error",
|
|
221
|
+
code: "manual_turn_cancelled",
|
|
222
|
+
retryable: false,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return new ChatGptWebAdapterError("The Zero Risk browser tab failed before ChatGPT completed the turn", {
|
|
226
|
+
status: 502,
|
|
227
|
+
errorType: "server_error",
|
|
228
|
+
code: "manual_launcher_failed",
|
|
229
|
+
retryable: false,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function chatGptWebExecutionNamespace(provider: CodexProviderConfig): string {
|
|
234
|
+
return createHash("sha256").update(JSON.stringify({
|
|
235
|
+
baseUrl: provider.baseUrl,
|
|
236
|
+
chatgptWeb: provider.chatgptWeb ?? {},
|
|
237
|
+
})).digest("hex");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function chatGptWebTraceId(provider: CodexProviderConfig, parsed: CodexParsedRequest): string {
|
|
241
|
+
return createHash("sha256")
|
|
242
|
+
.update(`${chatGptWebExecutionNamespace(provider)}:${chatGptTurnExecutionKey(parsed)}`)
|
|
243
|
+
.digest("hex")
|
|
244
|
+
.slice(0, 12);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function structuredContent(text: string): unknown | undefined {
|
|
248
|
+
try {
|
|
249
|
+
const parsed: unknown = JSON.parse(text);
|
|
250
|
+
return parsed !== null && typeof parsed === "object" ? parsed : undefined;
|
|
251
|
+
} catch {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function brokerContent(content: string | CodexContentPart[]): unknown[] {
|
|
257
|
+
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
258
|
+
return content.map(part => {
|
|
259
|
+
if (part.type === "text") return { type: "text", text: part.text };
|
|
260
|
+
const parsed = parseDataUrl(part.imageUrl);
|
|
261
|
+
if (parsed) return { type: "image", data: parsed.base64, mimeType: parsed.mediaType };
|
|
262
|
+
return { type: "resource_link", uri: part.imageUrl, name: "Codex tool image", mimeType: "image/*" };
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function brokerResult(message: CodexToolResultMessage): BrokerToolResult {
|
|
267
|
+
const content = brokerContent(message.content);
|
|
268
|
+
const text = typeof message.content === "string"
|
|
269
|
+
? message.content
|
|
270
|
+
: message.content.filter(part => part.type === "text").map(part => part.text).join("\n");
|
|
271
|
+
const structured = structuredContent(text);
|
|
272
|
+
return {
|
|
273
|
+
content,
|
|
274
|
+
...(structured !== undefined ? { structuredContent: structured } : {}),
|
|
275
|
+
...(message.isError ? { isError: true } : {}),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function emitToolBatch(requests: BrokerToolRequest[], usage: CodexUsage, emit: (event: AdapterEvent) => void): void {
|
|
280
|
+
for (const request of requests) {
|
|
281
|
+
emit({ type: "tool_call_start", id: request.callId, name: request.wireName });
|
|
282
|
+
emit({
|
|
283
|
+
type: "tool_call_delta",
|
|
284
|
+
arguments: request.freeform
|
|
285
|
+
? JSON.stringify({ input: request.input ?? "" })
|
|
286
|
+
: JSON.stringify(request.arguments ?? {}),
|
|
287
|
+
});
|
|
288
|
+
emit({ type: "tool_call_end" });
|
|
289
|
+
}
|
|
290
|
+
emit({ type: "done", stopReason: "tool_use", endTurn: false, usage });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function emitBrowserCompletion(outcome: ChatGptBrowserOutcome, usage: CodexUsage, emit: (event: AdapterEvent) => void): void {
|
|
294
|
+
if (outcome.type === "error") throw outcome.error;
|
|
295
|
+
emit({ type: "done", stopReason: "stop", endTurn: true, usage });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function emitTraceEvents(trace: ChatGptTraceEvent[], emit: (event: AdapterEvent) => void): void {
|
|
299
|
+
for (const event of trace) {
|
|
300
|
+
if (!event.continuation) emit({ type: "assistant_boundary" });
|
|
301
|
+
if (event.kind === "commentary") {
|
|
302
|
+
emit({ type: "text_delta", text: event.text, phase: "commentary" });
|
|
303
|
+
} else {
|
|
304
|
+
emit({ type: "thinking_delta", thinking: event.text });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function emitTextDeltas(deltas: string[], emit: (event: AdapterEvent) => void): void {
|
|
310
|
+
for (const text of deltas) emit({ type: "text_delta", text, phase: "final_answer" });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function emitReadOnlyContextWarning(
|
|
314
|
+
parsed: CodexParsedRequest,
|
|
315
|
+
capabilities: ChatGptWebCapabilities,
|
|
316
|
+
emit: (event: AdapterEvent) => void,
|
|
317
|
+
): void {
|
|
318
|
+
const warning = chatGptReadOnlyContextWarning(parsed, capabilities);
|
|
319
|
+
if (!warning) return;
|
|
320
|
+
emit({ type: "assistant_boundary" });
|
|
321
|
+
emit({ type: "text_delta", text: warning, phase: "commentary" });
|
|
322
|
+
emit({ type: "assistant_boundary" });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function replayEvents(events: AdapterEvent[], emit: (event: AdapterEvent) => void): void {
|
|
326
|
+
for (const event of events) emit(event);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function submittedTurnFailure(session: ChatGptTurnSession, error: unknown): Error {
|
|
330
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
331
|
+
if (normalized instanceof ChatGptWebAdapterError) return normalized;
|
|
332
|
+
const phase = session.runtime.submission?.phase;
|
|
333
|
+
if (!phase || phase === "prepared") return normalized;
|
|
334
|
+
const ambiguous = phase === "send_activated";
|
|
335
|
+
return new ChatGptWebAdapterError(
|
|
336
|
+
ambiguous
|
|
337
|
+
? "ChatGPT did not confirm that the prompt was sent. Check the ChatGPT tab before continuing."
|
|
338
|
+
: "ChatGPT stopped responding after the task started. Check the ChatGPT tab before continuing.",
|
|
339
|
+
{
|
|
340
|
+
status: 502,
|
|
341
|
+
errorType: "server_error",
|
|
342
|
+
code: ambiguous ? "chatgpt_submission_ambiguous" : "chatgpt_submitted_turn_failed",
|
|
343
|
+
retryable: false,
|
|
344
|
+
cause: normalized,
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function currentToolResults(parsed: CodexParsedRequest, session: ChatGptTurnSession): CodexToolResultMessage[] {
|
|
350
|
+
const byId = new Map<string, CodexToolResultMessage>();
|
|
351
|
+
for (const message of parsed.context.messages) {
|
|
352
|
+
if (message.role !== "toolResult" || !session.hasOutstanding(message.toolCallId)) continue;
|
|
353
|
+
if (byId.has(message.toolCallId)) throw new Error(`Codex returned duplicate results for tool call ${message.toolCallId}`);
|
|
354
|
+
byId.set(message.toolCallId, message);
|
|
355
|
+
}
|
|
356
|
+
return [...byId.values()];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function validateBatchTools(parsed: CodexParsedRequest, requests: BrokerToolRequest[]): void {
|
|
360
|
+
const available = new Set((parsed.context.tools ?? []).map(tool => namespacedToolName(tool.namespace, tool.name)));
|
|
361
|
+
for (const request of requests) {
|
|
362
|
+
if (!available.has(request.wireName)) {
|
|
363
|
+
throw new Error(`ChatGPT requested a tool that the active Codex round did not advertise: ${request.wireName}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Keep the Responses bridge alive during every awaited phase of a browser turn. */
|
|
369
|
+
export const CHATGPT_WEB_ADAPTER_HEARTBEAT_MS = 10_000;
|
|
370
|
+
|
|
371
|
+
export function createChatGptWebAdapter(
|
|
372
|
+
provider: CodexProviderConfig,
|
|
373
|
+
dependencies: {
|
|
374
|
+
broker?: TurnBrokerOwner;
|
|
375
|
+
zeroRiskManualControl?: ChatGptZeroRiskManualControl;
|
|
376
|
+
} = {},
|
|
377
|
+
): ProviderAdapter {
|
|
378
|
+
const worker = ChatGptBrowserWorker.forProvider(provider);
|
|
379
|
+
const broker = dependencies.broker ?? TurnBroker.forSocket(brokerSocketPath(provider));
|
|
380
|
+
const zeroRiskManualControl = dependencies.zeroRiskManualControl ?? launcherZeroRiskManualControl;
|
|
381
|
+
const structuredBroker = broker instanceof TurnBroker ? broker : undefined;
|
|
382
|
+
const timeoutMs = provider.chatgptWeb?.turnTimeoutMs;
|
|
383
|
+
const experimentalBiggerContext = provider.chatgptWeb?.experimentalBiggerContext;
|
|
384
|
+
if (experimentalBiggerContext !== undefined && typeof experimentalBiggerContext !== "boolean") {
|
|
385
|
+
throw new Error("ChatGPT Bigger Context preference must be a boolean");
|
|
386
|
+
}
|
|
387
|
+
const configuredCapabilities: ChatGptWebCapabilities = {
|
|
388
|
+
localToolsEnabled: provider.chatgptWeb?.localToolsEnabled === true,
|
|
389
|
+
solAvailable: provider.chatgptWeb?.solAvailable !== false,
|
|
390
|
+
proAvailable: provider.chatgptWeb?.proAvailable === true,
|
|
391
|
+
};
|
|
392
|
+
const manualInteraction = provider.chatgptWeb?.browserInteractionMode === "manual";
|
|
393
|
+
const executionNamespace = chatGptWebExecutionNamespace(provider);
|
|
394
|
+
const retainedLauncherDescriptor = provider.chatgptWeb?.browserHost === "launcher"
|
|
395
|
+
&& provider.chatgptWeb.browserHostDescriptorPath
|
|
396
|
+
? resolve(expandUserPath(provider.chatgptWeb.browserHostDescriptorPath))
|
|
397
|
+
: undefined;
|
|
398
|
+
if (manualInteraction) {
|
|
399
|
+
if (!configuredCapabilities.localToolsEnabled) {
|
|
400
|
+
throw new Error("ChatGPT Zero Risk requires the Full Codex harness");
|
|
401
|
+
}
|
|
402
|
+
if (!retainedLauncherDescriptor) {
|
|
403
|
+
throw new Error("ChatGPT Zero Risk requires the Launcher browser host");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const environmentStore = new ChatGptThreadEnvironmentStore(
|
|
407
|
+
provider.chatgptWeb?.threadEnvironmentStatePath
|
|
408
|
+
? resolve(expandUserPath(provider.chatgptWeb.threadEnvironmentStatePath))
|
|
409
|
+
: undefined,
|
|
410
|
+
);
|
|
411
|
+
const lunaCheckpointStore = new ChatGptLunaCheckpointStore(
|
|
412
|
+
provider.chatgptWeb?.lunaCheckpointStatePath
|
|
413
|
+
? resolve(expandUserPath(provider.chatgptWeb.lunaCheckpointStatePath))
|
|
414
|
+
: undefined,
|
|
415
|
+
);
|
|
416
|
+
const currentUsageInput = (parsed: CodexParsedRequest): CodexParsedRequest => (
|
|
417
|
+
parsed.modelId === CHATGPT_WEB_LUNA_MODEL_ID && !parsed._compactionRequest
|
|
418
|
+
? lunaCheckpointStore.apply(parsed).parsed
|
|
419
|
+
: parsed
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const startRuntime = (
|
|
423
|
+
parsed: CodexParsedRequest,
|
|
424
|
+
environment: ReturnType<typeof extractChatGptTurnEnvironment> | undefined,
|
|
425
|
+
traceId: string,
|
|
426
|
+
turnCapabilities: ChatGptWebCapabilities,
|
|
427
|
+
): ChatGptTurnRuntime => {
|
|
428
|
+
const manualRequest = isChatGptWebZeroRiskBackendModel(parsed.modelId);
|
|
429
|
+
if (manualRequest !== manualInteraction) {
|
|
430
|
+
throw new Error(
|
|
431
|
+
manualInteraction
|
|
432
|
+
? "ChatGPT Zero Risk requires the Zero Risk Web model route"
|
|
433
|
+
: "The Zero Risk Web model route requires ChatGPT Zero Risk interaction mode",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
const mode = manualRequest
|
|
437
|
+
? { localTools: true }
|
|
438
|
+
: resolveChatGptWebModelMode(parsed.modelId, parsed.options.reasoning, turnCapabilities);
|
|
439
|
+
const identity = extractChatGptTurnIdentity(parsed);
|
|
440
|
+
const captureLunaCheckpoint = parsed.modelId === CHATGPT_WEB_LUNA_MODEL_ID
|
|
441
|
+
&& !parsed._compactionRequest
|
|
442
|
+
&& Boolean(identity.threadId && identity.turnId);
|
|
443
|
+
const checkpointInput = captureLunaCheckpoint
|
|
444
|
+
? lunaCheckpointStore.apply(parsed)
|
|
445
|
+
: { parsed, applied: false };
|
|
446
|
+
const conversationKey = !parsed._compactionRequest
|
|
447
|
+
&& parsed.modelId !== CHATGPT_WEB_LUNA_MODEL_ID
|
|
448
|
+
&& mode.localTools
|
|
449
|
+
&& retainedLauncherDescriptor
|
|
450
|
+
? chatGptConversationKey(checkpointInput.parsed, executionNamespace)
|
|
451
|
+
: undefined;
|
|
452
|
+
const resumeInput = conversationKey
|
|
453
|
+
? retainedConversationResumeRequest(checkpointInput.parsed)
|
|
454
|
+
: undefined;
|
|
455
|
+
const retainConversation = conversationKey !== undefined;
|
|
456
|
+
const releaseRetainedConversation = conversationKey && retainedLauncherDescriptor
|
|
457
|
+
? async () => {
|
|
458
|
+
await releaseLauncherRetainedConversation(retainedLauncherDescriptor, conversationKey);
|
|
459
|
+
}
|
|
460
|
+
: undefined;
|
|
461
|
+
const compileOptionsFor = (input: CodexParsedRequest) => {
|
|
462
|
+
if (manualRequest) return {};
|
|
463
|
+
const experimentalMultipartParts = experimentalBiggerContext
|
|
464
|
+
? resolveBiggerContextMultipartParts(input, turnCapabilities)
|
|
465
|
+
: undefined;
|
|
466
|
+
return {
|
|
467
|
+
captureLunaCheckpoint,
|
|
468
|
+
...(experimentalMultipartParts !== undefined
|
|
469
|
+
? { experimentalMultipartParts }
|
|
470
|
+
: {}),
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
if (captureLunaCheckpoint) {
|
|
474
|
+
console.info(
|
|
475
|
+
`[chatgpt-web] Luna rolling checkpoint applied=${checkpointInput.applied}${checkpointInput.reason ? ` reason=${checkpointInput.reason}` : ""}`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
let capturedCheckpoint: CapturedChatGptLunaCheckpoint | undefined;
|
|
479
|
+
let checkpointCaptureError: Error | undefined;
|
|
480
|
+
const captureCheckpoint = (captured: CapturedChatGptLunaCheckpoint): void => {
|
|
481
|
+
if (capturedCheckpoint) {
|
|
482
|
+
checkpointCaptureError = new Error("ChatGPT Luna emitted more than one rolling checkpoint");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
capturedCheckpoint = captured;
|
|
486
|
+
};
|
|
487
|
+
const finalizeCheckpoint = (browser: Promise<string>): Promise<string> => browser.then(answer => {
|
|
488
|
+
if (!captureLunaCheckpoint) return answer;
|
|
489
|
+
if (checkpointCaptureError) throw checkpointCaptureError;
|
|
490
|
+
if (capturedCheckpoint) lunaCheckpointStore.commit(parsed, capturedCheckpoint, answer);
|
|
491
|
+
return answer;
|
|
492
|
+
});
|
|
493
|
+
const browserAbort = new AbortController();
|
|
494
|
+
let browserOwnerSettled = false;
|
|
495
|
+
const trackBrowserOwner = (browser: Promise<string>): Promise<string> => browser.finally(() => {
|
|
496
|
+
browserOwnerSettled = true;
|
|
497
|
+
});
|
|
498
|
+
const trace = new ChatGptTraceFeed();
|
|
499
|
+
const text = new ChatGptTextFeed();
|
|
500
|
+
const observedCapabilityTokens = new Set<string>();
|
|
501
|
+
const observeCapabilityRetirement = (
|
|
502
|
+
turnToken: string,
|
|
503
|
+
externalProgress: ChatGptExternalTurnProgress,
|
|
504
|
+
): void => {
|
|
505
|
+
if (observedCapabilityTokens.has(turnToken)) return;
|
|
506
|
+
observedCapabilityTokens.add(turnToken);
|
|
507
|
+
void broker.waitForRetirement(turnToken).then(
|
|
508
|
+
() => {
|
|
509
|
+
const retirement = new Error("Codex Native retired the turn binding before its tool work completed");
|
|
510
|
+
externalProgress.retire(retirement);
|
|
511
|
+
if (!browserOwnerSettled && !browserAbort.signal.aborted) browserAbort.abort(retirement);
|
|
512
|
+
},
|
|
513
|
+
error => {
|
|
514
|
+
const failure = new Error("ChatGPT could not observe Codex Native turn retirement", {
|
|
515
|
+
cause: error,
|
|
516
|
+
});
|
|
517
|
+
externalProgress.retire(failure);
|
|
518
|
+
if (!browserAbort.signal.aborted) browserAbort.abort(failure);
|
|
519
|
+
},
|
|
520
|
+
);
|
|
521
|
+
};
|
|
522
|
+
const submission: NonNullable<ChatGptTurnRuntime["submission"]> = { phase: "prepared" };
|
|
523
|
+
// A canonical compaction request is side-effect free and remains safe to rebuild after an
|
|
524
|
+
// ambiguous browser send. Normal task prompts must never be replayed after Send activation.
|
|
525
|
+
const submissionLifecycle = parsed._compactionRequest ? {} : {
|
|
526
|
+
onSendActivated: () => { submission.phase = "send_activated" as const; },
|
|
527
|
+
onSubmitted: () => { submission.phase = "accepted" as const; },
|
|
528
|
+
};
|
|
529
|
+
if (manualRequest) {
|
|
530
|
+
if (!environment) throw new Error("ChatGPT Zero Risk requires a trusted Codex environment");
|
|
531
|
+
if (!retainedLauncherDescriptor) throw new Error("ChatGPT Zero Risk requires the Launcher browser host");
|
|
532
|
+
const token = deferred<string>();
|
|
533
|
+
const externalProgress = new ChatGptExternalTurnProgress();
|
|
534
|
+
const surfaceNonce = randomBytes(32).toString("base64url");
|
|
535
|
+
const owner: LauncherManualTurnOwner = { traceId, helperPid: process.pid };
|
|
536
|
+
let tokenSettled = false;
|
|
537
|
+
let activeToken: string | undefined;
|
|
538
|
+
let launcherStarted = false;
|
|
539
|
+
let launcherEnded = false;
|
|
540
|
+
const finishLauncher = async (status: LauncherManualTurnEnd["status"]): Promise<void> => {
|
|
541
|
+
if (!launcherStarted || launcherEnded) return;
|
|
542
|
+
await zeroRiskManualControl.end(retainedLauncherDescriptor, {
|
|
543
|
+
...owner,
|
|
544
|
+
status,
|
|
545
|
+
...(status === "completed" && retainConversation ? { retain: true } : {}),
|
|
546
|
+
});
|
|
547
|
+
launcherEnded = true;
|
|
548
|
+
};
|
|
549
|
+
const runManual = async (): Promise<string> => {
|
|
550
|
+
try {
|
|
551
|
+
activeToken = await broker.registerSafe(environment, surfaceNonce, undefined, traceId);
|
|
552
|
+
observeCapabilityRetirement(activeToken, externalProgress);
|
|
553
|
+
const compiled = compileChatGptWebPrompt(
|
|
554
|
+
checkpointInput.parsed,
|
|
555
|
+
turnCapabilities,
|
|
556
|
+
activeToken,
|
|
557
|
+
{ manualControl: true },
|
|
558
|
+
);
|
|
559
|
+
const resumeCompiled = resumeInput
|
|
560
|
+
? compileChatGptWebPrompt(
|
|
561
|
+
resumeInput,
|
|
562
|
+
turnCapabilities,
|
|
563
|
+
activeToken,
|
|
564
|
+
{ manualControl: true },
|
|
565
|
+
)
|
|
566
|
+
: undefined;
|
|
567
|
+
for (const candidate of [compiled, resumeCompiled]) {
|
|
568
|
+
if (!candidate) continue;
|
|
569
|
+
if (candidate.multipart) {
|
|
570
|
+
throw new ChatGptWebAdapterError("ChatGPT Zero Risk does not support multipart browser transport", {
|
|
571
|
+
status: 409,
|
|
572
|
+
errorType: "invalid_request_error",
|
|
573
|
+
code: "manual_multipart_unsupported",
|
|
574
|
+
retryable: false,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
tokenSettled = true;
|
|
579
|
+
token.resolve(activeToken);
|
|
580
|
+
if (!parsed._compactionRequest) {
|
|
581
|
+
trace.push({
|
|
582
|
+
kind: "commentary",
|
|
583
|
+
text: "> **Action required in Zero Risk**\n>\n> Open the launcher, copy and paste the prompt into ChatGPT, add any images yourself because Zero Risk cannot transfer them, select the `Codex Zero Risk` plugin and the model you want, send the prompt, then confirm it was sent in the launcher.",
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
await zeroRiskManualControl.start(retainedLauncherDescriptor, {
|
|
587
|
+
...owner,
|
|
588
|
+
prompt: compiled.text,
|
|
589
|
+
...(resumeCompiled ? { resumePrompt: resumeCompiled.text } : {}),
|
|
590
|
+
...(conversationKey ? { conversationKey } : {}),
|
|
591
|
+
});
|
|
592
|
+
launcherStarted = true;
|
|
593
|
+
await zeroRiskManualControl.waitSent(retainedLauncherDescriptor, owner, {
|
|
594
|
+
abortSignal: browserAbort.signal,
|
|
595
|
+
});
|
|
596
|
+
await broker.confirmSafeTurnSent(activeToken, surfaceNonce);
|
|
597
|
+
submission.phase = "accepted";
|
|
598
|
+
if (!parsed._compactionRequest) trace.push({
|
|
599
|
+
kind: "commentary",
|
|
600
|
+
text: "> **Waiting for ChatGPT**\n>\n> The prompt is marked `Sent`. Waiting for `Codex Zero Risk` to bind this turn through the selected ChatGPT connector.",
|
|
601
|
+
});
|
|
602
|
+
const terminalAbort = new AbortController();
|
|
603
|
+
const abortTerminal = () => terminalAbort.abort();
|
|
604
|
+
browserAbort.signal.addEventListener("abort", abortTerminal, { once: true });
|
|
605
|
+
const terminalFailure = zeroRiskManualControl.waitTerminal(
|
|
606
|
+
retainedLauncherDescriptor,
|
|
607
|
+
owner,
|
|
608
|
+
{ abortSignal: terminalAbort.signal },
|
|
609
|
+
).then(observed => Promise.reject(safeManualTerminalError(observed.status)))
|
|
610
|
+
.catch(error => terminalAbort.signal.aborted
|
|
611
|
+
? new Promise<never>(() => {})
|
|
612
|
+
: Promise.reject(error));
|
|
613
|
+
let answer: string;
|
|
614
|
+
try {
|
|
615
|
+
await Promise.race([
|
|
616
|
+
broker.waitForSafeStart(activeToken, browserAbort.signal),
|
|
617
|
+
terminalFailure,
|
|
618
|
+
]);
|
|
619
|
+
await zeroRiskManualControl.markStarted(retainedLauncherDescriptor, owner);
|
|
620
|
+
if (!parsed._compactionRequest) trace.push({
|
|
621
|
+
kind: "commentary",
|
|
622
|
+
text: "> **Zero Risk connected**\n>\n> `Codex Zero Risk` is connected. ChatGPT is now working through the native Codex harness; progress remains visible in the launcher.",
|
|
623
|
+
});
|
|
624
|
+
answer = await Promise.race([
|
|
625
|
+
broker.waitForSafeCompletion(activeToken, browserAbort.signal),
|
|
626
|
+
terminalFailure,
|
|
627
|
+
]);
|
|
628
|
+
} finally {
|
|
629
|
+
terminalAbort.abort();
|
|
630
|
+
browserAbort.signal.removeEventListener("abort", abortTerminal);
|
|
631
|
+
}
|
|
632
|
+
text.push(answer);
|
|
633
|
+
try {
|
|
634
|
+
await finishLauncher("completed");
|
|
635
|
+
} catch (controlError) {
|
|
636
|
+
// The broker result is already authoritative. A launcher acknowledgement failure may
|
|
637
|
+
// leave UI cleanup pending, but it must not replace a completed Codex answer with an
|
|
638
|
+
// error or trigger a contradictory failed terminal mutation.
|
|
639
|
+
console.error(
|
|
640
|
+
`[chatgpt-web] completed Zero Risk turn but could not confirm launcher cleanup: ${controlError instanceof Error ? controlError.message : String(controlError)}`,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
return answer;
|
|
644
|
+
} catch (error) {
|
|
645
|
+
const normalized = safeManualAdapterError(error);
|
|
646
|
+
// Capture the causal state before our own cleanup revokes the broker capability. The
|
|
647
|
+
// retirement observer also aborts browserAbort, but that self-induced abort must not turn
|
|
648
|
+
// an ordinary launcher/runtime failure into a user cancellation.
|
|
649
|
+
const externallyAborted = browserAbort.signal.aborted;
|
|
650
|
+
if (activeToken) await Promise.resolve(broker.revoke(activeToken, normalized)).catch(() => {});
|
|
651
|
+
try {
|
|
652
|
+
await finishLauncher(externallyAborted ? "aborted" : "failed");
|
|
653
|
+
} catch (controlError) {
|
|
654
|
+
console.error(
|
|
655
|
+
`[chatgpt-web] failed to release Zero Risk launcher turn: ${controlError instanceof Error ? controlError.message : String(controlError)}`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
throw normalized;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
const browserTurn = cancellableBrowserTurn(trackBrowserOwner(runManual()), browserAbort);
|
|
662
|
+
void browserTurn.browser.catch(error => {
|
|
663
|
+
if (tokenSettled) return;
|
|
664
|
+
tokenSettled = true;
|
|
665
|
+
token.reject(error instanceof Error ? error : new Error(String(error)));
|
|
666
|
+
});
|
|
667
|
+
return {
|
|
668
|
+
mode: "tools",
|
|
669
|
+
token: token.promise,
|
|
670
|
+
externalProgress,
|
|
671
|
+
browser: browserTurn.browser,
|
|
672
|
+
physicalSettlement: browserTurn.physicalSettlement,
|
|
673
|
+
trace,
|
|
674
|
+
text,
|
|
675
|
+
usageInput: checkpointInput.parsed,
|
|
676
|
+
manualControl: { surfaceNonce },
|
|
677
|
+
...(conversationKey ? { conversationKey } : {}),
|
|
678
|
+
...(releaseRetainedConversation ? { releaseRetainedConversation } : {}),
|
|
679
|
+
retireCapability: async () => {
|
|
680
|
+
if (activeToken) await broker.revoke(activeToken);
|
|
681
|
+
},
|
|
682
|
+
submission,
|
|
683
|
+
cancel: (reason?: Error) => {
|
|
684
|
+
browserTurn.cancel(reason);
|
|
685
|
+
if (activeToken) {
|
|
686
|
+
void Promise.resolve(broker.revoke(activeToken, reason)).catch(error => {
|
|
687
|
+
console.error(`[chatgpt-web] failed to revoke cancelled Zero Risk request: ${error instanceof Error ? error.message : String(error)}`);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
if (!mode.localTools) {
|
|
694
|
+
const browserTurn = cancellableBrowserTurn(finalizeCheckpoint(worker.run({
|
|
695
|
+
traceId,
|
|
696
|
+
modelId: parsed.modelId,
|
|
697
|
+
reasoning: parsed.options.reasoning,
|
|
698
|
+
capabilities: turnCapabilities,
|
|
699
|
+
prepare: async () => ({
|
|
700
|
+
...compileChatGptWebPrompt(
|
|
701
|
+
checkpointInput.parsed,
|
|
702
|
+
turnCapabilities,
|
|
703
|
+
undefined,
|
|
704
|
+
compileOptionsFor(checkpointInput.parsed),
|
|
705
|
+
),
|
|
706
|
+
release: () => {},
|
|
707
|
+
}),
|
|
708
|
+
abortSignal: browserAbort.signal,
|
|
709
|
+
...(parsed._compactionRequest ? { compaction: true } : {}),
|
|
710
|
+
...submissionLifecycle,
|
|
711
|
+
onReasoningSummary: (text, continuation) => trace.push({ kind: "reasoning", text, ...(continuation ? { continuation: true } : {}) }),
|
|
712
|
+
onCommentary: (text, continuation) => trace.push({ kind: "commentary", text, ...(continuation ? { continuation: true } : {}) }),
|
|
713
|
+
onTextDelta: delta => text.push(delta),
|
|
714
|
+
...(captureLunaCheckpoint ? {
|
|
715
|
+
captureLunaCheckpoint: true,
|
|
716
|
+
onLunaCheckpoint: captureCheckpoint,
|
|
717
|
+
} : {}),
|
|
718
|
+
})), browserAbort);
|
|
719
|
+
return {
|
|
720
|
+
mode: "read-only",
|
|
721
|
+
browser: browserTurn.browser,
|
|
722
|
+
physicalSettlement: browserTurn.physicalSettlement,
|
|
723
|
+
trace,
|
|
724
|
+
text,
|
|
725
|
+
usageInput: checkpointInput.parsed,
|
|
726
|
+
submission,
|
|
727
|
+
cancel: browserTurn.cancel,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
if (!environment) throw new Error("Tool-capable ChatGPT web mode requires a trusted Codex environment");
|
|
731
|
+
const token = deferred<string>();
|
|
732
|
+
const externalProgress = new ChatGptExternalTurnProgress();
|
|
733
|
+
let tokenSettled = false;
|
|
734
|
+
let activeToken: string | undefined;
|
|
735
|
+
const prepareWith = async (input: CodexParsedRequest) => {
|
|
736
|
+
const turnToken = activeToken ?? await broker.register(
|
|
737
|
+
environment,
|
|
738
|
+
timeoutMs === undefined ? undefined : timeoutMs + 60_000,
|
|
739
|
+
traceId,
|
|
740
|
+
);
|
|
741
|
+
activeToken = turnToken;
|
|
742
|
+
observeCapabilityRetirement(turnToken, externalProgress);
|
|
743
|
+
if (!tokenSettled) {
|
|
744
|
+
tokenSettled = true;
|
|
745
|
+
token.resolve(turnToken);
|
|
746
|
+
}
|
|
747
|
+
try {
|
|
748
|
+
const compiled = compileChatGptWebPrompt(
|
|
749
|
+
input,
|
|
750
|
+
turnCapabilities,
|
|
751
|
+
turnToken,
|
|
752
|
+
compileOptionsFor(input),
|
|
753
|
+
);
|
|
754
|
+
return { ...compiled, release: () => {} };
|
|
755
|
+
} catch (error) {
|
|
756
|
+
await broker.revoke(turnToken);
|
|
757
|
+
activeToken = undefined;
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
const browserTurn = cancellableBrowserTurn(trackBrowserOwner(finalizeCheckpoint(worker.run({
|
|
762
|
+
traceId,
|
|
763
|
+
modelId: parsed.modelId,
|
|
764
|
+
reasoning: parsed.options.reasoning,
|
|
765
|
+
capabilities: turnCapabilities,
|
|
766
|
+
prepare: () => prepareWith(checkpointInput.parsed),
|
|
767
|
+
...(resumeInput ? { prepareResume: () => prepareWith(resumeInput) } : {}),
|
|
768
|
+
...(retainConversation ? { retainConversation: true, conversationKey } : {}),
|
|
769
|
+
abortSignal: browserAbort.signal,
|
|
770
|
+
...(parsed._compactionRequest ? { compaction: true } : {}),
|
|
771
|
+
...submissionLifecycle,
|
|
772
|
+
onReasoningSummary: (text, continuation) => trace.push({ kind: "reasoning", text, ...(continuation ? { continuation: true } : {}) }),
|
|
773
|
+
onCommentary: (text, continuation) => trace.push({ kind: "commentary", text, ...(continuation ? { continuation: true } : {}) }),
|
|
774
|
+
onTextDelta: delta => text.push(delta),
|
|
775
|
+
externalProgress,
|
|
776
|
+
completionFence: {
|
|
777
|
+
begin: async () => broker.beginCompletionFence(await token.promise),
|
|
778
|
+
commit: async revision => broker.commitCompletionFence(await token.promise, revision),
|
|
779
|
+
},
|
|
780
|
+
...(captureLunaCheckpoint ? {
|
|
781
|
+
captureLunaCheckpoint: true,
|
|
782
|
+
onLunaCheckpoint: captureCheckpoint,
|
|
783
|
+
} : {}),
|
|
784
|
+
}))), browserAbort);
|
|
785
|
+
void browserTurn.browser.catch(error => {
|
|
786
|
+
if (!tokenSettled) {
|
|
787
|
+
tokenSettled = true;
|
|
788
|
+
token.reject(error instanceof Error ? error : new Error(String(error)));
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
return {
|
|
792
|
+
mode: "tools",
|
|
793
|
+
token: token.promise,
|
|
794
|
+
externalProgress,
|
|
795
|
+
browser: browserTurn.browser,
|
|
796
|
+
physicalSettlement: browserTurn.physicalSettlement,
|
|
797
|
+
trace,
|
|
798
|
+
text,
|
|
799
|
+
usageInput: checkpointInput.parsed,
|
|
800
|
+
...(conversationKey ? { conversationKey } : {}),
|
|
801
|
+
...(releaseRetainedConversation ? { releaseRetainedConversation } : {}),
|
|
802
|
+
retireCapability: async () => {
|
|
803
|
+
if (activeToken) await broker.revoke(activeToken);
|
|
804
|
+
},
|
|
805
|
+
submission,
|
|
806
|
+
cancel: (reason?: Error) => {
|
|
807
|
+
browserTurn.cancel(reason);
|
|
808
|
+
if (activeToken) {
|
|
809
|
+
void Promise.resolve(broker.revoke(activeToken, reason)).catch(error => {
|
|
810
|
+
console.error(`[chatgpt-web] failed to revoke cancelled turn token: ${error instanceof Error ? error.message : String(error)}`);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
};
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
return {
|
|
818
|
+
name: "chatgpt-web",
|
|
819
|
+
async runTurn(parsed, incoming, emit) {
|
|
820
|
+
const runChatGptWebTurn = async (): Promise<void> => {
|
|
821
|
+
const manualRequest = isChatGptWebZeroRiskBackendModel(parsed.modelId);
|
|
822
|
+
if (manualRequest !== manualInteraction) {
|
|
823
|
+
emit({
|
|
824
|
+
type: "error",
|
|
825
|
+
message: manualInteraction
|
|
826
|
+
? "ChatGPT Zero Risk requires the Zero Risk Web model route."
|
|
827
|
+
: "The Zero Risk Web model route is unavailable while automatic browser interaction is enabled.",
|
|
828
|
+
status: 409,
|
|
829
|
+
errorType: "invalid_request_error",
|
|
830
|
+
code: "browser_interaction_mode_mismatch",
|
|
831
|
+
retryable: false,
|
|
832
|
+
});
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const turnCapabilities = parsed._compactionRequest && !manualRequest
|
|
836
|
+
? { ...configuredCapabilities, localToolsEnabled: false }
|
|
837
|
+
: configuredCapabilities;
|
|
838
|
+
const mode = manualRequest
|
|
839
|
+
? { localTools: true }
|
|
840
|
+
: resolveChatGptWebModelMode(parsed.modelId, parsed.options.reasoning, turnCapabilities);
|
|
841
|
+
const structuredOutputValidator = parsed._compactionRequest
|
|
842
|
+
? undefined
|
|
843
|
+
: createChatGptStructuredOutputValidator(parsed.options.outputFormat);
|
|
844
|
+
const bufferStructuredOutput = structuredOutputValidator !== undefined;
|
|
845
|
+
const retryKey = `${executionNamespace}:${chatGptTurnRetryKey(parsed)}`;
|
|
846
|
+
const exhaustedRetry = chatGptWebTurnRetryPolicy.exhaustedError(retryKey);
|
|
847
|
+
if (exhaustedRetry) {
|
|
848
|
+
emit({
|
|
849
|
+
type: "error",
|
|
850
|
+
message: exhaustedRetry.message,
|
|
851
|
+
status: exhaustedRetry.status,
|
|
852
|
+
errorType: exhaustedRetry.errorType,
|
|
853
|
+
code: exhaustedRetry.code,
|
|
854
|
+
retryable: false,
|
|
855
|
+
});
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
let environment: ReturnType<typeof extractChatGptTurnEnvironment> | undefined;
|
|
859
|
+
if (mode.localTools) {
|
|
860
|
+
try {
|
|
861
|
+
environment = environmentStore.resolve(parsed);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
const identity = extractChatGptTurnIdentity(parsed);
|
|
864
|
+
console.warn(
|
|
865
|
+
`[chatgpt-web] trusted environment unavailable (thread_id=${identity.threadId ? "present" : "missing"}, turn_id=${identity.turnId ? "present" : "missing"}, previous_response_id=${parsed.previousResponseId ?? "none"}, replay_prefix_items=${parsed._replayPrefixLen ?? 0}, context_messages=${parsed.context.messages.length})`,
|
|
866
|
+
);
|
|
867
|
+
throw error;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (parsed._compactionRequest) {
|
|
871
|
+
const structuredCompactionRequired = parsed.modelId !== CHATGPT_WEB_LUNA_MODEL_ID
|
|
872
|
+
&& configuredCapabilities.localToolsEnabled;
|
|
873
|
+
if (structuredCompactionRequired
|
|
874
|
+
&& (!retainedLauncherDescriptor || (!manualRequest && !structuredBroker))) {
|
|
875
|
+
emit({
|
|
876
|
+
type: "error",
|
|
877
|
+
message: manualRequest
|
|
878
|
+
? "Zero Risk could not resume the active ChatGPT conversation for context handoff. Retry the task from the Launcher."
|
|
879
|
+
: "ChatGPT could not resume the active conversation for context handoff. Retry the task.",
|
|
880
|
+
status: 409,
|
|
881
|
+
errorType: "invalid_request_error",
|
|
882
|
+
code: "compaction_control_unavailable",
|
|
883
|
+
retryable: false,
|
|
884
|
+
});
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (structuredCompactionRequired) {
|
|
888
|
+
const compactionExecutionKey = `${executionNamespace}:${chatGptTurnExecutionKey(parsed)}`;
|
|
889
|
+
const compactedSourceExecutionKey = `${executionNamespace}:${chatGptCompactionSourceExecutionKey(parsed)}`;
|
|
890
|
+
const handoffTraceId = createHash("sha256")
|
|
891
|
+
.update(`${compactionExecutionKey}:handoff`)
|
|
892
|
+
.digest("hex")
|
|
893
|
+
.slice(0, 12);
|
|
894
|
+
const compactionTraceId = createHash("sha256")
|
|
895
|
+
.update(compactionExecutionKey)
|
|
896
|
+
.digest("hex")
|
|
897
|
+
.slice(0, 12);
|
|
898
|
+
const compactionNativeIdentity = extractChatGptTurnIdentity(parsed);
|
|
899
|
+
let sharedSummary = existingStructuredCompactionRun(compactionExecutionKey);
|
|
900
|
+
if (!sharedSummary) {
|
|
901
|
+
sharedSummary = runStructuredCompactionOnce(
|
|
902
|
+
compactionExecutionKey,
|
|
903
|
+
{
|
|
904
|
+
ownerKey: `${executionNamespace}:${chatGptThreadOwnershipKey(parsed)}`,
|
|
905
|
+
traceIds: [
|
|
906
|
+
compactionTraceId,
|
|
907
|
+
handoffTraceId,
|
|
908
|
+
`${handoffTraceId}_fallback`,
|
|
909
|
+
],
|
|
910
|
+
...(compactionNativeIdentity.threadId
|
|
911
|
+
? { nativeThreadId: compactionNativeIdentity.threadId }
|
|
912
|
+
: {}),
|
|
913
|
+
...(compactionNativeIdentity.turnId
|
|
914
|
+
? { nativeTurnId: compactionNativeIdentity.turnId }
|
|
915
|
+
: {}),
|
|
916
|
+
},
|
|
917
|
+
async operatorSignal => {
|
|
918
|
+
const handoffTimeoutMs = Math.min(
|
|
919
|
+
timeoutMs ?? MAX_COMPACTION_HANDOFF_TIMEOUT_MS,
|
|
920
|
+
MAX_COMPACTION_HANDOFF_TIMEOUT_MS,
|
|
921
|
+
);
|
|
922
|
+
const handoffDeadline = new AbortController();
|
|
923
|
+
const handoffTimeoutError = new ChatGptWebAdapterError(
|
|
924
|
+
`ChatGPT compaction did not fully settle within ${handoffTimeoutMs}ms`,
|
|
925
|
+
{
|
|
926
|
+
status: 409,
|
|
927
|
+
errorType: "invalid_request_error",
|
|
928
|
+
code: "compaction_handoff_timeout",
|
|
929
|
+
retryable: false,
|
|
930
|
+
},
|
|
931
|
+
);
|
|
932
|
+
const handoffTimer = setTimeout(
|
|
933
|
+
() => handoffDeadline.abort(handoffTimeoutError),
|
|
934
|
+
handoffTimeoutMs,
|
|
935
|
+
);
|
|
936
|
+
handoffTimer.unref?.();
|
|
937
|
+
const operationSignal = AbortSignal.any([operatorSignal, handoffDeadline.signal]);
|
|
938
|
+
const sourceConversationKey = chatGptConversationKey(parsed, executionNamespace);
|
|
939
|
+
const runFreshCompactionFallback = async (reason: string): Promise<string> => {
|
|
940
|
+
console.warn(`[chatgpt-web] retained compaction fallback=${reason}`);
|
|
941
|
+
const fallbackRuntime = startRuntime(
|
|
942
|
+
parsed,
|
|
943
|
+
manualRequest ? environment : undefined,
|
|
944
|
+
`${handoffTraceId}_fallback`,
|
|
945
|
+
turnCapabilities,
|
|
946
|
+
);
|
|
947
|
+
try {
|
|
948
|
+
const rawSummary = await withAbort(fallbackRuntime.browser, operationSignal);
|
|
949
|
+
await withAbort(fallbackRuntime.physicalSettlement, operationSignal);
|
|
950
|
+
return canonicalizeCompactionHandoff(parsed, rawSummary);
|
|
951
|
+
} catch (error) {
|
|
952
|
+
fallbackRuntime.cancel(error instanceof Error ? error : new Error(String(error)));
|
|
953
|
+
await withAbort(
|
|
954
|
+
fallbackRuntime.physicalSettlement,
|
|
955
|
+
// Operator cancellation must still honor the physical fallback owner.
|
|
956
|
+
// The handoff deadline is independent, so cancel-all cannot acknowledge
|
|
957
|
+
// before the Launcher/worker cleanup handshake has completed.
|
|
958
|
+
handoffDeadline.signal,
|
|
959
|
+
).catch(() => {});
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
let source: ChatGptTurnSession | undefined;
|
|
964
|
+
let preserveFinalResponse = false;
|
|
965
|
+
try {
|
|
966
|
+
// The previous compaction may already have detached the retained head while
|
|
967
|
+
// its browser/helper is still unwinding. Do not inspect that old epoch or
|
|
968
|
+
// decide to open a fresh fallback until physical release has completed.
|
|
969
|
+
if (sourceConversationKey) {
|
|
970
|
+
await chatGptTurnSessions.waitForConversationRetirement(
|
|
971
|
+
sourceConversationKey,
|
|
972
|
+
operationSignal,
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
source = sourceConversationKey
|
|
976
|
+
? chatGptTurnSessions.findConversationHead(sourceConversationKey)
|
|
977
|
+
: undefined;
|
|
978
|
+
preserveFinalResponse = !source?.isActive()
|
|
979
|
+
&& source?.settledOutcome()?.type === "final";
|
|
980
|
+
const retainedKey = source?.conversationKey();
|
|
981
|
+
if (!source || !retainedKey) {
|
|
982
|
+
return await runFreshCompactionFallback("source_unavailable_before_handoff");
|
|
983
|
+
}
|
|
984
|
+
let rawSummary: string;
|
|
985
|
+
if (manualRequest && source.isActive() && source.runtime.mode === "tools") {
|
|
986
|
+
const zeroRiskSummary = await settleActiveZeroRiskCompactionSource(
|
|
987
|
+
parsed,
|
|
988
|
+
source,
|
|
989
|
+
broker,
|
|
990
|
+
operationSignal,
|
|
991
|
+
);
|
|
992
|
+
if (zeroRiskSummary === undefined) {
|
|
993
|
+
preserveFinalResponse = true;
|
|
994
|
+
rawSummary = await runFreshCompactionFallback("zero_risk_source_had_no_compaction_boundary");
|
|
995
|
+
} else {
|
|
996
|
+
rawSummary = zeroRiskSummary;
|
|
997
|
+
}
|
|
998
|
+
} else if (manualRequest) {
|
|
999
|
+
if (source.isActive()) {
|
|
1000
|
+
const outcome = await withAbort(source.browserOutcome, operationSignal);
|
|
1001
|
+
if (outcome.type === "error") throw outcome.error;
|
|
1002
|
+
await withAbort(source.physicalSettlement, operationSignal);
|
|
1003
|
+
preserveFinalResponse = true;
|
|
1004
|
+
}
|
|
1005
|
+
rawSummary = await runFreshCompactionFallback("zero_risk_source_already_completed");
|
|
1006
|
+
} else if (source.isActive() && source.runtime.mode === "tools") {
|
|
1007
|
+
const settlement = await settleActiveCompactionSource(
|
|
1008
|
+
parsed,
|
|
1009
|
+
source,
|
|
1010
|
+
structuredBroker!,
|
|
1011
|
+
operationSignal,
|
|
1012
|
+
);
|
|
1013
|
+
preserveFinalResponse = !settlement.compactionInstructionDelivered;
|
|
1014
|
+
rawSummary = await requestRetainedCompactionHandoff(
|
|
1015
|
+
worker,
|
|
1016
|
+
parsed,
|
|
1017
|
+
source,
|
|
1018
|
+
structuredBroker!,
|
|
1019
|
+
configuredCapabilities,
|
|
1020
|
+
handoffTraceId,
|
|
1021
|
+
operationSignal,
|
|
1022
|
+
handoffTimeoutMs,
|
|
1023
|
+
);
|
|
1024
|
+
} else {
|
|
1025
|
+
if (source.isActive()) {
|
|
1026
|
+
const outcome = await withAbort(source.browserOutcome, operationSignal);
|
|
1027
|
+
if (outcome.type === "error") throw outcome.error;
|
|
1028
|
+
await withAbort(source.physicalSettlement, operationSignal);
|
|
1029
|
+
preserveFinalResponse = true;
|
|
1030
|
+
}
|
|
1031
|
+
rawSummary = await requestRetainedCompactionHandoff(
|
|
1032
|
+
worker,
|
|
1033
|
+
parsed,
|
|
1034
|
+
source,
|
|
1035
|
+
structuredBroker!,
|
|
1036
|
+
configuredCapabilities,
|
|
1037
|
+
handoffTraceId,
|
|
1038
|
+
operationSignal,
|
|
1039
|
+
handoffTimeoutMs,
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
const summary = canonicalizeCompactionHandoff(parsed, rawSummary);
|
|
1043
|
+
await withAbort(
|
|
1044
|
+
preserveFinalResponse
|
|
1045
|
+
? chatGptTurnSessions.retireConversationPreservingFinalResponse(
|
|
1046
|
+
retainedKey,
|
|
1047
|
+
source,
|
|
1048
|
+
compactedSourceExecutionKey,
|
|
1049
|
+
)
|
|
1050
|
+
: chatGptTurnSessions.retireConversationAndWait(retainedKey),
|
|
1051
|
+
operationSignal,
|
|
1052
|
+
);
|
|
1053
|
+
return summary;
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
const retainedKey = source?.conversationKey();
|
|
1056
|
+
if (!retainedKey) throw error;
|
|
1057
|
+
let handoffError = error instanceof Error ? error : new Error(String(error));
|
|
1058
|
+
try {
|
|
1059
|
+
// Operator cancellation ends the logical compaction, but cancel-all must not
|
|
1060
|
+
// acknowledge until the retained browser/helper owner has physically retired.
|
|
1061
|
+
await (preserveFinalResponse
|
|
1062
|
+
? chatGptTurnSessions.retireConversationPreservingFinalResponse(
|
|
1063
|
+
retainedKey,
|
|
1064
|
+
source!,
|
|
1065
|
+
compactedSourceExecutionKey,
|
|
1066
|
+
)
|
|
1067
|
+
: chatGptTurnSessions.retireConversationAndWait(retainedKey));
|
|
1068
|
+
} catch (retirementError) {
|
|
1069
|
+
handoffError = new AggregateError(
|
|
1070
|
+
[handoffError, retirementError instanceof Error ? retirementError : new Error(String(retirementError))],
|
|
1071
|
+
"Structured compaction failed and its retained conversation could not be retired",
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
if (handoffError instanceof ChatGptWebAdapterError
|
|
1075
|
+
&& handoffError.code === "compaction_source_unavailable") {
|
|
1076
|
+
return await runFreshCompactionFallback("source_disappeared_before_handoff");
|
|
1077
|
+
}
|
|
1078
|
+
throw handoffError;
|
|
1079
|
+
} finally {
|
|
1080
|
+
clearTimeout(handoffTimer);
|
|
1081
|
+
}
|
|
1082
|
+
},
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
emit({ type: "heartbeat" });
|
|
1086
|
+
let summary: string;
|
|
1087
|
+
try {
|
|
1088
|
+
summary = await withAbort(sharedSummary, incoming.abortSignal);
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
if (incoming.abortSignal?.aborted
|
|
1091
|
+
&& error instanceof DOMException
|
|
1092
|
+
&& error.name === "AbortError") {
|
|
1093
|
+
// The observer detached; the shared exact compaction round continues and remains
|
|
1094
|
+
// available to a canonical reconnect without a second browser submission.
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
const handoffError = error instanceof Error ? error : new Error(String(error));
|
|
1098
|
+
console.error("[chatgpt-web] structured context handoff failed:", handoffError);
|
|
1099
|
+
emit({
|
|
1100
|
+
type: "error",
|
|
1101
|
+
message: "ChatGPT did not complete the context handoff. Retry the task.",
|
|
1102
|
+
status: 409,
|
|
1103
|
+
errorType: "invalid_request_error",
|
|
1104
|
+
code: "compaction_handoff_failed",
|
|
1105
|
+
retryable: false,
|
|
1106
|
+
});
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
emit({ type: "text_delta", text: summary, phase: "final_answer" });
|
|
1110
|
+
emitBrowserCompletion(
|
|
1111
|
+
{ type: "final", answer: summary },
|
|
1112
|
+
estimateChatGptWebUsage(parsed, { answer: summary, reasoning: [] }, turnCapabilities),
|
|
1113
|
+
emit,
|
|
1114
|
+
);
|
|
1115
|
+
chatGptWebTurnRetryPolicy.clear(retryKey);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const responseExecutionKey = `${executionNamespace}:${chatGptCompactionSourceExecutionKey(parsed)}`;
|
|
1119
|
+
await chatGptTurnSessions.retireAndWait(responseExecutionKey, incoming.abortSignal);
|
|
1120
|
+
}
|
|
1121
|
+
const executionKey = `${executionNamespace}:${chatGptTurnExecutionKey(parsed)}`;
|
|
1122
|
+
const ownerKey = `${executionNamespace}:${chatGptThreadOwnershipKey(parsed)}`;
|
|
1123
|
+
const nativeIdentity = extractChatGptTurnIdentity(parsed);
|
|
1124
|
+
const nativeTurnId = nativeIdentity.turnId;
|
|
1125
|
+
if (!nativeTurnId) throw new Error("ChatGPT web requires native Codex turn_id metadata for browser ownership");
|
|
1126
|
+
const abortedTurnIds = manualRequest ? new Set(priorChatGptAbortedTurnIds(parsed)) : undefined;
|
|
1127
|
+
if (abortedTurnIds?.size) {
|
|
1128
|
+
chatGptTurnSessions.retireAbortedOwnerTurns(ownerKey, abortedTurnIds, executionKey);
|
|
1129
|
+
}
|
|
1130
|
+
const traceId = createHash("sha256").update(executionKey).digest("hex").slice(0, 12);
|
|
1131
|
+
const session = await chatGptTurnSessions.getOrCreateAfterOwnerRetirement(
|
|
1132
|
+
executionKey,
|
|
1133
|
+
ownerKey,
|
|
1134
|
+
() => startRuntime(parsed, environment, traceId, turnCapabilities),
|
|
1135
|
+
traceId,
|
|
1136
|
+
incoming.abortSignal,
|
|
1137
|
+
nativeTurnId,
|
|
1138
|
+
nativeIdentity.threadId,
|
|
1139
|
+
);
|
|
1140
|
+
const roundKey = chatGptTurnRoundKey(parsed);
|
|
1141
|
+
const emitRoundEvents = (events: readonly AdapterEvent[]): void => {
|
|
1142
|
+
// Journal the complete synchronous event batch before touching the HTTP observer. If the
|
|
1143
|
+
// observer disconnects midway through emission, an exact reconnect can replay the entire
|
|
1144
|
+
// canonical batch instead of losing the already-drained tail.
|
|
1145
|
+
session.appendRoundEvents(roundKey, events);
|
|
1146
|
+
for (const event of events) emit(event);
|
|
1147
|
+
};
|
|
1148
|
+
const emitRoundBatch = (
|
|
1149
|
+
produce: (buffer: (event: AdapterEvent) => void) => void,
|
|
1150
|
+
): void => {
|
|
1151
|
+
const events: AdapterEvent[] = [];
|
|
1152
|
+
produce(event => events.push(event));
|
|
1153
|
+
emitRoundEvents(events);
|
|
1154
|
+
};
|
|
1155
|
+
const emitRoundEvent = (event: AdapterEvent): void => emitRoundEvents([event]);
|
|
1156
|
+
try {
|
|
1157
|
+
await session.runExclusive(async () => {
|
|
1158
|
+
const replay = session.roundEvents(roundKey);
|
|
1159
|
+
replayEvents(replay, emit);
|
|
1160
|
+
if (session.roundCompleted(roundKey)) {
|
|
1161
|
+
const failure = session.roundFailure(roundKey);
|
|
1162
|
+
if (failure) throw failure;
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
if (session.roundHasTerminalEvent(roundKey)) {
|
|
1166
|
+
session.completeRound(roundKey);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const settled = session.settledOutcome();
|
|
1170
|
+
if (settled) {
|
|
1171
|
+
if (settled.type === "error") throw settled.error;
|
|
1172
|
+
const trace = session.runtime.trace.drain();
|
|
1173
|
+
const completedTextDeltas = session.runtime.text.drain();
|
|
1174
|
+
const finalReplay = replay.length === 0
|
|
1175
|
+
&& trace.length === 0
|
|
1176
|
+
&& completedTextDeltas.length === 0
|
|
1177
|
+
? session.eventsForFinalReplay()
|
|
1178
|
+
: [];
|
|
1179
|
+
if (finalReplay.length > 0) {
|
|
1180
|
+
session.appendRoundReasoning(roundKey, session.reasoningForFinalReplay());
|
|
1181
|
+
emitRoundEvents(finalReplay);
|
|
1182
|
+
} else {
|
|
1183
|
+
const userPrompt = extractLatestUserPrompt(parsed);
|
|
1184
|
+
const streamParser = new ChatGptToolStreamParser(userPrompt);
|
|
1185
|
+
const collectedToolCalls: ParsedToolCall[] = [];
|
|
1186
|
+
const parsedDeltas: string[] = [];
|
|
1187
|
+
const parsedThinking: string[] = [];
|
|
1188
|
+
for (const delta of completedTextDeltas) {
|
|
1189
|
+
const chunk = streamParser.feed(delta);
|
|
1190
|
+
if (chunk.thinking) parsedThinking.push(chunk.thinking);
|
|
1191
|
+
if (chunk.text) parsedDeltas.push(chunk.text);
|
|
1192
|
+
if (chunk.toolCalls.length > 0) collectedToolCalls.push(...chunk.toolCalls);
|
|
1193
|
+
}
|
|
1194
|
+
const flushed = streamParser.flush();
|
|
1195
|
+
if (flushed.thinking) parsedThinking.push(flushed.thinking);
|
|
1196
|
+
if (flushed.text) parsedDeltas.push(flushed.text);
|
|
1197
|
+
if (flushed.toolCalls.length > 0) collectedToolCalls.push(...flushed.toolCalls);
|
|
1198
|
+
|
|
1199
|
+
const traceTexts = trace.map(event => event.text);
|
|
1200
|
+
session.appendRoundReasoning(roundKey, [...traceTexts, ...parsedThinking]);
|
|
1201
|
+
if (replay.length === 0 && !parsed._compactionRequest) {
|
|
1202
|
+
emitRoundBatch(buffer => emitReadOnlyContextWarning(parsed, turnCapabilities, buffer));
|
|
1203
|
+
}
|
|
1204
|
+
emitRoundBatch(buffer => emitTraceEvents(trace, buffer));
|
|
1205
|
+
for (const thinking of parsedThinking) {
|
|
1206
|
+
emitRoundBatch(buffer => buffer({ type: "thinking_delta", thinking }));
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (turnCapabilities.localToolsEnabled && collectedToolCalls.length > 0) {
|
|
1210
|
+
console.info(`[chatgpt-web] collectedToolCalls:`, JSON.stringify(collectedToolCalls));
|
|
1211
|
+
const requests: BrokerToolRequest[] = collectedToolCalls.map(tc => ({
|
|
1212
|
+
callId: tc.id,
|
|
1213
|
+
wireName: tc.name,
|
|
1214
|
+
freeform: false,
|
|
1215
|
+
arguments: tc.arguments,
|
|
1216
|
+
}));
|
|
1217
|
+
session.setOutstanding(requests, session.roundReasoning(roundKey), session.roundEvents(roundKey));
|
|
1218
|
+
emitRoundBatch(buffer => emitToolBatch(
|
|
1219
|
+
requests,
|
|
1220
|
+
estimateChatGptWebUsage(currentUsageInput(parsed), { reasoning: session.roundReasoning(roundKey), toolRequests: requests }, turnCapabilities),
|
|
1221
|
+
buffer,
|
|
1222
|
+
));
|
|
1223
|
+
session.completeRound(roundKey);
|
|
1224
|
+
chatGptWebTurnRetryPolicy.clear(retryKey);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (!bufferStructuredOutput) {
|
|
1229
|
+
emitRoundBatch(buffer => emitTextDeltas(parsedDeltas, buffer));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (session.runtime.text.value() !== settled.answer) {
|
|
1233
|
+
throw new Error("ChatGPT browser Markdown stream did not reproduce the completed answer");
|
|
1234
|
+
}
|
|
1235
|
+
structuredOutputValidator?.(settled.answer);
|
|
1236
|
+
if (bufferStructuredOutput) {
|
|
1237
|
+
emitRoundBatch(buffer => emitTextDeltas([settled.answer], buffer));
|
|
1238
|
+
}
|
|
1239
|
+
const reasoning = session.roundReasoning(roundKey);
|
|
1240
|
+
session.setFinalReasoning(reasoning);
|
|
1241
|
+
session.setFinalEvents(session.roundEvents(roundKey));
|
|
1242
|
+
emitRoundBatch(buffer => emitBrowserCompletion(
|
|
1243
|
+
settled,
|
|
1244
|
+
estimateChatGptWebUsage(currentUsageInput(parsed), { answer: settled.answer, reasoning }, turnCapabilities),
|
|
1245
|
+
buffer,
|
|
1246
|
+
));
|
|
1247
|
+
session.completeRound(roundKey);
|
|
1248
|
+
chatGptWebTurnRetryPolicy.clear(retryKey);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
let turnToken: string | undefined;
|
|
1253
|
+
if (session.runtime.mode === "tools") {
|
|
1254
|
+
turnToken = await withAbort(session.runtime.token, incoming.abortSignal);
|
|
1255
|
+
if (!environment) throw new Error("Tool-capable ChatGPT web runtime lost its trusted environment");
|
|
1256
|
+
await broker.updateEnvironment(turnToken, environment);
|
|
1257
|
+
|
|
1258
|
+
const outstanding = session.outstanding();
|
|
1259
|
+
if (outstanding.length > 0) {
|
|
1260
|
+
const results = currentToolResults(parsed, session);
|
|
1261
|
+
if (results.length === 0) {
|
|
1262
|
+
const reasoning = session.reasoningForOutstandingReplay();
|
|
1263
|
+
if (replay.length === 0) emitRoundEvents(session.eventsForOutstandingReplay());
|
|
1264
|
+
emitRoundBatch(buffer => emitToolBatch(
|
|
1265
|
+
outstanding,
|
|
1266
|
+
estimateChatGptWebUsage(currentUsageInput(parsed), { reasoning, toolRequests: outstanding }, turnCapabilities),
|
|
1267
|
+
buffer,
|
|
1268
|
+
));
|
|
1269
|
+
session.completeRound(roundKey);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
if (results.length !== outstanding.length) {
|
|
1273
|
+
throw new Error(`Codex returned ${results.length} of ${outstanding.length} results for a parallel ChatGPT tool batch`);
|
|
1274
|
+
}
|
|
1275
|
+
for (const message of results) {
|
|
1276
|
+
await broker.completeTool(turnToken, message.toolCallId, brokerResult(message));
|
|
1277
|
+
session.runtime.externalProgress.recordToolResult();
|
|
1278
|
+
session.markResultDelivered(message.toolCallId);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
} else if (session.outstanding().length > 0) {
|
|
1282
|
+
const outstanding = session.outstanding();
|
|
1283
|
+
const reasoning = session.reasoningForOutstandingReplay();
|
|
1284
|
+
if (replay.length === 0) emitRoundEvents(session.eventsForOutstandingReplay());
|
|
1285
|
+
emitRoundBatch(buffer => emitToolBatch(
|
|
1286
|
+
outstanding,
|
|
1287
|
+
estimateChatGptWebUsage(currentUsageInput(parsed), { reasoning, toolRequests: outstanding }, turnCapabilities),
|
|
1288
|
+
buffer,
|
|
1289
|
+
));
|
|
1290
|
+
session.completeRound(roundKey);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const toolWaitAbort = new AbortController();
|
|
1295
|
+
try {
|
|
1296
|
+
const roundReasoning = session.roundReasoning(roundKey);
|
|
1297
|
+
const userPrompt = extractLatestUserPrompt(parsed);
|
|
1298
|
+
const streamParser = new ChatGptToolStreamParser(userPrompt);
|
|
1299
|
+
const collectedToolCalls: ParsedToolCall[] = [];
|
|
1300
|
+
|
|
1301
|
+
const emitNewTrace = (trace: ChatGptTraceEvent[]) => {
|
|
1302
|
+
roundReasoning.push(...trace.map(event => event.text));
|
|
1303
|
+
session.appendRoundReasoning(roundKey, trace.map(event => event.text));
|
|
1304
|
+
emitRoundBatch(buffer => emitTraceEvents(trace, buffer));
|
|
1305
|
+
};
|
|
1306
|
+
const emitNewText = (deltas: string[]) => {
|
|
1307
|
+
const textChunks: string[] = [];
|
|
1308
|
+
for (const delta of deltas) {
|
|
1309
|
+
const chunk = streamParser.feed(delta);
|
|
1310
|
+
if (chunk.thinking) {
|
|
1311
|
+
roundReasoning.push(chunk.thinking);
|
|
1312
|
+
session.appendRoundReasoning(roundKey, [chunk.thinking]);
|
|
1313
|
+
emitRoundBatch(buffer => buffer({ type: "thinking_delta", thinking: chunk.thinking }));
|
|
1314
|
+
}
|
|
1315
|
+
if (chunk.text) {
|
|
1316
|
+
textChunks.push(chunk.text);
|
|
1317
|
+
}
|
|
1318
|
+
if (turnCapabilities.localToolsEnabled && chunk.toolCalls.length > 0) {
|
|
1319
|
+
collectedToolCalls.push(...chunk.toolCalls);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (textChunks.length > 0 && !bufferStructuredOutput) {
|
|
1323
|
+
emitRoundBatch(buffer => emitTextDeltas(textChunks, buffer));
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
if (replay.length === 0 && !parsed._compactionRequest) {
|
|
1327
|
+
emitRoundBatch(buffer => emitReadOnlyContextWarning(parsed, turnCapabilities, buffer));
|
|
1328
|
+
}
|
|
1329
|
+
emitNewTrace(session.runtime.trace.drain());
|
|
1330
|
+
emitNewText(session.runtime.text.drain());
|
|
1331
|
+
const externalProgress = session.runtime.mode === "tools"
|
|
1332
|
+
? session.runtime.externalProgress
|
|
1333
|
+
: undefined;
|
|
1334
|
+
const armNextTools = () => turnToken
|
|
1335
|
+
? broker.nextToolBatch(turnToken, toolWaitAbort.signal).then(async requests => {
|
|
1336
|
+
if (!externalProgress) {
|
|
1337
|
+
throw new Error("ChatGPT broker returned tools for a read-only browser turn");
|
|
1338
|
+
}
|
|
1339
|
+
if (requests.length > 0) {
|
|
1340
|
+
const revision = externalProgress.recordToolBatch(requests.length);
|
|
1341
|
+
if (!session.runtime.manualControl) {
|
|
1342
|
+
// The browser outcome is in the same race below and owns the semantic DOM and
|
|
1343
|
+
// renderer deadlines. A second fixed timer here can retire an accepted turn
|
|
1344
|
+
// while its same-tab observer is still recovering. Keep the causal barrier —
|
|
1345
|
+
// tools are not emitted until the browser captures their text boundary — but
|
|
1346
|
+
// let browser settlement or request cancellation end the wait.
|
|
1347
|
+
await externalProgress.waitForToolBatchObservation(
|
|
1348
|
+
revision,
|
|
1349
|
+
toolWaitAbort.signal,
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
externalProgress.assertToolBatchActive(revision);
|
|
1353
|
+
}
|
|
1354
|
+
return { type: "tools" as const, requests };
|
|
1355
|
+
}).catch(error => toolWaitAbort.signal.aborted
|
|
1356
|
+
? new Promise<never>(() => {})
|
|
1357
|
+
: Promise.reject(error))
|
|
1358
|
+
: undefined;
|
|
1359
|
+
let nextTools = armNextTools();
|
|
1360
|
+
const browserOutcome = session.browserOutcome.then(outcome => ({ type: "browser" as const, outcome }));
|
|
1361
|
+
const finishBrowserOutcome = async (completedOutcome: ChatGptBrowserOutcome): Promise<void> => {
|
|
1362
|
+
// Zero Risk completion and its owner-only empty-batch signal are resolved by the
|
|
1363
|
+
// same broker transition. Drain once more so the accepted final answer cannot be
|
|
1364
|
+
// overtaken by the terminal owner notification.
|
|
1365
|
+
emitNewTrace(session.runtime.trace.drain());
|
|
1366
|
+
emitNewText(session.runtime.text.drain());
|
|
1367
|
+
const flushed = streamParser.flush();
|
|
1368
|
+
if (flushed.thinking) {
|
|
1369
|
+
roundReasoning.push(flushed.thinking);
|
|
1370
|
+
session.appendRoundReasoning(roundKey, [flushed.thinking]);
|
|
1371
|
+
emitRoundBatch(buffer => buffer({ type: "thinking_delta", thinking: flushed.thinking }));
|
|
1372
|
+
}
|
|
1373
|
+
if (flushed.text && !bufferStructuredOutput) {
|
|
1374
|
+
emitRoundBatch(buffer => emitTextDeltas([flushed.text], buffer));
|
|
1375
|
+
}
|
|
1376
|
+
if (flushed.toolCalls.length > 0) {
|
|
1377
|
+
collectedToolCalls.push(...flushed.toolCalls);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (turnToken) await broker.revoke(turnToken);
|
|
1381
|
+
if (completedOutcome.type === "error") throw completedOutcome.error;
|
|
1382
|
+
if (session.runtime.text.value() !== completedOutcome.answer) {
|
|
1383
|
+
throw new Error("ChatGPT browser Markdown stream did not reproduce the completed answer");
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (collectedToolCalls.length > 0) {
|
|
1387
|
+
console.info(`[chatgpt-web] collectedToolCalls:`, JSON.stringify(collectedToolCalls));
|
|
1388
|
+
const requests: BrokerToolRequest[] = collectedToolCalls.map(tc => ({
|
|
1389
|
+
callId: tc.id,
|
|
1390
|
+
wireName: tc.name,
|
|
1391
|
+
freeform: false,
|
|
1392
|
+
arguments: tc.arguments,
|
|
1393
|
+
}));
|
|
1394
|
+
session.setOutstanding(requests, roundReasoning, session.roundEvents(roundKey));
|
|
1395
|
+
emitRoundBatch(buffer => emitToolBatch(
|
|
1396
|
+
requests,
|
|
1397
|
+
estimateChatGptWebUsage(currentUsageInput(parsed), { reasoning: roundReasoning, toolRequests: requests }, turnCapabilities),
|
|
1398
|
+
buffer,
|
|
1399
|
+
));
|
|
1400
|
+
session.completeRound(roundKey);
|
|
1401
|
+
chatGptWebTurnRetryPolicy.clear(retryKey);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
session.setFinalReasoning(roundReasoning);
|
|
1406
|
+
session.setFinalEvents(session.roundEvents(roundKey));
|
|
1407
|
+
structuredOutputValidator?.(completedOutcome.answer);
|
|
1408
|
+
if (bufferStructuredOutput) {
|
|
1409
|
+
emitRoundBatch(buffer => emitTextDeltas([completedOutcome.answer], buffer));
|
|
1410
|
+
}
|
|
1411
|
+
emitRoundBatch(buffer => emitBrowserCompletion(
|
|
1412
|
+
completedOutcome,
|
|
1413
|
+
estimateChatGptWebUsage(currentUsageInput(parsed), { answer: completedOutcome.answer, reasoning: roundReasoning }, turnCapabilities),
|
|
1414
|
+
buffer,
|
|
1415
|
+
));
|
|
1416
|
+
session.completeRound(roundKey);
|
|
1417
|
+
chatGptWebTurnRetryPolicy.clear(retryKey);
|
|
1418
|
+
};
|
|
1419
|
+
const waitForTrace = () => session.runtime.trace.wait(toolWaitAbort.signal)
|
|
1420
|
+
.then(() => ({ type: "trace" as const }))
|
|
1421
|
+
.catch(error => toolWaitAbort.signal.aborted
|
|
1422
|
+
? new Promise<never>(() => {})
|
|
1423
|
+
: Promise.reject(error));
|
|
1424
|
+
const waitForText = () => session.runtime.text.wait(toolWaitAbort.signal)
|
|
1425
|
+
.then(() => ({ type: "text" as const }))
|
|
1426
|
+
.catch(error => toolWaitAbort.signal.aborted
|
|
1427
|
+
? new Promise<never>(() => {})
|
|
1428
|
+
: Promise.reject(error));
|
|
1429
|
+
let nextTrace = waitForTrace();
|
|
1430
|
+
let nextText = waitForText();
|
|
1431
|
+
for (;;) {
|
|
1432
|
+
const next = await withAbort(
|
|
1433
|
+
Promise.race([
|
|
1434
|
+
...(nextTools ? [nextTools] : []),
|
|
1435
|
+
browserOutcome,
|
|
1436
|
+
nextTrace,
|
|
1437
|
+
nextText,
|
|
1438
|
+
]),
|
|
1439
|
+
incoming.abortSignal,
|
|
1440
|
+
);
|
|
1441
|
+
if (next.type === "trace") {
|
|
1442
|
+
emitNewTrace(session.runtime.trace.drain());
|
|
1443
|
+
nextTrace = waitForTrace();
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
if (next.type === "text") {
|
|
1447
|
+
emitNewText(session.runtime.text.drain());
|
|
1448
|
+
nextText = waitForText();
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
emitNewTrace(session.runtime.trace.drain());
|
|
1452
|
+
emitNewText(session.runtime.text.drain());
|
|
1453
|
+
if (next.type === "browser") {
|
|
1454
|
+
await finishBrowserOutcome(next.outcome);
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
if (!turnToken || session.runtime.mode !== "tools" || !externalProgress) {
|
|
1458
|
+
throw new Error("Read-only ChatGPT Web runtime received a broker tool batch");
|
|
1459
|
+
}
|
|
1460
|
+
if (next.requests.length === 0) {
|
|
1461
|
+
if (!session.runtime.manualControl) {
|
|
1462
|
+
throw new Error("ChatGPT tool bridge returned an empty batch");
|
|
1463
|
+
}
|
|
1464
|
+
await finishBrowserOutcome(await session.browserOutcome);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
validateBatchTools(parsed, next.requests);
|
|
1468
|
+
session.setOutstanding(next.requests, roundReasoning, session.roundEvents(roundKey));
|
|
1469
|
+
emitRoundBatch(buffer => emitToolBatch(
|
|
1470
|
+
next.requests,
|
|
1471
|
+
estimateChatGptWebUsage(currentUsageInput(parsed), { reasoning: roundReasoning, toolRequests: next.requests }, turnCapabilities),
|
|
1472
|
+
buffer,
|
|
1473
|
+
));
|
|
1474
|
+
session.completeRound(roundKey);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
} finally {
|
|
1478
|
+
toolWaitAbort.abort();
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
if (incoming.abortSignal?.aborted && error instanceof DOMException && error.name === "AbortError") {
|
|
1483
|
+
if (session.runtime.manualControl) {
|
|
1484
|
+
// Zero Risk is user-driven and has no DOM observer that can distinguish continued
|
|
1485
|
+
// work from a stopped native turn. A closed Responses stream is therefore terminal:
|
|
1486
|
+
// revoke the MCP capability and release the Launcher tab instead of leaving a task
|
|
1487
|
+
// that Codex already shows as stopped waiting forever.
|
|
1488
|
+
chatGptTurnSessions.retire(executionKey, session);
|
|
1489
|
+
}
|
|
1490
|
+
// Automatic browser turns keep their exact execution and journal for reconnect. Their
|
|
1491
|
+
// owned DOM observer can continue proving the same accepted ChatGPT submission.
|
|
1492
|
+
throw error;
|
|
1493
|
+
}
|
|
1494
|
+
const turnError = submittedTurnFailure(session, error);
|
|
1495
|
+
const handledError = turnError instanceof ChatGptWebAdapterError && turnError.retryable
|
|
1496
|
+
? chatGptWebTurnRetryPolicy.recordRetryableFailure(retryKey, turnError)
|
|
1497
|
+
: turnError;
|
|
1498
|
+
if (!(turnError instanceof ChatGptWebAdapterError && turnError.retryable)) {
|
|
1499
|
+
chatGptWebTurnRetryPolicy.clear(retryKey);
|
|
1500
|
+
}
|
|
1501
|
+
if (handledError instanceof ChatGptWebAdapterError && !handledError.retryable) {
|
|
1502
|
+
// A deterministic request failure remains replayable so a native reconnect cannot burn
|
|
1503
|
+
// another browser attempt. Every other failure retires the browser session: client
|
|
1504
|
+
// disconnects, stage failures, and retryable ChatGPT errors must start a fresh surface
|
|
1505
|
+
// instead of replaying one rejected browser outcome for the registry's full TTL.
|
|
1506
|
+
session.cancel();
|
|
1507
|
+
} else {
|
|
1508
|
+
chatGptTurnSessions.retire(executionKey, session);
|
|
1509
|
+
}
|
|
1510
|
+
if (session.runtime.mode === "tools") {
|
|
1511
|
+
void session.runtime.token.then(turnToken => broker.revoke(turnToken)).catch(() => {});
|
|
1512
|
+
}
|
|
1513
|
+
if (handledError instanceof ChatGptWebAdapterError) {
|
|
1514
|
+
emitRoundEvent({
|
|
1515
|
+
type: "error",
|
|
1516
|
+
message: handledError.message,
|
|
1517
|
+
status: handledError.status,
|
|
1518
|
+
errorType: handledError.errorType,
|
|
1519
|
+
code: handledError.code,
|
|
1520
|
+
retryable: handledError.retryable,
|
|
1521
|
+
});
|
|
1522
|
+
session.completeRound(roundKey);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
session.failRound(roundKey, turnError);
|
|
1526
|
+
chatGptWebTurnRetryPolicy.clear(retryKey);
|
|
1527
|
+
throw turnError;
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
// Arm this before any awaited work, including environment lookup and owner retirement.
|
|
1532
|
+
const heartbeat = setInterval(
|
|
1533
|
+
() => emit({ type: "heartbeat" }),
|
|
1534
|
+
CHATGPT_WEB_ADAPTER_HEARTBEAT_MS,
|
|
1535
|
+
);
|
|
1536
|
+
try {
|
|
1537
|
+
emit({ type: "heartbeat" });
|
|
1538
|
+
await runChatGptWebTurn();
|
|
1539
|
+
} finally {
|
|
1540
|
+
clearInterval(heartbeat);
|
|
1541
|
+
}
|
|
1542
|
+
},
|
|
1543
|
+
};
|
|
1544
|
+
}
|