@glyphteck/veyl 0.67.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,737 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createInterface } from 'node:readline';
3
+ import process from 'node:process';
4
+
5
+ const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
6
+ const DEFAULT_CLOSE_TIMEOUT_MS = 5_000;
7
+ const MAX_STDERR_CHARS = 2_000;
8
+
9
+ const modernDeclines = new Set([
10
+ 'item/commandExecution/requestApproval',
11
+ 'item/fileChange/requestApproval',
12
+ ]);
13
+
14
+ const legacyDeclines = new Set([
15
+ 'applyPatchApproval',
16
+ 'execCommandApproval',
17
+ ]);
18
+
19
+ function cleanText(value) {
20
+ return typeof value === 'string' ? value.trim() : '';
21
+ }
22
+
23
+ function optional(target, key, value) {
24
+ if (value !== undefined && value !== null && value !== '') target[key] = value;
25
+ return target;
26
+ }
27
+
28
+ function processError(message, details = {}) {
29
+ const error = new Error(message);
30
+ error.name = 'CodexAppServerProcessError';
31
+ Object.assign(error, details);
32
+ return error;
33
+ }
34
+
35
+ function missingRollout(error) {
36
+ return error instanceof CodexRpcError
37
+ && error.method === 'thread/resume'
38
+ && /no rollout found for thread id/iu.test(error.message);
39
+ }
40
+
41
+ function turnMessage(items = []) {
42
+ const messages = items.filter((item) => item?.type === 'agentMessage');
43
+ const final = messages.findLast((item) => item.phase === 'final_answer');
44
+ const selected = final || messages.at(-1);
45
+ return cleanText(selected?.text);
46
+ }
47
+
48
+ function normalizeTurn(turn = {}) {
49
+ const items = Array.isArray(turn.items) ? turn.items : [];
50
+ const inputIds = items
51
+ .filter((item) => item?.type === 'userMessage')
52
+ .map((item) => cleanText(item.clientId))
53
+ .filter(Boolean);
54
+ return {
55
+ id: cleanText(turn.id),
56
+ status: cleanText(turn.status),
57
+ error: turn.error ?? null,
58
+ inputIds: inputIds.length || turn.itemsView === 'full'
59
+ ? inputIds
60
+ : null,
61
+ finalText: turnMessage(items),
62
+ };
63
+ }
64
+
65
+ function normalizeThread(result, threadId) {
66
+ const turns = Array.isArray(result?.initialTurnsPage?.data)
67
+ ? result.initialTurnsPage.data
68
+ : result?.thread?.turns || [];
69
+ return {
70
+ threadId,
71
+ turns: turns
72
+ .map(normalizeTurn)
73
+ .filter((turn) => turn.id),
74
+ };
75
+ }
76
+
77
+ function inputParams(input, clientUserMessageId) {
78
+ if (!Array.isArray(input) || input.length === 0) {
79
+ throw new Error('Codex turn input must be a non-empty array');
80
+ }
81
+ const params = { input };
82
+ if (clientUserMessageId !== undefined) {
83
+ params.clientUserMessageId = clientUserMessageId;
84
+ }
85
+ return params;
86
+ }
87
+
88
+ export class CodexRpcError extends Error {
89
+ constructor(method, error = {}) {
90
+ super(cleanText(error.message) || `${method} failed`);
91
+ this.name = 'CodexRpcError';
92
+ this.method = method;
93
+ if (error.code !== undefined) this.code = error.code;
94
+ if (error.data !== undefined) this.data = error.data;
95
+ }
96
+ }
97
+
98
+ export class CodexAppServer {
99
+ constructor(options = {}) {
100
+ this.codexPath = cleanText(options.codexPath)
101
+ || cleanText(process.env.CODEX_PATH)
102
+ || 'codex';
103
+ this.cwd = cleanText(options.cwd) || process.cwd();
104
+ this.threadId = cleanText(options.threadId) || '';
105
+ this.model = cleanText(options.model) || '';
106
+ this.effort = cleanText(options.effort) || '';
107
+ this.developerInstructions = cleanText(options.developerInstructions) || '';
108
+ this.replaceMissingThread = options.replaceMissingThread === true;
109
+ this.serviceName = cleanText(options.serviceName) || 'veyl';
110
+ this.sandbox = options.sandbox ?? 'danger-full-access';
111
+ this.sandboxPolicy = options.sandboxPolicy ?? { type: 'dangerFullAccess' };
112
+ this.requestTimeoutMs = options.requestTimeoutMs
113
+ ?? DEFAULT_REQUEST_TIMEOUT_MS;
114
+ this.closeTimeoutMs = options.closeTimeoutMs
115
+ ?? DEFAULT_CLOSE_TIMEOUT_MS;
116
+ this.spawnProcess = options.spawnProcess || spawn;
117
+ this.onEventError = typeof options.onEventError === 'function'
118
+ ? options.onEventError
119
+ : () => {};
120
+ this.now = typeof options.now === 'function' ? options.now : Date.now;
121
+ this.listeners = new Set();
122
+ if (typeof options.onEvent === 'function') this.listeners.add(options.onEvent);
123
+
124
+ this.process = null;
125
+ this.reader = null;
126
+ this.opening = null;
127
+ this.openResult = null;
128
+ this.nextId = 1;
129
+ this.pending = new Map();
130
+ this.turns = new Map();
131
+ this.writeChain = Promise.resolve();
132
+ this.exitPromise = Promise.resolve();
133
+ this.resolveExit = null;
134
+ this.closed = false;
135
+ this.closing = false;
136
+ }
137
+
138
+ subscribe(listener) {
139
+ if (typeof listener !== 'function') {
140
+ throw new Error('Codex event listener must be a function');
141
+ }
142
+ this.listeners.add(listener);
143
+ return () => this.listeners.delete(listener);
144
+ }
145
+
146
+ async open() {
147
+ if (this.closed) throw new Error('Codex App Server is closed');
148
+ if (this.openResult) return this.openResult;
149
+ if (!this.opening) {
150
+ const opening = this.startProcess();
151
+ this.opening = opening;
152
+ opening.finally(() => {
153
+ if (this.opening === opening) this.opening = null;
154
+ }).catch(() => {});
155
+ }
156
+ return this.opening;
157
+ }
158
+
159
+ async startTurn({ input, clientUserMessageId } = {}) {
160
+ await this.open();
161
+ const params = {
162
+ threadId: this.threadId,
163
+ ...inputParams(input, clientUserMessageId),
164
+ cwd: this.cwd,
165
+ approvalPolicy: 'never',
166
+ sandboxPolicy: this.sandboxPolicy,
167
+ };
168
+ optional(params, 'model', this.model);
169
+ optional(params, 'effort', this.effort);
170
+ const result = await this.request('turn/start', params);
171
+ const turnId = cleanText(result?.turn?.id);
172
+ if (!turnId) throw new Error('Codex App Server did not return a turn id');
173
+ return { turnId };
174
+ }
175
+
176
+ async steerTurn({ input, clientUserMessageId, expectedTurnId } = {}) {
177
+ const turnId = cleanText(expectedTurnId);
178
+ if (!turnId) throw new Error('expectedTurnId is required to steer a Codex turn');
179
+ await this.open();
180
+ return this.request('turn/steer', {
181
+ threadId: this.threadId,
182
+ ...inputParams(input, clientUserMessageId),
183
+ expectedTurnId: turnId,
184
+ });
185
+ }
186
+
187
+ async close() {
188
+ if (this.closed) return;
189
+ this.closed = true;
190
+ this.closing = true;
191
+ const child = this.process;
192
+ if (!child) return;
193
+
194
+ this.rejectPending(processError('Codex App Server closed'));
195
+ if (child.stdin?.writable) child.stdin.end();
196
+ if (child.exitCode === null && child.signalCode === null) child.kill('SIGTERM');
197
+
198
+ const timeout = new Promise((resolve) => {
199
+ const timer = setTimeout(() => resolve('timeout'), this.closeTimeoutMs);
200
+ timer.unref?.();
201
+ });
202
+ if (await Promise.race([this.exitPromise, timeout]) === 'timeout'
203
+ && child.exitCode === null && child.signalCode === null) {
204
+ child.kill('SIGKILL');
205
+ await this.exitPromise;
206
+ }
207
+ }
208
+
209
+ async startProcess() {
210
+ let child;
211
+ try {
212
+ child = this.spawnProcess(this.codexPath, ['app-server'], {
213
+ cwd: this.cwd,
214
+ stdio: ['pipe', 'pipe', 'pipe'],
215
+ });
216
+ } catch (error) {
217
+ throw processError('Could not start Codex App Server', { cause: error });
218
+ }
219
+ this.attachProcess(child);
220
+
221
+ try {
222
+ await this.request('initialize', {
223
+ clientInfo: {
224
+ name: 'veyl_codex_agent',
225
+ title: 'Veyl Codex Agent',
226
+ version: '1.0.0',
227
+ },
228
+ capabilities: {
229
+ experimentalApi: true,
230
+ },
231
+ });
232
+ await this.notify('initialized', {});
233
+ let resumed = Boolean(this.threadId);
234
+ let replacedThreadId = '';
235
+ let result;
236
+ if (resumed) {
237
+ try {
238
+ result = await this.resumeThread();
239
+ } catch (error) {
240
+ if (!this.replaceMissingThread || !missingRollout(error)) {
241
+ throw error;
242
+ }
243
+ replacedThreadId = this.threadId;
244
+ this.threadId = '';
245
+ resumed = false;
246
+ result = await this.createThread();
247
+ }
248
+ } else {
249
+ result = await this.createThread();
250
+ }
251
+ const actualThreadId = cleanText(result?.thread?.id);
252
+ if (!actualThreadId) {
253
+ throw new Error('Codex App Server did not return a thread id');
254
+ }
255
+ if (this.threadId && actualThreadId !== this.threadId) {
256
+ throw new Error(`Codex resumed an unexpected thread: ${actualThreadId}`);
257
+ }
258
+ this.threadId = actualThreadId;
259
+ const opened = {
260
+ ...normalizeThread(result, actualThreadId),
261
+ ...(replacedThreadId ? { replacedThreadId } : {}),
262
+ };
263
+ this.openResult = opened;
264
+ this.emit({
265
+ type: 'server.ready',
266
+ threadId: actualThreadId,
267
+ resumed,
268
+ result: opened,
269
+ });
270
+ return opened;
271
+ } catch (error) {
272
+ await this.stopFailedProcess(child);
273
+ throw error;
274
+ }
275
+ }
276
+
277
+ attachProcess(child) {
278
+ if (!child?.stdin || !child?.stdout || !child?.stderr) {
279
+ throw processError('Codex App Server did not provide stdio streams');
280
+ }
281
+ this.process = child;
282
+ this.openResult = null;
283
+ this.writeChain = Promise.resolve();
284
+ this.exitPromise = new Promise((resolve) => {
285
+ this.resolveExit = resolve;
286
+ });
287
+ child.stdout.setEncoding?.('utf8');
288
+ child.stderr.setEncoding?.('utf8');
289
+ this.reader = createInterface({ input: child.stdout });
290
+ this.reader.on('line', (line) => this.handleLine(line));
291
+ child.stderr.on('data', (chunk) => {
292
+ const text = String(chunk).slice(0, MAX_STDERR_CHARS);
293
+ this.emit({ type: 'server.stderr', text });
294
+ });
295
+ child.stdin.on('error', (error) => this.handleProcessError(child, error));
296
+ child.once('error', (error) => this.handleProcessError(child, error));
297
+ child.once('exit', (code, signal) => this.handleExit(child, code, signal));
298
+ child.once('close', (code, signal) => this.handleExit(child, code, signal));
299
+ }
300
+
301
+ async createThread() {
302
+ const params = {
303
+ cwd: this.cwd,
304
+ approvalPolicy: 'never',
305
+ sandbox: this.sandbox,
306
+ serviceName: this.serviceName,
307
+ };
308
+ optional(params, 'model', this.model);
309
+ optional(params, 'developerInstructions', this.developerInstructions);
310
+ return this.request('thread/start', params);
311
+ }
312
+
313
+ async resumeThread() {
314
+ const params = {
315
+ threadId: this.threadId,
316
+ cwd: this.cwd,
317
+ approvalPolicy: 'never',
318
+ sandbox: this.sandbox,
319
+ excludeTurns: true,
320
+ initialTurnsPage: {
321
+ limit: 100,
322
+ sortDirection: 'desc',
323
+ itemsView: 'full',
324
+ },
325
+ };
326
+ optional(params, 'model', this.model);
327
+ optional(params, 'developerInstructions', this.developerInstructions);
328
+ return this.request('thread/resume', params);
329
+ }
330
+
331
+ async turnItems(turnId) {
332
+ const items = [];
333
+ let cursor = '';
334
+ do {
335
+ const params = {
336
+ threadId: this.threadId,
337
+ turnId,
338
+ limit: 200,
339
+ sortDirection: 'asc',
340
+ };
341
+ optional(params, 'cursor', cursor);
342
+ const result = await this.request('thread/items/list', params);
343
+ for (const entry of result?.data || []) {
344
+ if (entry?.item) items.push(entry.item);
345
+ }
346
+ cursor = cleanText(result?.nextCursor);
347
+ } while (cursor);
348
+ return items;
349
+ }
350
+
351
+ async hydrateCompletedTurn(turn) {
352
+ if (turn?.itemsView !== 'summary') return turn;
353
+ try {
354
+ return {
355
+ ...turn,
356
+ items: await this.turnItems(cleanText(turn.id)),
357
+ itemsView: 'full',
358
+ };
359
+ } catch (error) {
360
+ this.emit({
361
+ type: 'protocol.error',
362
+ reason: 'turn-items-unavailable',
363
+ error,
364
+ });
365
+ return turn;
366
+ }
367
+ }
368
+
369
+ request(method, params, timeoutMs = this.requestTimeoutMs) {
370
+ const id = this.nextId;
371
+ this.nextId += 1;
372
+ let timer;
373
+ const response = new Promise((resolve, reject) => {
374
+ timer = setTimeout(() => {
375
+ this.pending.delete(id);
376
+ reject(processError(`${method} timed out`, {
377
+ method,
378
+ outcomeUnknown: true,
379
+ }));
380
+ const child = this.process;
381
+ if (
382
+ child
383
+ && child.exitCode === null
384
+ && child.signalCode === null
385
+ ) {
386
+ child.kill('SIGTERM');
387
+ }
388
+ }, timeoutMs);
389
+ timer.unref?.();
390
+ this.pending.set(id, { method, resolve, reject, timer });
391
+ });
392
+ const sent = this.write({ id, method, params }).catch((error) => {
393
+ const pending = this.pending.get(id);
394
+ if (pending) {
395
+ this.pending.delete(id);
396
+ clearTimeout(pending.timer);
397
+ pending.reject(error);
398
+ }
399
+ throw error;
400
+ });
401
+ return Promise.all([sent, response]).then(([, result]) => result);
402
+ }
403
+
404
+ notify(method, params) {
405
+ return this.write({ method, params });
406
+ }
407
+
408
+ write(message) {
409
+ const operation = this.writeChain.then(() => this.writeLine(message));
410
+ this.writeChain = operation.catch(() => {});
411
+ return operation;
412
+ }
413
+
414
+ async writeLine(message) {
415
+ const input = this.process?.stdin;
416
+ if (!input?.writable) {
417
+ throw processError('Codex App Server is not writable', {
418
+ outcomeUnknown: true,
419
+ });
420
+ }
421
+ if (input.write(`${JSON.stringify(message)}\n`)) return;
422
+ await new Promise((resolve, reject) => {
423
+ const cleanup = () => {
424
+ input.off('drain', onDrain);
425
+ input.off('error', onError);
426
+ input.off('close', onClose);
427
+ };
428
+ const onDrain = () => {
429
+ cleanup();
430
+ resolve();
431
+ };
432
+ const onError = (error) => {
433
+ cleanup();
434
+ reject(processError('Could not write to Codex App Server', {
435
+ cause: error,
436
+ outcomeUnknown: true,
437
+ }));
438
+ };
439
+ const onClose = () => {
440
+ cleanup();
441
+ reject(processError('Codex App Server input closed', {
442
+ outcomeUnknown: true,
443
+ }));
444
+ };
445
+ input.once('drain', onDrain);
446
+ input.once('error', onError);
447
+ input.once('close', onClose);
448
+ });
449
+ }
450
+
451
+ handleLine(line) {
452
+ if (!line.trim()) return;
453
+ let message;
454
+ try {
455
+ message = JSON.parse(line);
456
+ } catch {
457
+ this.emit({ type: 'protocol.error', reason: 'invalid-json' });
458
+ return;
459
+ }
460
+
461
+ if (message.id !== undefined
462
+ && (Object.hasOwn(message, 'result') || Object.hasOwn(message, 'error'))) {
463
+ this.handleResponse(message);
464
+ return;
465
+ }
466
+ if (message.id !== undefined && cleanText(message.method)) {
467
+ void this.handleServerRequest(message).catch((error) => {
468
+ this.emit({ type: 'protocol.error', reason: 'response-failed', error });
469
+ });
470
+ return;
471
+ }
472
+ if (cleanText(message.method)) {
473
+ this.handleNotification(message.method, message.params || {});
474
+ }
475
+ }
476
+
477
+ handleResponse(message) {
478
+ const pending = this.pending.get(message.id);
479
+ if (!pending) return;
480
+ this.pending.delete(message.id);
481
+ clearTimeout(pending.timer);
482
+ if (message.error) {
483
+ pending.reject(new CodexRpcError(pending.method, message.error));
484
+ return;
485
+ }
486
+ pending.resolve(message.result);
487
+ }
488
+
489
+ async handleServerRequest(message) {
490
+ const method = cleanText(message.method);
491
+ if (modernDeclines.has(method)) {
492
+ await this.write({ id: message.id, result: { decision: 'decline' } });
493
+ return;
494
+ }
495
+ if (legacyDeclines.has(method)) {
496
+ await this.write({ id: message.id, result: { decision: 'abort' } });
497
+ return;
498
+ }
499
+ if (method === 'item/tool/requestUserInput') {
500
+ const answers = Object.fromEntries(
501
+ (message.params?.questions || []).map((question) => [
502
+ question.id,
503
+ { answers: [] },
504
+ ])
505
+ );
506
+ await this.write({ id: message.id, result: { answers } });
507
+ return;
508
+ }
509
+ if (method === 'mcpServer/elicitation/request') {
510
+ await this.write({
511
+ id: message.id,
512
+ result: { action: 'decline', content: null, _meta: null },
513
+ });
514
+ return;
515
+ }
516
+ if (method === 'item/permissions/requestApproval') {
517
+ await this.write({
518
+ id: message.id,
519
+ result: { permissions: {}, scope: 'turn' },
520
+ });
521
+ return;
522
+ }
523
+ if (method === 'currentTime/read') {
524
+ await this.write({
525
+ id: message.id,
526
+ result: { currentTimeAt: Math.floor(this.now() / 1_000) },
527
+ });
528
+ return;
529
+ }
530
+ await this.write({
531
+ id: message.id,
532
+ error: {
533
+ code: -32601,
534
+ message: `Unsupported Codex client request: ${method}`,
535
+ },
536
+ });
537
+ }
538
+
539
+ handleNotification(method, params) {
540
+ if (params.threadId && this.threadId && params.threadId !== this.threadId) return;
541
+ if (method === 'turn/started') {
542
+ const turnId = cleanText(params.turn?.id);
543
+ if (!turnId) return;
544
+ this.turns.set(turnId, {
545
+ deltas: new Map(),
546
+ finalText: '',
547
+ lastText: '',
548
+ lastItemId: '',
549
+ });
550
+ this.emit({
551
+ type: 'turn.started',
552
+ threadId: params.threadId,
553
+ turnId,
554
+ turn: params.turn,
555
+ });
556
+ return;
557
+ }
558
+ if (method === 'item/agentMessage/delta') {
559
+ this.handleAgentDelta(params);
560
+ return;
561
+ }
562
+ if (method === 'item/completed') {
563
+ this.handleItemCompleted(params);
564
+ return;
565
+ }
566
+ if (method === 'turn/completed') {
567
+ void this.handleTurnCompleted(params).catch((error) => {
568
+ this.emit({
569
+ type: 'protocol.error',
570
+ reason: 'turn-completion-failed',
571
+ error,
572
+ });
573
+ });
574
+ return;
575
+ }
576
+ this.emit({ type: 'notification', method, params });
577
+ }
578
+
579
+ turnState(turnId) {
580
+ let state = this.turns.get(turnId);
581
+ if (!state) {
582
+ state = {
583
+ deltas: new Map(),
584
+ finalText: '',
585
+ lastText: '',
586
+ lastItemId: '',
587
+ };
588
+ this.turns.set(turnId, state);
589
+ }
590
+ return state;
591
+ }
592
+
593
+ handleAgentDelta(params) {
594
+ const turnId = cleanText(params.turnId);
595
+ const itemId = cleanText(params.itemId);
596
+ if (!turnId || !itemId) return;
597
+ const state = this.turnState(turnId);
598
+ const delta = typeof params.delta === 'string' ? params.delta : '';
599
+ const text = `${state.deltas.get(itemId) || ''}${delta}`;
600
+ state.deltas.set(itemId, text);
601
+ state.lastItemId = itemId;
602
+ this.emit({
603
+ type: 'message.delta',
604
+ threadId: params.threadId,
605
+ turnId,
606
+ itemId,
607
+ delta,
608
+ text,
609
+ });
610
+ }
611
+
612
+ handleItemCompleted(params) {
613
+ const item = params.item;
614
+ if (item?.type !== 'agentMessage') {
615
+ this.emit({
616
+ type: 'item.completed',
617
+ threadId: params.threadId,
618
+ turnId: params.turnId,
619
+ item,
620
+ });
621
+ return;
622
+ }
623
+ const turnId = cleanText(params.turnId);
624
+ if (!turnId) return;
625
+ const state = this.turnState(turnId);
626
+ const itemId = cleanText(item.id);
627
+ const text = cleanText(item.text)
628
+ || cleanText(state.deltas.get(itemId));
629
+ state.lastText = text;
630
+ state.lastItemId = itemId;
631
+ if (item.phase === 'final_answer') state.finalText = text;
632
+ this.emit({
633
+ type: 'message.completed',
634
+ threadId: params.threadId,
635
+ turnId,
636
+ itemId,
637
+ phase: item.phase ?? null,
638
+ text,
639
+ item,
640
+ });
641
+ }
642
+
643
+ async handleTurnCompleted(params) {
644
+ const turn = await this.hydrateCompletedTurn(params.turn || {});
645
+ const turnId = cleanText(turn.id);
646
+ if (!turnId) return;
647
+ const normalized = normalizeTurn(turn);
648
+ const state = this.turns.get(turnId);
649
+ const turnFinal = turnMessage(turn.items);
650
+ const explicitTurnFinal = turnMessage(
651
+ (turn.items || []).filter((item) => item?.phase === 'final_answer')
652
+ );
653
+ const deltaText = cleanText(state?.deltas.get(state?.lastItemId));
654
+ const finalText = explicitTurnFinal
655
+ || state?.finalText
656
+ || turnFinal
657
+ || state?.lastText
658
+ || deltaText;
659
+ this.turns.delete(turnId);
660
+ this.emit({
661
+ type: 'turn.completed',
662
+ threadId: params.threadId,
663
+ turnId,
664
+ status: turn.status,
665
+ error: turn.error ?? null,
666
+ inputIds: normalized.inputIds,
667
+ finalText: finalText || '',
668
+ turn,
669
+ });
670
+ }
671
+
672
+ emit(event) {
673
+ for (const listener of this.listeners) {
674
+ try {
675
+ Promise.resolve(listener(event)).catch(this.onEventError);
676
+ } catch (error) {
677
+ this.onEventError(error);
678
+ }
679
+ }
680
+ }
681
+
682
+ handleProcessError(child, error) {
683
+ if (this.process !== child) return;
684
+ const wrapped = processError('Codex App Server process failed', {
685
+ cause: error,
686
+ outcomeUnknown: true,
687
+ });
688
+ this.rejectPending(wrapped);
689
+ this.emit({ type: 'server.error', error: wrapped });
690
+ if (
691
+ child.exitCode === null
692
+ && child.signalCode === null
693
+ ) {
694
+ child.kill('SIGTERM');
695
+ }
696
+ }
697
+
698
+ handleExit(child, code, signal) {
699
+ if (this.process !== child) return;
700
+ this.reader?.close();
701
+ this.reader = null;
702
+ this.process = null;
703
+ this.openResult = null;
704
+ this.resolveExit?.({ code, signal });
705
+ this.resolveExit = null;
706
+ const error = processError('Codex App Server exited', {
707
+ code,
708
+ signal,
709
+ outcomeUnknown: true,
710
+ });
711
+ this.rejectPending(error);
712
+ this.emit({
713
+ type: 'server.exit',
714
+ code,
715
+ signal,
716
+ expected: this.closing,
717
+ });
718
+ }
719
+
720
+ rejectPending(error) {
721
+ for (const pending of this.pending.values()) {
722
+ clearTimeout(pending.timer);
723
+ pending.reject(error);
724
+ }
725
+ this.pending.clear();
726
+ }
727
+
728
+ async stopFailedProcess(child) {
729
+ if (this.process !== child) return;
730
+ if (child.exitCode === null && child.signalCode === null) child.kill('SIGTERM');
731
+ await this.exitPromise;
732
+ }
733
+ }
734
+
735
+ export function createCodexAppServer(options = {}) {
736
+ return new CodexAppServer(options);
737
+ }