@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.
- package/README.AI.md +6 -4
- package/README.CN.md +9 -6
- package/README.md +9 -6
- package/dist/app/bridge.js +11 -0
- package/dist/app/get-status.js +9 -1
- package/dist/app/run-doctor.js +2 -0
- package/dist/app/switch-provider.js +10 -3
- package/dist/cli/output.js +53 -1
- package/dist/interaction/interactive.js +9 -2
- package/dist/runtime/copilot-adapter.js +120 -0
- package/dist/runtime/copilot-bridge-worker.js +19 -0
- package/dist/runtime/copilot-bridge.js +393 -80
- package/dist/storage/runtime-state-repo.js +8 -0
- package/docs/Design/codex-switch-v0.1.4-design.md +18 -0
- package/docs/Design/codex-switch-v0.1.5-design.md +17 -0
- package/docs/PRD/codex-switch-prd-v0.1.4.md +37 -0
- package/docs/PRD/codex-switch-prd-v0.1.5.md +42 -0
- package/docs/Tests/testing.md +4 -1
- package/docs/cli-usage.md +11 -5
- package/docs/codex-switch-product-overview.md +24 -19
- package/docs/codex-switch-technical-architecture.md +2 -0
- package/package.json +2 -2
|
@@ -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:
|
|
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
|
|
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.
|
|
125
|
-
details:
|
|
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:
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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 (
|
|
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
|
|
977
|
+
const result = await probeBridgeEndpoint({ host, port, stage: "health" });
|
|
813
978
|
if (result.ok) {
|
|
814
|
-
return
|
|
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
|
|
1064
|
+
const result = await probeBridgeEndpoint({ host, port, stage: "health" });
|
|
900
1065
|
if (result.ok) {
|
|
901
|
-
return
|
|
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
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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:
|
|
965
|
-
headers:
|
|
966
|
-
|
|
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:
|
|
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:
|
|
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
|
*/
|