remote-codex 0.1.5 → 0.1.7

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 (36) hide show
  1. package/apps/supervisor-api/dist/index.js +7749 -5501
  2. package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-D-RjOTTL.js → highlighted-body-OFNGDK62-0cYcfOfd.js} +1 -1
  3. package/apps/supervisor-web/dist/assets/index-CbIt0KnL.css +32 -0
  4. package/apps/supervisor-web/dist/assets/index-nH6a8Wwn.js +377 -0
  5. package/apps/supervisor-web/dist/assets/{xterm-D8iZbRww.js → xterm-DisVWgDR.js} +1 -1
  6. package/apps/supervisor-web/dist/index.html +2 -2
  7. package/package.json +5 -1
  8. package/packages/agent-runtime/src/index.ts +2 -0
  9. package/packages/agent-runtime/src/registry.ts +44 -0
  10. package/packages/agent-runtime/src/types.ts +531 -0
  11. package/packages/codex/src/appServerManager.test.ts +328 -0
  12. package/packages/codex/src/appServerManager.ts +656 -0
  13. package/packages/codex/src/historyItems.ts +1185 -0
  14. package/packages/codex/src/hookHistory.ts +224 -0
  15. package/packages/codex/src/index.ts +6 -0
  16. package/packages/codex/src/jsonrpc.test.ts +58 -0
  17. package/packages/codex/src/jsonrpc.ts +198 -0
  18. package/packages/codex/src/requestMapper.test.ts +127 -0
  19. package/packages/codex/src/requestMapper.ts +511 -0
  20. package/packages/codex/src/runtimeAdapter.ts +692 -0
  21. package/packages/codex/src/types.ts +403 -0
  22. package/packages/db/migrations/0014_thread_history_items.sql +12 -0
  23. package/packages/db/migrations/0015_agent_provider_fields.sql +14 -0
  24. package/packages/db/migrations/0016_remove_codex_thread_goal_id.sql +46 -0
  25. package/packages/db/migrations/0017_remove_codex_thread_columns.sql +85 -0
  26. package/packages/db/src/client.ts +53 -0
  27. package/packages/db/src/index.ts +5 -0
  28. package/packages/db/src/migrate.test.ts +36 -0
  29. package/packages/db/src/migrate.ts +84 -0
  30. package/packages/db/src/repositories.ts +893 -0
  31. package/packages/db/src/schema.ts +177 -0
  32. package/packages/db/src/seed.ts +51 -0
  33. package/packages/shared/src/index.ts +878 -0
  34. package/scripts/service-manager.mjs +6 -4
  35. package/apps/supervisor-web/dist/assets/index-CdG3ogmZ.js +0 -376
  36. package/apps/supervisor-web/dist/assets/index-QM8NQf3e.css +0 -32
@@ -0,0 +1,656 @@
1
+ import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process';
2
+ import { EventEmitter } from 'node:events';
3
+
4
+ import { JsonRpcClient, JsonRpcClientError } from './jsonrpc';
5
+ import {
6
+ AppServerStatusSnapshot,
7
+ CodexClientInfo,
8
+ CodexHookTrustInput,
9
+ CodexHooksListEntry,
10
+ CodexHookRecord,
11
+ CodexMcpServerRecord,
12
+ CodexModelRecord,
13
+ CodexServerRequest,
14
+ CodexServerEvent,
15
+ CodexSkillsListEntry,
16
+ CodexThreadGoalRecord,
17
+ CodexThreadRecord,
18
+ CodexTurnRecord,
19
+ ReasoningEffort,
20
+ ThreadGoalSetInput,
21
+ ThreadForkInput,
22
+ ThreadRollbackInput,
23
+ ThreadResumeInput,
24
+ ThreadStartInput,
25
+ TurnStartInput,
26
+ TurnSteerInput
27
+ } from './types';
28
+
29
+ interface SpawnedChild {
30
+ stdout: NodeJS.ReadableStream;
31
+ stdin: NodeJS.WritableStream;
32
+ stderr: NodeJS.ReadableStream;
33
+ once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
34
+ once(event: 'error', listener: (error: Error) => void): this;
35
+ kill(signal?: NodeJS.Signals): boolean;
36
+ }
37
+
38
+ export interface CodexAppServerManagerOptions {
39
+ command: string;
40
+ startupTimeoutMs: number;
41
+ clientInfo: CodexClientInfo;
42
+ maxRestarts?: number;
43
+ spawnProcess?: (command: string, args: string[]) => SpawnedChild;
44
+ }
45
+
46
+ function mapThread(record: any): CodexThreadRecord {
47
+ return {
48
+ id: record.id,
49
+ preview: record.preview ?? '',
50
+ createdAt: record.createdAt,
51
+ updatedAt: record.updatedAt,
52
+ status: record.status,
53
+ cwd: record.cwd,
54
+ name: record.name ?? null,
55
+ turns: Array.isArray(record.turns) ? record.turns.map(mapTurn) : []
56
+ };
57
+ }
58
+
59
+ function mapTurn(record: any): CodexTurnRecord {
60
+ return {
61
+ id: record.id,
62
+ status: record.status,
63
+ error: record.error ?? null,
64
+ items: Array.isArray(record.items) ? record.items : []
65
+ };
66
+ }
67
+
68
+ function mapModel(record: any): CodexModelRecord {
69
+ return {
70
+ id: record.id,
71
+ model: record.model,
72
+ displayName: record.displayName,
73
+ description: record.description,
74
+ hidden: record.hidden,
75
+ isDefault: record.isDefault,
76
+ supportedReasoningEfforts: Array.isArray(record.supportedReasoningEfforts)
77
+ ? record.supportedReasoningEfforts.map((entry: any) => ({
78
+ reasoningEffort: entry.reasoningEffort,
79
+ description: entry.description
80
+ }))
81
+ : [],
82
+ defaultReasoningEffort: record.defaultReasoningEffort ?? 'medium'
83
+ };
84
+ }
85
+
86
+ function mapSkillsListEntry(record: any): CodexSkillsListEntry {
87
+ return {
88
+ cwd: record.cwd,
89
+ skills: Array.isArray(record.skills)
90
+ ? record.skills.map((skill: any) => ({
91
+ name: skill.name,
92
+ description: skill.description ?? '',
93
+ shortDescription: skill.shortDescription ?? null,
94
+ interface: skill.interface
95
+ ? {
96
+ displayName: skill.interface.displayName ?? null,
97
+ shortDescription: skill.interface.shortDescription ?? null,
98
+ brandColor: skill.interface.brandColor ?? null,
99
+ defaultPrompt: skill.interface.defaultPrompt ?? null,
100
+ }
101
+ : null,
102
+ path: skill.path,
103
+ scope: skill.scope,
104
+ enabled: skill.enabled === true,
105
+ }))
106
+ : [],
107
+ errors: Array.isArray(record.errors)
108
+ ? record.errors.map((error: any) => ({
109
+ path: error.path,
110
+ message: error.message,
111
+ }))
112
+ : [],
113
+ };
114
+ }
115
+
116
+ function mapMcpServer(record: any): CodexMcpServerRecord {
117
+ const tools = record.tools ?? {};
118
+ return {
119
+ name: record.name,
120
+ authStatus: record.authStatus ?? record.auth_status ?? 'unsupported',
121
+ tools: Object.values(tools).map((tool: any) => ({
122
+ name: tool.name,
123
+ title: tool.title ?? null,
124
+ description: tool.description ?? null,
125
+ })),
126
+ resourceCount: Array.isArray(record.resources) ? record.resources.length : 0,
127
+ resourceTemplateCount: Array.isArray(record.resourceTemplates)
128
+ ? record.resourceTemplates.length
129
+ : 0,
130
+ };
131
+ }
132
+
133
+ function numberFromProtocolInteger(value: unknown, fallback = 0) {
134
+ if (typeof value === 'number' && Number.isFinite(value)) {
135
+ return value;
136
+ }
137
+ if (typeof value === 'bigint') {
138
+ return Number(value);
139
+ }
140
+ if (typeof value === 'string') {
141
+ const numericValue = Number(value);
142
+ return Number.isFinite(numericValue) ? numericValue : fallback;
143
+ }
144
+ return fallback;
145
+ }
146
+
147
+ function mapHook(record: any): CodexHookRecord {
148
+ return {
149
+ key: record.key,
150
+ eventName: record.eventName,
151
+ handlerType: record.handlerType,
152
+ matcher: record.matcher ?? null,
153
+ command: record.command ?? null,
154
+ timeoutSec: numberFromProtocolInteger(record.timeoutSec ?? record.timeout_sec, 600),
155
+ statusMessage: record.statusMessage ?? record.status_message ?? null,
156
+ sourcePath: String(record.sourcePath ?? record.source_path ?? ''),
157
+ source: record.source ?? 'unknown',
158
+ pluginId: record.pluginId ?? record.plugin_id ?? null,
159
+ displayOrder: numberFromProtocolInteger(record.displayOrder ?? record.display_order),
160
+ enabled: record.enabled === true,
161
+ isManaged: record.isManaged === true || record.is_managed === true,
162
+ currentHash: record.currentHash ?? record.current_hash ?? '',
163
+ trustStatus: record.trustStatus ?? record.trust_status ?? 'untrusted',
164
+ };
165
+ }
166
+
167
+ function mapHooksListEntry(record: any): CodexHooksListEntry {
168
+ return {
169
+ cwd: record.cwd,
170
+ hooks: Array.isArray(record.hooks) ? record.hooks.map(mapHook) : [],
171
+ warnings: Array.isArray(record.warnings) ? record.warnings.map(String) : [],
172
+ errors: Array.isArray(record.errors)
173
+ ? record.errors.map((error: any) => ({
174
+ path: String(error.path ?? ''),
175
+ message: String(error.message ?? ''),
176
+ }))
177
+ : [],
178
+ };
179
+ }
180
+
181
+ function parseGoalTimestamp(value: unknown): number {
182
+ if (typeof value === 'number' && Number.isFinite(value)) {
183
+ return value;
184
+ }
185
+
186
+ if (typeof value === 'string') {
187
+ const numericValue = Number(value);
188
+ if (Number.isFinite(numericValue) && value.trim() !== '') {
189
+ return numericValue;
190
+ }
191
+
192
+ const parsed = Date.parse(value);
193
+ if (Number.isFinite(parsed)) {
194
+ return Math.floor(parsed / 1000);
195
+ }
196
+ }
197
+
198
+ return Math.floor(Date.now() / 1000);
199
+ }
200
+
201
+ function mapThreadGoal(record: any): CodexThreadGoalRecord {
202
+ return {
203
+ threadId: record.threadId,
204
+ objective: record.objective,
205
+ status: record.status,
206
+ tokenBudget: record.tokenBudget ?? null,
207
+ tokensUsed: record.tokensUsed ?? 0,
208
+ timeUsedSeconds: record.timeUsedSeconds ?? 0,
209
+ createdAt: parseGoalTimestamp(record.createdAt),
210
+ updatedAt: parseGoalTimestamp(record.updatedAt),
211
+ };
212
+ }
213
+
214
+ export class CodexAppServerManager extends EventEmitter {
215
+ private readonly maxRestarts: number;
216
+ private readonly spawnProcess: (command: string, args: string[]) => SpawnedChild;
217
+ private process: SpawnedChild | null = null;
218
+ private client: JsonRpcClient | null = null;
219
+ private readonly intentionallyStopping = new Set<SpawnedChild>();
220
+ private status: AppServerStatusSnapshot = {
221
+ state: 'stopped',
222
+ transport: 'stdio',
223
+ lastStartedAt: null,
224
+ lastError: null,
225
+ restartCount: 0
226
+ };
227
+ private startPromise: Promise<void> | null = null;
228
+ private intentionalStop = false;
229
+
230
+ constructor(private readonly options: CodexAppServerManagerOptions) {
231
+ super();
232
+ this.maxRestarts = options.maxRestarts ?? 3;
233
+ this.spawnProcess =
234
+ options.spawnProcess ??
235
+ ((command: string, args: string[]) =>
236
+ spawn(command, args, {
237
+ stdio: 'pipe'
238
+ }) as unknown as ChildProcessWithoutNullStreams);
239
+ }
240
+
241
+ getStatus(): AppServerStatusSnapshot {
242
+ return { ...this.status };
243
+ }
244
+
245
+ async start(): Promise<void> {
246
+ if (this.status.state === 'ready') {
247
+ return;
248
+ }
249
+
250
+ if (this.startPromise) {
251
+ return this.startPromise;
252
+ }
253
+
254
+ this.intentionalStop = false;
255
+ this.setStatus('starting', null);
256
+
257
+ this.startPromise = this.doStart().finally(() => {
258
+ this.startPromise = null;
259
+ });
260
+
261
+ return this.startPromise;
262
+ }
263
+
264
+ async stop(): Promise<void> {
265
+ this.intentionalStop = true;
266
+ const client = this.client;
267
+ const process = this.process;
268
+
269
+ client?.close();
270
+ if (this.client === client) {
271
+ this.client = null;
272
+ }
273
+
274
+ if (process) {
275
+ this.intentionallyStopping.add(process);
276
+ process.kill('SIGTERM');
277
+ if (this.process === process) {
278
+ this.process = null;
279
+ }
280
+ }
281
+
282
+ this.setStatus('stopped', null);
283
+ }
284
+
285
+ async ensureReady(): Promise<void> {
286
+ if (this.status.state !== 'ready') {
287
+ await this.start();
288
+ }
289
+
290
+ if (this.status.state !== 'ready' || !this.client) {
291
+ throw new JsonRpcClientError(
292
+ this.status.lastError ?? 'Codex app-server is unavailable.',
293
+ 'app_server_unavailable'
294
+ );
295
+ }
296
+ }
297
+
298
+ async listModels(): Promise<CodexModelRecord[]> {
299
+ await this.ensureReady();
300
+ const response = await this.client!.request<{ data: any[] }>('model/list', {
301
+ includeHidden: false
302
+ });
303
+ return response.data.map(mapModel);
304
+ }
305
+
306
+ async listThreads(): Promise<CodexThreadRecord[]> {
307
+ await this.ensureReady();
308
+ const response = await this.client!.request<{ data: any[] }>('thread/list', {
309
+ archived: false
310
+ });
311
+ return response.data.map(mapThread);
312
+ }
313
+
314
+ async listLoadedThreads(): Promise<string[]> {
315
+ await this.ensureReady();
316
+ const response = await this.client!.request<{ data: string[] }>('thread/loaded/list', {});
317
+ return response.data;
318
+ }
319
+
320
+ async listSkills(input: { cwds?: string[]; forceReload?: boolean } = {}) {
321
+ await this.ensureReady();
322
+ const response = await this.client!.request<{ data: any[] }>('skills/list', {
323
+ ...(input.cwds && input.cwds.length > 0 ? { cwds: input.cwds } : {}),
324
+ ...(input.forceReload !== undefined ? { forceReload: input.forceReload } : {}),
325
+ });
326
+ return response.data.map(mapSkillsListEntry);
327
+ }
328
+
329
+ async listMcpServers() {
330
+ await this.ensureReady();
331
+ const servers: CodexMcpServerRecord[] = [];
332
+ let cursor: string | null = null;
333
+
334
+ do {
335
+ const response: {
336
+ data: any[];
337
+ nextCursor?: string | null;
338
+ next_cursor?: string | null;
339
+ } = await this.client!.request('mcpServerStatus/list', {
340
+ cursor,
341
+ limit: 100,
342
+ detail: 'full',
343
+ });
344
+ servers.push(...response.data.map(mapMcpServer));
345
+ cursor = response.nextCursor ?? response.next_cursor ?? null;
346
+ } while (cursor);
347
+
348
+ return servers;
349
+ }
350
+
351
+ async listHooks(input: { cwds?: string[] } = {}) {
352
+ await this.ensureReady();
353
+ const response = await this.client!.request<{ data: any[] }>('hooks/list', {
354
+ ...(input.cwds && input.cwds.length > 0 ? { cwds: input.cwds } : {}),
355
+ });
356
+ return response.data.map(mapHooksListEntry);
357
+ }
358
+
359
+ async setHookTrust(input: CodexHookTrustInput) {
360
+ await this.ensureReady();
361
+ await this.client!.request('config/batchWrite', {
362
+ edits: [
363
+ {
364
+ keyPath: 'hooks.state',
365
+ value: {
366
+ [input.key]: {
367
+ trusted_hash: input.trustedHash ?? '',
368
+ ...(input.trustedHash ? { enabled: true } : {}),
369
+ },
370
+ },
371
+ mergeStrategy: 'upsert',
372
+ },
373
+ ],
374
+ reloadUserConfig: true,
375
+ });
376
+ }
377
+
378
+ async startThread(input: ThreadStartInput) {
379
+ await this.ensureReady();
380
+ const response = await this.client!.request<{ thread: any; model: string; reasoningEffort?: ReasoningEffort | null; sandbox?: string | null }>('thread/start', {
381
+ cwd: input.cwd,
382
+ model: input.model,
383
+ serviceTier: input.serviceTier,
384
+ approvalPolicy: input.approvalPolicy,
385
+ sandbox: input.sandbox ?? null,
386
+ experimentalRawEvents: false,
387
+ persistExtendedHistory: true
388
+ });
389
+
390
+ return {
391
+ thread: mapThread(response.thread),
392
+ model: response.model,
393
+ reasoningEffort: response.reasoningEffort ?? null,
394
+ sandbox: response.sandbox ?? null,
395
+ };
396
+ }
397
+
398
+ async readThread(threadId: string) {
399
+ await this.ensureReady();
400
+ const response = await this.client!.request<{ thread: any }>('thread/read', {
401
+ threadId,
402
+ includeTurns: true
403
+ });
404
+ return mapThread(response.thread);
405
+ }
406
+
407
+ async resumeThread(input: ThreadResumeInput) {
408
+ await this.ensureReady();
409
+ const response = await this.client!.request<{ thread: any; model: string; reasoningEffort?: ReasoningEffort | null; sandbox?: string | null }>('thread/resume', {
410
+ threadId: input.threadId,
411
+ model: input.model ?? null,
412
+ serviceTier: input.serviceTier,
413
+ sandbox: input.sandbox ?? null,
414
+ persistExtendedHistory: true
415
+ });
416
+ return {
417
+ thread: mapThread(response.thread),
418
+ model: response.model,
419
+ reasoningEffort: response.reasoningEffort ?? null,
420
+ sandbox: response.sandbox ?? null,
421
+ };
422
+ }
423
+
424
+ async forkThread(input: ThreadForkInput) {
425
+ await this.ensureReady();
426
+ const response = await this.client!.request<{ thread: any }>('thread/fork', {
427
+ threadId: input.threadId,
428
+ });
429
+ return mapThread(response.thread);
430
+ }
431
+
432
+ async rollbackThread(input: ThreadRollbackInput) {
433
+ await this.ensureReady();
434
+ const response = await this.client!.request<{ thread: any }>('thread/rollback', {
435
+ threadId: input.threadId,
436
+ count: input.count,
437
+ });
438
+ return mapThread(response.thread);
439
+ }
440
+
441
+ async startTurn(input: TurnStartInput) {
442
+ await this.ensureReady();
443
+ const response = await this.client!.request<{ turn: any }>('turn/start', {
444
+ threadId: input.threadId,
445
+ input: [
446
+ {
447
+ type: 'text',
448
+ text: input.prompt,
449
+ text_elements: []
450
+ }
451
+ ],
452
+ model: input.model ?? null,
453
+ serviceTier:
454
+ input.serviceTier === undefined ? undefined : input.serviceTier,
455
+ effort: input.effort ?? null,
456
+ sandboxPolicy: input.sandboxPolicy ?? null,
457
+ collaborationMode: input.collaborationMode
458
+ ? {
459
+ mode: input.collaborationMode,
460
+ settings: {
461
+ model: input.model ?? '',
462
+ reasoning_effort: input.effort ?? null,
463
+ developer_instructions: null
464
+ }
465
+ }
466
+ : null
467
+ });
468
+ return mapTurn(response.turn);
469
+ }
470
+
471
+ async steerTurn(input: TurnSteerInput) {
472
+ await this.ensureReady();
473
+ const response = await this.client!.request<{ turn?: any }>('turn/steer', {
474
+ threadId: input.threadId,
475
+ expectedTurnId: input.turnId,
476
+ input: [
477
+ {
478
+ type: 'text',
479
+ text: input.prompt,
480
+ text_elements: []
481
+ }
482
+ ]
483
+ });
484
+ return response.turn ? mapTurn(response.turn) : null;
485
+ }
486
+
487
+ async compactThread(threadId: string) {
488
+ await this.ensureReady();
489
+ await this.client!.request<unknown>('thread/compact/start', {
490
+ threadId,
491
+ });
492
+ }
493
+
494
+ async getThreadGoal(threadId: string) {
495
+ await this.ensureReady();
496
+ const response = await this.client!.request<{ goal: any | null }>('thread/goal/get', {
497
+ threadId,
498
+ });
499
+ return response.goal ? mapThreadGoal(response.goal) : null;
500
+ }
501
+
502
+ async setThreadGoal(input: ThreadGoalSetInput) {
503
+ await this.ensureReady();
504
+ const response = await this.client!.request<{ goal: any }>('thread/goal/set', {
505
+ threadId: input.threadId,
506
+ ...(input.objective !== undefined ? { objective: input.objective } : {}),
507
+ ...(input.status !== undefined ? { status: input.status } : {}),
508
+ ...(input.tokenBudget !== undefined ? { tokenBudget: input.tokenBudget } : {}),
509
+ });
510
+ return mapThreadGoal(response.goal);
511
+ }
512
+
513
+ async clearThreadGoal(threadId: string) {
514
+ await this.ensureReady();
515
+ const response = await this.client!.request<{ cleared: boolean }>('thread/goal/clear', {
516
+ threadId,
517
+ });
518
+ return response.cleared;
519
+ }
520
+
521
+ async setExperimentalFeatureEnablement(enablement: Record<string, boolean>) {
522
+ await this.ensureReady();
523
+ await this.client!.request<unknown>('experimentalFeature/enablement/set', {
524
+ enablement,
525
+ });
526
+ }
527
+
528
+ async interruptTurn(threadId: string, turnId: string) {
529
+ await this.ensureReady();
530
+ const response = await this.client!.request<{ turn?: any }>('turn/interrupt', {
531
+ threadId,
532
+ turnId
533
+ });
534
+ return response.turn ? mapTurn(response.turn) : null;
535
+ }
536
+
537
+ respondToServerRequest(id: number, result: unknown) {
538
+ if (!this.client) {
539
+ throw new JsonRpcClientError('Codex app-server is unavailable.', 'app_server_unavailable');
540
+ }
541
+
542
+ this.client.respond(id, result);
543
+ }
544
+
545
+ private async doStart() {
546
+ const child = this.spawnProcess(this.options.command, ['app-server', '--listen', 'stdio://']);
547
+ this.process = child;
548
+ this.status.lastStartedAt = new Date().toISOString();
549
+ const startupError = new Promise<never>((_, reject) => {
550
+ child.once('error', (error) => {
551
+ reject(
552
+ new JsonRpcClientError(
553
+ `Failed to spawn Codex app-server: ${error.message}`,
554
+ 'spawn_failed'
555
+ )
556
+ );
557
+ });
558
+ });
559
+
560
+ child.stderr.on('data', (chunk) => {
561
+ const message = chunk.toString().trim();
562
+ if (!message) {
563
+ return;
564
+ }
565
+
566
+ this.emit('stderr', message);
567
+ });
568
+
569
+ child.once('exit', (code, signal) => {
570
+ const intentionallyStopping = this.intentionallyStopping.delete(child);
571
+ const isCurrentClient = this.client === client;
572
+ const isCurrentProcess = this.process === child;
573
+
574
+ if (isCurrentClient) {
575
+ this.client?.close();
576
+ this.client = null;
577
+ }
578
+ if (isCurrentProcess) {
579
+ this.process = null;
580
+ }
581
+
582
+ if (!isCurrentClient && !isCurrentProcess) {
583
+ return;
584
+ }
585
+
586
+ if (intentionallyStopping || this.intentionalStop) {
587
+ if (isCurrentProcess || isCurrentClient) {
588
+ this.setStatus('stopped', null);
589
+ }
590
+ return;
591
+ }
592
+
593
+ const reason = `Codex app-server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}).`;
594
+ if (this.status.restartCount < this.maxRestarts) {
595
+ this.status.restartCount += 1;
596
+ this.setStatus('degraded', reason);
597
+ void this.start();
598
+ return;
599
+ }
600
+
601
+ this.setStatus('failed', reason);
602
+ });
603
+
604
+ const client = new JsonRpcClient(child.stdout as any, child.stdin as any);
605
+ this.client = client;
606
+
607
+ client.on('notification', (notification) => {
608
+ this.emit('notification', notification as CodexServerEvent);
609
+ });
610
+
611
+ client.on('request', (request) => {
612
+ this.emit('request', request as CodexServerRequest);
613
+ });
614
+
615
+ client.on('warning', (warning) => {
616
+ this.emit('warning', warning);
617
+ });
618
+
619
+ await Promise.race([
620
+ client.request('initialize', {
621
+ clientInfo: this.options.clientInfo,
622
+ capabilities: {
623
+ experimentalApi: true
624
+ }
625
+ }),
626
+ startupError,
627
+ new Promise((_, reject) => {
628
+ setTimeout(() => {
629
+ reject(
630
+ new JsonRpcClientError(
631
+ 'Codex app-server initialize handshake timed out.',
632
+ 'initialize_timeout'
633
+ )
634
+ );
635
+ }, this.options.startupTimeoutMs);
636
+ })
637
+ ]).catch((error) => {
638
+ this.client = null;
639
+ this.process?.kill('SIGTERM');
640
+ this.process = null;
641
+ this.setStatus('failed', error instanceof Error ? error.message : String(error));
642
+ throw error;
643
+ });
644
+
645
+ this.setStatus('ready', null);
646
+ }
647
+
648
+ private setStatus(state: AppServerStatusSnapshot['state'], lastError: string | null) {
649
+ this.status = {
650
+ ...this.status,
651
+ state,
652
+ lastError
653
+ };
654
+ this.emit('status', this.getStatus());
655
+ }
656
+ }