@peakflo/agent-harness 0.0.0-stage → 0.2.1-rc.3

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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -2
  3. package/dist/adapters/acp-adapter.d.ts +177 -0
  4. package/dist/adapters/acp-adapter.js +1732 -0
  5. package/dist/adapters/claude-code-adapter.d.ts +171 -0
  6. package/dist/adapters/claude-code-adapter.js +1918 -0
  7. package/dist/adapters/codex-adapter.d.ts +67 -0
  8. package/dist/adapters/codex-adapter.js +681 -0
  9. package/dist/adapters/codex-app-server-adapter.d.ts +105 -0
  10. package/dist/adapters/codex-app-server-adapter.js +1309 -0
  11. package/dist/adapters/coding-agent-adapter.d.ts +289 -0
  12. package/dist/adapters/coding-agent-adapter.js +31 -0
  13. package/dist/adapters/index.d.ts +17 -0
  14. package/dist/adapters/index.js +35 -0
  15. package/dist/adapters/model-gateway.d.ts +84 -0
  16. package/dist/adapters/model-gateway.js +270 -0
  17. package/dist/adapters/opencode-adapter.d.ts +403 -0
  18. package/dist/adapters/opencode-adapter.js +2710 -0
  19. package/dist/adapters/opencode-config.d.ts +41 -0
  20. package/dist/adapters/opencode-config.js +147 -0
  21. package/dist/adapters/opencode-server-policy.d.ts +82 -0
  22. package/dist/adapters/opencode-server-policy.js +148 -0
  23. package/dist/adapters/pi-adapter.d.ts +168 -0
  24. package/dist/adapters/pi-adapter.js +1468 -0
  25. package/dist/capabilities.d.ts +58 -0
  26. package/dist/capabilities.js +161 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +231 -0
  29. package/dist/contract.d.ts +273 -0
  30. package/dist/contract.js +357 -0
  31. package/dist/control-plane.d.ts +178 -0
  32. package/dist/control-plane.js +431 -0
  33. package/dist/credential-store.d.ts +110 -0
  34. package/dist/credential-store.js +181 -0
  35. package/dist/enrol/browser-enrol.d.ts +85 -0
  36. package/dist/enrol/browser-enrol.js +387 -0
  37. package/dist/github-command-guards.d.ts +20 -0
  38. package/dist/github-command-guards.js +330 -0
  39. package/dist/github-proxy-certificates.d.ts +25 -0
  40. package/dist/github-proxy-certificates.js +145 -0
  41. package/dist/github-proxy.d.ts +37 -0
  42. package/dist/github-proxy.js +476 -0
  43. package/dist/github-workspace.d.ts +55 -0
  44. package/dist/github-workspace.js +537 -0
  45. package/dist/harness.d.ts +333 -0
  46. package/dist/harness.js +1414 -0
  47. package/dist/health.d.ts +45 -0
  48. package/dist/health.js +116 -0
  49. package/dist/index.d.ts +53 -0
  50. package/dist/index.js +123 -0
  51. package/dist/model-probe.d.ts +96 -0
  52. package/dist/model-probe.js +195 -0
  53. package/dist/options.d.ts +15 -0
  54. package/dist/options.js +212 -0
  55. package/dist/ports-default.d.ts +212 -0
  56. package/dist/ports-default.js +333 -0
  57. package/dist/ports.d.ts +436 -0
  58. package/dist/ports.js +27 -0
  59. package/dist/runtime/attachments.d.ts +92 -0
  60. package/dist/runtime/attachments.js +377 -0
  61. package/dist/runtime/child-stream-guards.d.ts +45 -0
  62. package/dist/runtime/child-stream-guards.js +111 -0
  63. package/dist/runtime/conversation.d.ts +87 -0
  64. package/dist/runtime/conversation.js +156 -0
  65. package/dist/runtime/lease-renewal.d.ts +169 -0
  66. package/dist/runtime/lease-renewal.js +433 -0
  67. package/dist/runtime/mcp-auth-proxy.d.ts +82 -0
  68. package/dist/runtime/mcp-auth-proxy.js +300 -0
  69. package/dist/runtime/permission-mode.d.ts +116 -0
  70. package/dist/runtime/permission-mode.js +146 -0
  71. package/dist/runtime/projection.d.ts +71 -0
  72. package/dist/runtime/projection.js +211 -0
  73. package/dist/runtime/session-runner.d.ts +520 -0
  74. package/dist/runtime/session-runner.js +2122 -0
  75. package/dist/shared/reasoning-effort.d.ts +11 -0
  76. package/dist/shared/reasoning-effort.js +20 -0
  77. package/dist/store/conversation-store.d.ts +100 -0
  78. package/dist/store/conversation-store.js +305 -0
  79. package/dist/store/sqlite-store.d.ts +120 -0
  80. package/dist/store/sqlite-store.js +346 -0
  81. package/dist/types.d.ts +952 -0
  82. package/dist/types.js +55 -0
  83. package/dist/version.d.ts +32 -0
  84. package/dist/version.js +77 -0
  85. package/package.json +53 -4
@@ -0,0 +1,1309 @@
1
+ "use strict";
2
+ /**
3
+ * Codex App Server adapter.
4
+ *
5
+ * Experimental replacement path for Codex ACP. This talks to `codex app-server`
6
+ * over JSON-RPC stdio and maps the app-server thread/turn/item protocol onto
7
+ * 20x's CodingAgentAdapter contract.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.CodexAppServerAdapter = void 0;
44
+ /**
45
+ * Extracted verbatim from 20x `src/main/adapters/codex-app-server-adapter.ts`.
46
+ * CHANGED: imports only — `../child-stream-guards` ->
47
+ * `../runtime/child-stream-guards`. Source formatting (no semicolons)
48
+ * preserved.
49
+ */
50
+ const child_process_1 = require("child_process");
51
+ const fs_1 = require("fs");
52
+ const child_stream_guards_1 = require("../runtime/child-stream-guards");
53
+ const os_1 = require("os");
54
+ const path_1 = require("path");
55
+ const util_1 = require("util");
56
+ const coding_agent_adapter_1 = require("./coding-agent-adapter");
57
+ const DEFAULT_CODEX_APP_SERVER_MODEL = 'gpt-5.5';
58
+ const MAX_IPC_TOOL_INPUT_CHARS = 20_000;
59
+ const MAX_IPC_TOOL_OUTPUT_CHARS = 100_000;
60
+ const MIN_THREAD_LEVEL_ASSISTANT_DEDUPE_CHARS = 40;
61
+ function isObject(value) {
62
+ return !!value && typeof value === 'object' && !Array.isArray(value);
63
+ }
64
+ function asString(value) {
65
+ return typeof value === 'string' ? value : undefined;
66
+ }
67
+ function extractThreadId(value) {
68
+ if (!isObject(value))
69
+ return null;
70
+ const direct = asString(value.threadId) || asString(value.thread_id) || asString(value.id);
71
+ if (direct)
72
+ return direct;
73
+ if (isObject(value.thread)) {
74
+ return asString(value.thread.id) || asString(value.thread.threadId) || asString(value.thread.thread_id) || null;
75
+ }
76
+ return null;
77
+ }
78
+ /**
79
+ * Deterministic 32-bit string hash (djb2). Used to build stable dedup keys for
80
+ * thread items that carry no id — never use Date.now()/random here, or the same
81
+ * item would produce a new key on every reconcile pass and be re-emitted.
82
+ */
83
+ function hashString(value) {
84
+ let hash = 5381;
85
+ for (let i = 0; i < value.length; i++) {
86
+ hash = ((hash << 5) + hash + value.charCodeAt(i)) | 0;
87
+ }
88
+ return (hash >>> 0).toString(36);
89
+ }
90
+ /**
91
+ * Derives a STABLE identity for a Codex thread item. Many item types
92
+ * (function_call_output, custom_tool_call_output, tool_search_output, and
93
+ * user/developer input messages) have no top-level `id` — only a `call_id`, or
94
+ * nothing at all. This must return the same value across reconcile passes for the
95
+ * same logical item, otherwise the transcript repeats older messages after every
96
+ * idle. Returns null when no id-like field exists (caller falls back to a
97
+ * content-based key).
98
+ */
99
+ function deriveItemIdentity(item) {
100
+ if (!item)
101
+ return null;
102
+ const direct = asString(item.id) || asString(item.itemId) || asString(item.item_id);
103
+ if (direct)
104
+ return direct;
105
+ const callId = asString(item.call_id) || asString(item.callId);
106
+ if (callId) {
107
+ const type = asString(item.type) || 'item';
108
+ return `${type}:${callId}`;
109
+ }
110
+ return null;
111
+ }
112
+ function extractItemId(params) {
113
+ const item = isObject(params.item) ? params.item : undefined;
114
+ const identity = asString(params.itemId) || deriveItemIdentity(item);
115
+ if (identity)
116
+ return identity;
117
+ // Last resort: a deterministic key derived from the item's content so the same
118
+ // item maps to the same part id across reconciles (was `item-${Date.now()}`,
119
+ // which minted a new id every pass and duplicated the message on each idle).
120
+ const fingerprint = item ? `${asString(item.type) || 'item'}:${extractText(item)}` : 'item';
121
+ return `item-${hashString(fingerprint)}`;
122
+ }
123
+ function extractText(value) {
124
+ if (!value)
125
+ return '';
126
+ if (typeof value === 'string')
127
+ return value;
128
+ if (Array.isArray(value))
129
+ return value.map(extractText).filter(Boolean).join('\n');
130
+ if (!isObject(value))
131
+ return '';
132
+ for (const key of ['text', 'content', 'delta', 'message', 'output']) {
133
+ const text = extractText(value[key]);
134
+ if (text)
135
+ return text;
136
+ }
137
+ return '';
138
+ }
139
+ function extractRole(item) {
140
+ return (asString(item.role) || asString(item.author) || asString(item.sender) || '').toLowerCase();
141
+ }
142
+ function extractToolName(item) {
143
+ const server = asString(item.server);
144
+ const tool = asString(item.tool);
145
+ if ((item.type === 'mcpToolCall' || server || tool) && (server || tool)) {
146
+ return [server, tool].filter(Boolean).join('.');
147
+ }
148
+ return (asString(item.toolName) ||
149
+ asString(item.tool_name) ||
150
+ asString(item.name) ||
151
+ asString(item.type) ||
152
+ 'tool');
153
+ }
154
+ function extractToolTitle(item, toolName) {
155
+ if (item.type === 'commandExecution') {
156
+ const actionCommand = Array.isArray(item.commandActions)
157
+ ? item.commandActions
158
+ .filter(isObject)
159
+ .map((action) => asString(action.command))
160
+ .find(Boolean)
161
+ : undefined;
162
+ const command = actionCommand || asString(item.command);
163
+ if (command)
164
+ return command;
165
+ }
166
+ if (item.type === 'fileChange') {
167
+ const paths = Array.isArray(item.changes)
168
+ ? item.changes
169
+ .filter(isObject)
170
+ .map((change) => asString(change.path))
171
+ .filter((path) => !!path)
172
+ : [];
173
+ const directPath = asString(item.path);
174
+ if (paths.length === 1)
175
+ return (0, path_1.basename)(paths[0]);
176
+ if (paths.length > 1)
177
+ return `${(0, path_1.basename)(paths[0])} +${paths.length - 1}`;
178
+ if (directPath)
179
+ return (0, path_1.basename)(directPath);
180
+ }
181
+ return toolName;
182
+ }
183
+ function truncateForIpc(value, maxChars) {
184
+ if (value.length <= maxChars)
185
+ return value;
186
+ return `${value.slice(0, maxChars)}\n... (truncated for display)`;
187
+ }
188
+ function stringifyForIpc(value, maxChars) {
189
+ if (value == null)
190
+ return undefined;
191
+ if (typeof value === 'string')
192
+ return truncateForIpc(value, maxChars);
193
+ try {
194
+ const seen = new WeakSet();
195
+ return truncateForIpc(JSON.stringify(value, (_key, nested) => {
196
+ if (typeof nested === 'bigint')
197
+ return nested.toString();
198
+ if (typeof nested === 'function' || typeof nested === 'symbol')
199
+ return undefined;
200
+ if (nested && typeof nested === 'object') {
201
+ if (seen.has(nested))
202
+ return '[Circular]';
203
+ seen.add(nested);
204
+ }
205
+ return nested;
206
+ }, 2), maxChars);
207
+ }
208
+ catch {
209
+ return truncateForIpc(String(value), maxChars);
210
+ }
211
+ }
212
+ function normalizeCodexMcpServerName(name) {
213
+ const normalized = name.replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
214
+ return normalized || 'mcp_server';
215
+ }
216
+ function uniquePaths(paths) {
217
+ return Array.from(new Set(paths.filter(Boolean).map((path) => (0, path_1.resolve)(path))));
218
+ }
219
+ function isSubpath(parent, child) {
220
+ const rel = (0, path_1.relative)((0, path_1.resolve)(parent), (0, path_1.resolve)(child));
221
+ return rel === '' || (!!rel && !rel.startsWith('..') && !(0, path_1.isAbsolute)(rel));
222
+ }
223
+ function summarizeApproval(params, fallback) {
224
+ const command = asString(params.command);
225
+ const reason = asString(params.reason);
226
+ const itemId = asString(params.itemId) || asString(params.callId);
227
+ return [command || fallback, reason, itemId ? `id: ${itemId}` : ''].filter(Boolean).join('\n');
228
+ }
229
+ function normalizeDecisionName(decision) {
230
+ if (typeof decision === 'string')
231
+ return decision;
232
+ if (isObject(decision)) {
233
+ return Object.keys(decision)[0] || 'accept';
234
+ }
235
+ return 'accept';
236
+ }
237
+ function decisionLabel(decision) {
238
+ switch (decision) {
239
+ case 'accept':
240
+ case 'approved':
241
+ return 'Allow';
242
+ case 'acceptForSession':
243
+ case 'approved_for_session':
244
+ return 'Allow for Session';
245
+ case 'decline':
246
+ case 'denied':
247
+ return 'Deny';
248
+ case 'cancel':
249
+ case 'abort':
250
+ return 'Deny and Stop';
251
+ default:
252
+ return decision;
253
+ }
254
+ }
255
+ class CodexAppServerAdapter {
256
+ sessions = new Map();
257
+ codexExecutablePath = null;
258
+ onDataAvailable;
259
+ async initialize() {
260
+ const health = await this.checkHealth();
261
+ if (!health.available) {
262
+ throw new Error(health.reason || 'Codex app-server not available');
263
+ }
264
+ }
265
+ async createSession(config) {
266
+ const session = await this.startAppServerProcess(config, config.taskId);
267
+ this.sessions.set(config.taskId, session);
268
+ await this.initializeAppServer(session);
269
+ const result = await this.sendRpcRequest(session, 'thread/start', {
270
+ cwd: config.workspaceDir,
271
+ model: config.model || DEFAULT_CODEX_APP_SERVER_MODEL,
272
+ approvalPolicy: config.permissionMode === 'allow' ? 'never' : 'on-request',
273
+ approvalsReviewer: 'user',
274
+ sandbox: this.resolveSandboxMode(config),
275
+ developerInstructions: config.systemPrompt || null,
276
+ runtimeWorkspaceRoots: this.buildRuntimeWorkspaceRoots(config.workspaceDir),
277
+ config: this.buildConfigOverrides(config)
278
+ });
279
+ const threadId = extractThreadId(result);
280
+ if (!threadId) {
281
+ throw new Error('Codex app-server did not return a thread id');
282
+ }
283
+ session.threadId = threadId;
284
+ this.sessions.delete(config.taskId);
285
+ this.sessions.set(threadId, session);
286
+ void this.logMcpServerInventory(session, threadId, 'thread/start');
287
+ return threadId;
288
+ }
289
+ async resumeSession(sessionId, config) {
290
+ const session = await this.startAppServerProcess(config, sessionId);
291
+ session.threadId = sessionId;
292
+ this.sessions.set(sessionId, session);
293
+ await this.initializeAppServer(session);
294
+ await this.sendRpcRequest(session, 'thread/resume', {
295
+ threadId: sessionId,
296
+ cwd: config.workspaceDir,
297
+ model: config.model || DEFAULT_CODEX_APP_SERVER_MODEL,
298
+ approvalPolicy: config.permissionMode === 'allow' ? 'never' : 'on-request',
299
+ approvalsReviewer: 'user',
300
+ sandbox: this.resolveSandboxMode(config),
301
+ runtimeWorkspaceRoots: this.buildRuntimeWorkspaceRoots(config.workspaceDir),
302
+ initialTurnsPage: { limit: 50 },
303
+ config: this.buildConfigOverrides(config)
304
+ });
305
+ void this.logMcpServerInventory(session, sessionId, 'thread/resume');
306
+ try {
307
+ await this.bufferAllThreadItems(session, sessionId);
308
+ }
309
+ catch (error) {
310
+ console.warn('[CodexAppServerAdapter] Failed to list thread items after resume:', error);
311
+ }
312
+ const messages = await this.getAllMessages(sessionId, config);
313
+ session.messageBuffer = [];
314
+ return messages;
315
+ }
316
+ async sendPrompt(sessionId, parts, config) {
317
+ const session = this.requireSession(sessionId);
318
+ if (!session.threadId) {
319
+ throw new Error(`Codex app-server session has no thread id: ${sessionId}`);
320
+ }
321
+ const promptText = parts
322
+ .filter((part) => part.type === coding_agent_adapter_1.MessagePartType.TEXT && part.text)
323
+ .map((part) => part.text)
324
+ .join('\n');
325
+ if (!promptText) {
326
+ throw new Error('No text content in message parts');
327
+ }
328
+ session.messageBuffer = [];
329
+ const userItem = {
330
+ method: 'item/completed',
331
+ params: {
332
+ threadId: session.threadId,
333
+ turnId: session.activeTurnId || `local-${Date.now()}`,
334
+ item: {
335
+ id: `user-${Date.now()}`,
336
+ type: 'user_message',
337
+ text: promptText
338
+ }
339
+ }
340
+ };
341
+ this.addEvent(session, userItem);
342
+ session.status = coding_agent_adapter_1.SessionStatusType.BUSY;
343
+ session.lastError = null;
344
+ const result = await this.sendRpcRequest(session, 'turn/start', {
345
+ threadId: session.threadId,
346
+ input: [{ type: 'text', text: promptText }],
347
+ cwd: config.workspaceDir,
348
+ model: config.model || DEFAULT_CODEX_APP_SERVER_MODEL,
349
+ effort: config.reasoningEffort && config.reasoningEffort !== 'max' ? config.reasoningEffort : null,
350
+ approvalPolicy: config.permissionMode === 'allow' ? 'never' : 'on-request',
351
+ approvalsReviewer: 'user',
352
+ sandbox: this.resolveSandboxMode(config),
353
+ sandboxPolicy: this.buildSandboxPolicy(config),
354
+ runtimeWorkspaceRoots: this.buildRuntimeWorkspaceRoots(config.workspaceDir),
355
+ config: this.buildConfigOverrides(config)
356
+ });
357
+ if (isObject(result)) {
358
+ session.activeTurnId = asString(result.turnId) || asString(result.turn_id) || session.activeTurnId;
359
+ }
360
+ }
361
+ async getStatus(sessionId, _config) {
362
+ const session = this.sessions.get(sessionId);
363
+ if (!session) {
364
+ return { type: coding_agent_adapter_1.SessionStatusType.ERROR, message: 'Session not found' };
365
+ }
366
+ return {
367
+ type: session.status,
368
+ message: session.status === coding_agent_adapter_1.SessionStatusType.ERROR ? (session.lastError || 'Process error') : undefined
369
+ };
370
+ }
371
+ async pollMessages(sessionId, seenMessageIds, seenPartIds, partContentLengths, _config) {
372
+ const session = this.sessions.get(sessionId);
373
+ if (!session)
374
+ return [];
375
+ const parts = session.messageBuffer.flatMap((event) => this.convertEventToMessageParts(event, seenMessageIds, seenPartIds, partContentLengths, session));
376
+ session.messageBuffer = [];
377
+ return parts;
378
+ }
379
+ async getAllMessages(sessionId, _config) {
380
+ const session = this.requireSession(sessionId);
381
+ const seenMessageIds = new Set();
382
+ const seenPartIds = new Set();
383
+ const partContentLengths = new Map();
384
+ const partsByIdAndRole = new Map();
385
+ const assistantTextKeysByTurn = session.assistantTextKeysByTurn;
386
+ session.assistantTextKeysByTurn = new Map();
387
+ try {
388
+ for (const event of session.permanentMessages) {
389
+ const parts = this.convertEventToMessageParts(event, seenMessageIds, seenPartIds, partContentLengths, session);
390
+ for (const part of parts) {
391
+ const key = `${part.id || `part-${partsByIdAndRole.size}`}-${part.role || coding_agent_adapter_1.MessageRole.ASSISTANT}`;
392
+ if (!partsByIdAndRole.has(key) || part.update) {
393
+ partsByIdAndRole.set(key, part);
394
+ }
395
+ }
396
+ }
397
+ }
398
+ finally {
399
+ session.assistantTextKeysByTurn = assistantTextKeysByTurn;
400
+ }
401
+ const messages = [];
402
+ let currentMessage = null;
403
+ let previousPart = null;
404
+ let messageCounter = 0;
405
+ for (const part of partsByIdAndRole.values()) {
406
+ const role = part.role === coding_agent_adapter_1.MessageRole.USER
407
+ ? coding_agent_adapter_1.MessageRole.USER
408
+ : part.role === coding_agent_adapter_1.MessageRole.SYSTEM
409
+ ? coding_agent_adapter_1.MessageRole.SYSTEM
410
+ : coding_agent_adapter_1.MessageRole.ASSISTANT;
411
+ const startsNewMessage = !currentMessage
412
+ || currentMessage.role !== role
413
+ || !previousPart
414
+ || part.id !== previousPart.id;
415
+ if (startsNewMessage) {
416
+ if (currentMessage)
417
+ messages.push(currentMessage);
418
+ currentMessage = {
419
+ id: `msg-${messageCounter++}`,
420
+ role,
421
+ parts: []
422
+ };
423
+ }
424
+ currentMessage.parts.push(part);
425
+ previousPart = part;
426
+ }
427
+ if (currentMessage)
428
+ messages.push(currentMessage);
429
+ return messages;
430
+ }
431
+ async abortPrompt(sessionId, _config) {
432
+ const session = this.requireSession(sessionId);
433
+ if (!session.threadId || !session.activeTurnId)
434
+ return;
435
+ await this.sendRpcRequest(session, 'turn/interrupt', {
436
+ threadId: session.threadId,
437
+ turnId: session.activeTurnId
438
+ });
439
+ session.status = coding_agent_adapter_1.SessionStatusType.IDLE;
440
+ }
441
+ async destroySession(sessionId, _config) {
442
+ const session = this.sessions.get(sessionId);
443
+ if (!session)
444
+ return;
445
+ session.process.kill('SIGTERM');
446
+ setTimeout(() => {
447
+ if (!session.process.killed) {
448
+ session.process.kill('SIGKILL');
449
+ }
450
+ }, 1000);
451
+ session.messageBuffer.length = 0;
452
+ session.permanentMessages.length = 0;
453
+ session.pendingRequests.clear();
454
+ session.streamedTextByItemId.clear();
455
+ session.assistantTextKeysByTurn.clear();
456
+ session.runningTools.clear();
457
+ this.sessions.delete(sessionId);
458
+ }
459
+ async registerMcpServer(_serverName, _mcpConfig, _workspaceDir) {
460
+ // App Server reads MCP configuration from Codex config. Per-session MCP
461
+ // injection will be added after the thread config shape is verified in app.
462
+ console.log('[CodexAppServerAdapter] MCP server registration deferred to Codex app-server config');
463
+ }
464
+ async checkHealth() {
465
+ try {
466
+ const executable = await this.findCodexExecutable();
467
+ await this.execFileAsync(executable, ['app-server', '--help'], { timeout: 5000 });
468
+ return { available: true };
469
+ }
470
+ catch (error) {
471
+ return {
472
+ available: false,
473
+ reason: error instanceof Error ? error.message : String(error)
474
+ };
475
+ }
476
+ }
477
+ getPendingApproval(sessionId) {
478
+ return this.sessions.get(sessionId)?.pendingApproval || null;
479
+ }
480
+ async respondToApproval(sessionId, approved, optionId) {
481
+ const session = this.requireSession(sessionId);
482
+ const approval = session.pendingApproval;
483
+ if (!approval)
484
+ return;
485
+ const selected = optionId || approval.options.find((option) => approved
486
+ ? ['acceptForSession', 'accept', 'approved_for_session', 'approved'].includes(option.optionId)
487
+ : ['cancel', 'abort', 'decline', 'denied'].includes(option.optionId))?.optionId || (approved ? 'accept' : 'cancel');
488
+ const response = this.buildApprovalResponse(approval.responseKind, selected, approved);
489
+ this.sendRpcResponse(session, approval.requestId, response);
490
+ session.pendingApproval = null;
491
+ if (!approved) {
492
+ session.status = coding_agent_adapter_1.SessionStatusType.IDLE;
493
+ }
494
+ }
495
+ async getRunningTools(sessionId, _config) {
496
+ return Array.from(this.sessions.get(sessionId)?.runningTools.values() || []);
497
+ }
498
+ async startAppServerProcess(config, sessionId) {
499
+ const executable = await this.findCodexExecutable();
500
+ const authEnv = this.buildEnvironment(config);
501
+ const env = authEnv.env;
502
+ const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(executable);
503
+ const child = (0, child_process_1.spawn)(executable, ['app-server', '--stdio'], {
504
+ cwd: config.workspaceDir,
505
+ env,
506
+ stdio: ['pipe', 'pipe', 'pipe'],
507
+ ...(needsShell ? { shell: true } : {})
508
+ });
509
+ // Every pipe needs an error listener before the first write. The app server
510
+ // can exit at any moment, and an unhandled EPIPE on its stdin crashes the
511
+ // main process.
512
+ (0, child_stream_guards_1.guardChildStreams)(child, 'CodexAppServerAdapter');
513
+ const session = {
514
+ sessionId,
515
+ threadId: null,
516
+ activeTurnId: null,
517
+ process: child,
518
+ stdoutBuffer: '',
519
+ status: coding_agent_adapter_1.SessionStatusType.IDLE,
520
+ messageBuffer: [],
521
+ permanentMessages: [],
522
+ bufferedThreadItemIds: new Set(),
523
+ pendingCompletionRefreshes: 0,
524
+ sawThreadStatusNotification: false,
525
+ pendingThreadIdle: false,
526
+ pendingRequests: new Map(),
527
+ pendingApproval: null,
528
+ nextRequestId: 1,
529
+ lastError: null,
530
+ config,
531
+ streamedTextByItemId: new Map(),
532
+ assistantTextKeysByTurn: new Map(),
533
+ runningTools: new Map(),
534
+ codexUseApiKey: authEnv.usesApiKey,
535
+ codexAuthSummary: authEnv.summary
536
+ };
537
+ this.setupStdoutParser(child, session);
538
+ child.stderr?.on('data', (chunk) => {
539
+ console.log('[CodexAppServerAdapter] stderr:', chunk.toString());
540
+ });
541
+ child.on('exit', (code, signal) => {
542
+ console.log(`[CodexAppServerAdapter] process exited: code=${code}, signal=${signal}`);
543
+ if (code !== 0 && code !== null) {
544
+ session.status = coding_agent_adapter_1.SessionStatusType.ERROR;
545
+ session.lastError = `Codex app-server exited with code ${code}`;
546
+ }
547
+ });
548
+ return session;
549
+ }
550
+ async initializeAppServer(session) {
551
+ await this.sendRpcRequest(session, 'initialize', {
552
+ clientInfo: {
553
+ name: '20x',
554
+ title: '20x',
555
+ version: '0.0.1'
556
+ },
557
+ capabilities: {
558
+ experimentalApi: true,
559
+ mcpServerOpenaiFormElicitation: true
560
+ }
561
+ });
562
+ this.sendRpcNotification(session, 'initialized', {});
563
+ }
564
+ async findCodexExecutable() {
565
+ if (this.codexExecutablePath)
566
+ return this.codexExecutablePath;
567
+ const isWin = process.platform === 'win32';
568
+ const finder = isWin ? 'where' : 'which';
569
+ const binary = isWin ? 'codex.cmd' : 'codex';
570
+ const { stdout } = await this.execFileAsync(finder, [binary]);
571
+ this.codexExecutablePath = stdout.trim().split(/\r?\n/)[0];
572
+ return this.codexExecutablePath;
573
+ }
574
+ async execFileAsync(command, args, options) {
575
+ const { execFile } = await Promise.resolve().then(() => __importStar(require('child_process')));
576
+ const execFileAsync = (0, util_1.promisify)(execFile);
577
+ return await execFileAsync(command, args, options);
578
+ }
579
+ buildEnvironment(config) {
580
+ const env = { ...process.env };
581
+ for (const [key, value] of Object.entries(config.secretEnvVars || {})) {
582
+ env[key] = value;
583
+ }
584
+ const explicitApiKey = config.apiKeys?.openai;
585
+ const useApiKey = config.authMethod === 'api_key' ? true
586
+ : config.authMethod === 'subscription' ? false
587
+ : !!explicitApiKey;
588
+ if (useApiKey) {
589
+ const key = explicitApiKey || env.OPENAI_API_KEY || env.CODEX_API_KEY;
590
+ if (key) {
591
+ env.OPENAI_API_KEY = key;
592
+ env.CODEX_API_KEY = key;
593
+ }
594
+ env.NO_BROWSER = '1';
595
+ env.CODEX_HOME = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'codex-app-server-session-'));
596
+ return {
597
+ env,
598
+ usesApiKey: true,
599
+ summary: `API key (authMethod=${config.authMethod ?? 'legacy'}, isolated CODEX_HOME)`
600
+ };
601
+ }
602
+ if (!useApiKey) {
603
+ delete env.OPENAI_API_KEY;
604
+ delete env.CODEX_API_KEY;
605
+ if (!env.CODEX_HOME) {
606
+ env.CODEX_HOME = (0, path_1.join)((0, os_1.homedir)(), '.codex');
607
+ }
608
+ }
609
+ return {
610
+ env,
611
+ usesApiKey: false,
612
+ summary: `subscription (authMethod=${config.authMethod ?? 'legacy'}, CODEX_HOME=${env.CODEX_HOME ?? 'default'})`
613
+ };
614
+ }
615
+ buildConfigOverrides(config) {
616
+ const overrides = {};
617
+ if (config.reasoningEffort && config.reasoningEffort !== 'max') {
618
+ overrides.model_reasoning_effort = config.reasoningEffort;
619
+ }
620
+ if (this.resolveSandboxMode(config) === 'workspace-write') {
621
+ overrides.sandbox_workspace_write = {
622
+ network_access: true,
623
+ writable_roots: this.buildRuntimeWorkspaceRoots(config.workspaceDir)
624
+ };
625
+ }
626
+ if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
627
+ overrides.mcp_servers = this.convertMcpServers(config.mcpServers);
628
+ }
629
+ return overrides;
630
+ }
631
+ buildRuntimeWorkspaceRoots(workspaceDir) {
632
+ return uniquePaths([workspaceDir, ...this.resolveExternalGitRoots(workspaceDir)]);
633
+ }
634
+ resolveExternalGitRoots(workspaceDir) {
635
+ const workspaceRoot = (0, path_1.resolve)(workspaceDir);
636
+ const dotGitPath = (0, path_1.join)(workspaceRoot, '.git');
637
+ if (!(0, fs_1.existsSync)(dotGitPath))
638
+ return [];
639
+ try {
640
+ const dotGitContent = (0, fs_1.readFileSync)(dotGitPath, 'utf8').trim();
641
+ if (!dotGitContent.startsWith('gitdir:'))
642
+ return [];
643
+ const rawGitDir = dotGitContent.slice('gitdir:'.length).trim();
644
+ if (!rawGitDir)
645
+ return [];
646
+ const gitDir = (0, path_1.isAbsolute)(rawGitDir)
647
+ ? (0, path_1.resolve)(rawGitDir)
648
+ : (0, path_1.resolve)(workspaceRoot, rawGitDir);
649
+ const commonDirPath = (0, path_1.join)(gitDir, 'commondir');
650
+ const rawCommonDir = (0, fs_1.existsSync)(commonDirPath)
651
+ ? (0, fs_1.readFileSync)(commonDirPath, 'utf8').trim()
652
+ : '';
653
+ const commonDir = rawCommonDir
654
+ ? ((0, path_1.isAbsolute)(rawCommonDir) ? (0, path_1.resolve)(rawCommonDir) : (0, path_1.resolve)(gitDir, rawCommonDir))
655
+ : gitDir;
656
+ return [commonDir, gitDir].filter((path) => !isSubpath(workspaceRoot, path));
657
+ }
658
+ catch (error) {
659
+ console.warn('[CodexAppServerAdapter] Failed to resolve external git metadata roots:', error);
660
+ return [];
661
+ }
662
+ }
663
+ convertMcpServers(servers) {
664
+ const result = {};
665
+ for (const [name, server] of Object.entries(servers)) {
666
+ const codexName = normalizeCodexMcpServerName(name);
667
+ if (server.type === 'stdio') {
668
+ result[codexName] = {
669
+ command: server.command,
670
+ args: server.args || [],
671
+ env: server.env || {}
672
+ };
673
+ }
674
+ else {
675
+ const remoteConfig = { url: server.url };
676
+ if (server.headers && Object.keys(server.headers).length > 0) {
677
+ remoteConfig.http_headers = server.headers;
678
+ }
679
+ result[codexName] = remoteConfig;
680
+ }
681
+ }
682
+ return result;
683
+ }
684
+ async logMcpServerInventory(session, threadId, context) {
685
+ try {
686
+ const result = await this.sendRpcRequest(session, 'mcpServerStatus/list', {
687
+ threadId,
688
+ detail: 'toolsAndAuthOnly',
689
+ limit: 100
690
+ });
691
+ if (!isObject(result) || !Array.isArray(result.data))
692
+ return;
693
+ const servers = result.data
694
+ .filter(isObject)
695
+ .map((server) => {
696
+ const tools = server.tools;
697
+ const toolNames = Array.isArray(tools)
698
+ ? tools.map((tool) => isObject(tool) ? asString(tool.name) : undefined).filter(Boolean)
699
+ : isObject(tools)
700
+ ? Object.keys(tools)
701
+ : [];
702
+ return {
703
+ name: asString(server.name) || 'unknown',
704
+ authStatus: asString(server.authStatus) || null,
705
+ toolCount: toolNames.length,
706
+ toolNames: toolNames.slice(0, 50)
707
+ };
708
+ });
709
+ console.log('[CodexAppServerAdapter] MCP inventory', { context, threadId, servers });
710
+ }
711
+ catch (error) {
712
+ console.warn('[CodexAppServerAdapter] Failed to list MCP inventory:', error);
713
+ }
714
+ }
715
+ setupStdoutParser(process, session) {
716
+ process.stdout?.on('data', (chunk) => {
717
+ session.stdoutBuffer += chunk.toString();
718
+ let newlineIndex;
719
+ while ((newlineIndex = session.stdoutBuffer.indexOf('\n')) !== -1) {
720
+ const line = session.stdoutBuffer.slice(0, newlineIndex).trim();
721
+ session.stdoutBuffer = session.stdoutBuffer.slice(newlineIndex + 1);
722
+ if (!line)
723
+ continue;
724
+ try {
725
+ this.handleRpcMessage(session, JSON.parse(line));
726
+ }
727
+ catch (error) {
728
+ console.error('[CodexAppServerAdapter] Failed to parse JSON-RPC:', line, error);
729
+ }
730
+ }
731
+ });
732
+ }
733
+ handleRpcMessage(session, message) {
734
+ if ('id' in message && 'method' in message) {
735
+ this.handleServerRequest(session, message);
736
+ return;
737
+ }
738
+ if ('id' in message) {
739
+ const pending = session.pendingRequests.get(message.id);
740
+ if (!pending)
741
+ return;
742
+ session.pendingRequests.delete(message.id);
743
+ const response = message;
744
+ if (response.error) {
745
+ pending.reject(new Error(response.error.message));
746
+ }
747
+ else {
748
+ pending.resolve(response.result);
749
+ }
750
+ return;
751
+ }
752
+ if ('method' in message) {
753
+ this.handleNotification(session, message);
754
+ }
755
+ }
756
+ handleServerRequest(session, request) {
757
+ const params = isObject(request.params) ? request.params : {};
758
+ if (request.method.includes('Approval') || request.method.includes('requestApproval')) {
759
+ this.handleApprovalRequest(session, request, params);
760
+ return;
761
+ }
762
+ if (request.method === 'mcpServer/elicitation/request' || request.method === 'item/tool/requestUserInput') {
763
+ this.handleApprovalRequest(session, request, params);
764
+ return;
765
+ }
766
+ this.sendRpcResponse(session, request.id, {});
767
+ }
768
+ handleNotification(session, notification) {
769
+ const params = isObject(notification.params) ? notification.params : {};
770
+ if (notification.method === 'thread/status/changed') {
771
+ const status = asString(params.status);
772
+ session.status = status === 'running' || status === 'busy' ? coding_agent_adapter_1.SessionStatusType.BUSY : coding_agent_adapter_1.SessionStatusType.IDLE;
773
+ }
774
+ if (notification.method === 'turn/started') {
775
+ session.status = coding_agent_adapter_1.SessionStatusType.BUSY;
776
+ session.activeTurnId = asString(params.turnId) || session.activeTurnId;
777
+ session.pendingThreadIdle = false;
778
+ }
779
+ if (notification.method === 'turn/completed') {
780
+ session.activeTurnId = null;
781
+ if (session.threadId) {
782
+ session.pendingCompletionRefreshes += 1;
783
+ void this.reconcileCompletedTurn(session);
784
+ }
785
+ else {
786
+ session.status = coding_agent_adapter_1.SessionStatusType.IDLE;
787
+ }
788
+ }
789
+ if (notification.method === 'thread/status/changed') {
790
+ session.sawThreadStatusNotification = true;
791
+ const status = isObject(params.status) ? asString(params.status.type) : '';
792
+ if (status === 'active') {
793
+ session.status = coding_agent_adapter_1.SessionStatusType.BUSY;
794
+ session.pendingThreadIdle = false;
795
+ }
796
+ else if (status === 'idle') {
797
+ session.activeTurnId = null;
798
+ session.pendingThreadIdle = true;
799
+ this.markIdleIfSettled(session);
800
+ }
801
+ else if (status === 'systemError') {
802
+ session.status = coding_agent_adapter_1.SessionStatusType.ERROR;
803
+ session.lastError = 'Codex app-server thread entered system error state';
804
+ }
805
+ }
806
+ if (notification.method === 'error') {
807
+ session.status = coding_agent_adapter_1.SessionStatusType.ERROR;
808
+ session.lastError = extractText(params) || 'Codex app-server error';
809
+ }
810
+ if (notification.method === 'serverRequest/resolved') {
811
+ const requestId = params.requestId;
812
+ if (session.pendingApproval && String(session.pendingApproval.requestId) === String(requestId)) {
813
+ session.pendingApproval = null;
814
+ if (session.status === coding_agent_adapter_1.SessionStatusType.WAITING_APPROVAL) {
815
+ session.status = coding_agent_adapter_1.SessionStatusType.BUSY;
816
+ }
817
+ }
818
+ }
819
+ this.addEvent(session, notification);
820
+ }
821
+ handleApprovalRequest(session, request, params) {
822
+ if (session.config.permissionMode === 'allow') {
823
+ const responseKind = this.getApprovalResponseKind(request.method);
824
+ const selected = responseKind === 'execCommand' ? 'approved' : 'accept';
825
+ this.sendRpcResponse(session, request.id, this.buildApprovalResponse(responseKind, selected, true));
826
+ return;
827
+ }
828
+ const toolCallId = asString(params.approvalId) || asString(params.itemId) || asString(params.callId) || String(request.id);
829
+ const responseKind = this.getApprovalResponseKind(request.method);
830
+ const rawDecisions = Array.isArray(params.availableDecisions) ? params.availableDecisions : [];
831
+ const approvalOptions = rawDecisions.length > 0
832
+ ? rawDecisions.map((decision) => {
833
+ const optionId = normalizeDecisionName(decision);
834
+ return {
835
+ optionId,
836
+ name: decisionLabel(optionId),
837
+ kind: optionId.includes('accept') || optionId === 'approved' ? 'allow' : 'reject'
838
+ };
839
+ })
840
+ : [
841
+ { optionId: 'accept', name: 'Allow', kind: 'allow' },
842
+ { optionId: 'cancel', name: 'Deny', kind: 'reject' }
843
+ ];
844
+ session.pendingApproval = {
845
+ requestId: request.id,
846
+ toolCallId,
847
+ question: summarizeApproval(params, request.method),
848
+ options: approvalOptions,
849
+ responseKind
850
+ };
851
+ session.status = coding_agent_adapter_1.SessionStatusType.WAITING_APPROVAL;
852
+ this.onDataAvailable?.(session.threadId || session.sessionId);
853
+ }
854
+ buildApprovalResponse(responseKind, selected, approved) {
855
+ switch (responseKind) {
856
+ case 'execCommand':
857
+ return { decision: approved ? (selected === 'approved_for_session' ? 'approved_for_session' : 'approved') : (selected === 'denied' ? 'denied' : 'abort') };
858
+ case 'commandExecution':
859
+ return { decision: selected };
860
+ case 'fileChange':
861
+ case 'permissions':
862
+ return { decision: selected };
863
+ case 'elicitation':
864
+ return approved
865
+ ? { action: 'accept', content: {} }
866
+ : { action: 'decline' };
867
+ case 'userInput':
868
+ return approved
869
+ ? { response: selected }
870
+ : { response: null };
871
+ default:
872
+ return { decision: approved ? selected : 'cancel' };
873
+ }
874
+ }
875
+ getApprovalResponseKind(method) {
876
+ if (method === 'execCommandApproval')
877
+ return 'execCommand';
878
+ if (method.includes('commandExecution'))
879
+ return 'commandExecution';
880
+ if (method.includes('fileChange'))
881
+ return 'fileChange';
882
+ if (method.includes('permissions'))
883
+ return 'permissions';
884
+ if (method === 'mcpServer/elicitation/request')
885
+ return 'elicitation';
886
+ if (method === 'item/tool/requestUserInput')
887
+ return 'userInput';
888
+ return 'generic';
889
+ }
890
+ resolveSandboxMode(config) {
891
+ switch (config.sandboxMode) {
892
+ case 'read-only':
893
+ case 'workspace-write':
894
+ case 'danger-full-access':
895
+ return config.sandboxMode;
896
+ default:
897
+ return 'danger-full-access';
898
+ }
899
+ }
900
+ buildSandboxPolicy(config) {
901
+ switch (this.resolveSandboxMode(config)) {
902
+ case 'read-only':
903
+ return { type: 'readOnly', networkAccess: true };
904
+ case 'danger-full-access':
905
+ return { type: 'dangerFullAccess' };
906
+ case 'workspace-write':
907
+ default:
908
+ return {
909
+ type: 'workspaceWrite',
910
+ networkAccess: true,
911
+ writableRoots: this.buildRuntimeWorkspaceRoots(config.workspaceDir)
912
+ };
913
+ }
914
+ }
915
+ addEvent(session, event) {
916
+ session.messageBuffer.push(event);
917
+ session.permanentMessages.push(event);
918
+ if (session.permanentMessages.length > 1000) {
919
+ session.permanentMessages.splice(0, 250);
920
+ }
921
+ this.onDataAvailable?.(session.threadId || session.sessionId);
922
+ }
923
+ bufferThreadItems(session, result) {
924
+ const items = isObject(result) && Array.isArray(result.data) ? result.data : [];
925
+ for (const item of items) {
926
+ this.bufferReconciledThreadItem(session, item, isObject(item) ? asString(item.turnId) : undefined);
927
+ }
928
+ }
929
+ /**
930
+ * Buffers a single thread item from a reconcile pass, skipping it if it has
931
+ * already been buffered. reconcileCompletedTurn() re-lists the entire thread on
932
+ * every turn/completed, so this MUST be idempotent for every item — including
933
+ * the many Codex item types that carry no top-level `id` (function_call_output,
934
+ * custom_tool_call_output, tool_search_output, user/developer messages). We key
935
+ * off a stable identity (id/itemId/call_id) or, failing that, a deterministic
936
+ * content hash, and forward that key as `itemId` so the derived part id stays
937
+ * stable across passes. Without this, id-less items were re-emitted with a fresh
938
+ * id on every idle and the transcript repeated older messages.
939
+ */
940
+ bufferReconciledThreadItem(session, item, turnId) {
941
+ let stableItemId;
942
+ if (isObject(item)) {
943
+ stableItemId = this.computeThreadItemKey(item, turnId);
944
+ if (session.bufferedThreadItemIds.has(stableItemId))
945
+ return;
946
+ session.bufferedThreadItemIds.add(stableItemId);
947
+ }
948
+ this.addEvent(session, {
949
+ method: 'item/completed',
950
+ params: {
951
+ threadId: session.threadId,
952
+ turnId,
953
+ item,
954
+ ...(stableItemId ? { itemId: stableItemId } : {})
955
+ }
956
+ });
957
+ }
958
+ computeThreadItemKey(item, turnId) {
959
+ const identity = deriveItemIdentity(item);
960
+ if (identity)
961
+ return identity;
962
+ // No id-like field (e.g. user/developer input messages). Build a deterministic
963
+ // key from turn + type + role + content so the same item maps to the same key
964
+ // across reconcile passes instead of duplicating.
965
+ const type = asString(item.type) || 'item';
966
+ const role = extractRole(item);
967
+ return `synthetic:${turnId || 'noturn'}:${type}:${role}:${hashString(extractText(item))}`;
968
+ }
969
+ async bufferAllThreadItems(session, threadId) {
970
+ try {
971
+ let cursor = null;
972
+ for (let page = 0; page < 20; page++) {
973
+ const result = await this.sendRpcRequest(session, 'thread/items/list', {
974
+ threadId,
975
+ cursor,
976
+ limit: 200,
977
+ sortDirection: 'asc'
978
+ });
979
+ this.bufferThreadItems(session, result);
980
+ cursor = isObject(result) ? (asString(result.nextCursor) || null) : null;
981
+ if (!cursor)
982
+ return;
983
+ }
984
+ console.warn('[CodexAppServerAdapter] Stopped thread/items pagination after 20 pages');
985
+ }
986
+ catch (error) {
987
+ const message = error instanceof Error ? error.message : String(error);
988
+ if (!message.includes('not supported'))
989
+ throw error;
990
+ await this.bufferAllThreadTurns(session, threadId);
991
+ }
992
+ }
993
+ async bufferAllThreadTurns(session, threadId) {
994
+ let cursor = null;
995
+ for (let page = 0; page < 20; page++) {
996
+ let result;
997
+ try {
998
+ result = await this.sendRpcRequest(session, 'thread/turns/list', {
999
+ threadId,
1000
+ cursor,
1001
+ limit: 100,
1002
+ sortDirection: 'asc',
1003
+ itemsView: 'full'
1004
+ });
1005
+ }
1006
+ catch (error) {
1007
+ const message = error instanceof Error ? error.message : String(error);
1008
+ if (message.includes('not materialized yet') || message.includes('before first user message')) {
1009
+ return;
1010
+ }
1011
+ throw error;
1012
+ }
1013
+ const turns = isObject(result) && Array.isArray(result.data) ? result.data : [];
1014
+ for (const turn of turns) {
1015
+ if (!isObject(turn) || !Array.isArray(turn.items))
1016
+ continue;
1017
+ for (const item of turn.items) {
1018
+ this.bufferReconciledThreadItem(session, item, asString(turn.id));
1019
+ }
1020
+ }
1021
+ cursor = isObject(result) ? (asString(result.nextCursor) || null) : null;
1022
+ if (!cursor)
1023
+ return;
1024
+ }
1025
+ console.warn('[CodexAppServerAdapter] Stopped thread/turns pagination after 20 pages');
1026
+ }
1027
+ async reconcileCompletedTurn(session) {
1028
+ try {
1029
+ if (!session.threadId)
1030
+ return;
1031
+ await this.bufferAllThreadItems(session, session.threadId);
1032
+ }
1033
+ catch (error) {
1034
+ console.warn('[CodexAppServerAdapter] Failed to reconcile completed turn items:', error);
1035
+ }
1036
+ finally {
1037
+ session.pendingCompletionRefreshes = Math.max(0, session.pendingCompletionRefreshes - 1);
1038
+ this.markIdleIfSettled(session);
1039
+ }
1040
+ }
1041
+ markIdleIfSettled(session) {
1042
+ if (session.status === coding_agent_adapter_1.SessionStatusType.ERROR)
1043
+ return;
1044
+ if (session.pendingCompletionRefreshes > 0)
1045
+ return;
1046
+ if (session.activeTurnId)
1047
+ return;
1048
+ if (session.sawThreadStatusNotification && !session.pendingThreadIdle)
1049
+ return;
1050
+ session.pendingThreadIdle = false;
1051
+ session.status = coding_agent_adapter_1.SessionStatusType.IDLE;
1052
+ this.onDataAvailable?.(session.threadId || session.sessionId);
1053
+ }
1054
+ convertEventToMessageParts(event, seenMessageIds, seenPartIds, partContentLengths, session) {
1055
+ if (!isObject(event) || !asString(event.method))
1056
+ return [];
1057
+ const method = event.method;
1058
+ const params = isObject(event.params) ? event.params : {};
1059
+ const item = isObject(params.item) ? params.item : undefined;
1060
+ if (method === 'item/agentMessage/delta') {
1061
+ const itemId = asString(params.itemId) || `agent-${Date.now()}`;
1062
+ const partId = `agent-${itemId}`;
1063
+ const delta = asString(params.delta) || '';
1064
+ const previous = session.streamedTextByItemId.get(itemId) || '';
1065
+ const next = previous + delta;
1066
+ const update = seenPartIds.has(partId);
1067
+ seenPartIds.add(partId);
1068
+ session.streamedTextByItemId.set(itemId, next);
1069
+ this.markAssistantTextForTurn(session, params, {}, next);
1070
+ partContentLengths.set(partId, String(next.length));
1071
+ return [{
1072
+ id: partId,
1073
+ type: coding_agent_adapter_1.MessagePartType.TEXT,
1074
+ text: next,
1075
+ role: coding_agent_adapter_1.MessageRole.ASSISTANT,
1076
+ update
1077
+ }];
1078
+ }
1079
+ if (method === 'item/reasoning/textDelta' || method === 'item/reasoning/summaryTextDelta') {
1080
+ const itemId = asString(params.itemId) || `reasoning-${Date.now()}`;
1081
+ const partId = `reasoning-${itemId}`;
1082
+ const text = asString(params.delta) || '';
1083
+ if (!text && seenPartIds.has(partId))
1084
+ return [];
1085
+ seenPartIds.add(partId);
1086
+ partContentLengths.set(partId, String(text.length));
1087
+ return [{
1088
+ id: partId,
1089
+ type: coding_agent_adapter_1.MessagePartType.REASONING,
1090
+ text,
1091
+ role: coding_agent_adapter_1.MessageRole.ASSISTANT
1092
+ }];
1093
+ }
1094
+ if (method === 'item/commandExecution/outputDelta' ||
1095
+ method === 'command/exec/outputDelta' ||
1096
+ method === 'process/outputDelta') {
1097
+ return this.convertOutputDeltaEvent('command', params, seenPartIds, partContentLengths, session);
1098
+ }
1099
+ if (method === 'item/fileChange/outputDelta' ||
1100
+ method === 'item/fileChange/patchUpdated') {
1101
+ return this.convertOutputDeltaEvent('file_change', params, seenPartIds, partContentLengths, session);
1102
+ }
1103
+ if (method === 'item/plan/delta' ||
1104
+ method === 'turn/plan/updated') {
1105
+ return this.convertOutputDeltaEvent('plan', params, seenPartIds, partContentLengths, session);
1106
+ }
1107
+ if (method === 'item/mcpToolCall/progress') {
1108
+ return this.convertOutputDeltaEvent('mcp_tool', params, seenPartIds, partContentLengths, session);
1109
+ }
1110
+ if ((method === 'item/started' || method === 'item/completed') && item) {
1111
+ return this.convertThreadItem(method, item, params, seenMessageIds, seenPartIds, partContentLengths, session);
1112
+ }
1113
+ if (method === 'error') {
1114
+ const partId = `error-${Date.now()}`;
1115
+ return [{
1116
+ id: partId,
1117
+ type: coding_agent_adapter_1.MessagePartType.ERROR,
1118
+ text: extractText(params) || 'Codex app-server error',
1119
+ role: coding_agent_adapter_1.MessageRole.ASSISTANT
1120
+ }];
1121
+ }
1122
+ return [];
1123
+ }
1124
+ convertOutputDeltaEvent(toolName, params, seenPartIds, partContentLengths, session) {
1125
+ const itemId = asString(params.itemId) || asString(params.processId) || asString(params.turnId) || `${toolName}-${Date.now()}`;
1126
+ const partId = `tool-${itemId}`;
1127
+ const previous = session.streamedTextByItemId.get(partId) || '';
1128
+ const next = truncateForIpc(previous + extractText(params), MAX_IPC_TOOL_OUTPUT_CHARS);
1129
+ const update = seenPartIds.has(partId);
1130
+ seenPartIds.add(partId);
1131
+ session.streamedTextByItemId.set(partId, next);
1132
+ partContentLengths.set(partId, `${next.length}:${toolName}`);
1133
+ return [{
1134
+ id: partId,
1135
+ type: coding_agent_adapter_1.MessagePartType.TOOL,
1136
+ role: coding_agent_adapter_1.MessageRole.ASSISTANT,
1137
+ tool: {
1138
+ name: toolName,
1139
+ status: 'running',
1140
+ title: toolName,
1141
+ input: stringifyForIpc(params, MAX_IPC_TOOL_INPUT_CHARS),
1142
+ output: next
1143
+ },
1144
+ update
1145
+ }];
1146
+ }
1147
+ convertThreadItem(method, item, params, _seenMessageIds, seenPartIds, partContentLengths, session) {
1148
+ const itemId = extractItemId(params);
1149
+ const type = asString(item.type) || '';
1150
+ const role = extractRole(item);
1151
+ const text = extractText(item);
1152
+ if (type.includes('reasoning')) {
1153
+ const partId = `reasoning-${itemId}`;
1154
+ if (seenPartIds.has(partId))
1155
+ return [];
1156
+ seenPartIds.add(partId);
1157
+ partContentLengths.set(partId, String(text.length));
1158
+ return [{
1159
+ id: partId,
1160
+ type: coding_agent_adapter_1.MessagePartType.REASONING,
1161
+ text,
1162
+ role: coding_agent_adapter_1.MessageRole.ASSISTANT
1163
+ }];
1164
+ }
1165
+ if (role === 'user' || type.includes('user') || type === 'user_message') {
1166
+ const partId = `user-${itemId}`;
1167
+ if (seenPartIds.has(partId))
1168
+ return [];
1169
+ seenPartIds.add(partId);
1170
+ partContentLengths.set(partId, String(text.length));
1171
+ return [{
1172
+ id: partId,
1173
+ type: coding_agent_adapter_1.MessagePartType.TEXT,
1174
+ text,
1175
+ role: coding_agent_adapter_1.MessageRole.USER
1176
+ }];
1177
+ }
1178
+ if (role === 'assistant' || type.includes('agent') || type.includes('assistant') || (!type && text)) {
1179
+ const partId = `agent-${itemId}`;
1180
+ if (seenPartIds.has(partId) && method !== 'item/completed')
1181
+ return [];
1182
+ const finalText = text || session.streamedTextByItemId.get(itemId) || '';
1183
+ const alreadySeen = seenPartIds.has(partId);
1184
+ if (!alreadySeen && method === 'item/completed' && this.hasSeenAssistantTextForTurn(session, params, item, finalText)) {
1185
+ return [];
1186
+ }
1187
+ seenPartIds.add(partId);
1188
+ if (method === 'item/completed') {
1189
+ this.markAssistantTextForTurn(session, params, item, finalText);
1190
+ }
1191
+ partContentLengths.set(partId, String(finalText.length));
1192
+ return [{
1193
+ id: partId,
1194
+ type: coding_agent_adapter_1.MessagePartType.TEXT,
1195
+ text: finalText,
1196
+ role: coding_agent_adapter_1.MessageRole.ASSISTANT,
1197
+ update: method === 'item/completed' && session.streamedTextByItemId.has(itemId)
1198
+ }];
1199
+ }
1200
+ const toolName = extractToolName(item);
1201
+ const toolTitle = extractToolTitle(item, toolName);
1202
+ const partId = `tool-${itemId}`;
1203
+ const isCompleted = method === 'item/completed';
1204
+ if (!isCompleted && seenPartIds.has(partId))
1205
+ return [];
1206
+ seenPartIds.add(partId);
1207
+ const part = {
1208
+ id: partId,
1209
+ type: coding_agent_adapter_1.MessagePartType.TOOL,
1210
+ role: coding_agent_adapter_1.MessageRole.ASSISTANT,
1211
+ tool: {
1212
+ name: toolName,
1213
+ status: isCompleted ? 'completed' : 'running',
1214
+ title: toolTitle,
1215
+ input: stringifyForIpc(item, MAX_IPC_TOOL_INPUT_CHARS),
1216
+ output: isCompleted ? stringifyForIpc(extractText(item) || item, MAX_IPC_TOOL_OUTPUT_CHARS) : undefined
1217
+ },
1218
+ update: isCompleted
1219
+ };
1220
+ if (isCompleted) {
1221
+ session.runningTools.delete(partId);
1222
+ }
1223
+ else {
1224
+ session.runningTools.set(partId, {
1225
+ partId,
1226
+ toolName,
1227
+ startTime: typeof params.startedAtMs === 'number' ? params.startedAtMs : Date.now(),
1228
+ input: item
1229
+ });
1230
+ }
1231
+ partContentLengths.set(partId, `${part.tool?.status}:${toolName}`);
1232
+ return [part];
1233
+ }
1234
+ hasSeenAssistantTextForTurn(session, params, item, text) {
1235
+ const key = this.buildAssistantTextKey(text);
1236
+ if (!key)
1237
+ return false;
1238
+ for (const scopeKey of this.extractAssistantTextScopeKeys(params, item, key)) {
1239
+ if (session.assistantTextKeysByTurn.get(scopeKey)?.has(key))
1240
+ return true;
1241
+ }
1242
+ return false;
1243
+ }
1244
+ markAssistantTextForTurn(session, params, item, text) {
1245
+ const key = this.buildAssistantTextKey(text);
1246
+ if (!key)
1247
+ return;
1248
+ for (const scopeKey of this.extractAssistantTextScopeKeys(params, item, key)) {
1249
+ const seenForScope = session.assistantTextKeysByTurn.get(scopeKey) ?? new Set();
1250
+ seenForScope.add(key);
1251
+ session.assistantTextKeysByTurn.set(scopeKey, seenForScope);
1252
+ }
1253
+ }
1254
+ extractTurnKey(params, item) {
1255
+ const metadata = isObject(item.internal_chat_message_metadata_passthrough)
1256
+ ? item.internal_chat_message_metadata_passthrough
1257
+ : {};
1258
+ return (asString(params.turnId) ||
1259
+ asString(params.turn_id) ||
1260
+ asString(item.turnId) ||
1261
+ asString(item.turn_id) ||
1262
+ asString(metadata.turn_id) ||
1263
+ asString(metadata.turnId) ||
1264
+ asString(params.threadId) ||
1265
+ 'unknown-turn');
1266
+ }
1267
+ extractAssistantTextScopeKeys(params, item, textKey) {
1268
+ const keys = new Set([this.extractTurnKey(params, item)]);
1269
+ const threadKey = asString(params.threadId) || asString(item.threadId) || asString(item.thread_id);
1270
+ if (threadKey && textKey.length >= MIN_THREAD_LEVEL_ASSISTANT_DEDUPE_CHARS) {
1271
+ keys.add(`thread:${threadKey}`);
1272
+ }
1273
+ return Array.from(keys);
1274
+ }
1275
+ buildAssistantTextKey(text) {
1276
+ return text.trim().replace(/\s+/g, ' ');
1277
+ }
1278
+ sendRpcRequest(session, method, params) {
1279
+ return new Promise((resolve, reject) => {
1280
+ const id = session.nextRequestId++;
1281
+ session.pendingRequests.set(id, { resolve, reject });
1282
+ this.writeJson(session, { jsonrpc: '2.0', id, method, params });
1283
+ setTimeout(() => {
1284
+ if (!session.pendingRequests.has(id))
1285
+ return;
1286
+ session.pendingRequests.delete(id);
1287
+ reject(new Error(`Codex app-server RPC timed out: ${method}`));
1288
+ }, 30000);
1289
+ });
1290
+ }
1291
+ sendRpcResponse(session, id, result) {
1292
+ this.writeJson(session, { jsonrpc: '2.0', id, result });
1293
+ }
1294
+ sendRpcNotification(session, method, params) {
1295
+ this.writeJson(session, { jsonrpc: '2.0', method, params });
1296
+ }
1297
+ writeJson(session, payload) {
1298
+ // A dead app server must not take the application with it.
1299
+ (0, child_stream_guards_1.writeToChildStdin)(session.process, `${JSON.stringify(payload)}\n`, 'CodexAppServerAdapter');
1300
+ }
1301
+ requireSession(sessionId) {
1302
+ const session = this.sessions.get(sessionId);
1303
+ if (!session) {
1304
+ throw new Error(`Session not found: ${sessionId}`);
1305
+ }
1306
+ return session;
1307
+ }
1308
+ }
1309
+ exports.CodexAppServerAdapter = CodexAppServerAdapter;