@howaboua/pi-codex-conversion 1.0.29 → 1.0.31-dev.3.4b87b70

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/README.md CHANGED
@@ -9,7 +9,7 @@ This package replaces Pi's default Codex/GPT experience with a narrower Codex-li
9
9
  - preserves Pi's composed system prompt and applies a narrow Codex-oriented delta on top
10
10
  - renders exec activity with Codex-style command and background-terminal labels
11
11
  - renders `apply_patch` calls with Codex-style `Added` / `Edited` / `Deleted` diff blocks and Pi-style colored diff lines
12
- - targets modern Pi tool/rendering APIs and is aligned with Pi `0.72.x`
12
+ - targets modern Pi tool/rendering APIs and is aligned with Pi `0.74.x` / the `@earendil-works/*` package scope
13
13
 
14
14
  ![Available tools](./available-tools.png)
15
15
 
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
8
+ const platform = process.platform;
9
+ const arch = process.arch;
10
+ const exe = platform === "win32" ? "apply_patch.exe" : "apply_patch";
11
+ const platformArch = `${platform}-${arch}`;
12
+ const binary = join(root, "vendor", "apply-patch", platformArch, exe);
13
+
14
+ if (!existsSync(binary)) {
15
+ console.error(`apply_patch binary is not bundled for ${platformArch}.`);
16
+ console.error("Expected bundled binaries under vendor/apply-patch/<platform>-<arch>/.");
17
+ process.exit(127);
18
+ }
19
+
20
+ const result = spawnSync(binary, process.argv.slice(2), {
21
+ stdio: "inherit",
22
+ env: process.env,
23
+ cwd: process.cwd(),
24
+ });
25
+
26
+ if (result.error) {
27
+ console.error(result.error.message);
28
+ process.exit(1);
29
+ }
30
+ process.exit(result.status ?? 0);
@@ -0,0 +1,2 @@
1
+ @echo off
2
+ node "%~dp0apply_patch" %*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.0.29",
3
+ "version": "1.0.31-dev.3.4b87b70",
4
4
  "description": "Codex-oriented tool and prompt adapter for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
@@ -21,10 +21,6 @@
21
21
  "apply-patch"
22
22
  ],
23
23
  "license": "MIT",
24
- "os": [
25
- "darwin",
26
- "linux"
27
- ],
28
24
  "pi": {
29
25
  "extensions": [
30
26
  "./src/index.ts"
@@ -33,6 +29,9 @@
33
29
  "files": [
34
30
  "src/**/*.ts",
35
31
  "src/**/*.md",
32
+ "bin/apply_patch",
33
+ "bin/apply_patch.cmd",
34
+ "vendor/apply-patch/**",
36
35
  "available-tools.png",
37
36
  "README.md",
38
37
  "LICENSE"
@@ -45,21 +44,24 @@
45
44
  "publish:dev": "npm publish --tag dev",
46
45
  "release:dev": "npm version prerelease --preid dev && npm publish --tag dev",
47
46
  "prepack": "npm run check",
48
- "prepublishOnly": "npm run check"
47
+ "prepublishOnly": "npm run verify:apply-patch-binaries && npm run check",
48
+ "build:apply-patch": "node scripts/build-apply-patch-binary.mjs",
49
+ "sync:apply-patch-source": "node scripts/sync-apply-patch-source.mjs",
50
+ "verify:apply-patch-binaries": "node scripts/verify-apply-patch-binaries.mjs"
49
51
  },
50
52
  "publishConfig": {
51
53
  "access": "public"
52
54
  },
53
55
  "peerDependencies": {
54
- "@mariozechner/pi-ai": "^0.72.0",
55
- "@mariozechner/pi-coding-agent": "^0.72.0",
56
- "@mariozechner/pi-tui": "^0.72.0",
57
- "typebox": "^1.1.24"
56
+ "@earendil-works/pi-ai": "*",
57
+ "@earendil-works/pi-coding-agent": "*",
58
+ "@earendil-works/pi-tui": "*",
59
+ "typebox": "*"
58
60
  },
59
61
  "devDependencies": {
60
- "@mariozechner/pi-ai": "^0.72.0",
61
- "@mariozechner/pi-coding-agent": "^0.72.0",
62
- "@mariozechner/pi-tui": "^0.72.0",
62
+ "@earendil-works/pi-ai": "^0.74.0",
63
+ "@earendil-works/pi-coding-agent": "^0.74.0",
64
+ "@earendil-works/pi-tui": "^0.74.0",
63
65
  "tsx": "^4.20.5",
64
66
  "typebox": "^1.1.24",
65
67
  "typescript": "^5.9.3"
@@ -69,5 +71,8 @@
69
71
  "partial-json": "^0.1.7",
70
72
  "tree-sitter-bash": "^0.25.1",
71
73
  "web-tree-sitter": "^0.26.7"
74
+ },
75
+ "bin": {
76
+ "apply_patch": "./bin/apply_patch"
72
77
  }
73
78
  }
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  export interface CodexLikeModelDescriptor {
4
4
  provider: string;
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { getCodexRuntimeShell } from "./adapter/runtime-shell.ts";
3
3
  import {
4
4
  CORE_ADAPTER_TOOL_NAMES,
@@ -24,21 +24,18 @@ import { buildCodexSystemPrompt, extractPiPromptSkills, type PromptSkill } from
24
24
  import { registerViewImageTool, supportsOriginalImageDetail } from "./tools/view-image-tool.ts";
25
25
  import {
26
26
  registerWebSearchTool,
27
- registerWebSearchSessionNoteRenderer,
28
27
  rewriteNativeWebSearchTool,
29
- shouldShowWebSearchSessionNote,
30
28
  supportsNativeWebSearch,
31
- WEB_SEARCH_SESSION_NOTE_TEXT,
32
29
  WEB_SEARCH_SESSION_NOTE_TYPE,
33
30
  } from "./tools/web-search-tool.ts";
34
31
  import { registerWriteStdinTool } from "./tools/write-stdin-tool.ts";
32
+ import { ensureBundledApplyPatchOnPath } from "./tools/apply-patch-binary.ts";
35
33
 
36
34
  interface AdapterState {
37
35
  enabled: boolean;
38
36
  cwd: string;
39
37
  previousToolNames?: string[];
40
38
  promptSkills: PromptSkill[];
41
- webSearchNoticeShown: boolean;
42
39
  }
43
40
 
44
41
  const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, WEB_SEARCH_TOOL_NAME, IMAGE_GENERATION_TOOL_NAME, VIEW_IMAGE_TOOL_NAME];
@@ -61,8 +58,9 @@ function isToolCallOnlyAssistantMessage(message: unknown): boolean {
61
58
  }
62
59
 
63
60
  export default function codexConversion(pi: ExtensionAPI) {
61
+ ensureBundledApplyPatchOnPath();
64
62
  const tracker = createExecCommandTracker();
65
- const state: AdapterState = { enabled: false, cwd: process.cwd(), promptSkills: [], webSearchNoticeShown: false };
63
+ const state: AdapterState = { enabled: false, cwd: process.cwd(), promptSkills: [] };
66
64
  const sessions = createExecSessionManager();
67
65
 
68
66
  registerOpenAICodexCustomProvider(pi, {
@@ -73,7 +71,6 @@ export default function codexConversion(pi: ExtensionAPI) {
73
71
  registerWriteStdinTool(pi, sessions);
74
72
  registerImageGenerationTool(pi);
75
73
  registerWebSearchTool(pi);
76
- registerWebSearchSessionNoteRenderer(pi);
77
74
 
78
75
  sessions.onSessionExit((sessionId) => {
79
76
  tracker.recordSessionFinished(sessionId);
@@ -81,7 +78,6 @@ export default function codexConversion(pi: ExtensionAPI) {
81
78
 
82
79
  pi.on("session_start", async (_event, ctx) => {
83
80
  state.cwd = ctx.cwd;
84
- state.webSearchNoticeShown = false;
85
81
  clearApplyPatchRenderState();
86
82
  tracker.clear();
87
83
  syncAdapter(pi, ctx, state);
@@ -157,7 +153,6 @@ function syncAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterStat
157
153
  state.promptSkills = extractPiPromptSkills(ctx.getSystemPrompt());
158
154
 
159
155
  registerViewImageTool(pi, { allowOriginalDetail: supportsOriginalImageDetail(ctx.model) });
160
- maybeShowWebSearchSessionNote(pi, ctx, state);
161
156
 
162
157
  if (isCodexLikeContext(ctx)) {
163
158
  enableAdapter(pi, ctx, state);
@@ -227,15 +222,3 @@ export function restoreTools(previousTools: string[], activeTools: string[]): st
227
222
  function hasAdapterTools(activeTools: string[]): boolean {
228
223
  return activeTools.some((toolName) => ADAPTER_TOOL_NAMES.includes(toolName));
229
224
  }
230
-
231
- function maybeShowWebSearchSessionNote(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
232
- if (!shouldShowWebSearchSessionNote(ctx.model, ctx.hasUI, state.webSearchNoticeShown)) {
233
- return;
234
- }
235
- pi.sendMessage({
236
- customType: WEB_SEARCH_SESSION_NOTE_TYPE,
237
- content: WEB_SEARCH_SESSION_NOTE_TEXT,
238
- display: true,
239
- });
240
- state.webSearchNoticeShown = true;
241
- }
@@ -1,8 +1,10 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { Box, Image, Spacer, Text } from "@mariozechner/pi-tui";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Image, Spacer, Text } from "@earendil-works/pi-tui";
3
3
  import {
4
4
  createAssistantMessageEventStream,
5
+ appendAssistantMessageDiagnostic,
5
6
  clampThinkingLevel,
7
+ createAssistantMessageDiagnostic,
6
8
  getEnvApiKey,
7
9
  type Api,
8
10
  type AssistantMessage,
@@ -10,7 +12,7 @@ import {
10
12
  type Context,
11
13
  type Model,
12
14
  type SimpleStreamOptions,
13
- } from "@mariozechner/pi-ai";
15
+ } from "@earendil-works/pi-ai";
14
16
  import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
15
17
  import {
16
18
  convertResponsesMessages,
@@ -29,6 +31,7 @@ const BASE_DELAY_MS = 1000;
29
31
  const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
30
32
  const CODEX_RESPONSE_STATUSES = new Set(["completed", "incomplete", "failed", "cancelled", "queued", "in_progress"]);
31
33
  const OPENAI_BETA_RESPONSES_WEBSOCKETS = "responses_websockets=2026-02-06";
34
+ const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
32
35
  const SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1000;
33
36
  const dynamicImport = (specifier: string) => import(specifier);
34
37
  let _os: { platform(): string; release(): string; arch(): string } | null = null;
@@ -412,16 +415,6 @@ function headersToRecord(headers: Headers): Record<string, string> {
412
415
  return Object.fromEntries(headers.entries());
413
416
  }
414
417
 
415
- function buildWebSocketCacheKey(url: string, headers: Headers, sessionId: string | undefined): string | undefined {
416
- if (!sessionId) return undefined;
417
- const headerFingerprint = Object.entries(headersToRecord(headers))
418
- .map(([key, value]) => [key.toLowerCase(), value] as const)
419
- .sort(([a], [b]) => a.localeCompare(b))
420
- .map(([key, value]) => `${key}:${value}`)
421
- .join("\n");
422
- return `${sessionId}:${shortHash(`${url}\n${headerFingerprint}`)}`;
423
- }
424
-
425
418
  function createCodexRequestId(): string {
426
419
  if (typeof globalThis.crypto?.randomUUID === "function") {
427
420
  return globalThis.crypto.randomUUID();
@@ -523,7 +516,7 @@ function resolveCodexServiceTier(responseServiceTier: ServiceTier, requestServic
523
516
  return responseServiceTier ?? requestServiceTier;
524
517
  }
525
518
 
526
- function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context, options?: SimpleStreamOptions): ResponsesBody {
519
+ export function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context, options?: SimpleStreamOptions): ResponsesBody {
527
520
  const messages = convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {
528
521
  includeSystemPrompt: false,
529
522
  });
@@ -532,7 +525,7 @@ function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context
532
525
  model: model.id,
533
526
  store: false,
534
527
  stream: true,
535
- instructions: context.systemPrompt,
528
+ instructions: context.systemPrompt || "You are a helpful assistant.",
536
529
  input: messages,
537
530
  text: { verbosity: ((options as { textVerbosity?: string } | undefined)?.textVerbosity ?? "low") as string },
538
531
  include: ["reasoning.encrypted_content"],
@@ -603,7 +596,7 @@ function sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
603
596
  });
604
597
  }
605
598
 
606
- async function* parseSSE(response: Response): AsyncIterable<StreamEventShape> {
599
+ export async function* parseSSE(response: Response): AsyncIterable<StreamEventShape> {
607
600
  if (!response.body) return;
608
601
 
609
602
  const reader = response.body.getReader();
@@ -630,8 +623,8 @@ async function* parseSSE(response: Response): AsyncIterable<StreamEventShape> {
630
623
  if (data && data !== "[DONE]") {
631
624
  try {
632
625
  yield JSON.parse(data) as StreamEventShape;
633
- } catch {
634
- // Ignore malformed SSE chunks and continue consuming the stream.
626
+ } catch (error) {
627
+ throw new Error(`Invalid Codex SSE JSON: ${error instanceof Error ? error.message : String(error)}`);
635
628
  }
636
629
  }
637
630
  }
@@ -674,6 +667,29 @@ function closeWebSocketSilently(socket: WebSocketLike, code = 1000, reason = "do
674
667
  }
675
668
  }
676
669
 
670
+ export function closeOpenAICodexWebSocketSessions(sessionId?: string): void {
671
+ const closeEntry = (entry: SessionWebSocketCacheEntry) => {
672
+ if (entry.idleTimer) {
673
+ clearTimeout(entry.idleTimer);
674
+ entry.idleTimer = undefined;
675
+ }
676
+ closeWebSocketSilently(entry.socket, 1000, "session_shutdown");
677
+ };
678
+
679
+ if (sessionId) {
680
+ const entry = websocketSessionCache.get(sessionId);
681
+ if (entry) closeEntry(entry);
682
+ websocketSessionCache.delete(sessionId);
683
+ return;
684
+ }
685
+
686
+ for (const entry of websocketSessionCache.values()) {
687
+ closeEntry(entry);
688
+ }
689
+ websocketSessionCache.clear();
690
+ }
691
+
692
+
677
693
  function scheduleSessionWebSocketExpiry(cacheKey: string, entry: SessionWebSocketCacheEntry): void {
678
694
  if (entry.idleTimer) {
679
695
  clearTimeout(entry.idleTimer);
@@ -700,7 +716,10 @@ function extractWebSocketCloseError(event: unknown): Error {
700
716
  const code = "code" in event ? (event as { code?: unknown }).code : undefined;
701
717
  const reason = "reason" in event ? (event as { reason?: unknown }).reason : undefined;
702
718
  const codeText = typeof code === "number" ? ` ${code}` : "";
703
- const reasonText = typeof reason === "string" && reason.length > 0 ? ` ${reason}` : "";
719
+ let reasonText = typeof reason === "string" && reason.length > 0 ? ` ${reason}` : "";
720
+ if (!reasonText && code === WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE) {
721
+ reasonText = " message too big";
722
+ }
704
723
  return new Error(`WebSocket closed${codeText}${reasonText}`.trim());
705
724
  }
706
725
  return new Error("WebSocket closed");
@@ -772,8 +791,7 @@ async function acquireWebSocket(
772
791
  sessionId: string | undefined,
773
792
  signal: AbortSignal | undefined,
774
793
  ): Promise<AcquiredWebSocket> {
775
- const cacheKey = buildWebSocketCacheKey(url, headers, sessionId);
776
- if (!cacheKey) {
794
+ if (!sessionId) {
777
795
  const socket = await connectWebSocket(url, headers, signal);
778
796
  return {
779
797
  socket,
@@ -788,7 +806,7 @@ async function acquireWebSocket(
788
806
  };
789
807
  }
790
808
 
791
- const cached = websocketSessionCache.get(cacheKey);
809
+ const cached = websocketSessionCache.get(sessionId);
792
810
  if (cached) {
793
811
  if (cached.idleTimer) {
794
812
  clearTimeout(cached.idleTimer);
@@ -804,11 +822,11 @@ async function acquireWebSocket(
804
822
  release: ({ keep } = {}) => {
805
823
  if (!keep || !isWebSocketReusable(cached.socket)) {
806
824
  closeWebSocketSilently(cached.socket);
807
- websocketSessionCache.delete(cacheKey);
825
+ websocketSessionCache.delete(sessionId);
808
826
  return;
809
827
  }
810
828
  cached.busy = false;
811
- scheduleSessionWebSocketExpiry(cacheKey, cached);
829
+ scheduleSessionWebSocketExpiry(sessionId, cached);
812
830
  },
813
831
  };
814
832
  }
@@ -826,13 +844,13 @@ async function acquireWebSocket(
826
844
 
827
845
  if (!isWebSocketReusable(cached.socket)) {
828
846
  closeWebSocketSilently(cached.socket);
829
- websocketSessionCache.delete(cacheKey);
847
+ websocketSessionCache.delete(sessionId);
830
848
  }
831
849
  }
832
850
 
833
851
  const socket = await connectWebSocket(url, headers, signal);
834
852
  const entry: SessionWebSocketCacheEntry = { socket, busy: true };
835
- websocketSessionCache.set(cacheKey, entry);
853
+ websocketSessionCache.set(sessionId, entry);
836
854
  return {
837
855
  socket,
838
856
  entry,
@@ -841,13 +859,13 @@ async function acquireWebSocket(
841
859
  if (!keep || !isWebSocketReusable(entry.socket)) {
842
860
  closeWebSocketSilently(entry.socket);
843
861
  if (entry.idleTimer) clearTimeout(entry.idleTimer);
844
- if (websocketSessionCache.get(cacheKey) === entry) {
845
- websocketSessionCache.delete(cacheKey);
862
+ if (websocketSessionCache.get(sessionId) === entry) {
863
+ websocketSessionCache.delete(sessionId);
846
864
  }
847
865
  return;
848
866
  }
849
867
  entry.busy = false;
850
- scheduleSessionWebSocketExpiry(cacheKey, entry);
868
+ scheduleSessionWebSocketExpiry(sessionId, entry);
851
869
  },
852
870
  };
853
871
  }
@@ -951,8 +969,9 @@ async function* parseWebSocket(socket: WebSocketLike, signal: AbortSignal | unde
951
969
  done = true;
952
970
  }
953
971
  queue.push(parsed);
954
- } catch {
955
- // ignore malformed websocket messages
972
+ } catch (error) {
973
+ failed = new Error(`Invalid Codex WebSocket JSON: ${error instanceof Error ? error.message : String(error)}`);
974
+ done = true;
956
975
  }
957
976
  })
958
977
  .catch((error: unknown) => {
@@ -1033,9 +1052,27 @@ async function* countWebSocketEvents(
1033
1052
  }
1034
1053
  }
1035
1054
 
1055
+ async function* startWebSocketOutputOnFirstEvent(
1056
+ events: AsyncIterable<StreamEventShape>,
1057
+ output: AssistantMessage,
1058
+ stream: AssistantMessageEventStream,
1059
+ onStart: () => void,
1060
+ ): AsyncIterable<StreamEventShape> {
1061
+ let started = false;
1062
+ for await (const event of events) {
1063
+ if (!started) {
1064
+ started = true;
1065
+ onStart();
1066
+ stream.push({ type: "start", partial: output });
1067
+ }
1068
+ yield event;
1069
+ }
1070
+ }
1071
+
1036
1072
  function isRetryableEarlyWebSocketError(error: unknown): boolean {
1037
1073
  const message = error instanceof Error ? error.message : String(error);
1038
- return /^WebSocket (error|closed)(?:\s|$)/.test(message);
1074
+ if (/message too big/i.test(message)) return false;
1075
+ return /^(?:WebSocket (?:error|closed)(?:\s|$)|Invalid Codex WebSocket JSON)/.test(message);
1039
1076
  }
1040
1077
 
1041
1078
  async function* mapCodexEvents(events: AsyncIterable<StreamEventShape>): AsyncIterable<StreamEventShape> {
@@ -1193,11 +1230,12 @@ async function processWebSocketStream<TApi extends Api>(
1193
1230
  let streamStarted = false;
1194
1231
 
1195
1232
  for (let attempt = 0; attempt < 2; attempt++) {
1196
- const { socket, entry, release, reused } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
1233
+ const { socket, entry, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
1197
1234
  let keepConnection = true;
1198
1235
  let released = false;
1199
1236
  let eventCount = 0;
1200
- const useCachedContext = (options as { transport?: string } | undefined)?.transport === "websocket-cached";
1237
+ const transport = (options as { transport?: string } | undefined)?.transport ?? "auto";
1238
+ const useCachedContext = transport === "websocket-cached" || transport === "auto";
1201
1239
  // ChatGPT Codex Responses rejects `store: true` ("Store must be set to false").
1202
1240
  // WebSocket continuation still works via connection-scoped previous_response_id state.
1203
1241
  const fullBody = body;
@@ -1211,15 +1249,18 @@ async function processWebSocketStream<TApi extends Api>(
1211
1249
 
1212
1250
  try {
1213
1251
  socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
1214
- if (!streamStarted) {
1215
- onStart();
1216
- stream.push({ type: "start", partial: output });
1217
- streamStarted = true;
1218
- }
1219
1252
  await processCapturedResponsesStream(
1220
- countWebSocketEvents(parseWebSocket(socket, options?.signal), () => {
1221
- eventCount++;
1222
- }),
1253
+ startWebSocketOutputOnFirstEvent(
1254
+ countWebSocketEvents(parseWebSocket(socket, options?.signal), () => {
1255
+ eventCount++;
1256
+ }),
1257
+ output,
1258
+ stream,
1259
+ () => {
1260
+ streamStarted = true;
1261
+ onStart();
1262
+ },
1263
+ ),
1223
1264
  output,
1224
1265
  stream,
1225
1266
  model,
@@ -1248,11 +1289,10 @@ async function processWebSocketStream<TApi extends Api>(
1248
1289
  }
1249
1290
  keepConnection = false;
1250
1291
  releaseOnce({ keep: false });
1251
- // Pi's stock provider reuses session WebSockets. In practice the Codex
1252
- // backend sometimes cleanly closes an idle cached socket between turns;
1253
- // if that stale socket fails before any response event, retry once on a
1254
- // fresh WebSocket without changing request shape or falling back transports.
1255
- if (attempt === 0 && reused && eventCount === 0 && !options?.signal?.aborted && isRetryableEarlyWebSocketError(error)) {
1292
+ // If WebSocket fails before the first response event, nothing has been
1293
+ // emitted to the UI/history yet. Retry once on a fresh WebSocket; if that
1294
+ // also fails, the caller can fall back to SSE for `auto` transport.
1295
+ if (attempt === 0 && eventCount === 0 && !streamStarted && !options?.signal?.aborted && isRetryableEarlyWebSocketError(error)) {
1256
1296
  continue;
1257
1297
  }
1258
1298
  throw error;
@@ -1460,7 +1500,7 @@ function createCodexStream<TApi extends Api>(
1460
1500
  const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
1461
1501
  const websocketHeaders = buildWebSocketHeaders(model.headers, options?.headers, accountId, apiKey, websocketRequestId);
1462
1502
  const bodyJson = JSON.stringify(body);
1463
- const transport = options?.transport || "sse";
1503
+ const transport = options?.transport || "auto";
1464
1504
 
1465
1505
  if (transport !== "sse") {
1466
1506
  let websocketStarted = false;
@@ -1488,6 +1528,16 @@ function createCodexStream<TApi extends Api>(
1488
1528
  stream.end();
1489
1529
  return;
1490
1530
  } catch (error) {
1531
+ appendAssistantMessageDiagnostic(
1532
+ output,
1533
+ createAssistantMessageDiagnostic("provider_transport_failure", error, {
1534
+ configuredTransport: transport,
1535
+ fallbackTransport: websocketStarted ? undefined : "sse",
1536
+ eventsEmitted: websocketStarted,
1537
+ phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start",
1538
+ requestBytes: new TextEncoder().encode(bodyJson).byteLength,
1539
+ }),
1540
+ );
1491
1541
  if (transport === "websocket" || transport === "websocket-cached" || websocketStarted) {
1492
1542
  throw error;
1493
1543
  }
@@ -1656,6 +1706,7 @@ export function registerOpenAICodexCustomProvider(pi: ExtensionAPI, options: { g
1656
1706
  flushPendingMessages();
1657
1707
  }
1658
1708
  clearPendingMessages();
1709
+ closeOpenAICodexWebSocketSessions();
1659
1710
  });
1660
1711
 
1661
1712
  pi.on("agent_end", async () => {
@@ -1,7 +1,7 @@
1
- import { calculateCost, type Api, type AssistantMessage, type Context, type Model, type Tool, type Usage } from "@mariozechner/pi-ai";
1
+ import { calculateCost, type Api, type AssistantMessage, type Context, type Model, type Tool, type Usage } from "@earendil-works/pi-ai";
2
2
  import type { ResponseCreateParamsStreaming, ResponseInput, ResponseStreamEvent, Tool as OpenAITool } from "openai/resources/responses/responses.js";
3
3
  import { parse as partialParse } from "partial-json";
4
- import type { AssistantMessageEventStream } from "@mariozechner/pi-ai";
4
+ import type { AssistantMessageEventStream } from "@earendil-works/pi-ai";
5
5
 
6
6
  type MessageRole = Context["messages"][number]["role"];
7
7
  type Message = Context["messages"][number];
@@ -0,0 +1,21 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, delimiter, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export function getBundledApplyPatchBinDir(): string {
6
+ return join(dirname(dirname(dirname(fileURLToPath(import.meta.url)))), "bin");
7
+ }
8
+
9
+ export function ensureBundledApplyPatchOnPath(env: NodeJS.ProcessEnv = process.env): string | undefined {
10
+ const binDir = getBundledApplyPatchBinDir();
11
+ const wrapperPath = join(binDir, process.platform === "win32" ? "apply_patch.cmd" : "apply_patch");
12
+ if (!existsSync(wrapperPath)) {
13
+ return undefined;
14
+ }
15
+ const currentPath = env.PATH ?? "";
16
+ const entries = currentPath.split(delimiter).filter(Boolean);
17
+ if (!entries.includes(binDir)) {
18
+ env.PATH = [binDir, ...entries].join(delimiter);
19
+ }
20
+ return binDir;
21
+ }
@@ -1,5 +1,5 @@
1
1
  import { isAbsolute, relative } from "node:path";
2
- import { renderDiff } from "@mariozechner/pi-coding-agent";
2
+ import { renderDiff } from "@earendil-works/pi-coding-agent";
3
3
  import { openFileAtPath } from "../patch/paths.ts";
4
4
  import { parsePatchActions } from "../patch/parser.ts";
5
5
  import type { ParsedPatchAction } from "../patch/types.ts";
@@ -1,6 +1,6 @@
1
1
  import { Type } from "typebox";
2
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
- import { Container, Text } from "@mariozechner/pi-tui";
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import { Container, Text } from "@earendil-works/pi-tui";
4
4
  import { executePatch } from "../patch/core.ts";
5
5
  import { ExecutePatchError, type ExecutePatchResult } from "../patch/types.ts";
6
6
  import { formatApplyPatchSummary, formatPatchTarget, renderApplyPatchCall } from "./apply-patch-rendering.ts";
@@ -1,6 +1,6 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import { Container, Text } from "@mariozechner/pi-tui";
3
+ import { Container, Text } from "@earendil-works/pi-tui";
4
4
  import { renderExecCommandCall, renderGroupedExecCommandCall } from "./codex-rendering.ts";
5
5
  import type { ExecCommandTracker } from "./exec-command-state.ts";
6
6
  import type { ExecSessionManager, UnifiedExecResult } from "./exec-session-manager.ts";
@@ -1,6 +1,6 @@
1
- import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import { Container, Text } from "@mariozechner/pi-tui";
3
+ import { Container, Text } from "@earendil-works/pi-tui";
4
4
  import { isOpenAICodexModel } from "../adapter/codex-model.ts";
5
5
 
6
6
  export const IMAGE_GENERATION_UNSUPPORTED_MESSAGE =
@@ -6,9 +6,9 @@ import {
6
6
  type ExtensionAPI,
7
7
  type ExtensionContext,
8
8
  type ToolDefinition,
9
- } from "@mariozechner/pi-coding-agent";
9
+ } from "@earendil-works/pi-coding-agent";
10
10
  import { Type, type TSchema } from "typebox";
11
- import { Text } from "@mariozechner/pi-tui";
11
+ import { Text } from "@earendil-works/pi-tui";
12
12
 
13
13
  const VIEW_IMAGE_UNSUPPORTED_MESSAGE = "view_image is not allowed because you do not support image inputs";
14
14
  const DETAIL_DESCRIPTION =
@@ -1,14 +1,12 @@
1
- import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import { Box, Container, Text } from "@mariozechner/pi-tui";
3
+ import { Container, Text } from "@earendil-works/pi-tui";
4
4
  import { isOpenAICodexModel } from "../adapter/codex-model.ts";
5
5
 
6
6
  export const WEB_SEARCH_UNSUPPORTED_MESSAGE = "web_search is only available with the openai-codex provider";
7
7
  const WEB_SEARCH_LOCAL_EXECUTION_MESSAGE =
8
8
  "web_search is a native openai-codex provider tool and should not execute locally";
9
9
  export const WEB_SEARCH_SESSION_NOTE_TYPE = "codex-web-search-session-note";
10
- export const WEB_SEARCH_SESSION_NOTE_TEXT =
11
- "Native OpenAI Codex web search is enabled for this session. Search activity is surfaced as merged foldable status messages instead of native tool-call rows.";
12
10
  const WEB_SEARCH_MULTIMODAL_CONTENT_TYPES = ["text", "image"] as const;
13
11
 
14
12
  const WEB_SEARCH_PARAMETERS = Type.Unsafe<Record<string, never>>({
@@ -36,14 +34,6 @@ export function supportsNativeWebSearch(model: ExtensionContext["model"]): boole
36
34
  return isOpenAICodexModel(model);
37
35
  }
38
36
 
39
- export function shouldShowWebSearchSessionNote(
40
- model: ExtensionContext["model"],
41
- hasUI: boolean,
42
- alreadyShown: boolean,
43
- ): boolean {
44
- return hasUI && !alreadyShown && supportsNativeWebSearch(model);
45
- }
46
-
47
37
  export function supportsMultimodalNativeWebSearch(model: ExtensionContext["model"]): boolean {
48
38
  if (!supportsNativeWebSearch(model)) {
49
39
  return false;
@@ -130,12 +120,3 @@ export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETE
130
120
  export function registerWebSearchTool(pi: ExtensionAPI): void {
131
121
  pi.registerTool(createWebSearchTool());
132
122
  }
133
-
134
- export function registerWebSearchSessionNoteRenderer(pi: ExtensionAPI): void {
135
- pi.registerMessageRenderer(WEB_SEARCH_SESSION_NOTE_TYPE, (_message, _options, theme) => {
136
- const box = new Box(1, 1, (text) => theme.bg("toolSuccessBg", text));
137
- box.addChild(new Text(theme.bold("Web search enabled"), 0, 0));
138
- box.addChild(new Text(`\n${theme.fg("dim", WEB_SEARCH_SESSION_NOTE_TEXT)}`, 0, 0));
139
- return box;
140
- });
141
- }
@@ -1,6 +1,6 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import { Container, Text } from "@mariozechner/pi-tui";
3
+ import { Container, Text } from "@earendil-works/pi-tui";
4
4
  import { renderWriteStdinCall } from "./codex-rendering.ts";
5
5
  import type { ExecSessionManager, UnifiedExecResult } from "./exec-session-manager.ts";
6
6
  import { formatUnifiedExecResult } from "./unified-exec-format.ts";