@howaboua/pi-codex-conversion 1.0.25 → 1.0.27

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "Codex-oriented tool and prompt adapter for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
@@ -105,6 +105,12 @@ interface SessionWebSocketCacheEntry {
105
105
  idleTimer?: ReturnType<typeof setTimeout>;
106
106
  }
107
107
 
108
+ interface AcquiredWebSocket {
109
+ socket: WebSocketLike;
110
+ reused: boolean;
111
+ release: (options?: { keep?: boolean }) => void;
112
+ }
113
+
108
114
  let fsPromisesPromise: Promise<typeof import("node:fs/promises")> | undefined;
109
115
  const workspaceRootCache = new Map<string, Promise<string>>();
110
116
 
@@ -753,12 +759,13 @@ async function acquireWebSocket(
753
759
  headers: Headers,
754
760
  sessionId: string | undefined,
755
761
  signal: AbortSignal | undefined,
756
- ): Promise<{ socket: WebSocketLike; release: (options?: { keep?: boolean }) => void }> {
762
+ ): Promise<AcquiredWebSocket> {
757
763
  const cacheKey = buildWebSocketCacheKey(url, headers, sessionId);
758
764
  if (!cacheKey) {
759
765
  const socket = await connectWebSocket(url, headers, signal);
760
766
  return {
761
767
  socket,
768
+ reused: false,
762
769
  release: ({ keep } = {}) => {
763
770
  if (keep === false) {
764
771
  closeWebSocketSilently(socket);
@@ -780,6 +787,7 @@ async function acquireWebSocket(
780
787
  cached.busy = true;
781
788
  return {
782
789
  socket: cached.socket,
790
+ reused: true,
783
791
  release: ({ keep } = {}) => {
784
792
  if (!keep || !isWebSocketReusable(cached.socket)) {
785
793
  closeWebSocketSilently(cached.socket);
@@ -796,6 +804,7 @@ async function acquireWebSocket(
796
804
  const socket = await connectWebSocket(url, headers, signal);
797
805
  return {
798
806
  socket,
807
+ reused: false,
799
808
  release: () => {
800
809
  closeWebSocketSilently(socket);
801
810
  },
@@ -813,6 +822,7 @@ async function acquireWebSocket(
813
822
  websocketSessionCache.set(cacheKey, entry);
814
823
  return {
815
824
  socket,
825
+ reused: false,
816
826
  release: ({ keep } = {}) => {
817
827
  if (!keep || !isWebSocketReusable(entry.socket)) {
818
828
  closeWebSocketSilently(entry.socket);
@@ -948,6 +958,21 @@ async function* parseWebSocket(socket: WebSocketLike, signal: AbortSignal | unde
948
958
  }
949
959
  }
950
960
 
961
+ async function* countWebSocketEvents(
962
+ events: AsyncIterable<StreamEventShape>,
963
+ onEvent: () => void,
964
+ ): AsyncIterable<StreamEventShape> {
965
+ for await (const event of events) {
966
+ onEvent();
967
+ yield event;
968
+ }
969
+ }
970
+
971
+ function isRetryableEarlyWebSocketError(error: unknown): boolean {
972
+ const message = error instanceof Error ? error.message : String(error);
973
+ return /^WebSocket (error|closed)(?:\s|$)/.test(message);
974
+ }
975
+
951
976
  async function* mapCodexEvents(events: AsyncIterable<StreamEventShape>): AsyncIterable<StreamEventShape> {
952
977
  let sawTerminalResponse = false;
953
978
  for await (const event of events) {
@@ -1100,22 +1125,58 @@ async function processWebSocketStream<TApi extends Api>(
1100
1125
  cwd: string,
1101
1126
  requestPrompt: string | undefined,
1102
1127
  ): Promise<void> {
1103
- const { socket, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
1104
- let keepConnection = true;
1128
+ let streamStarted = false;
1129
+
1130
+ for (let attempt = 0; attempt < 2; attempt++) {
1131
+ const { socket, release, reused } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
1132
+ let keepConnection = true;
1133
+ let released = false;
1134
+ let eventCount = 0;
1135
+
1136
+ const releaseOnce = (releaseOptions?: { keep?: boolean }) => {
1137
+ if (released) return;
1138
+ released = true;
1139
+ release(releaseOptions);
1140
+ };
1105
1141
 
1106
- try {
1107
- socket.send(JSON.stringify({ type: "response.create", ...body }));
1108
- onStart();
1109
- stream.push({ type: "start", partial: output });
1110
- await processCapturedResponsesStream(parseWebSocket(socket, options?.signal), output, stream, model, options, deps, cwd, requestPrompt);
1111
- if (options?.signal?.aborted) {
1142
+ try {
1143
+ socket.send(JSON.stringify({ type: "response.create", ...body }));
1144
+ if (!streamStarted) {
1145
+ onStart();
1146
+ stream.push({ type: "start", partial: output });
1147
+ streamStarted = true;
1148
+ }
1149
+ await processCapturedResponsesStream(
1150
+ countWebSocketEvents(parseWebSocket(socket, options?.signal), () => {
1151
+ eventCount++;
1152
+ }),
1153
+ output,
1154
+ stream,
1155
+ model,
1156
+ options,
1157
+ deps,
1158
+ cwd,
1159
+ requestPrompt,
1160
+ );
1161
+ if (options?.signal?.aborted) {
1162
+ keepConnection = false;
1163
+ }
1164
+ releaseOnce({ keep: keepConnection });
1165
+ return;
1166
+ } catch (error) {
1112
1167
  keepConnection = false;
1168
+ releaseOnce({ keep: false });
1169
+ // Pi's stock provider reuses session WebSockets. In practice the Codex
1170
+ // backend sometimes cleanly closes an idle cached socket between turns;
1171
+ // if that stale socket fails before any response event, retry once on a
1172
+ // fresh WebSocket without changing request shape or falling back transports.
1173
+ if (attempt === 0 && reused && eventCount === 0 && !options?.signal?.aborted && isRetryableEarlyWebSocketError(error)) {
1174
+ continue;
1175
+ }
1176
+ throw error;
1177
+ } finally {
1178
+ releaseOnce({ keep: keepConnection });
1113
1179
  }
1114
- } catch (error) {
1115
- keepConnection = false;
1116
- throw error;
1117
- } finally {
1118
- release({ keep: keepConnection });
1119
1180
  }
1120
1181
  }
1121
1182
 
@@ -1242,7 +1303,15 @@ function createErrorMessage(message: AssistantMessage, error: unknown, aborted:
1242
1303
  }
1243
1304
  }
1244
1305
  message.stopReason = aborted ? "aborted" : "error";
1245
- message.errorMessage = error instanceof Error ? error.message : String(error);
1306
+ message.errorMessage = buildProviderErrorMessage(error);
1307
+ return message;
1308
+ }
1309
+
1310
+ export function buildProviderErrorMessage(error: unknown): string {
1311
+ const message = error instanceof Error ? error.message : String(error);
1312
+ if (/^(?:WebSocket (?:error|closed)|WebSocket stream closed before response\.completed|Stream closed before response\.completed)/.test(message)) {
1313
+ return `Connection error: ${message}`;
1314
+ }
1246
1315
  return message;
1247
1316
  }
1248
1317