@wlv-zedd/dsh-chatgpt-web 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +140 -140
  3. package/cordis.patch.yml +4 -4
  4. package/lib/cli.js +13269 -11860
  5. package/lib/plugin.js +3 -50
  6. package/package.json +89 -88
  7. package/src/adapters/base.ts +16 -16
  8. package/src/adapters/chatgpt-web/adapter-error.ts +59 -59
  9. package/src/adapters/chatgpt-web/browser-helper-main.ts +513 -513
  10. package/src/adapters/chatgpt-web/browser-helper-prompt-selection.ts +27 -27
  11. package/src/adapters/chatgpt-web/browser-worker.ts +4944 -4944
  12. package/src/adapters/chatgpt-web/codex-rollout-environment.ts +628 -628
  13. package/src/adapters/chatgpt-web/compaction-handoff.ts +533 -533
  14. package/src/adapters/chatgpt-web/compaction-transaction.ts +142 -142
  15. package/src/adapters/chatgpt-web/concurrency.ts +6 -6
  16. package/src/adapters/chatgpt-web/conversation-key.ts +58 -58
  17. package/src/adapters/chatgpt-web/environment.ts +669 -669
  18. package/src/adapters/chatgpt-web/index.ts +1544 -1544
  19. package/src/adapters/chatgpt-web/input-tokens.ts +74 -74
  20. package/src/adapters/chatgpt-web/launcher-helper-client.ts +695 -695
  21. package/src/adapters/chatgpt-web/markdown.ts +418 -418
  22. package/src/adapters/chatgpt-web/mcp-main.ts +25 -25
  23. package/src/adapters/chatgpt-web/mcp-server.ts +933 -933
  24. package/src/adapters/chatgpt-web/model.ts +70 -70
  25. package/src/adapters/chatgpt-web/native-compaction-control.ts +74 -74
  26. package/src/adapters/chatgpt-web/output-validation.ts +62 -62
  27. package/src/adapters/chatgpt-web/process-line-writer.ts +46 -46
  28. package/src/adapters/chatgpt-web/prompt.ts +702 -702
  29. package/src/adapters/chatgpt-web/retry-policy.ts +73 -73
  30. package/src/adapters/chatgpt-web/rolling-checkpoint.ts +384 -384
  31. package/src/adapters/chatgpt-web/thread-environment.ts +238 -238
  32. package/src/adapters/chatgpt-web/tool-stream-parser.ts +1 -1
  33. package/src/adapters/chatgpt-web/turn-broker.ts +1481 -1481
  34. package/src/adapters/chatgpt-web/turn-execution.ts +816 -816
  35. package/src/adapters/chatgpt-web/turn-progress.ts +292 -292
  36. package/src/adapters/chatgpt-web/usage.ts +121 -121
  37. package/src/adapters/image.ts +9 -9
  38. package/src/bridge.ts +1083 -1083
  39. package/src/browser-login.ts +521 -521
  40. package/src/chatgpt-session.ts +240 -240
  41. package/src/chatgpt-web-models.ts +400 -400
  42. package/src/cli.ts +568 -568
  43. package/src/codex-integration-document.ts +824 -824
  44. package/src/codex-integration-journal.ts +212 -212
  45. package/src/codex-integration-route.ts +515 -515
  46. package/src/codex-integration-shared.ts +332 -332
  47. package/src/codex-integration.ts +529 -529
  48. package/src/codex-interrupt-hook.ts +158 -158
  49. package/src/config.ts +616 -616
  50. package/src/dev-chat/cli.ts +432 -432
  51. package/src/dev-chat/constants.ts +3 -3
  52. package/src/dev-chat/driver.ts +655 -655
  53. package/src/dev-chat/profile.ts +223 -223
  54. package/src/dev-chat/session.ts +287 -287
  55. package/src/dev-chat/transport.ts +54 -54
  56. package/src/doctor.ts +237 -237
  57. package/src/event-queue.ts +45 -45
  58. package/src/http-body.ts +30 -30
  59. package/src/launcher-browser-host.ts +695 -695
  60. package/src/lib/errors.ts +281 -281
  61. package/src/lib/token-estimate.ts +42 -42
  62. package/src/login-helper.cjs +1 -1
  63. package/src/model-catalog.ts +197 -197
  64. package/src/native-passthrough.ts +261 -261
  65. package/src/plugin.ts +191 -191
  66. package/src/process.ts +45 -45
  67. package/src/responses/compaction.ts +199 -199
  68. package/src/responses/parser.ts +633 -633
  69. package/src/responses/reasoning-envelope.ts +49 -49
  70. package/src/responses/schema.ts +172 -172
  71. package/src/responses/state.ts +230 -230
  72. package/src/server.ts +1111 -1111
  73. package/src/service.ts +315 -315
  74. package/src/setup.ts +671 -671
  75. package/src/stall-timeout.ts +23 -23
  76. package/src/tunnel-service.ts +160 -160
  77. package/src/tunnel.ts +417 -417
  78. package/src/turndown-plugin-gfm.d.ts +5 -5
  79. package/src/types.ts +307 -307
  80. package/src/usage/totals.ts +12 -12
  81. package/src/version.ts +1 -1
@@ -1,655 +1,655 @@
1
- import { existsSync } from "node:fs";
2
- import { join, resolve } from "node:path";
3
- import type { ProviderAdapter } from "../adapters/base";
4
- import { closeChatGptBrowserWorkers } from "../adapters/chatgpt-web/browser-worker";
5
- import { createChatGptWebAdapter } from "../adapters/chatgpt-web";
6
- import { estimateChatGptWebInputTokens } from "../adapters/chatgpt-web/usage";
7
- import { RemoteTurnBroker, type TurnBrokerOwner } from "../adapters/chatgpt-web/turn-broker";
8
- import {
9
- CHATGPT_LUNA_BROWSER_INPUT_TOKEN_BUDGET,
10
- } from "../adapters/chatgpt-web/input-tokens";
11
- import {
12
- CHATGPT_WEB_LUNA_BACKEND_MODEL,
13
- requireChatGptWebModelRoute,
14
- resolveChatGptWebContextLimits,
15
- } from "../chatgpt-web-models";
16
- import type { AppConfig } from "../config";
17
- import { parseRequest } from "../responses/parser";
18
- import { compactRequest, responseRequest, routeChatGptWebRequest } from "../server";
19
- import { namespacedToolName, type AdapterEvent, type CodexProviderConfig } from "../types";
20
- import {
21
- createDevCoherentContextPayload,
22
- createDevContextFiller,
23
- type DevChatModel,
24
- type DevChatState,
25
- type DevChatStore,
26
- type DevChatUsage,
27
- } from "./session";
28
-
29
- type AdapterFactory = (provider: CodexProviderConfig) => ProviderAdapter;
30
-
31
- export type DevChatEvent =
32
- | { type: "reasoning"; text: string }
33
- | { type: "commentary"; text: string }
34
- | { type: "text"; text: string }
35
- | { type: "tool_call"; name: string; input: unknown }
36
- | { type: "tool_result"; name: string; receipt: Record<string, unknown> }
37
- | { type: "compaction_start"; reason: "automatic" | "manual"; inputItems: number }
38
- | { type: "compaction_done"; reason: "automatic" | "manual"; inputItems: number };
39
-
40
- export interface DevContextStatus {
41
- model: DevChatModel;
42
- inputTokens: number;
43
- autoCompactTokenLimit: number;
44
- contextWindow: number;
45
- browserInputTokenLimit?: number;
46
- percent: number;
47
- inputItems: number;
48
- }
49
-
50
- export interface DevChatTurnResult {
51
- text: string;
52
- usage: DevChatUsage;
53
- toolCalls: number;
54
- compactions: number;
55
- status: DevContextStatus;
56
- }
57
-
58
- export interface DevChatFeatures {
59
- biggerContext: boolean;
60
- }
61
-
62
- const DEFAULT_DEV_CHAT_FEATURES: DevChatFeatures = { biggerContext: false };
63
-
64
- interface ResponsesEnvelope {
65
- id?: string;
66
- status?: string;
67
- output?: unknown[];
68
- end_turn?: boolean;
69
- usage?: {
70
- input_tokens?: number;
71
- output_tokens?: number;
72
- total_tokens?: number;
73
- };
74
- error?: { message?: string; type?: string; code?: string } | null;
75
- incomplete_details?: { reason?: string; message?: string } | null;
76
- }
77
-
78
- interface DevToolCall {
79
- kind: "function" | "custom";
80
- callId: string;
81
- name: string;
82
- input: unknown;
83
- }
84
-
85
- export const DEV_CHAT_SYSTEM_INSTRUCTIONS = [
86
- "You are running inside the Codex Web GPT DEV outer-harness simulator.",
87
- "Behave like the normal Codex model backend and use the available Codex Native tools whenever they help answer the user's request.",
88
- "Every outer tool result is an explicit simulation receipt. No command, file edit, image read, user prompt, or external side effect actually occurs.",
89
- "Never describe a simulated receipt as a real-world effect. Continue reasoning from the receipt exactly as test evidence for the transport flow.",
90
- ].join(" ");
91
-
92
- export const DEV_CHAT_BROWSER_ONLY_INSTRUCTIONS = [
93
- "You are running inside the Codex Web GPT DEV outer-harness simulator.",
94
- "Behave like the normal Codex model backend.",
95
- "This browser-only DEV profile exposes no outer tools. Do not claim that commands, file edits, UI actions, or external side effects occurred.",
96
- ].join(" ");
97
-
98
- const ANY_ARGUMENTS = { type: "object", additionalProperties: true } as const;
99
- const simulatedFunction = (name: string, description: string) => ({
100
- type: "function", name, parameters: ANY_ARGUMENTS,
101
- description: `DEV simulator: ${description}. Arguments are recorded and no side effect occurs.`,
102
- });
103
-
104
- const DEV_LARGE_CONTEXT_TOOL = "mcp__dev_simulator__large_context_payload";
105
- const largeContextPayloadTool = {
106
- type: "function",
107
- name: DEV_LARGE_CONTEXT_TOOL,
108
- description: "Return a deterministic, coherent, inert project dossier segment for context-retention and compaction tests.",
109
- parameters: {
110
- type: "object",
111
- properties: {
112
- segment: { type: "integer", enum: [1, 2, 3] },
113
- target_tokens: { type: "integer", minimum: 1_000, maximum: 95_000 },
114
- },
115
- required: ["segment", "target_tokens"],
116
- additionalProperties: false,
117
- },
118
- } as const;
119
-
120
- export const DEV_CHAT_TOOLS: readonly Record<string, unknown>[] = [
121
- simulatedFunction("exec_command", "native command execution"),
122
- simulatedFunction("write_stdin", "native command-session continuation"),
123
- { type: "custom", name: "apply_patch", description: "DEV simulator: records patch text and changes no file." },
124
- simulatedFunction("view_image", "native image inspection"),
125
- simulatedFunction("request_user_input", "native user input"),
126
- largeContextPayloadTool,
127
- {
128
- type: "namespace", name: "mcp__dev_simulator",
129
- description: "Synthetic deferred MCP tools for inventory and generic dispatch tests.",
130
- tools: [simulatedFunction("echo", "structured MCP echo")],
131
- },
132
- ];
133
-
134
- function xml(value: string): string {
135
- return value
136
- .replaceAll("&", "&amp;")
137
- .replaceAll("<", "&lt;")
138
- .replaceAll(">", "&gt;")
139
- .replaceAll('"', "&quot;")
140
- .replaceAll("'", "&#39;");
141
- }
142
-
143
- function environmentContext(cwd: string): string {
144
- const escaped = xml(cwd);
145
- return `<environment_context>
146
- <cwd>${escaped}</cwd>
147
- <filesystem><workspace_roots><root>${escaped}</root></workspace_roots><permission_profile type="disabled"><file_system type="unrestricted" /></permission_profile></filesystem>
148
- <codex_dev_mode>All outer tool effects are explicitly simulated.</codex_dev_mode>
149
- </environment_context>`;
150
- }
151
-
152
- function id(prefix: string): string {
153
- return `${prefix}_${crypto.randomUUID().replaceAll("-", "")}`;
154
- }
155
-
156
- function turnMetadata(threadId: string, turnId: string, cwd: string): string {
157
- return JSON.stringify({
158
- thread_id: threadId,
159
- turn_id: turnId,
160
- request_kind: "turn",
161
- sandbox: "none",
162
- workspaces: { [cwd]: {} },
163
- });
164
- }
165
-
166
- function currentTurnItems(cwd: string, turnId: string, message: string): unknown[] {
167
- const itemMetadata = { turn_id: turnId };
168
- return [
169
- {
170
- type: "message",
171
- id: id("msg_dev_environment"),
172
- role: "user",
173
- content: [{ type: "input_text", text: environmentContext(cwd) }],
174
- internal_chat_message_metadata_passthrough: itemMetadata,
175
- },
176
- {
177
- type: "message",
178
- id: id("msg_dev_user"),
179
- role: "user",
180
- content: [{ type: "input_text", text: message }],
181
- internal_chat_message_metadata_passthrough: itemMetadata,
182
- },
183
- ];
184
- }
185
-
186
- function requestBody(
187
- state: DevChatState,
188
- cwd: string,
189
- turnId: string,
190
- input: unknown[],
191
- stream: boolean,
192
- localToolsEnabled: boolean,
193
- ): Record<string, unknown> {
194
- return {
195
- model: state.model,
196
- instructions: localToolsEnabled ? DEV_CHAT_SYSTEM_INSTRUCTIONS : DEV_CHAT_BROWSER_ONLY_INSTRUCTIONS,
197
- input,
198
- tools: localToolsEnabled ? DEV_CHAT_TOOLS : [],
199
- tool_choice: "auto",
200
- parallel_tool_calls: true,
201
- reasoning: { summary: "auto" },
202
- stream,
203
- store: false,
204
- prompt_cache_key: state.threadId,
205
- client_metadata: {
206
- "x-codex-turn-metadata": turnMetadata(state.threadId, turnId, cwd),
207
- },
208
- metadata: { codex_chatgpt_web_dev: true, chat_name: state.name },
209
- };
210
- }
211
-
212
- function responseError(response: ResponsesEnvelope): string {
213
- if (response.error?.message) {
214
- const suffix = [response.error.type, response.error.code].filter(Boolean).join("/");
215
- return suffix ? `${response.error.message} (${suffix})` : response.error.message;
216
- }
217
- if (response.incomplete_details?.message) return response.incomplete_details.message;
218
- if (response.incomplete_details?.reason) return `Responses turn was incomplete: ${response.incomplete_details.reason}`;
219
- return `Responses turn ended with status ${String(response.status ?? "unknown")}`;
220
- }
221
-
222
- function observeAdapterEvent(event: AdapterEvent, emit: (event: DevChatEvent) => void): void {
223
- if (event.type === "thinking_delta") emit({ type: "reasoning", text: event.thinking });
224
- else if (event.type === "text_delta") {
225
- emit({ type: event.phase === "commentary" ? "commentary" : "text", text: event.text });
226
- }
227
- }
228
-
229
- function toolCalls(output: unknown[]): DevToolCall[] {
230
- const calls: DevToolCall[] = [];
231
- for (const value of output) {
232
- if (!value || typeof value !== "object" || Array.isArray(value)) continue;
233
- const item = value as Record<string, unknown>;
234
- if (typeof item.call_id !== "string") continue;
235
- if (item.type === "function_call" && typeof item.name === "string") {
236
- let input: unknown = {};
237
- if (typeof item.arguments === "string" && item.arguments.trim()) {
238
- try { input = JSON.parse(item.arguments); }
239
- catch { input = item.arguments; }
240
- }
241
- calls.push({
242
- kind: "function",
243
- callId: item.call_id,
244
- name: namespacedToolName(typeof item.namespace === "string" ? item.namespace : undefined, item.name),
245
- input,
246
- });
247
- } else if (item.type === "custom_tool_call" && typeof item.name === "string") {
248
- calls.push({ kind: "custom", callId: item.call_id, name: item.name, input: item.input ?? "" });
249
- }
250
- }
251
- return calls;
252
- }
253
-
254
- function simulatedReceipt(state: DevChatState, turnId: string, call: DevToolCall): Record<string, unknown> {
255
- if (call.name === DEV_LARGE_CONTEXT_TOOL) {
256
- if (!call.input || typeof call.input !== "object" || Array.isArray(call.input)) {
257
- throw new Error("DEV large context payload requires a JSON object");
258
- }
259
- const input = call.input as Record<string, unknown>;
260
- if (Object.keys(input).some(key => key !== "segment" && key !== "target_tokens")) {
261
- throw new Error("DEV large context payload received an unexpected argument");
262
- }
263
- const payload = createDevCoherentContextPayload(
264
- Number(input.segment),
265
- Number(input.target_tokens),
266
- );
267
- return {
268
- type: "codex_dev_coherent_context_payload",
269
- simulated: true,
270
- side_effects_performed: false,
271
- chat: state.name,
272
- turn_id: turnId,
273
- call_id: call.callId,
274
- tool: { name: call.name, kind: call.kind },
275
- segment: input.segment,
276
- measured_tokens: payload.tokens,
277
- output: payload.text,
278
- };
279
- }
280
- return {
281
- type: "codex_dev_simulated_tool_result",
282
- simulated: true,
283
- side_effects_performed: false,
284
- chat: state.name,
285
- turn_id: turnId,
286
- call_id: call.callId,
287
- tool: { name: call.name, kind: call.kind },
288
- received_input: call.input,
289
- output: `Simulated ${call.name}; no command, file, UI, user, or external side effect was performed.`,
290
- };
291
- }
292
-
293
- function toolOutput(call: DevToolCall, receipt: Record<string, unknown>): Record<string, unknown> {
294
- const metadata = { turn_id: receipt.turn_id };
295
- if (call.kind === "custom") {
296
- return {
297
- type: "custom_tool_call_output",
298
- call_id: call.callId,
299
- output: JSON.stringify(receipt),
300
- internal_chat_message_metadata_passthrough: metadata,
301
- };
302
- }
303
- return {
304
- type: "function_call_output",
305
- call_id: call.callId,
306
- output: JSON.stringify(receipt),
307
- internal_chat_message_metadata_passthrough: metadata,
308
- };
309
- }
310
-
311
- function historyOutput(output: unknown[], turnId: string): unknown[] {
312
- return output.map(value => {
313
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
314
- const item = value as Record<string, unknown>;
315
- const metadata = item.internal_chat_message_metadata_passthrough;
316
- if (metadata !== undefined && (!metadata || typeof metadata !== "object" || Array.isArray(metadata))) {
317
- throw new Error("DEV Responses output carried invalid native item metadata");
318
- }
319
- const existing = (metadata as Record<string, unknown> | undefined)?.turn_id;
320
- if (existing !== undefined && existing !== turnId) {
321
- throw new Error(`DEV Responses output belongs to another turn: ${String(existing)}`);
322
- }
323
- return {
324
- ...item,
325
- internal_chat_message_metadata_passthrough: {
326
- ...(metadata as Record<string, unknown> | undefined),
327
- turn_id: turnId,
328
- },
329
- };
330
- });
331
- }
332
-
333
- function outputText(output: unknown[]): string {
334
- const parts: string[] = [];
335
- for (const value of output) {
336
- if (!value || typeof value !== "object" || Array.isArray(value)) continue;
337
- const item = value as { type?: unknown; phase?: unknown; content?: unknown };
338
- if (item.type !== "message" || !Array.isArray(item.content) || item.phase === "commentary") continue;
339
- for (const raw of item.content) {
340
- const block = raw as { type?: unknown; text?: unknown };
341
- if ((block.type === "output_text" || block.type === "text") && typeof block.text === "string") {
342
- parts.push(block.text);
343
- }
344
- }
345
- }
346
- return parts.join("");
347
- }
348
-
349
- function usageOf(response: ResponsesEnvelope): DevChatUsage {
350
- const inputTokens = Number.isInteger(response.usage?.input_tokens) ? response.usage!.input_tokens! : 0;
351
- const outputTokens = Number.isInteger(response.usage?.output_tokens) ? response.usage!.output_tokens! : 0;
352
- const totalTokens = Number.isInteger(response.usage?.total_tokens)
353
- ? response.usage!.total_tokens!
354
- : inputTokens + outputTokens;
355
- return { inputTokens, outputTokens, totalTokens };
356
- }
357
-
358
- export function defaultDevChatModel(config: AppConfig): DevChatModel {
359
- if (config.browserInteractionMode === "manual") return "chatgpt-web/zero-risk";
360
- return config.solAvailable ? "chatgpt-web/light" : "chatgpt-web/luna";
361
- }
362
-
363
- function isLunaDevChatModel(model: DevChatModel): boolean {
364
- return model === "chatgpt-web/luna" || model === "chatgpt-web/think";
365
- }
366
-
367
- export function prepareWorkingTreeBrowserHelper(): string | undefined {
368
- const root = resolve(import.meta.dir, "..", "..");
369
- const buildScript = join(root, "scripts", "build-browser-helper.ts");
370
- if (!existsSync(buildScript)) return undefined;
371
- const output = join(root, ".launcher-runtime", "browser-helper.cjs");
372
- const build = Bun.spawnSync([process.execPath, "run", buildScript, output], {
373
- cwd: root,
374
- stdout: "pipe",
375
- stderr: "pipe",
376
- });
377
- if (build.exitCode !== 0) {
378
- throw new Error(`Could not build the working-tree browser helper: ${build.stderr.toString().trim() || build.stdout.toString().trim()}`);
379
- }
380
- return output;
381
- }
382
-
383
- export function createLauncherDevAdapter(
384
- config: AppConfig,
385
- runtimeStateRoot: string,
386
- options: {
387
- broker?: TurnBrokerOwner;
388
- browserHelperScriptPath?: string;
389
- } = {},
390
- ): { broker: TurnBrokerOwner; adapterFactory: AdapterFactory } {
391
- const broker = options.broker ?? new RemoteTurnBroker(config.brokerSocketPath);
392
- const browserHelperScriptPath = options.browserHelperScriptPath ?? prepareWorkingTreeBrowserHelper();
393
- const adapterFactory: AdapterFactory = provider => createChatGptWebAdapter({
394
- ...provider,
395
- chatgptWeb: {
396
- ...provider.chatgptWeb,
397
- ...(browserHelperScriptPath ? { browserHelperScriptPath } : {}),
398
- browserDiagnosticsPath: join(runtimeStateRoot, "diagnostics", "browser-turns"),
399
- threadEnvironmentStatePath: join(runtimeStateRoot, "thread-environments.json"),
400
- lunaCheckpointStatePath: join(runtimeStateRoot, "luna-checkpoints.json"),
401
- turnTimeoutMs: 60 * 60_000,
402
- ...(config.experimentalBiggerContext
403
- ? { experimentalBiggerContext: true }
404
- : {}),
405
- },
406
- }, { broker });
407
- return { broker, adapterFactory };
408
- }
409
-
410
- export class DevChatDriver {
411
- constructor(
412
- readonly config: AppConfig,
413
- readonly store: DevChatStore,
414
- readonly adapterFactory: AdapterFactory,
415
- readonly cwd = process.cwd(),
416
- readonly features: DevChatFeatures = DEFAULT_DEV_CHAT_FEATURES,
417
- ) {}
418
-
419
- open(name: string, requestedModel?: DevChatModel): { state: DevChatState; created: boolean } {
420
- const model = requestedModel ?? defaultDevChatModel(this.config);
421
- requireChatGptWebModelRoute(model, this.config);
422
- this.assertBiggerContextModel(model);
423
- const opened = this.store.loadOrCreate(name, model, this.cwd);
424
- if (resolve(opened.state.cwd) !== resolve(this.cwd)) {
425
- throw new Error(`DEV chat ${JSON.stringify(name)} belongs to ${opened.state.cwd}; use another name for ${this.cwd}`);
426
- }
427
- if (!opened.created && requestedModel && opened.state.model !== requestedModel) {
428
- opened.state.model = requestedModel;
429
- this.store.save(opened.state);
430
- }
431
- requireChatGptWebModelRoute(opened.state.model, this.config);
432
- this.assertBiggerContextModel(opened.state.model);
433
- if (opened.created) this.store.save(opened.state);
434
- return opened;
435
- }
436
-
437
- setModel(state: DevChatState, model: DevChatModel): void {
438
- requireChatGptWebModelRoute(model, this.config);
439
- this.assertBiggerContextModel(model);
440
- state.model = model;
441
- this.store.save(state);
442
- }
443
-
444
- reset(state: DevChatState): void {
445
- this.store.reset(state);
446
- }
447
-
448
- fill(state: DevChatState, targetTokens: number): { addedTokens: number; status: DevContextStatus } {
449
- const filler = createDevContextFiller(targetTokens);
450
- const fillerTurnId = id("dev_fill_turn");
451
- state.input.push({
452
- type: "message",
453
- id: id("msg_dev_fill"),
454
- role: "user",
455
- content: [{ type: "input_text", text: filler.text }],
456
- internal_chat_message_metadata_passthrough: { turn_id: fillerTurnId },
457
- });
458
- state.syntheticFills += 1;
459
- this.store.save(state);
460
- return { addedTokens: filler.tokens, status: this.status(state) };
461
- }
462
-
463
- status(state: DevChatState): DevContextStatus {
464
- const probeTurnId = id("dev_status_turn");
465
- const input = [...state.input, ...currentTurnItems(this.cwd, probeTurnId, "(DEV context status probe)")];
466
- return { ...this.statusForInput(state, probeTurnId, input), inputItems: state.input.length };
467
- }
468
-
469
- async compact(
470
- state: DevChatState,
471
- emit: (event: DevChatEvent) => void = () => {},
472
- ): Promise<DevContextStatus> {
473
- if (state.input.length === 0) throw new Error("DEV chat has no history to compact");
474
- const output = await this.compactInput(state, state.input, "manual", emit);
475
- state.input = output;
476
- state.compactions += 1;
477
- this.store.save(state);
478
- return this.status(state);
479
- }
480
-
481
- async send(
482
- state: DevChatState,
483
- message: string,
484
- emit: (event: DevChatEvent) => void = () => {},
485
- ): Promise<DevChatTurnResult> {
486
- const prompt = message.trim();
487
- if (!prompt) throw new Error("DEV chat message must not be empty");
488
- const turnId = id("dev_turn");
489
- let compactions = 0;
490
- let pendingCompactions = 0;
491
- let workingInput = [...state.input, ...currentTurnItems(this.cwd, turnId, prompt)];
492
- let context = this.statusForInput(state, turnId, workingInput);
493
-
494
- if (this.shouldAutoCompact(state, context) && state.input.length > 0) {
495
- state.input = await this.compactInput(state, state.input, "automatic", emit);
496
- state.compactions += 1;
497
- compactions += 1;
498
- this.store.save(state);
499
- workingInput = [...state.input, ...currentTurnItems(this.cwd, turnId, prompt)];
500
- context = this.statusForInput(state, turnId, workingInput);
501
- }
502
- if (this.shouldAutoCompact(state, context)) {
503
- throw new Error(
504
- `DEV turn still requires ${context.inputTokens.toLocaleString("en-US")} tokens after compaction; `
505
- + `the selected mode compacts at ${context.autoCompactTokenLimit.toLocaleString("en-US")}`,
506
- );
507
- }
508
-
509
- let totalToolCalls = 0;
510
- const usage: DevChatUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
511
- let finalText = "";
512
- for (let round = 0; round < 64; round += 1) {
513
- const body = requestBody(state, this.cwd, turnId, workingInput, false, this.config.mode === "full");
514
- const response = await responseRequest(new Request("http://codex-web-gpt.dev/v1/responses", {
515
- method: "POST",
516
- headers: { "content-type": "application/json" },
517
- body: JSON.stringify(body),
518
- }), this.config, this.adapterFactory, {
519
- rememberState: false,
520
- onAdapterEvent: event => observeAdapterEvent(event, emit),
521
- });
522
- const envelope = await response.json() as ResponsesEnvelope;
523
- if (!Array.isArray(envelope.output)) throw new Error("DEV Responses handler returned no output array");
524
- if (envelope.status !== "completed") throw new Error(responseError(envelope));
525
- const output = historyOutput(envelope.output!, turnId);
526
- workingInput.push(...output);
527
- const roundUsage = usageOf(envelope);
528
- usage.inputTokens += roundUsage.inputTokens;
529
- usage.outputTokens += roundUsage.outputTokens;
530
- usage.totalTokens += roundUsage.totalTokens;
531
- const calls = toolCalls(output);
532
- if (calls.length === 0) {
533
- if (envelope.end_turn !== true) {
534
- throw new Error("DEV Responses turn completed without tool calls or end_turn=true");
535
- }
536
- finalText = outputText(output);
537
- state.input = workingInput;
538
- state.turns += 1;
539
- state.compactions += pendingCompactions;
540
- state.lastUsage = usage;
541
- this.store.save(state);
542
- return {
543
- text: finalText,
544
- usage,
545
- toolCalls: totalToolCalls,
546
- compactions,
547
- status: this.status(state),
548
- };
549
- }
550
-
551
- totalToolCalls += calls.length;
552
- for (const call of calls) {
553
- emit({ type: "tool_call", name: call.name, input: call.input });
554
- const receipt = simulatedReceipt(state, turnId, call);
555
- emit({ type: "tool_result", name: call.name, receipt });
556
- workingInput.push(toolOutput(call, receipt));
557
- }
558
-
559
- context = this.statusForInput(state, turnId, workingInput);
560
- if (this.shouldAutoCompact(state, context)) {
561
- workingInput = await this.compactInput(state, workingInput, "automatic", emit);
562
- pendingCompactions += 1;
563
- compactions += 1;
564
- }
565
- }
566
- throw new Error("DEV chat exceeded 64 simulated tool rounds without a final answer");
567
- }
568
-
569
- async close(): Promise<void> {
570
- await closeChatGptBrowserWorkers();
571
- }
572
-
573
- private shouldAutoCompact(state: DevChatState, context: DevContextStatus): boolean {
574
- return !isLunaDevChatModel(state.model) && context.inputTokens >= context.autoCompactTokenLimit;
575
- }
576
-
577
- private assertBiggerContextModel(model: DevChatModel): void {
578
- if (this.features.biggerContext && isLunaDevChatModel(model)) {
579
- throw new Error(
580
- "Bigger Context is unavailable for Luna because its accumulated browser transcript still shares one 28,000-token transport budget",
581
- );
582
- }
583
- }
584
-
585
- private statusForInput(state: DevChatState, turnId: string, input: unknown[]): DevContextStatus {
586
- const parsed = parseRequest(requestBody(
587
- state,
588
- this.cwd,
589
- turnId,
590
- input,
591
- false,
592
- this.config.mode === "full",
593
- ));
594
- const route = routeChatGptWebRequest(parsed, this.config);
595
- const inputTokens = estimateChatGptWebInputTokens(parsed, {
596
- localToolsEnabled: this.config.mode === "full",
597
- solAvailable: this.config.solAvailable,
598
- proAvailable: this.config.proAvailable,
599
- });
600
- const limits = resolveChatGptWebContextLimits(route.backendModel, route.adapterEffort, this.config);
601
- const autoCompactTokenLimit = limits.autoCompactTokenLimit;
602
- const contextWindow = limits.contextWindow;
603
- return {
604
- model: state.model,
605
- inputTokens,
606
- autoCompactTokenLimit,
607
- contextWindow,
608
- ...(route.backendModel === CHATGPT_WEB_LUNA_BACKEND_MODEL
609
- ? { browserInputTokenLimit: CHATGPT_LUNA_BROWSER_INPUT_TOKEN_BUDGET }
610
- : {}),
611
- percent: Math.round((inputTokens / autoCompactTokenLimit) * 1_000) / 10,
612
- inputItems: input.length,
613
- };
614
- }
615
-
616
- private async compactInput(
617
- state: DevChatState,
618
- input: unknown[],
619
- reason: "automatic" | "manual",
620
- emit: (event: DevChatEvent) => void,
621
- ): Promise<unknown[]> {
622
- if (isLunaDevChatModel(state.model)) {
623
- throw new Error("ChatGPT Web Luna uses its production rolling checkpoint and does not support a separate compact command");
624
- }
625
- const compactTurnId = id("dev_compact_turn");
626
- emit({ type: "compaction_start", reason, inputItems: input.length });
627
- const response = await compactRequest(new Request("http://codex-web-gpt.dev/v1/responses/compact", {
628
- method: "POST",
629
- headers: {
630
- "content-type": "application/json",
631
- "x-codex-turn-metadata": turnMetadata(state.threadId, compactTurnId, this.cwd),
632
- },
633
- body: JSON.stringify({
634
- model: state.model,
635
- input,
636
- instructions: DEV_CHAT_SYSTEM_INSTRUCTIONS,
637
- store: false,
638
- }),
639
- }), this.config, this.adapterFactory);
640
- if (!response.ok) {
641
- let message = `DEV compaction failed with HTTP ${response.status}`;
642
- try {
643
- const body = await response.json() as { error?: { message?: unknown } };
644
- if (typeof body.error?.message === "string") message = body.error.message;
645
- } catch {}
646
- throw new Error(message);
647
- }
648
- const body = await response.json() as { output?: unknown };
649
- if (!Array.isArray(body.output) || body.output.length === 0) {
650
- throw new Error("DEV compaction returned no replacement history");
651
- }
652
- emit({ type: "compaction_done", reason, inputItems: body.output.length });
653
- return body.output;
654
- }
655
- }
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import type { ProviderAdapter } from "../adapters/base";
4
+ import { closeChatGptBrowserWorkers } from "../adapters/chatgpt-web/browser-worker";
5
+ import { createChatGptWebAdapter } from "../adapters/chatgpt-web";
6
+ import { estimateChatGptWebInputTokens } from "../adapters/chatgpt-web/usage";
7
+ import { RemoteTurnBroker, type TurnBrokerOwner } from "../adapters/chatgpt-web/turn-broker";
8
+ import {
9
+ CHATGPT_LUNA_BROWSER_INPUT_TOKEN_BUDGET,
10
+ } from "../adapters/chatgpt-web/input-tokens";
11
+ import {
12
+ CHATGPT_WEB_LUNA_BACKEND_MODEL,
13
+ requireChatGptWebModelRoute,
14
+ resolveChatGptWebContextLimits,
15
+ } from "../chatgpt-web-models";
16
+ import type { AppConfig } from "../config";
17
+ import { parseRequest } from "../responses/parser";
18
+ import { compactRequest, responseRequest, routeChatGptWebRequest } from "../server";
19
+ import { namespacedToolName, type AdapterEvent, type CodexProviderConfig } from "../types";
20
+ import {
21
+ createDevCoherentContextPayload,
22
+ createDevContextFiller,
23
+ type DevChatModel,
24
+ type DevChatState,
25
+ type DevChatStore,
26
+ type DevChatUsage,
27
+ } from "./session";
28
+
29
+ type AdapterFactory = (provider: CodexProviderConfig) => ProviderAdapter;
30
+
31
+ export type DevChatEvent =
32
+ | { type: "reasoning"; text: string }
33
+ | { type: "commentary"; text: string }
34
+ | { type: "text"; text: string }
35
+ | { type: "tool_call"; name: string; input: unknown }
36
+ | { type: "tool_result"; name: string; receipt: Record<string, unknown> }
37
+ | { type: "compaction_start"; reason: "automatic" | "manual"; inputItems: number }
38
+ | { type: "compaction_done"; reason: "automatic" | "manual"; inputItems: number };
39
+
40
+ export interface DevContextStatus {
41
+ model: DevChatModel;
42
+ inputTokens: number;
43
+ autoCompactTokenLimit: number;
44
+ contextWindow: number;
45
+ browserInputTokenLimit?: number;
46
+ percent: number;
47
+ inputItems: number;
48
+ }
49
+
50
+ export interface DevChatTurnResult {
51
+ text: string;
52
+ usage: DevChatUsage;
53
+ toolCalls: number;
54
+ compactions: number;
55
+ status: DevContextStatus;
56
+ }
57
+
58
+ export interface DevChatFeatures {
59
+ biggerContext: boolean;
60
+ }
61
+
62
+ const DEFAULT_DEV_CHAT_FEATURES: DevChatFeatures = { biggerContext: false };
63
+
64
+ interface ResponsesEnvelope {
65
+ id?: string;
66
+ status?: string;
67
+ output?: unknown[];
68
+ end_turn?: boolean;
69
+ usage?: {
70
+ input_tokens?: number;
71
+ output_tokens?: number;
72
+ total_tokens?: number;
73
+ };
74
+ error?: { message?: string; type?: string; code?: string } | null;
75
+ incomplete_details?: { reason?: string; message?: string } | null;
76
+ }
77
+
78
+ interface DevToolCall {
79
+ kind: "function" | "custom";
80
+ callId: string;
81
+ name: string;
82
+ input: unknown;
83
+ }
84
+
85
+ export const DEV_CHAT_SYSTEM_INSTRUCTIONS = [
86
+ "You are running inside the Codex Web GPT DEV outer-harness simulator.",
87
+ "Behave like the normal Codex model backend and use the available Codex Native tools whenever they help answer the user's request.",
88
+ "Every outer tool result is an explicit simulation receipt. No command, file edit, image read, user prompt, or external side effect actually occurs.",
89
+ "Never describe a simulated receipt as a real-world effect. Continue reasoning from the receipt exactly as test evidence for the transport flow.",
90
+ ].join(" ");
91
+
92
+ export const DEV_CHAT_BROWSER_ONLY_INSTRUCTIONS = [
93
+ "You are running inside the Codex Web GPT DEV outer-harness simulator.",
94
+ "Behave like the normal Codex model backend.",
95
+ "This browser-only DEV profile exposes no outer tools. Do not claim that commands, file edits, UI actions, or external side effects occurred.",
96
+ ].join(" ");
97
+
98
+ const ANY_ARGUMENTS = { type: "object", additionalProperties: true } as const;
99
+ const simulatedFunction = (name: string, description: string) => ({
100
+ type: "function", name, parameters: ANY_ARGUMENTS,
101
+ description: `DEV simulator: ${description}. Arguments are recorded and no side effect occurs.`,
102
+ });
103
+
104
+ const DEV_LARGE_CONTEXT_TOOL = "mcp__dev_simulator__large_context_payload";
105
+ const largeContextPayloadTool = {
106
+ type: "function",
107
+ name: DEV_LARGE_CONTEXT_TOOL,
108
+ description: "Return a deterministic, coherent, inert project dossier segment for context-retention and compaction tests.",
109
+ parameters: {
110
+ type: "object",
111
+ properties: {
112
+ segment: { type: "integer", enum: [1, 2, 3] },
113
+ target_tokens: { type: "integer", minimum: 1_000, maximum: 95_000 },
114
+ },
115
+ required: ["segment", "target_tokens"],
116
+ additionalProperties: false,
117
+ },
118
+ } as const;
119
+
120
+ export const DEV_CHAT_TOOLS: readonly Record<string, unknown>[] = [
121
+ simulatedFunction("exec_command", "native command execution"),
122
+ simulatedFunction("write_stdin", "native command-session continuation"),
123
+ { type: "custom", name: "apply_patch", description: "DEV simulator: records patch text and changes no file." },
124
+ simulatedFunction("view_image", "native image inspection"),
125
+ simulatedFunction("request_user_input", "native user input"),
126
+ largeContextPayloadTool,
127
+ {
128
+ type: "namespace", name: "mcp__dev_simulator",
129
+ description: "Synthetic deferred MCP tools for inventory and generic dispatch tests.",
130
+ tools: [simulatedFunction("echo", "structured MCP echo")],
131
+ },
132
+ ];
133
+
134
+ function xml(value: string): string {
135
+ return value
136
+ .replaceAll("&", "&amp;")
137
+ .replaceAll("<", "&lt;")
138
+ .replaceAll(">", "&gt;")
139
+ .replaceAll('"', "&quot;")
140
+ .replaceAll("'", "&#39;");
141
+ }
142
+
143
+ function environmentContext(cwd: string): string {
144
+ const escaped = xml(cwd);
145
+ return `<environment_context>
146
+ <cwd>${escaped}</cwd>
147
+ <filesystem><workspace_roots><root>${escaped}</root></workspace_roots><permission_profile type="disabled"><file_system type="unrestricted" /></permission_profile></filesystem>
148
+ <codex_dev_mode>All outer tool effects are explicitly simulated.</codex_dev_mode>
149
+ </environment_context>`;
150
+ }
151
+
152
+ function id(prefix: string): string {
153
+ return `${prefix}_${crypto.randomUUID().replaceAll("-", "")}`;
154
+ }
155
+
156
+ function turnMetadata(threadId: string, turnId: string, cwd: string): string {
157
+ return JSON.stringify({
158
+ thread_id: threadId,
159
+ turn_id: turnId,
160
+ request_kind: "turn",
161
+ sandbox: "none",
162
+ workspaces: { [cwd]: {} },
163
+ });
164
+ }
165
+
166
+ function currentTurnItems(cwd: string, turnId: string, message: string): unknown[] {
167
+ const itemMetadata = { turn_id: turnId };
168
+ return [
169
+ {
170
+ type: "message",
171
+ id: id("msg_dev_environment"),
172
+ role: "user",
173
+ content: [{ type: "input_text", text: environmentContext(cwd) }],
174
+ internal_chat_message_metadata_passthrough: itemMetadata,
175
+ },
176
+ {
177
+ type: "message",
178
+ id: id("msg_dev_user"),
179
+ role: "user",
180
+ content: [{ type: "input_text", text: message }],
181
+ internal_chat_message_metadata_passthrough: itemMetadata,
182
+ },
183
+ ];
184
+ }
185
+
186
+ function requestBody(
187
+ state: DevChatState,
188
+ cwd: string,
189
+ turnId: string,
190
+ input: unknown[],
191
+ stream: boolean,
192
+ localToolsEnabled: boolean,
193
+ ): Record<string, unknown> {
194
+ return {
195
+ model: state.model,
196
+ instructions: localToolsEnabled ? DEV_CHAT_SYSTEM_INSTRUCTIONS : DEV_CHAT_BROWSER_ONLY_INSTRUCTIONS,
197
+ input,
198
+ tools: localToolsEnabled ? DEV_CHAT_TOOLS : [],
199
+ tool_choice: "auto",
200
+ parallel_tool_calls: true,
201
+ reasoning: { summary: "auto" },
202
+ stream,
203
+ store: false,
204
+ prompt_cache_key: state.threadId,
205
+ client_metadata: {
206
+ "x-codex-turn-metadata": turnMetadata(state.threadId, turnId, cwd),
207
+ },
208
+ metadata: { codex_chatgpt_web_dev: true, chat_name: state.name },
209
+ };
210
+ }
211
+
212
+ function responseError(response: ResponsesEnvelope): string {
213
+ if (response.error?.message) {
214
+ const suffix = [response.error.type, response.error.code].filter(Boolean).join("/");
215
+ return suffix ? `${response.error.message} (${suffix})` : response.error.message;
216
+ }
217
+ if (response.incomplete_details?.message) return response.incomplete_details.message;
218
+ if (response.incomplete_details?.reason) return `Responses turn was incomplete: ${response.incomplete_details.reason}`;
219
+ return `Responses turn ended with status ${String(response.status ?? "unknown")}`;
220
+ }
221
+
222
+ function observeAdapterEvent(event: AdapterEvent, emit: (event: DevChatEvent) => void): void {
223
+ if (event.type === "thinking_delta") emit({ type: "reasoning", text: event.thinking });
224
+ else if (event.type === "text_delta") {
225
+ emit({ type: event.phase === "commentary" ? "commentary" : "text", text: event.text });
226
+ }
227
+ }
228
+
229
+ function toolCalls(output: unknown[]): DevToolCall[] {
230
+ const calls: DevToolCall[] = [];
231
+ for (const value of output) {
232
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
233
+ const item = value as Record<string, unknown>;
234
+ if (typeof item.call_id !== "string") continue;
235
+ if (item.type === "function_call" && typeof item.name === "string") {
236
+ let input: unknown = {};
237
+ if (typeof item.arguments === "string" && item.arguments.trim()) {
238
+ try { input = JSON.parse(item.arguments); }
239
+ catch { input = item.arguments; }
240
+ }
241
+ calls.push({
242
+ kind: "function",
243
+ callId: item.call_id,
244
+ name: namespacedToolName(typeof item.namespace === "string" ? item.namespace : undefined, item.name),
245
+ input,
246
+ });
247
+ } else if (item.type === "custom_tool_call" && typeof item.name === "string") {
248
+ calls.push({ kind: "custom", callId: item.call_id, name: item.name, input: item.input ?? "" });
249
+ }
250
+ }
251
+ return calls;
252
+ }
253
+
254
+ function simulatedReceipt(state: DevChatState, turnId: string, call: DevToolCall): Record<string, unknown> {
255
+ if (call.name === DEV_LARGE_CONTEXT_TOOL) {
256
+ if (!call.input || typeof call.input !== "object" || Array.isArray(call.input)) {
257
+ throw new Error("DEV large context payload requires a JSON object");
258
+ }
259
+ const input = call.input as Record<string, unknown>;
260
+ if (Object.keys(input).some(key => key !== "segment" && key !== "target_tokens")) {
261
+ throw new Error("DEV large context payload received an unexpected argument");
262
+ }
263
+ const payload = createDevCoherentContextPayload(
264
+ Number(input.segment),
265
+ Number(input.target_tokens),
266
+ );
267
+ return {
268
+ type: "codex_dev_coherent_context_payload",
269
+ simulated: true,
270
+ side_effects_performed: false,
271
+ chat: state.name,
272
+ turn_id: turnId,
273
+ call_id: call.callId,
274
+ tool: { name: call.name, kind: call.kind },
275
+ segment: input.segment,
276
+ measured_tokens: payload.tokens,
277
+ output: payload.text,
278
+ };
279
+ }
280
+ return {
281
+ type: "codex_dev_simulated_tool_result",
282
+ simulated: true,
283
+ side_effects_performed: false,
284
+ chat: state.name,
285
+ turn_id: turnId,
286
+ call_id: call.callId,
287
+ tool: { name: call.name, kind: call.kind },
288
+ received_input: call.input,
289
+ output: `Simulated ${call.name}; no command, file, UI, user, or external side effect was performed.`,
290
+ };
291
+ }
292
+
293
+ function toolOutput(call: DevToolCall, receipt: Record<string, unknown>): Record<string, unknown> {
294
+ const metadata = { turn_id: receipt.turn_id };
295
+ if (call.kind === "custom") {
296
+ return {
297
+ type: "custom_tool_call_output",
298
+ call_id: call.callId,
299
+ output: JSON.stringify(receipt),
300
+ internal_chat_message_metadata_passthrough: metadata,
301
+ };
302
+ }
303
+ return {
304
+ type: "function_call_output",
305
+ call_id: call.callId,
306
+ output: JSON.stringify(receipt),
307
+ internal_chat_message_metadata_passthrough: metadata,
308
+ };
309
+ }
310
+
311
+ function historyOutput(output: unknown[], turnId: string): unknown[] {
312
+ return output.map(value => {
313
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
314
+ const item = value as Record<string, unknown>;
315
+ const metadata = item.internal_chat_message_metadata_passthrough;
316
+ if (metadata !== undefined && (!metadata || typeof metadata !== "object" || Array.isArray(metadata))) {
317
+ throw new Error("DEV Responses output carried invalid native item metadata");
318
+ }
319
+ const existing = (metadata as Record<string, unknown> | undefined)?.turn_id;
320
+ if (existing !== undefined && existing !== turnId) {
321
+ throw new Error(`DEV Responses output belongs to another turn: ${String(existing)}`);
322
+ }
323
+ return {
324
+ ...item,
325
+ internal_chat_message_metadata_passthrough: {
326
+ ...(metadata as Record<string, unknown> | undefined),
327
+ turn_id: turnId,
328
+ },
329
+ };
330
+ });
331
+ }
332
+
333
+ function outputText(output: unknown[]): string {
334
+ const parts: string[] = [];
335
+ for (const value of output) {
336
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
337
+ const item = value as { type?: unknown; phase?: unknown; content?: unknown };
338
+ if (item.type !== "message" || !Array.isArray(item.content) || item.phase === "commentary") continue;
339
+ for (const raw of item.content) {
340
+ const block = raw as { type?: unknown; text?: unknown };
341
+ if ((block.type === "output_text" || block.type === "text") && typeof block.text === "string") {
342
+ parts.push(block.text);
343
+ }
344
+ }
345
+ }
346
+ return parts.join("");
347
+ }
348
+
349
+ function usageOf(response: ResponsesEnvelope): DevChatUsage {
350
+ const inputTokens = Number.isInteger(response.usage?.input_tokens) ? response.usage!.input_tokens! : 0;
351
+ const outputTokens = Number.isInteger(response.usage?.output_tokens) ? response.usage!.output_tokens! : 0;
352
+ const totalTokens = Number.isInteger(response.usage?.total_tokens)
353
+ ? response.usage!.total_tokens!
354
+ : inputTokens + outputTokens;
355
+ return { inputTokens, outputTokens, totalTokens };
356
+ }
357
+
358
+ export function defaultDevChatModel(config: AppConfig): DevChatModel {
359
+ if (config.browserInteractionMode === "manual") return "chatgpt-web/zero-risk";
360
+ return config.solAvailable ? "chatgpt-web/light" : "chatgpt-web/luna";
361
+ }
362
+
363
+ function isLunaDevChatModel(model: DevChatModel): boolean {
364
+ return model === "chatgpt-web/luna" || model === "chatgpt-web/think";
365
+ }
366
+
367
+ export function prepareWorkingTreeBrowserHelper(): string | undefined {
368
+ const root = resolve(import.meta.dir, "..", "..");
369
+ const buildScript = join(root, "scripts", "build-browser-helper.ts");
370
+ if (!existsSync(buildScript)) return undefined;
371
+ const output = join(root, ".launcher-runtime", "browser-helper.cjs");
372
+ const build = Bun.spawnSync([process.execPath, "run", buildScript, output], {
373
+ cwd: root,
374
+ stdout: "pipe",
375
+ stderr: "pipe",
376
+ });
377
+ if (build.exitCode !== 0) {
378
+ throw new Error(`Could not build the working-tree browser helper: ${build.stderr.toString().trim() || build.stdout.toString().trim()}`);
379
+ }
380
+ return output;
381
+ }
382
+
383
+ export function createLauncherDevAdapter(
384
+ config: AppConfig,
385
+ runtimeStateRoot: string,
386
+ options: {
387
+ broker?: TurnBrokerOwner;
388
+ browserHelperScriptPath?: string;
389
+ } = {},
390
+ ): { broker: TurnBrokerOwner; adapterFactory: AdapterFactory } {
391
+ const broker = options.broker ?? new RemoteTurnBroker(config.brokerSocketPath);
392
+ const browserHelperScriptPath = options.browserHelperScriptPath ?? prepareWorkingTreeBrowserHelper();
393
+ const adapterFactory: AdapterFactory = provider => createChatGptWebAdapter({
394
+ ...provider,
395
+ chatgptWeb: {
396
+ ...provider.chatgptWeb,
397
+ ...(browserHelperScriptPath ? { browserHelperScriptPath } : {}),
398
+ browserDiagnosticsPath: join(runtimeStateRoot, "diagnostics", "browser-turns"),
399
+ threadEnvironmentStatePath: join(runtimeStateRoot, "thread-environments.json"),
400
+ lunaCheckpointStatePath: join(runtimeStateRoot, "luna-checkpoints.json"),
401
+ turnTimeoutMs: 60 * 60_000,
402
+ ...(config.experimentalBiggerContext
403
+ ? { experimentalBiggerContext: true }
404
+ : {}),
405
+ },
406
+ }, { broker });
407
+ return { broker, adapterFactory };
408
+ }
409
+
410
+ export class DevChatDriver {
411
+ constructor(
412
+ readonly config: AppConfig,
413
+ readonly store: DevChatStore,
414
+ readonly adapterFactory: AdapterFactory,
415
+ readonly cwd = process.cwd(),
416
+ readonly features: DevChatFeatures = DEFAULT_DEV_CHAT_FEATURES,
417
+ ) {}
418
+
419
+ open(name: string, requestedModel?: DevChatModel): { state: DevChatState; created: boolean } {
420
+ const model = requestedModel ?? defaultDevChatModel(this.config);
421
+ requireChatGptWebModelRoute(model, this.config);
422
+ this.assertBiggerContextModel(model);
423
+ const opened = this.store.loadOrCreate(name, model, this.cwd);
424
+ if (resolve(opened.state.cwd) !== resolve(this.cwd)) {
425
+ throw new Error(`DEV chat ${JSON.stringify(name)} belongs to ${opened.state.cwd}; use another name for ${this.cwd}`);
426
+ }
427
+ if (!opened.created && requestedModel && opened.state.model !== requestedModel) {
428
+ opened.state.model = requestedModel;
429
+ this.store.save(opened.state);
430
+ }
431
+ requireChatGptWebModelRoute(opened.state.model, this.config);
432
+ this.assertBiggerContextModel(opened.state.model);
433
+ if (opened.created) this.store.save(opened.state);
434
+ return opened;
435
+ }
436
+
437
+ setModel(state: DevChatState, model: DevChatModel): void {
438
+ requireChatGptWebModelRoute(model, this.config);
439
+ this.assertBiggerContextModel(model);
440
+ state.model = model;
441
+ this.store.save(state);
442
+ }
443
+
444
+ reset(state: DevChatState): void {
445
+ this.store.reset(state);
446
+ }
447
+
448
+ fill(state: DevChatState, targetTokens: number): { addedTokens: number; status: DevContextStatus } {
449
+ const filler = createDevContextFiller(targetTokens);
450
+ const fillerTurnId = id("dev_fill_turn");
451
+ state.input.push({
452
+ type: "message",
453
+ id: id("msg_dev_fill"),
454
+ role: "user",
455
+ content: [{ type: "input_text", text: filler.text }],
456
+ internal_chat_message_metadata_passthrough: { turn_id: fillerTurnId },
457
+ });
458
+ state.syntheticFills += 1;
459
+ this.store.save(state);
460
+ return { addedTokens: filler.tokens, status: this.status(state) };
461
+ }
462
+
463
+ status(state: DevChatState): DevContextStatus {
464
+ const probeTurnId = id("dev_status_turn");
465
+ const input = [...state.input, ...currentTurnItems(this.cwd, probeTurnId, "(DEV context status probe)")];
466
+ return { ...this.statusForInput(state, probeTurnId, input), inputItems: state.input.length };
467
+ }
468
+
469
+ async compact(
470
+ state: DevChatState,
471
+ emit: (event: DevChatEvent) => void = () => {},
472
+ ): Promise<DevContextStatus> {
473
+ if (state.input.length === 0) throw new Error("DEV chat has no history to compact");
474
+ const output = await this.compactInput(state, state.input, "manual", emit);
475
+ state.input = output;
476
+ state.compactions += 1;
477
+ this.store.save(state);
478
+ return this.status(state);
479
+ }
480
+
481
+ async send(
482
+ state: DevChatState,
483
+ message: string,
484
+ emit: (event: DevChatEvent) => void = () => {},
485
+ ): Promise<DevChatTurnResult> {
486
+ const prompt = message.trim();
487
+ if (!prompt) throw new Error("DEV chat message must not be empty");
488
+ const turnId = id("dev_turn");
489
+ let compactions = 0;
490
+ let pendingCompactions = 0;
491
+ let workingInput = [...state.input, ...currentTurnItems(this.cwd, turnId, prompt)];
492
+ let context = this.statusForInput(state, turnId, workingInput);
493
+
494
+ if (this.shouldAutoCompact(state, context) && state.input.length > 0) {
495
+ state.input = await this.compactInput(state, state.input, "automatic", emit);
496
+ state.compactions += 1;
497
+ compactions += 1;
498
+ this.store.save(state);
499
+ workingInput = [...state.input, ...currentTurnItems(this.cwd, turnId, prompt)];
500
+ context = this.statusForInput(state, turnId, workingInput);
501
+ }
502
+ if (this.shouldAutoCompact(state, context)) {
503
+ throw new Error(
504
+ `DEV turn still requires ${context.inputTokens.toLocaleString("en-US")} tokens after compaction; `
505
+ + `the selected mode compacts at ${context.autoCompactTokenLimit.toLocaleString("en-US")}`,
506
+ );
507
+ }
508
+
509
+ let totalToolCalls = 0;
510
+ const usage: DevChatUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
511
+ let finalText = "";
512
+ for (let round = 0; round < 64; round += 1) {
513
+ const body = requestBody(state, this.cwd, turnId, workingInput, false, this.config.mode === "full");
514
+ const response = await responseRequest(new Request("http://codex-web-gpt.dev/v1/responses", {
515
+ method: "POST",
516
+ headers: { "content-type": "application/json" },
517
+ body: JSON.stringify(body),
518
+ }), this.config, this.adapterFactory, {
519
+ rememberState: false,
520
+ onAdapterEvent: event => observeAdapterEvent(event, emit),
521
+ });
522
+ const envelope = await response.json() as ResponsesEnvelope;
523
+ if (!Array.isArray(envelope.output)) throw new Error("DEV Responses handler returned no output array");
524
+ if (envelope.status !== "completed") throw new Error(responseError(envelope));
525
+ const output = historyOutput(envelope.output!, turnId);
526
+ workingInput.push(...output);
527
+ const roundUsage = usageOf(envelope);
528
+ usage.inputTokens += roundUsage.inputTokens;
529
+ usage.outputTokens += roundUsage.outputTokens;
530
+ usage.totalTokens += roundUsage.totalTokens;
531
+ const calls = toolCalls(output);
532
+ if (calls.length === 0) {
533
+ if (envelope.end_turn !== true) {
534
+ throw new Error("DEV Responses turn completed without tool calls or end_turn=true");
535
+ }
536
+ finalText = outputText(output);
537
+ state.input = workingInput;
538
+ state.turns += 1;
539
+ state.compactions += pendingCompactions;
540
+ state.lastUsage = usage;
541
+ this.store.save(state);
542
+ return {
543
+ text: finalText,
544
+ usage,
545
+ toolCalls: totalToolCalls,
546
+ compactions,
547
+ status: this.status(state),
548
+ };
549
+ }
550
+
551
+ totalToolCalls += calls.length;
552
+ for (const call of calls) {
553
+ emit({ type: "tool_call", name: call.name, input: call.input });
554
+ const receipt = simulatedReceipt(state, turnId, call);
555
+ emit({ type: "tool_result", name: call.name, receipt });
556
+ workingInput.push(toolOutput(call, receipt));
557
+ }
558
+
559
+ context = this.statusForInput(state, turnId, workingInput);
560
+ if (this.shouldAutoCompact(state, context)) {
561
+ workingInput = await this.compactInput(state, workingInput, "automatic", emit);
562
+ pendingCompactions += 1;
563
+ compactions += 1;
564
+ }
565
+ }
566
+ throw new Error("DEV chat exceeded 64 simulated tool rounds without a final answer");
567
+ }
568
+
569
+ async close(): Promise<void> {
570
+ await closeChatGptBrowserWorkers();
571
+ }
572
+
573
+ private shouldAutoCompact(state: DevChatState, context: DevContextStatus): boolean {
574
+ return !isLunaDevChatModel(state.model) && context.inputTokens >= context.autoCompactTokenLimit;
575
+ }
576
+
577
+ private assertBiggerContextModel(model: DevChatModel): void {
578
+ if (this.features.biggerContext && isLunaDevChatModel(model)) {
579
+ throw new Error(
580
+ "Bigger Context is unavailable for Luna because its accumulated browser transcript still shares one 28,000-token transport budget",
581
+ );
582
+ }
583
+ }
584
+
585
+ private statusForInput(state: DevChatState, turnId: string, input: unknown[]): DevContextStatus {
586
+ const parsed = parseRequest(requestBody(
587
+ state,
588
+ this.cwd,
589
+ turnId,
590
+ input,
591
+ false,
592
+ this.config.mode === "full",
593
+ ));
594
+ const route = routeChatGptWebRequest(parsed, this.config);
595
+ const inputTokens = estimateChatGptWebInputTokens(parsed, {
596
+ localToolsEnabled: this.config.mode === "full",
597
+ solAvailable: this.config.solAvailable,
598
+ proAvailable: this.config.proAvailable,
599
+ });
600
+ const limits = resolveChatGptWebContextLimits(route.backendModel, route.adapterEffort, this.config);
601
+ const autoCompactTokenLimit = limits.autoCompactTokenLimit;
602
+ const contextWindow = limits.contextWindow;
603
+ return {
604
+ model: state.model,
605
+ inputTokens,
606
+ autoCompactTokenLimit,
607
+ contextWindow,
608
+ ...(route.backendModel === CHATGPT_WEB_LUNA_BACKEND_MODEL
609
+ ? { browserInputTokenLimit: CHATGPT_LUNA_BROWSER_INPUT_TOKEN_BUDGET }
610
+ : {}),
611
+ percent: Math.round((inputTokens / autoCompactTokenLimit) * 1_000) / 10,
612
+ inputItems: input.length,
613
+ };
614
+ }
615
+
616
+ private async compactInput(
617
+ state: DevChatState,
618
+ input: unknown[],
619
+ reason: "automatic" | "manual",
620
+ emit: (event: DevChatEvent) => void,
621
+ ): Promise<unknown[]> {
622
+ if (isLunaDevChatModel(state.model)) {
623
+ throw new Error("ChatGPT Web Luna uses its production rolling checkpoint and does not support a separate compact command");
624
+ }
625
+ const compactTurnId = id("dev_compact_turn");
626
+ emit({ type: "compaction_start", reason, inputItems: input.length });
627
+ const response = await compactRequest(new Request("http://codex-web-gpt.dev/v1/responses/compact", {
628
+ method: "POST",
629
+ headers: {
630
+ "content-type": "application/json",
631
+ "x-codex-turn-metadata": turnMetadata(state.threadId, compactTurnId, this.cwd),
632
+ },
633
+ body: JSON.stringify({
634
+ model: state.model,
635
+ input,
636
+ instructions: DEV_CHAT_SYSTEM_INSTRUCTIONS,
637
+ store: false,
638
+ }),
639
+ }), this.config, this.adapterFactory);
640
+ if (!response.ok) {
641
+ let message = `DEV compaction failed with HTTP ${response.status}`;
642
+ try {
643
+ const body = await response.json() as { error?: { message?: unknown } };
644
+ if (typeof body.error?.message === "string") message = body.error.message;
645
+ } catch {}
646
+ throw new Error(message);
647
+ }
648
+ const body = await response.json() as { output?: unknown };
649
+ if (!Array.isArray(body.output) || body.output.length === 0) {
650
+ throw new Error("DEV compaction returned no replacement history");
651
+ }
652
+ emit({ type: "compaction_done", reason, inputItems: body.output.length });
653
+ return body.output;
654
+ }
655
+ }