@modelprofile.com/mcp-crossharness 5.0.0 → 5.2.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.
@@ -1,11 +1,9 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import * as helpers from './helpers.js';
3
3
  import { commitinfo } from './00_commitinfo_data.js';
4
+ import { CodexAppServerClient } from './classes.codexappserverclient.js';
4
5
  import type * as interfaces from './interfaces.js';
5
6
 
6
- const appServerVersion = '0.146.0';
7
- const maxSocketMessageBytes = 4 * 1024 * 1024;
8
- const maxSocketBufferedBytes = 4 * 1024 * 1024;
9
7
  const maxTurnTextChars = 1024 * 1024;
10
8
  const maxTurnDeltaChunks = 4096;
11
9
  const maxEarlyNotifications = 100;
@@ -28,14 +26,6 @@ interface ICodexNotification {
28
26
  params: Record<string, unknown>;
29
27
  }
30
28
 
31
- interface IPendingRequest {
32
- resolve: (valueArg: unknown) => void;
33
- reject: (errorArg: Error) => void;
34
- timer: ReturnType<typeof setTimeout>;
35
- signal?: AbortSignal;
36
- abortHandler?: () => void;
37
- }
38
-
39
29
  interface IPendingTurn {
40
30
  threadId: string;
41
31
  turnId?: string;
@@ -70,9 +60,7 @@ export class CodexConnection implements interfaces.IHarnessConnection {
70
60
  public state: interfaces.TConnectionState = 'connecting';
71
61
  private connectedAt = new Date().toISOString();
72
62
  private serverVersion: string | undefined;
73
- private socket: InstanceType<typeof plugins.WsWebSocket> | undefined;
74
- private requestCounter = 0;
75
- private pendingRequests = new Map<number, IPendingRequest>();
63
+ private client?: CodexAppServerClient;
76
64
  private pendingTurns = new Set<IPendingTurn>();
77
65
  private threadQueues = new Map<string, IThreadQueueEntry[]>();
78
66
  private activeThreads = new Set<string>();
@@ -93,79 +81,20 @@ export class CodexConnection implements interfaces.IHarnessConnection {
93
81
  throw new Error(`Codex connection ${this.connectionId} has already been started`);
94
82
  }
95
83
  this.connectStarted = true;
96
- const headers: Record<string, string> = {};
97
- const token = process.env.CODEX_REMOTE_TOKEN;
98
- if (token) headers.authorization = `Bearer ${token}`;
99
- const socket = new plugins.WsWebSocket(this.serverUrl, {
100
- perMessageDeflate: false,
101
- headers,
102
- followRedirects: false,
103
- maxPayload: maxSocketMessageBytes,
104
- });
105
- this.socket = socket;
106
- socket.on('message', (data) => this.handleMessage(data.toString()));
107
- socket.on('error', (error) => this.handleSocketFailure(error));
108
- socket.on('close', () => this.handleSocketClose());
109
- await new Promise<void>((resolve, reject) => {
110
- const cleanup = () => {
111
- clearTimeout(timer);
112
- socket.off('open', handleOpen);
113
- socket.off('error', handleError);
114
- socket.off('close', handleClose);
115
- };
116
- const handleOpen = () => {
117
- cleanup();
118
- resolve();
119
- };
120
- const handleError = (errorArg: Error) => {
121
- cleanup();
122
- reject(errorArg);
123
- };
124
- const handleClose = () => {
125
- cleanup();
126
- reject(new Error('Codex server WebSocket closed during handshake'));
127
- };
128
- const timer = setTimeout(() => {
129
- cleanup();
130
- socket.terminate();
131
- reject(new Error('Codex server WebSocket handshake timed out'));
132
- }, 5000);
133
- socket.once('open', handleOpen);
134
- socket.once('error', handleError);
135
- socket.once('close', handleClose);
84
+ const client = new CodexAppServerClient({
85
+ transport: { type: 'websocket', url: this.serverUrl, token: process.env.CODEX_REMOTE_TOKEN },
86
+ clientInfo: { name: 'mcp-crossharness', title: 'mcp-crossharness', version: commitinfo.version },
87
+ onNotification: ({ method, params }) => this.handleNotification(method, params),
88
+ onServerRequest: (request) => this.respondToServerRequest(request),
89
+ onClose: (error) => this.handleSocketFailure(error),
136
90
  });
137
- try {
138
- const initialized = await this.request(
139
- 'initialize',
140
- {
141
- clientInfo: {
142
- name: 'mcp-crossharness',
143
- title: 'mcp-crossharness',
144
- version: commitinfo.version,
145
- },
146
- capabilities: {
147
- experimentalApi: false,
148
- requestAttestation: false,
149
- },
150
- },
151
- 5000,
152
- true,
153
- );
154
- if (!helpers.isRecord(initialized) || typeof initialized.userAgent !== 'string') {
155
- throw new Error(
156
- `Codex server returned an invalid initialize response (expected app-server ${appServerVersion})`,
157
- );
158
- }
159
- this.serverVersion = initialized.userAgent.match(/\/(\d+\.\d+\.\d+[^\s]*)/)?.[1];
160
- if (this.state !== 'connecting' || socket.readyState !== plugins.WsWebSocket.OPEN) {
161
- throw new Error('Codex server connection closed during initialization');
162
- }
163
- this.sendNotification('initialized');
164
- this.state = 'connected';
165
- } catch (error) {
166
- socket.terminate();
167
- throw error;
91
+ this.client = client;
92
+ await client.connect();
93
+ if (this.state !== 'connecting' || client.state !== 'connected') {
94
+ throw new Error('Codex server connection closed during initialization');
168
95
  }
96
+ this.serverVersion = client.serverVersion;
97
+ this.state = 'connected';
169
98
  }
170
99
 
171
100
  public getInfo(): interfaces.IConnectionInfo {
@@ -418,7 +347,7 @@ export class CodexConnection implements interfaces.IHarnessConnection {
418
347
  this.onClosing?.();
419
348
  this.onClosing = undefined;
420
349
  }
421
- await this.closeSocket();
350
+ this.client?.close(reasonArg);
422
351
  this.finishClosed();
423
352
  }
424
353
 
@@ -476,118 +405,13 @@ export class CodexConnection implements interfaces.IHarnessConnection {
476
405
  methodArg: string,
477
406
  paramsArg: unknown,
478
407
  timeoutMsArg = 30000,
479
- allowConnectingArg = false,
408
+ _allowConnectingArg = false,
480
409
  signalArg?: AbortSignal,
481
410
  ): Promise<unknown> {
482
- if (!allowConnectingArg && this.state !== 'connected') {
411
+ if (this.state !== 'connected' || !this.client) {
483
412
  return Promise.reject(new Error(`Codex connection ${this.connectionId} is ${this.state}`));
484
413
  }
485
- const socket = this.socket;
486
- if (!socket || socket.readyState !== plugins.WsWebSocket.OPEN) {
487
- return Promise.reject(new Error('Codex server WebSocket is not open'));
488
- }
489
- if (this.pendingRequests.size >= 256) {
490
- return Promise.reject(new Error('Codex connection has too many active requests'));
491
- }
492
- if (signalArg?.aborted) return Promise.reject(this.abortError(signalArg.reason));
493
- return new Promise((resolve, reject) => {
494
- const id = ++this.requestCounter;
495
- const timer = setTimeout(() => {
496
- const pending = this.pendingRequests.get(id);
497
- if (!pending) return;
498
- this.pendingRequests.delete(id);
499
- this.clearPendingRequestResources(pending);
500
- reject(new Error(`Codex request timed out: ${methodArg}`));
501
- }, timeoutMsArg);
502
- const pending: IPendingRequest = { resolve, reject, timer, signal: signalArg };
503
- if (signalArg) {
504
- pending.abortHandler = () => {
505
- if (!this.pendingRequests.delete(id)) return;
506
- this.clearPendingRequestResources(pending);
507
- reject(this.abortError(signalArg.reason));
508
- };
509
- signalArg.addEventListener('abort', pending.abortHandler, { once: true });
510
- }
511
- this.pendingRequests.set(id, pending);
512
- const failSend = (errorArg: Error) => {
513
- const current = this.pendingRequests.get(id);
514
- if (!current) return;
515
- this.pendingRequests.delete(id);
516
- this.clearPendingRequestResources(current);
517
- current.reject(errorArg);
518
- };
519
- try {
520
- this.sendFrame({ id, method: methodArg, params: paramsArg });
521
- } catch (error) {
522
- failSend(error instanceof Error ? error : new Error(String(error)));
523
- }
524
- });
525
- }
526
-
527
- private sendNotification(methodArg: string, paramsArg?: unknown): void {
528
- this.sendFrame(
529
- paramsArg === undefined
530
- ? { method: methodArg }
531
- : { method: methodArg, params: paramsArg },
532
- );
533
- }
534
-
535
- private sendFrame(frameArg: Record<string, unknown>): void {
536
- const socket = this.socket;
537
- const payload = JSON.stringify(frameArg);
538
- const payloadBytes = Buffer.byteLength(payload, 'utf8');
539
- if (!socket || socket.readyState !== plugins.WsWebSocket.OPEN) {
540
- const error = new Error('Codex server WebSocket is not open');
541
- this.handleSocketFailure(error);
542
- throw error;
543
- }
544
- if (socket.bufferedAmount + payloadBytes > maxSocketBufferedBytes) {
545
- const error = new Error('Codex WebSocket send buffer exceeded the 4 MiB limit');
546
- this.handleSocketFailure(error);
547
- throw error;
548
- }
549
- try {
550
- socket.send(payload, (error) => {
551
- if (error) this.handleSocketFailure(error);
552
- });
553
- } catch (error) {
554
- const sendError = error instanceof Error ? error : new Error(String(error));
555
- this.handleSocketFailure(sendError);
556
- throw sendError;
557
- }
558
- }
559
-
560
- private handleMessage(rawArg: string): void {
561
- const message = helpers.parseJson(rawArg);
562
- if (!helpers.isRecord(message)) return;
563
- if (message.id !== undefined && typeof message.method === 'string') {
564
- this.respondToServerRequest(message);
565
- return;
566
- }
567
- if (typeof message.id === 'number' && this.pendingRequests.has(message.id)) {
568
- const pending = this.pendingRequests.get(message.id)!;
569
- this.pendingRequests.delete(message.id);
570
- this.clearPendingRequestResources(pending);
571
- if (message.error !== undefined) {
572
- pending.reject(
573
- new Error(
574
- `Codex server rejected request: ${JSON.stringify(message.error).slice(0, 4000)}`,
575
- ),
576
- );
577
- } else {
578
- pending.resolve(message.result);
579
- }
580
- return;
581
- }
582
- if (typeof message.method === 'string') {
583
- if (!helpers.isRecord(message.params)) {
584
- if (message.method === 'turn/completed') {
585
- this.handleSocketFailure(new Error('Codex returned malformed turn/completed params'));
586
- }
587
- return;
588
- }
589
- this.handleNotification(message.method, message.params);
590
- }
414
+ return this.client.request(methodArg, paramsArg, timeoutMsArg, signalArg);
591
415
  }
592
416
 
593
417
  private handleNotification(methodArg: string, paramsArg: Record<string, unknown>): void {
@@ -898,13 +722,6 @@ export class CodexConnection implements interfaces.IHarnessConnection {
898
722
  }
899
723
  }
900
724
 
901
- private clearPendingRequestResources(pendingArg: IPendingRequest): void {
902
- clearTimeout(pendingArg.timer);
903
- if (pendingArg.abortHandler) {
904
- pendingArg.signal?.removeEventListener('abort', pendingArg.abortHandler);
905
- }
906
- }
907
-
908
725
  private abortError(reasonArg: unknown): Error {
909
726
  return reasonArg instanceof Error ? reasonArg : new Error('operation cancelled');
910
727
  }
@@ -1002,7 +819,7 @@ export class CodexConnection implements interfaces.IHarnessConnection {
1002
819
  return true;
1003
820
  }
1004
821
 
1005
- private respondToServerRequest(messageArg: Record<string, unknown>): void {
822
+ private respondToServerRequest(messageArg: { id: string | number; method: string; params: Record<string, unknown> }): void {
1006
823
  const method = String(messageArg.method);
1007
824
  let result: unknown;
1008
825
  switch (method) {
@@ -1025,15 +842,12 @@ export class CodexConnection implements interfaces.IHarnessConnection {
1025
842
  break;
1026
843
  default:
1027
844
  try {
1028
- this.sendFrame({
1029
- id: messageArg.id,
1030
- error: { code: -32601, message: `Crossharness does not implement ${method}` },
1031
- });
845
+ this.client?.rejectRequest(messageArg.id, -32601, `Crossharness does not implement ${method}`);
1032
846
  } catch {}
1033
847
  return;
1034
848
  }
1035
849
  try {
1036
- this.sendFrame({ id: messageArg.id, result });
850
+ this.client?.respond(messageArg.id, result);
1037
851
  } catch {}
1038
852
  }
1039
853
 
@@ -1045,21 +859,7 @@ export class CodexConnection implements interfaces.IHarnessConnection {
1045
859
  this.onClosing?.();
1046
860
  this.onClosing = undefined;
1047
861
  }
1048
- const socket = this.socket;
1049
- if (socket && socket.readyState !== plugins.WsWebSocket.CLOSED) {
1050
- socket.terminate();
1051
- } else {
1052
- this.finishClosed();
1053
- }
1054
- }
1055
-
1056
- private handleSocketClose(): void {
1057
- if (this.state !== 'closing' && this.state !== 'closed') {
1058
- this.state = 'closing';
1059
- this.rejectAll(new Error('Codex server connection closed'));
1060
- this.onClosing?.();
1061
- this.onClosing = undefined;
1062
- }
862
+ this.client?.close(errorArg.message);
1063
863
  this.finishClosed();
1064
864
  }
1065
865
 
@@ -1072,11 +872,6 @@ export class CodexConnection implements interfaces.IHarnessConnection {
1072
872
  }
1073
873
 
1074
874
  private rejectAll(errorArg: Error): void {
1075
- for (const pending of this.pendingRequests.values()) {
1076
- this.clearPendingRequestResources(pending);
1077
- pending.reject(errorArg);
1078
- }
1079
- this.pendingRequests.clear();
1080
875
  for (const turn of [...this.pendingTurns]) this.failTurn(turn, errorArg);
1081
876
  for (const [threadId, queue] of this.threadQueues) {
1082
877
  const activeOffset = this.activeThreads.has(threadId) ? 1 : 0;
@@ -1088,33 +883,4 @@ export class CodexConnection implements interfaces.IHarnessConnection {
1088
883
  this.threadQueues.clear();
1089
884
  }
1090
885
 
1091
- private async closeSocket(): Promise<void> {
1092
- const socket = this.socket;
1093
- if (!socket || socket.readyState === plugins.WsWebSocket.CLOSED) return;
1094
- await new Promise<void>((resolve) => {
1095
- let settled = false;
1096
- const finish = () => {
1097
- if (settled) return;
1098
- settled = true;
1099
- clearTimeout(timer);
1100
- socket.off('close', finish);
1101
- resolve();
1102
- };
1103
- const timer = setTimeout(() => {
1104
- socket.terminate();
1105
- finish();
1106
- }, 1000);
1107
- socket.once('close', finish);
1108
- try {
1109
- if (socket.readyState === plugins.WsWebSocket.CONNECTING) {
1110
- socket.terminate();
1111
- } else {
1112
- socket.close();
1113
- }
1114
- } catch {
1115
- socket.terminate();
1116
- finish();
1117
- }
1118
- });
1119
- }
1120
886
  }