@howaboua/pi-codex-conversion 1.0.24 → 1.0.26

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.24",
3
+ "version": "1.0.26",
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
 
@@ -8,12 +8,10 @@ type Message = Context["messages"][number];
8
8
 
9
9
  interface ImageGenerationCallItem {
10
10
  type: "image_generation_call";
11
- id?: string;
12
- status?: string;
13
- result?: string | null;
14
- output_format?: string;
11
+ id: string;
12
+ status: string;
13
+ result: string | null;
15
14
  revised_prompt?: string;
16
- [key: string]: unknown;
17
15
  }
18
16
 
19
17
  interface ImageGenerationCallBlock {
@@ -76,6 +74,23 @@ function isImageGenerationCallBlock(block: InternalAssistantContent): block is I
76
74
  return block.type === "image_generation_call" && block.item?.type === "image_generation_call";
77
75
  }
78
76
 
77
+ function sanitizeImageGenerationCallItem(item: unknown): ImageGenerationCallItem | undefined {
78
+ if (!item || typeof item !== "object") return undefined;
79
+ const candidate = item as Record<string, unknown>;
80
+ if (candidate.type !== "image_generation_call") return undefined;
81
+ if (typeof candidate.id !== "string" || candidate.id === "") return undefined;
82
+ if (typeof candidate.status !== "string" || candidate.status === "") return undefined;
83
+ if (!(typeof candidate.result === "string" || candidate.result === null)) return undefined;
84
+
85
+ return {
86
+ type: "image_generation_call",
87
+ id: candidate.id,
88
+ status: candidate.status,
89
+ result: candidate.result,
90
+ ...(typeof candidate.revised_prompt === "string" ? { revised_prompt: candidate.revised_prompt } : {}),
91
+ };
92
+ }
93
+
79
94
  const NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
80
95
  const NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
81
96
 
@@ -287,7 +302,8 @@ export function convertResponsesMessages<TApi extends Api>(
287
302
  let assistantBlockIndex = 0;
288
303
  for (const block of msg.content as InternalAssistantContent[]) {
289
304
  if (isImageGenerationCallBlock(block)) {
290
- output.push(block.item as ResponseInput[number]);
305
+ const imageGenerationCall = sanitizeImageGenerationCallItem(block.item);
306
+ if (imageGenerationCall) output.push(imageGenerationCall as ResponseInput[number]);
291
307
  } else if (block.type === "thinking") {
292
308
  if (block.thinkingSignature) output.push(JSON.parse(block.thinkingSignature));
293
309
  } else if (block.type === "text") {
@@ -581,10 +597,13 @@ export async function processResponsesStream<TApi extends Api>(
581
597
  stream.push({ type: "toolcall_end", contentIndex: toolCallIndex, toolCall, partial: output });
582
598
  outputStates.delete(event.output_index);
583
599
  } else if (item.type === "image_generation_call") {
584
- (output.content as InternalAssistantContent[]).push({
585
- type: "image_generation_call",
586
- item: item as ImageGenerationCallItem,
587
- });
600
+ const imageGenerationCall = sanitizeImageGenerationCallItem(item);
601
+ if (imageGenerationCall) {
602
+ (output.content as InternalAssistantContent[]).push({
603
+ type: "image_generation_call",
604
+ item: imageGenerationCall,
605
+ });
606
+ }
588
607
  outputStates.delete(event.output_index);
589
608
  }
590
609
  } else if (event.type === "response.completed") {