@minniexcode/codex-switch 0.1.3 → 0.1.5

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.
@@ -50,6 +50,9 @@ const path = __importStar(require("node:path"));
50
50
  const providers_1 = require("../domain/providers");
51
51
  const errors_1 = require("../domain/errors");
52
52
  const runtime_state_repo_1 = require("../storage/runtime-state-repo");
53
+ const BRIDGE_REUSE_ATTEMPTS = 2;
54
+ const BRIDGE_REUSE_TIMEOUT_MS = 2500;
55
+ const BRIDGE_REUSE_DELAY_MS = 250;
53
56
  let spawnImplementation = node_child_process_1.spawn;
54
57
  let cachedBridgeWorkerBuildId = null;
55
58
  /**
@@ -69,13 +72,17 @@ function resetCopilotBridgeSpawnImplementation() {
69
72
  */
70
73
  async function probeCopilotBridgeRuntime(provider, persistedState, runtimeDir) {
71
74
  const state = persistedState === undefined ? (0, runtime_state_repo_1.readCopilotBridgeState)(runtimeDir) : persistedState;
75
+ const logPath = state?.logPath ?? (0, runtime_state_repo_1.getCopilotBridgeLogPath)(runtimeDir);
72
76
  if (state && (!provider || !(0, providers_1.isCopilotBridgeProvider)(provider))) {
73
77
  return {
74
78
  ok: false,
75
79
  runtime: "copilot-bridge",
76
80
  reason: "failed",
77
81
  cause: "Copilot bridge runtime state exists but no active Copilot bridge provider is selected.",
78
- details: state,
82
+ details: {
83
+ ...state,
84
+ logPath,
85
+ },
79
86
  };
80
87
  }
81
88
  if (!provider || !(0, providers_1.isCopilotBridgeProvider)(provider)) {
@@ -100,6 +107,7 @@ async function probeCopilotBridgeRuntime(provider, persistedState, runtimeDir) {
100
107
  cause: "Copilot bridge state manifest is missing.",
101
108
  details: {
102
109
  expectedBaseUrl: (0, providers_1.buildCopilotBridgeBaseUrl)(runtime),
110
+ logPath,
103
111
  },
104
112
  };
105
113
  }
@@ -112,27 +120,44 @@ async function probeCopilotBridgeRuntime(provider, persistedState, runtimeDir) {
112
120
  details: {
113
121
  stateBaseUrl: state.baseUrl,
114
122
  providerBaseUrl: (0, providers_1.buildCopilotBridgeBaseUrl)(runtime),
123
+ logPath,
115
124
  },
116
125
  };
117
126
  }
118
- const healthy = await healthcheckCopilotBridge(state.host, state.port);
127
+ const healthy = await probeBridgeEndpoint({
128
+ host: state.host,
129
+ port: state.port,
130
+ stage: "health",
131
+ });
119
132
  if (!healthy.ok) {
120
133
  return {
121
134
  ok: false,
122
135
  runtime: "copilot-bridge",
123
136
  reason: "failed",
124
- cause: healthy.cause,
125
- details: state,
137
+ cause: healthy.message,
138
+ details: {
139
+ ...state,
140
+ logPath,
141
+ probeStage: healthy.stage,
142
+ probeCause: healthy.cause,
143
+ retryable: healthy.retryable,
144
+ },
126
145
  };
127
146
  }
128
147
  (0, runtime_state_repo_1.writeCopilotBridgeState)({
129
148
  ...state,
130
149
  lastHealthcheckAt: new Date().toISOString(),
150
+ lastProbeAt: new Date().toISOString(),
151
+ logPath,
131
152
  }, runtimeDir);
132
153
  return {
133
154
  ok: true,
134
155
  runtime: "copilot-bridge",
135
- details: state,
156
+ details: {
157
+ ...state,
158
+ logPath,
159
+ lastProbeAt: new Date().toISOString(),
160
+ },
136
161
  };
137
162
  }
138
163
  /**
@@ -160,47 +185,50 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
160
185
  const current = (0, runtime_state_repo_1.readCopilotBridgeState)(runtimeDir);
161
186
  const workerBuildId = getCopilotBridgeWorkerBuildId();
162
187
  let replaced = false;
163
- if (current && current.provider === providerName && current.baseUrl === expectedBaseUrl) {
164
- if (current.workerBuildId === workerBuildId) {
165
- const healthy = await healthcheckCopilotBridge(current.host, current.port);
166
- if (healthy.ok) {
167
- const compatible = await verifyCopilotBridgeAuthorization(current.host, current.port, provider.apiKey);
168
- if (compatible.ok) {
169
- (0, runtime_state_repo_1.writeCopilotBridgeState)({
170
- ...current,
171
- lastHealthcheckAt: new Date().toISOString(),
172
- workerBuildId,
173
- }, runtimeDir);
174
- return {
175
- baseUrl: expectedBaseUrl,
176
- host: current.host,
177
- port: current.port,
178
- reused: true,
179
- portChanged: false,
180
- replaced: false,
181
- };
182
- }
183
- }
184
- stopCopilotBridge(runtimeDir);
185
- replaced = true;
186
- }
187
- else {
188
- stopCopilotBridge(runtimeDir);
189
- replaced = true;
190
- }
188
+ const logPath = current?.logPath ?? (0, runtime_state_repo_1.getCopilotBridgeLogPath)(runtimeDir);
189
+ const reuseDecision = await evaluateBridgeReuse({
190
+ current,
191
+ providerName,
192
+ expectedBaseUrl,
193
+ expectedApiKey: provider.apiKey,
194
+ workerBuildId,
195
+ runtimeDir,
196
+ logPath,
197
+ });
198
+ if (reuseDecision.reuse) {
199
+ (0, runtime_state_repo_1.writeCopilotBridgeState)({
200
+ ...current,
201
+ lastHealthcheckAt: new Date().toISOString(),
202
+ lastProbeAt: reuseDecision.probeAt,
203
+ workerBuildId,
204
+ logPath: reuseDecision.logPath,
205
+ }, runtimeDir);
206
+ appendBridgeLifecycleLog(logPath, `startup success reused provider=${providerName} host=${current.host} port=${String(current.port)}`);
207
+ return {
208
+ baseUrl: expectedBaseUrl,
209
+ host: current.host,
210
+ port: current.port,
211
+ reused: true,
212
+ portChanged: false,
213
+ replaced: false,
214
+ logPath: reuseDecision.logPath,
215
+ };
191
216
  }
192
- if (current && current.provider !== providerName) {
217
+ if (reuseDecision.replacedExisting) {
193
218
  stopCopilotBridge(runtimeDir);
194
219
  replaced = true;
220
+ appendBridgeLifecycleLog(logPath, `replacement reason=${reuseDecision.reason}`);
195
221
  }
196
222
  const selectedPort = await selectBridgePort(runtime.bridgeHost, runtime.bridgePort);
197
223
  const selectedBaseUrl = `http://${runtime.bridgeHost}:${selectedPort}${runtime.bridgePath}`;
198
224
  const workerPath = path.join(__dirname, "copilot-bridge-worker.js");
225
+ ensureBridgeLogFile(logPath);
226
+ appendBridgeLifecycleLog(logPath, `worker start provider=${providerName} host=${runtime.bridgeHost} port=${String(selectedPort)} replaced=${String(replaced)}`);
199
227
  let child;
200
228
  try {
201
229
  child = spawnImplementation(process.execPath, [workerPath], {
202
230
  detached: true,
203
- stdio: "ignore",
231
+ stdio: ["ignore", openBridgeLogFd(logPath), openBridgeLogFd(logPath)],
204
232
  env: {
205
233
  ...process.env,
206
234
  CODEX_SWITCH_BRIDGE_PROVIDER: providerName,
@@ -210,6 +238,7 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
210
238
  CODEX_SWITCH_BRIDGE_BASE_URL: selectedBaseUrl,
211
239
  CODEX_SWITCH_RUNTIME_DIR: runtimeDir ?? "",
212
240
  CODEX_SWITCH_RUNTIMES_DIR: runtimesDir ?? "",
241
+ CODEX_SWITCH_BRIDGE_LOG_PATH: logPath,
213
242
  },
214
243
  });
215
244
  }
@@ -218,6 +247,12 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
218
247
  provider: providerName,
219
248
  host: runtime.bridgeHost,
220
249
  port: selectedPort,
250
+ logPath,
251
+ probeStage: "startup",
252
+ probeCause: "startup-failed",
253
+ retryable: false,
254
+ replacedExisting: replaced,
255
+ providerName,
221
256
  cause: error instanceof Error ? error.message : String(error),
222
257
  });
223
258
  }
@@ -228,17 +263,29 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
228
263
  if (!healthy.ok) {
229
264
  (0, runtime_state_repo_1.clearCopilotBridgeState)(runtimeDir);
230
265
  if (healthy.reason === "start-failed") {
266
+ appendBridgeLifecycleLog(logPath, `startup failure provider=${providerName} cause=${healthy.cause}`);
231
267
  throw (0, errors_1.cliError)("BRIDGE_START_FAILED", "Copilot bridge worker exited before becoming healthy.", {
232
268
  provider: providerName,
233
269
  host: runtime.bridgeHost,
234
270
  port: selectedPort,
271
+ logPath,
272
+ probeStage: "startup",
273
+ probeCause: "startup-failed",
274
+ retryable: false,
275
+ replacedExisting: replaced,
235
276
  cause: healthy.cause,
236
277
  });
237
278
  }
279
+ appendBridgeLifecycleLog(logPath, `startup timeout provider=${providerName} host=${runtime.bridgeHost} port=${String(selectedPort)}`);
238
280
  throw (0, errors_1.cliError)("BRIDGE_HEALTHCHECK_FAILED", "Copilot bridge did not become healthy after startup.", {
239
281
  provider: providerName,
240
282
  host: runtime.bridgeHost,
241
283
  port: selectedPort,
284
+ logPath,
285
+ probeStage: "startup",
286
+ probeCause: "startup-timeout",
287
+ retryable: true,
288
+ replacedExisting: replaced,
242
289
  cause: healthy.cause,
243
290
  });
244
291
  }
@@ -251,8 +298,12 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
251
298
  startedAt,
252
299
  lastHealthcheckAt: new Date().toISOString(),
253
300
  workerBuildId,
301
+ logPath,
302
+ lastProbeAt: new Date().toISOString(),
303
+ lastRestartReason: reuseDecision.reuse ? undefined : reuseDecision.reason,
254
304
  };
255
305
  (0, runtime_state_repo_1.writeCopilotBridgeState)(state, runtimeDir);
306
+ appendBridgeLifecycleLog(logPath, `startup success provider=${providerName} host=${runtime.bridgeHost} port=${String(selectedPort)}`);
256
307
  return {
257
308
  baseUrl: selectedBaseUrl,
258
309
  host: runtime.bridgeHost,
@@ -260,6 +311,8 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
260
311
  reused: false,
261
312
  portChanged: selectedPort !== runtime.bridgePort,
262
313
  replaced,
314
+ logPath,
315
+ restartReason: replaced ? reuseDecision.reason : undefined,
263
316
  };
264
317
  }
265
318
  /**
@@ -350,9 +403,15 @@ function createCopilotBridgeRequestHandler(context) {
350
403
  writeResponsesTextDelta(response, messageId, doneText);
351
404
  }
352
405
  },
406
+ onRuntimeEvent: (event) => {
407
+ writeResponsesRuntimeEvent(response, responseId, event);
408
+ },
353
409
  });
354
410
  clearInterval(heartbeat);
355
411
  const outputText = text || getChatCompletionText(payload);
412
+ if (text.length === 0 && outputText.length > 0) {
413
+ writeResponsesTextDelta(response, messageId, outputText);
414
+ }
356
415
  writeResponsesStreamDone(response, responseId, normalized.model, messageId, outputText);
357
416
  response.end();
358
417
  return;
@@ -734,6 +793,112 @@ function writeResponsesStreamDone(response, responseId, model, messageId, output
734
793
  },
735
794
  });
736
795
  }
796
+ function writeResponsesRuntimeEvent(response, responseId, event) {
797
+ if (event.type === "assistant.message_delta") {
798
+ return;
799
+ }
800
+ if (event.type === "assistant.reasoning_delta") {
801
+ writeResponsesReasoningDelta(response, responseId, formatRuntimeEventText(event));
802
+ return;
803
+ }
804
+ if (event.type === "session.unknown") {
805
+ process.stderr.write(`[${new Date().toISOString()}] bridge runtime event ignored type=${event.sdkType} summary=${truncateBridgeText(event.summary, 240)}\n`);
806
+ return;
807
+ }
808
+ const text = formatRuntimeEventText(event);
809
+ if (text.length === 0) {
810
+ return;
811
+ }
812
+ writeResponsesCommentaryItem(response, responseId, text);
813
+ process.stderr.write(`[${new Date().toISOString()}] bridge runtime event type=${event.type} summary=${truncateBridgeText(text, 240)}\n`);
814
+ }
815
+ function writeResponsesReasoningDelta(response, responseId, text) {
816
+ const reasoningId = `${responseId}_rs_0`;
817
+ writeSseEvent(response, "response.reasoning_summary_part.added", {
818
+ type: "response.reasoning_summary_part.added",
819
+ item_id: reasoningId,
820
+ output_index: 0,
821
+ summary_index: 0,
822
+ part: {
823
+ type: "summary_text",
824
+ text: "",
825
+ },
826
+ });
827
+ writeSseEvent(response, "response.reasoning_summary_text.delta", {
828
+ type: "response.reasoning_summary_text.delta",
829
+ item_id: reasoningId,
830
+ output_index: 0,
831
+ summary_index: 0,
832
+ delta: text,
833
+ });
834
+ }
835
+ function writeResponsesCommentaryItem(response, responseId, text) {
836
+ const itemId = `${responseId}_commentary_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
837
+ writeSseEvent(response, "response.output_item.done", {
838
+ type: "response.output_item.done",
839
+ output_index: 0,
840
+ item: {
841
+ id: itemId,
842
+ type: "message",
843
+ status: "completed",
844
+ role: "assistant",
845
+ phase: "commentary",
846
+ content: [
847
+ {
848
+ type: "output_text",
849
+ text,
850
+ annotations: [],
851
+ },
852
+ ],
853
+ },
854
+ });
855
+ }
856
+ function formatRuntimeEventText(event) {
857
+ switch (event.type) {
858
+ case "assistant.intent":
859
+ return truncateBridgeText(event.text, 600);
860
+ case "assistant.reasoning_delta":
861
+ return truncateBridgeText(event.text, 600);
862
+ case "tool.execution_start":
863
+ return truncateBridgeText(`Copilot started ${formatToolName(event.name)}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
864
+ case "tool.execution_progress":
865
+ return truncateBridgeText(`Copilot progress ${formatToolName(event.name)}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
866
+ case "tool.execution_partial_result":
867
+ return truncateBridgeText(`Copilot partial result ${formatToolName(event.name)}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
868
+ case "tool.execution_complete":
869
+ return truncateBridgeText(`Copilot ${event.success === false ? "failed" : "completed"} ${formatToolName(event.name)}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
870
+ case "permission.requested":
871
+ return truncateBridgeText(`Copilot requested permission${event.kind ? `: ${event.kind}` : ""}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
872
+ case "permission.completed":
873
+ return truncateBridgeText(`Copilot permission ${event.approved === false ? "denied" : "approved"}${event.kind ? `: ${event.kind}` : ""}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
874
+ case "user_input.requested":
875
+ return truncateBridgeText(`Copilot requested user input${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
876
+ case "exit_plan_mode.requested":
877
+ return truncateBridgeText(`Copilot requested to exit plan mode${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
878
+ case "session.error":
879
+ return truncateBridgeText(`Copilot session error${formatSummarySuffix(event.summary)}`, 600);
880
+ case "session.idle":
881
+ return truncateBridgeText(event.summary, 600);
882
+ case "assistant.message_delta":
883
+ case "session.unknown":
884
+ return "";
885
+ }
886
+ }
887
+ function formatToolName(name) {
888
+ return name ? `tool ${name}` : "tool";
889
+ }
890
+ function formatRequestId(requestId) {
891
+ return requestId ? ` (${requestId})` : "";
892
+ }
893
+ function formatSummarySuffix(summary) {
894
+ return summary.length > 0 ? `: ${summary}` : "";
895
+ }
896
+ function truncateBridgeText(value, maxLength) {
897
+ if (value.length <= maxLength) {
898
+ return value;
899
+ }
900
+ return `${value.slice(0, maxLength)}... [truncated]`;
901
+ }
737
902
  function startSseHeartbeat(response) {
738
903
  return setInterval(() => {
739
904
  response.write(": keep-alive\n\n");
@@ -809,9 +974,9 @@ function startCopilotBridgeServer(args) {
809
974
  */
810
975
  async function waitForCopilotBridgeHealth(host, port, attempts = 10, delayMs = 150) {
811
976
  for (let index = 0; index < attempts; index += 1) {
812
- const result = await healthcheckCopilotBridge(host, port);
977
+ const result = await probeBridgeEndpoint({ host, port, stage: "health" });
813
978
  if (result.ok) {
814
- return result;
979
+ return { ok: true };
815
980
  }
816
981
  await new Promise((resolve) => setTimeout(resolve, delayMs));
817
982
  }
@@ -896,9 +1061,9 @@ async function waitForCopilotBridgeStartup(child, host, port, attempts, delayMs)
896
1061
  cause: startupFailure,
897
1062
  };
898
1063
  }
899
- const result = await healthcheckCopilotBridge(host, port);
1064
+ const result = await probeBridgeEndpoint({ host, port, stage: "health" });
900
1065
  if (result.ok) {
901
- return result;
1066
+ return { ok: true };
902
1067
  }
903
1068
  await new Promise((resolve) => setTimeout(resolve, delayMs));
904
1069
  }
@@ -920,74 +1085,222 @@ async function waitForCopilotBridgeStartup(child, host, port, attempts, delayMs)
920
1085
  child.off("exit", onExit);
921
1086
  }
922
1087
  }
923
- async function healthcheckCopilotBridge(host, port) {
924
- return new Promise((resolve) => {
925
- const request = http.request({
926
- host,
927
- port,
928
- method: "GET",
929
- path: "/healthz",
930
- timeout: 1000,
931
- }, (response) => {
932
- response.resume();
933
- if (response.statusCode === 200) {
934
- resolve({ ok: true });
935
- return;
936
- }
937
- resolve({
1088
+ async function evaluateBridgeReuse(args) {
1089
+ const probeAt = new Date().toISOString();
1090
+ if (!args.current) {
1091
+ return {
1092
+ reuse: false,
1093
+ reason: "no persisted bridge state",
1094
+ replacedExisting: false,
1095
+ probeAt,
1096
+ logPath: args.logPath,
1097
+ };
1098
+ }
1099
+ if (args.current.provider !== args.providerName) {
1100
+ return {
1101
+ reuse: false,
1102
+ reason: `provider mismatch: ${args.current.provider} -> ${args.providerName}`,
1103
+ replacedExisting: true,
1104
+ probeAt,
1105
+ logPath: args.logPath,
1106
+ probe: {
938
1107
  ok: false,
939
- cause: `Health endpoint returned status ${String(response.statusCode ?? 0)}.`,
940
- });
941
- });
942
- request.on("error", (error) => {
943
- resolve({
1108
+ stage: "health",
1109
+ attempts: 0,
1110
+ cause: "provider-mismatch",
1111
+ retryable: false,
1112
+ message: "Persisted bridge provider does not match the requested provider.",
1113
+ },
1114
+ };
1115
+ }
1116
+ if (args.current.baseUrl !== args.expectedBaseUrl) {
1117
+ return {
1118
+ reuse: false,
1119
+ reason: `base URL mismatch: ${args.current.baseUrl} -> ${args.expectedBaseUrl}`,
1120
+ replacedExisting: true,
1121
+ probeAt,
1122
+ logPath: args.logPath,
1123
+ probe: {
944
1124
  ok: false,
945
- cause: error.message,
946
- });
947
- });
948
- request.on("timeout", () => {
949
- request.destroy(new Error("Health endpoint timed out."));
950
- });
951
- request.end();
1125
+ stage: "health",
1126
+ attempts: 0,
1127
+ cause: "base-url-mismatch",
1128
+ retryable: false,
1129
+ message: "Persisted bridge base URL does not match the requested provider runtime base URL.",
1130
+ },
1131
+ };
1132
+ }
1133
+ if (args.current.workerBuildId !== args.workerBuildId) {
1134
+ return {
1135
+ reuse: false,
1136
+ reason: "worker build changed",
1137
+ replacedExisting: true,
1138
+ probeAt,
1139
+ logPath: args.logPath,
1140
+ probe: {
1141
+ ok: false,
1142
+ stage: "health",
1143
+ attempts: 0,
1144
+ cause: "worker-build-stale",
1145
+ retryable: false,
1146
+ message: "Persisted bridge worker build is stale.",
1147
+ },
1148
+ };
1149
+ }
1150
+ const health = await probeBridgeEndpoint({
1151
+ host: args.current.host,
1152
+ port: args.current.port,
1153
+ stage: "health",
1154
+ });
1155
+ appendBridgeLifecycleLog(args.logPath, `probe stage=health attempts=${String(health.attempts)} ok=${String(health.ok)}${health.ok ? "" : ` retryable=${String(health.retryable)} cause=${health.cause}`}`);
1156
+ if (!health.ok) {
1157
+ return {
1158
+ reuse: false,
1159
+ reason: `health probe failed: ${health.message}`,
1160
+ replacedExisting: true,
1161
+ probeAt,
1162
+ logPath: args.logPath,
1163
+ probe: health,
1164
+ };
1165
+ }
1166
+ const auth = await probeBridgeEndpoint({
1167
+ host: args.current.host,
1168
+ port: args.current.port,
1169
+ stage: "auth",
1170
+ apiKey: args.expectedApiKey,
952
1171
  });
1172
+ appendBridgeLifecycleLog(args.logPath, `probe stage=auth attempts=${String(auth.attempts)} ok=${String(auth.ok)}${auth.ok ? "" : ` retryable=${String(auth.retryable)} cause=${auth.cause}`}`);
1173
+ if (!auth.ok) {
1174
+ return {
1175
+ reuse: false,
1176
+ reason: `auth probe failed: ${auth.message}`,
1177
+ replacedExisting: true,
1178
+ probeAt,
1179
+ logPath: args.logPath,
1180
+ probe: auth,
1181
+ };
1182
+ }
1183
+ return {
1184
+ reuse: true,
1185
+ health,
1186
+ auth,
1187
+ probeAt,
1188
+ logPath: args.logPath,
1189
+ };
953
1190
  }
954
- /**
955
- * Checks whether a healthy bridge still accepts the provider's current bearer secret.
956
- */
957
- async function verifyCopilotBridgeAuthorization(host, port, apiKey) {
1191
+ async function probeBridgeEndpoint(args) {
1192
+ for (let attempt = 1; attempt <= BRIDGE_REUSE_ATTEMPTS; attempt += 1) {
1193
+ const result = await requestBridgeProbe(args);
1194
+ if (result.ok) {
1195
+ return {
1196
+ ok: true,
1197
+ stage: args.stage,
1198
+ attempts: attempt,
1199
+ };
1200
+ }
1201
+ if (!result.retryable || attempt === BRIDGE_REUSE_ATTEMPTS) {
1202
+ return {
1203
+ ...result,
1204
+ stage: args.stage,
1205
+ attempts: attempt,
1206
+ };
1207
+ }
1208
+ await new Promise((resolve) => setTimeout(resolve, BRIDGE_REUSE_DELAY_MS));
1209
+ }
1210
+ return {
1211
+ ok: false,
1212
+ stage: args.stage,
1213
+ attempts: BRIDGE_REUSE_ATTEMPTS,
1214
+ cause: "transport-timeout",
1215
+ retryable: true,
1216
+ message: "Bridge probe timed out.",
1217
+ };
1218
+ }
1219
+ async function requestBridgeProbe(args) {
958
1220
  return new Promise((resolve) => {
959
1221
  const request = http.request({
960
- host,
961
- port,
1222
+ host: args.host,
1223
+ port: args.port,
962
1224
  method: "GET",
963
- path: "/v1/models",
964
- timeout: 1000,
965
- headers: {
966
- authorization: `Bearer ${apiKey}`,
967
- },
1225
+ path: args.stage === "health" ? "/healthz" : "/v1/models",
1226
+ timeout: BRIDGE_REUSE_TIMEOUT_MS,
1227
+ headers: args.stage === "auth"
1228
+ ? {
1229
+ authorization: `Bearer ${args.apiKey ?? ""}`,
1230
+ }
1231
+ : undefined,
968
1232
  }, (response) => {
969
1233
  response.resume();
970
1234
  if (response.statusCode === 200) {
971
1235
  resolve({ ok: true });
972
1236
  return;
973
1237
  }
1238
+ if (args.stage === "auth" && (response.statusCode === 401 || response.statusCode === 403)) {
1239
+ resolve({
1240
+ ok: false,
1241
+ cause: "auth-rejected",
1242
+ retryable: false,
1243
+ statusCode: response.statusCode,
1244
+ message: `Authorization probe returned status ${String(response.statusCode)}.`,
1245
+ });
1246
+ return;
1247
+ }
974
1248
  resolve({
975
1249
  ok: false,
976
- cause: `Authorization probe returned status ${String(response.statusCode ?? 0)}.`,
1250
+ cause: args.stage === "health" ? "health-non-200" : "transport-error",
1251
+ retryable: false,
1252
+ statusCode: response.statusCode,
1253
+ message: args.stage === "health"
1254
+ ? `Health endpoint returned status ${String(response.statusCode ?? 0)}.`
1255
+ : `Authorization probe returned status ${String(response.statusCode ?? 0)}.`,
977
1256
  });
978
1257
  });
979
1258
  request.on("error", (error) => {
1259
+ const classified = classifyProbeTransportError(error);
980
1260
  resolve({
981
1261
  ok: false,
982
- cause: error.message,
1262
+ cause: classified.cause,
1263
+ retryable: classified.retryable,
1264
+ message: error.message,
983
1265
  });
984
1266
  });
985
1267
  request.on("timeout", () => {
986
- request.destroy(new Error("Authorization probe timed out."));
1268
+ request.destroy(new Error(args.stage === "health" ? "Health endpoint timed out." : "Authorization probe timed out."));
987
1269
  });
988
1270
  request.end();
989
1271
  });
990
1272
  }
1273
+ function classifyProbeTransportError(error) {
1274
+ const message = error.message.toLowerCase();
1275
+ if (message.includes("timed out") ||
1276
+ message.includes("econnrefused") ||
1277
+ message.includes("econnreset") ||
1278
+ message.includes("socket hang up") ||
1279
+ message.includes("epipe")) {
1280
+ return {
1281
+ cause: message.includes("timed out") ? "transport-timeout" : "transport-error",
1282
+ retryable: true,
1283
+ };
1284
+ }
1285
+ return {
1286
+ cause: "transport-error",
1287
+ retryable: false,
1288
+ };
1289
+ }
1290
+ function ensureBridgeLogFile(logPath) {
1291
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
1292
+ if (!fs.existsSync(logPath)) {
1293
+ fs.writeFileSync(logPath, "", "utf8");
1294
+ }
1295
+ }
1296
+ function openBridgeLogFd(logPath) {
1297
+ ensureBridgeLogFile(logPath);
1298
+ return fs.openSync(logPath, "a");
1299
+ }
1300
+ function appendBridgeLifecycleLog(logPath, message) {
1301
+ ensureBridgeLogFile(logPath);
1302
+ fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${message}\n`, "utf8");
1303
+ }
991
1304
  async function readJsonBody(request) {
992
1305
  const chunks = [];
993
1306
  for await (const chunk of request) {
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getCopilotBridgeStatePath = getCopilotBridgeStatePath;
37
+ exports.getCopilotBridgeLogPath = getCopilotBridgeLogPath;
37
38
  exports.readCopilotBridgeState = readCopilotBridgeState;
38
39
  exports.inspectCopilotBridgeState = inspectCopilotBridgeState;
39
40
  exports.writeCopilotBridgeState = writeCopilotBridgeState;
@@ -54,6 +55,13 @@ function getCopilotBridgeStatePath(runtimeDir) {
54
55
  const baseRuntimeDir = runtimeDir ? path.resolve(runtimeDir) : path.join((0, codex_paths_1.resolveCodexSwitchHome)(), "runtime");
55
56
  return path.join(baseRuntimeDir, "copilot-bridge-state.json");
56
57
  }
58
+ /**
59
+ * Returns the persisted bridge runtime log path colocated with the bridge state manifest.
60
+ */
61
+ function getCopilotBridgeLogPath(runtimeDir) {
62
+ const statePath = getCopilotBridgeStatePath(runtimeDir);
63
+ return path.join(path.dirname(statePath), "copilot-bridge.log");
64
+ }
57
65
  /**
58
66
  * Reads the Copilot bridge state manifest when present.
59
67
  */