parallel-codex-tui 0.1.3 → 0.1.4
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/.parallel-codex/config.example.toml +90 -3
- package/README.md +240 -21
- package/dist/bootstrap.js +37 -17
- package/dist/cli-args.js +34 -3
- package/dist/cli-help.js +20 -0
- package/dist/cli-startup-recovery.js +70 -0
- package/dist/cli-workspace-picker.js +330 -0
- package/dist/cli-workspace-transition.js +33 -0
- package/dist/cli-workspace.js +7 -71
- package/dist/cli.js +221 -23
- package/dist/core/collaboration-timeline.js +261 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +154 -24
- package/dist/core/file-store.js +119 -6
- package/dist/core/lease-finalization.js +22 -0
- package/dist/core/paths.js +7 -0
- package/dist/core/process-identity.js +48 -0
- package/dist/core/process-mutation-turn.js +128 -0
- package/dist/core/process-ownership.js +276 -0
- package/dist/core/process-tree.js +90 -0
- package/dist/core/router-audit.js +155 -0
- package/dist/core/router-redaction.js +31 -0
- package/dist/core/router.js +462 -35
- package/dist/core/session-index.js +188 -37
- package/dist/core/session-manager.js +1086 -40
- package/dist/core/task-state-machine.js +17 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +19 -11
- package/dist/doctor.js +343 -23
- package/dist/domain/schemas.js +127 -6
- package/dist/orchestrator/collaboration-channel.js +255 -4
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +1749 -202
- package/dist/orchestrator/prompts.js +126 -2
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +911 -0
- package/dist/tui/App.js +2830 -153
- package/dist/tui/AppShell.js +188 -23
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +227 -0
- package/dist/tui/InputBar.js +514 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/TaskSessionsView.js +207 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1403 -161
- package/dist/tui/WorkerOverviewView.js +250 -0
- package/dist/tui/chat-history.js +25 -0
- package/dist/tui/chat-input.js +67 -19
- package/dist/tui/chat-paste.js +76 -0
- package/dist/tui/display-width.js +41 -3
- package/dist/tui/incremental-text-file.js +101 -0
- package/dist/tui/keyboard.js +46 -0
- package/dist/tui/markdown-text.js +14 -0
- package/dist/tui/raw-input-decoder.js +3 -0
- package/dist/tui/scrolling.js +2 -1
- package/dist/tui/status-line.js +318 -11
- package/dist/tui/task-memory.js +13 -1
- package/dist/tui/task-result.js +105 -0
- package/dist/tui/terminal-screen.js +13 -1
- package/dist/tui/theme-contrast.js +144 -0
- package/dist/tui/theme-preview.js +109 -0
- package/dist/tui/theme.js +158 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +212 -0
- package/dist/workers/live-probe.js +176 -0
- package/dist/workers/mock-adapter.js +39 -6
- package/dist/workers/native-attach.js +78 -3
- package/dist/workers/process-adapter.js +570 -77
- package/dist/workers/registry.js +4 -2
- package/package.json +5 -2
package/dist/core/router.js
CHANGED
|
@@ -1,78 +1,484 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { StringDecoder } from "node:string_decoder";
|
|
2
4
|
import { RouteDecisionSchema } from "../domain/schemas.js";
|
|
3
|
-
|
|
5
|
+
import { ProcessTreeCleanupError, terminateProcessTree } from "./process-tree.js";
|
|
6
|
+
import { classifyRouterFailure } from "./router-audit.js";
|
|
7
|
+
import { sanitizeRouterText } from "./router-redaction.js";
|
|
8
|
+
export async function routeRequestWithCodex(request, config, runner = runCodexRouterProcess, cwd = config.projectRoot, signal, onProgress) {
|
|
9
|
+
const startedAt = Date.now();
|
|
10
|
+
const telemetryStartedAt = performance.now();
|
|
11
|
+
let dispatchMs;
|
|
12
|
+
if (signal?.aborted) {
|
|
13
|
+
throw cancellationError();
|
|
14
|
+
}
|
|
4
15
|
if (config.router.defaultMode === "simple") {
|
|
5
|
-
return simpleRoute("Forced simple mode from config.", config);
|
|
16
|
+
return annotateRoute(simpleRoute("Forced simple mode from config.", config), "forced", startedAt);
|
|
6
17
|
}
|
|
7
18
|
if (config.router.defaultMode === "complex") {
|
|
8
|
-
return complexRoute("Forced complex mode from config.", config);
|
|
19
|
+
return annotateRoute(complexRoute("Forced complex mode from config.", config), "forced", startedAt);
|
|
9
20
|
}
|
|
21
|
+
const proxyContext = routerProxyContext(config.router.codex.env);
|
|
22
|
+
const commandLabel = routerCommandLabel(config.router.codex.command);
|
|
23
|
+
const routerName = routerDisplayName(commandLabel);
|
|
10
24
|
try {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
25
|
+
const prompt = buildCodexRouterPrompt(request, config);
|
|
26
|
+
dispatchMs = Math.max(0, performance.now() - telemetryStartedAt);
|
|
27
|
+
emitRouterProgress(onProgress, "dispatching");
|
|
28
|
+
const result = normalizeRouterRunnerResult(await runner(prompt, config, cwd, signal, onProgress));
|
|
29
|
+
let route;
|
|
30
|
+
const parseStartedAt = performance.now();
|
|
31
|
+
emitRouterProgress(onProgress, "parsing");
|
|
32
|
+
try {
|
|
33
|
+
route = RouteDecisionSchema.parse(parseCodexRoute(result.output, config));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw routerExecutionError(error, mergeRouterTelemetry(result.telemetry, {
|
|
37
|
+
router_dispatch_ms: dispatchMs,
|
|
38
|
+
router_parse_ms: Math.max(0, performance.now() - parseStartedAt)
|
|
39
|
+
}), "response");
|
|
40
|
+
}
|
|
41
|
+
return annotateRoute(route, "codex", startedAt, mergeRouterTelemetry(result.telemetry, {
|
|
42
|
+
router_dispatch_ms: dispatchMs,
|
|
43
|
+
router_parse_ms: Math.max(0, performance.now() - parseStartedAt)
|
|
44
|
+
}), proxyContext, commandLabel);
|
|
14
45
|
}
|
|
15
46
|
catch (error) {
|
|
16
|
-
|
|
47
|
+
if (error instanceof ProcessTreeCleanupError) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
if (signal?.aborted || isAbortError(error)) {
|
|
51
|
+
throw cancellationError();
|
|
52
|
+
}
|
|
53
|
+
const context = routerExecutionErrorContext(error);
|
|
54
|
+
const failureSummary = summarizeRouterError(error);
|
|
55
|
+
const fallback = annotateRoute(fallbackRoute(config, routerName), "fallback", startedAt, mergeRouterTelemetry(context.telemetry, {
|
|
56
|
+
router_dispatch_ms: dispatchMs
|
|
57
|
+
}), proxyContext, commandLabel);
|
|
17
58
|
return {
|
|
18
59
|
...fallback,
|
|
19
|
-
|
|
60
|
+
...(context.stage ? { router_failure_stage: context.stage } : {}),
|
|
61
|
+
router_failure_kind: routerFailureKind(failureSummary, context.stage, context.timeoutKind),
|
|
62
|
+
...(context.timeoutKind ? { router_timeout_kind: context.timeoutKind } : {}),
|
|
63
|
+
reason: `${routerName} failed: ${failureSummary}. ${fallback.reason}`
|
|
20
64
|
};
|
|
21
65
|
}
|
|
22
66
|
}
|
|
23
|
-
|
|
24
|
-
|
|
67
|
+
function routerFailureKind(summary, stage, timeoutKind) {
|
|
68
|
+
if (timeoutKind) {
|
|
69
|
+
return "timeout";
|
|
70
|
+
}
|
|
71
|
+
const classified = classifyRouterFailure(summary);
|
|
72
|
+
if (classified) {
|
|
73
|
+
return classified;
|
|
74
|
+
}
|
|
75
|
+
if (stage === "spawn") {
|
|
76
|
+
return "unavailable";
|
|
77
|
+
}
|
|
78
|
+
if (stage === "input") {
|
|
79
|
+
return "input";
|
|
80
|
+
}
|
|
81
|
+
if (stage === "exit") {
|
|
82
|
+
return "exit";
|
|
83
|
+
}
|
|
84
|
+
if (stage === "response") {
|
|
85
|
+
return "invalid-output";
|
|
86
|
+
}
|
|
87
|
+
return "unknown";
|
|
88
|
+
}
|
|
89
|
+
function annotateRoute(route, source, startedAt, telemetry, proxyContext, routerCommand) {
|
|
90
|
+
return {
|
|
91
|
+
...route,
|
|
92
|
+
...normalizeRouterTelemetry(telemetry),
|
|
93
|
+
...routerProxyRouteFields(proxyContext),
|
|
94
|
+
...(routerCommand ? { router_command: routerCommand } : {}),
|
|
95
|
+
source,
|
|
96
|
+
duration_ms: Math.max(0, Date.now() - startedAt)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function routerCommandLabel(command) {
|
|
100
|
+
const executable = command.trim().split(/[\\/]/).filter(Boolean).at(-1) ?? "router";
|
|
101
|
+
const safe = sanitizeRouterText(executable)
|
|
102
|
+
.replace(/[\u0000-\u001f\u007f|·]+/g, "-")
|
|
103
|
+
.replace(/\s+/g, "-")
|
|
104
|
+
.replace(/-+/g, "-")
|
|
105
|
+
.replace(/^-|-$/g, "");
|
|
106
|
+
return Array.from(safe || "router").slice(0, 80).join("");
|
|
107
|
+
}
|
|
108
|
+
function routerDisplayName(commandLabel) {
|
|
109
|
+
return /^codex(?:\.exe)?$/i.test(commandLabel) ? "Codex router" : `Router ${commandLabel}`;
|
|
110
|
+
}
|
|
111
|
+
export async function runCodexRouterProcess(prompt, config, cwd = config.projectRoot, signal, onProgress) {
|
|
112
|
+
const { command, args, timeoutMs, firstOutputTimeoutMs, idleTimeoutMs, maxOutputBytes } = config.router.codex;
|
|
113
|
+
const routerName = routerDisplayName(routerCommandLabel(command));
|
|
114
|
+
if (signal?.aborted) {
|
|
115
|
+
throw cancellationError();
|
|
116
|
+
}
|
|
117
|
+
const configuredEnvironment = routerEnvironment(config.router.codex.env);
|
|
118
|
+
const env = {
|
|
119
|
+
...process.env,
|
|
120
|
+
...configuredEnvironment
|
|
121
|
+
};
|
|
122
|
+
const proxyConfigured = hasConfiguredProxy(env);
|
|
123
|
+
let progressPhase;
|
|
124
|
+
const reportProgress = (phase) => {
|
|
125
|
+
if (progressPhase === phase) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
progressPhase = phase;
|
|
129
|
+
emitRouterProgress(onProgress, phase);
|
|
130
|
+
};
|
|
131
|
+
reportProgress("starting");
|
|
25
132
|
return new Promise((resolve, reject) => {
|
|
133
|
+
const processStartedAt = Date.now();
|
|
134
|
+
const detached = process.platform !== "win32";
|
|
26
135
|
const child = spawn(command, args, {
|
|
27
136
|
cwd,
|
|
28
|
-
env
|
|
29
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
137
|
+
env,
|
|
138
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
139
|
+
detached
|
|
30
140
|
});
|
|
31
141
|
let stdout = "";
|
|
32
142
|
let stderr = "";
|
|
143
|
+
const stdoutDecoder = new StringDecoder("utf8");
|
|
144
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
145
|
+
let stdoutBytes = 0;
|
|
146
|
+
let stderrBytes = 0;
|
|
147
|
+
let spawnMs;
|
|
148
|
+
let firstOutputMs;
|
|
149
|
+
let firstStdoutMs;
|
|
150
|
+
let firstStderrMs;
|
|
33
151
|
let settled = false;
|
|
34
|
-
let
|
|
35
|
-
|
|
152
|
+
let terminating = false;
|
|
153
|
+
let totalTimeout;
|
|
154
|
+
let firstOutputTimeout;
|
|
155
|
+
let idleTimeout;
|
|
156
|
+
let abortListener;
|
|
157
|
+
const clearRunTimers = () => {
|
|
158
|
+
if (totalTimeout) {
|
|
159
|
+
clearTimeout(totalTimeout);
|
|
160
|
+
totalTimeout = undefined;
|
|
161
|
+
}
|
|
162
|
+
if (firstOutputTimeout) {
|
|
163
|
+
clearTimeout(firstOutputTimeout);
|
|
164
|
+
firstOutputTimeout = undefined;
|
|
165
|
+
}
|
|
166
|
+
if (idleTimeout) {
|
|
167
|
+
clearTimeout(idleTimeout);
|
|
168
|
+
idleTimeout = undefined;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const telemetry = () => ({
|
|
172
|
+
...(typeof spawnMs === "number" ? { router_spawn_ms: spawnMs } : {}),
|
|
173
|
+
...(typeof firstOutputMs === "number" ? { router_first_output_ms: firstOutputMs } : {}),
|
|
174
|
+
...(typeof firstStdoutMs === "number" ? { router_first_stdout_ms: firstStdoutMs } : {}),
|
|
175
|
+
...(typeof firstStderrMs === "number" ? { router_first_stderr_ms: firstStderrMs } : {}),
|
|
176
|
+
router_process_ms: Math.max(0, Date.now() - processStartedAt),
|
|
177
|
+
router_stdout_bytes: stdoutBytes,
|
|
178
|
+
router_stderr_bytes: stderrBytes
|
|
179
|
+
});
|
|
180
|
+
const finish = (error, output = stdout, stage, timeoutKind) => {
|
|
36
181
|
if (settled) {
|
|
37
182
|
return;
|
|
38
183
|
}
|
|
39
184
|
settled = true;
|
|
40
|
-
|
|
41
|
-
|
|
185
|
+
clearRunTimers();
|
|
186
|
+
if (abortListener) {
|
|
187
|
+
signal?.removeEventListener("abort", abortListener);
|
|
42
188
|
}
|
|
43
189
|
if (error) {
|
|
44
|
-
reject(error);
|
|
190
|
+
reject(stage ? routerExecutionError(error, telemetry(), stage, timeoutKind) : error);
|
|
45
191
|
}
|
|
46
192
|
else {
|
|
47
|
-
resolve(output);
|
|
193
|
+
resolve({ output, telemetry: telemetry() });
|
|
48
194
|
}
|
|
49
195
|
};
|
|
196
|
+
const terminateThenFinish = (error, output = stdout, stage, timeoutKind) => {
|
|
197
|
+
if (settled || terminating) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
terminating = true;
|
|
201
|
+
reportProgress("stopping");
|
|
202
|
+
clearRunTimers();
|
|
203
|
+
if (abortListener) {
|
|
204
|
+
signal?.removeEventListener("abort", abortListener);
|
|
205
|
+
abortListener = undefined;
|
|
206
|
+
}
|
|
207
|
+
void terminateProcessTree(child, {
|
|
208
|
+
processGroup: detached,
|
|
209
|
+
label: `${routerName} process`,
|
|
210
|
+
termGraceMs: 250,
|
|
211
|
+
killWaitMs: 500,
|
|
212
|
+
pollMs: 20
|
|
213
|
+
}).then(() => finish(error, output, stage, timeoutKind), (cleanupError) => finish(cleanupError instanceof ProcessTreeCleanupError
|
|
214
|
+
? cleanupError
|
|
215
|
+
: new ProcessTreeCleanupError(`${routerName} cleanup failed: ${errorMessage(cleanupError)}`)));
|
|
216
|
+
};
|
|
217
|
+
const timeoutRouter = (kind, limitMs, stage) => {
|
|
218
|
+
if (settled || terminating) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const detail = summarizeRouterProcessDetail(stderr);
|
|
222
|
+
const proxyContext = proxyConfigured && !/\bproxy\b|代理/i.test(detail)
|
|
223
|
+
? " with proxy configured"
|
|
224
|
+
: "";
|
|
225
|
+
const timeoutLabel = kind === "first-output" ? " first output" : kind === "idle" ? " idle" : "";
|
|
226
|
+
terminateThenFinish(new Error(`${routerName}${timeoutLabel} timed out after ${limitMs}ms${proxyContext}${detail ? `: ${detail}` : ""}`), stdout, stage, kind);
|
|
227
|
+
};
|
|
228
|
+
const stopIfOutputLimitExceeded = () => {
|
|
229
|
+
if (stdoutBytes + stderrBytes <= maxOutputBytes) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
terminateThenFinish(new Error(`${routerName} output exceeded ${maxOutputBytes} byte limit`), stdout, "response");
|
|
233
|
+
return true;
|
|
234
|
+
};
|
|
235
|
+
const resetIdleTimeout = () => {
|
|
236
|
+
if (idleTimeout) {
|
|
237
|
+
clearTimeout(idleTimeout);
|
|
238
|
+
idleTimeout = undefined;
|
|
239
|
+
}
|
|
240
|
+
if (idleTimeoutMs <= 0 || (timeoutMs > 0 && idleTimeoutMs >= timeoutMs)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
idleTimeout = setTimeout(() => timeoutRouter("idle", idleTimeoutMs, "streaming"), idleTimeoutMs);
|
|
244
|
+
};
|
|
245
|
+
const recordOutputActivity = () => {
|
|
246
|
+
if (firstOutputTimeout) {
|
|
247
|
+
clearTimeout(firstOutputTimeout);
|
|
248
|
+
firstOutputTimeout = undefined;
|
|
249
|
+
}
|
|
250
|
+
resetIdleTimeout();
|
|
251
|
+
};
|
|
252
|
+
abortListener = () => {
|
|
253
|
+
terminateThenFinish(cancellationError());
|
|
254
|
+
};
|
|
255
|
+
signal?.addEventListener("abort", abortListener, { once: true });
|
|
256
|
+
child.once("spawn", () => {
|
|
257
|
+
if (terminating) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
spawnMs = Math.max(0, Date.now() - processStartedAt);
|
|
261
|
+
reportProgress("waiting-output");
|
|
262
|
+
});
|
|
50
263
|
if (timeoutMs > 0) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
264
|
+
totalTimeout = setTimeout(() => {
|
|
265
|
+
const stage = stdoutBytes + stderrBytes === 0 ? "waiting-output" : "streaming";
|
|
266
|
+
timeoutRouter("total", timeoutMs, stage);
|
|
54
267
|
}, timeoutMs);
|
|
55
268
|
}
|
|
269
|
+
if (firstOutputTimeoutMs > 0 && (timeoutMs <= 0 || firstOutputTimeoutMs < timeoutMs)) {
|
|
270
|
+
firstOutputTimeout = setTimeout(() => timeoutRouter("first-output", firstOutputTimeoutMs, "waiting-output"), firstOutputTimeoutMs);
|
|
271
|
+
}
|
|
56
272
|
child.stdout.on("data", (chunk) => {
|
|
57
|
-
|
|
273
|
+
if (settled || terminating) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
stdoutBytes += chunk.byteLength;
|
|
277
|
+
firstOutputMs ??= Math.max(0, Date.now() - processStartedAt);
|
|
278
|
+
firstStdoutMs ??= Math.max(0, Date.now() - processStartedAt);
|
|
279
|
+
if (stopIfOutputLimitExceeded()) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
stdout += stdoutDecoder.write(chunk);
|
|
283
|
+
recordOutputActivity();
|
|
284
|
+
reportProgress("receiving-response");
|
|
58
285
|
});
|
|
59
286
|
child.stderr.on("data", (chunk) => {
|
|
60
|
-
|
|
287
|
+
if (settled || terminating) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
stderrBytes += chunk.byteLength;
|
|
291
|
+
firstOutputMs ??= Math.max(0, Date.now() - processStartedAt);
|
|
292
|
+
firstStderrMs ??= Math.max(0, Date.now() - processStartedAt);
|
|
293
|
+
if (stopIfOutputLimitExceeded()) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
stderr += stderrDecoder.write(chunk);
|
|
297
|
+
recordOutputActivity();
|
|
298
|
+
if (stdoutBytes === 0) {
|
|
299
|
+
reportProgress("receiving-stderr");
|
|
300
|
+
}
|
|
61
301
|
});
|
|
62
302
|
child.on("error", (error) => {
|
|
63
|
-
|
|
303
|
+
if (terminating) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
finish(error, stdout, "spawn");
|
|
64
307
|
});
|
|
65
308
|
child.on("close", (code, signal) => {
|
|
309
|
+
if (terminating) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
stdout += stdoutDecoder.end();
|
|
313
|
+
stderr += stderrDecoder.end();
|
|
66
314
|
if (code === 0) {
|
|
67
315
|
finish(null);
|
|
68
316
|
return;
|
|
69
317
|
}
|
|
70
318
|
const detail = (stderr || stdout).trim();
|
|
71
|
-
finish(new Error(
|
|
319
|
+
finish(new Error(`${routerName} exited with ${signal ? `signal ${signal}` : `code ${code ?? 1}`}${detail ? `: ${detail}` : ""}`), stdout, "exit");
|
|
72
320
|
});
|
|
73
|
-
child.stdin.
|
|
74
|
-
|
|
321
|
+
child.stdin.once("error", (error) => {
|
|
322
|
+
terminateThenFinish(new Error(`${routerName} input failed: ${error.message}`), stdout, "input");
|
|
323
|
+
});
|
|
324
|
+
if (signal?.aborted) {
|
|
325
|
+
abortListener();
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
child.stdin.end(prompt);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function errorMessage(error) {
|
|
333
|
+
return error instanceof Error ? error.message : String(error);
|
|
334
|
+
}
|
|
335
|
+
function normalizeRouterRunnerResult(result) {
|
|
336
|
+
return typeof result === "string"
|
|
337
|
+
? { output: result }
|
|
338
|
+
: { output: result.output, telemetry: normalizeRouterTelemetry(result.telemetry) };
|
|
339
|
+
}
|
|
340
|
+
function normalizeRouterTelemetry(telemetry) {
|
|
341
|
+
if (!telemetry) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
const normalized = {};
|
|
345
|
+
for (const key of [
|
|
346
|
+
"router_dispatch_ms",
|
|
347
|
+
"router_spawn_ms",
|
|
348
|
+
"router_first_output_ms",
|
|
349
|
+
"router_first_stdout_ms",
|
|
350
|
+
"router_first_stderr_ms",
|
|
351
|
+
"router_process_ms",
|
|
352
|
+
"router_parse_ms",
|
|
353
|
+
"router_stdout_bytes",
|
|
354
|
+
"router_stderr_bytes"
|
|
355
|
+
]) {
|
|
356
|
+
const value = telemetry[key];
|
|
357
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
normalized[key] = key.endsWith("_bytes") ? Math.trunc(value) : value;
|
|
361
|
+
}
|
|
362
|
+
return normalized;
|
|
363
|
+
}
|
|
364
|
+
function mergeRouterTelemetry(telemetry, additions) {
|
|
365
|
+
return normalizeRouterTelemetry({
|
|
366
|
+
...telemetry,
|
|
367
|
+
...Object.fromEntries(Object.entries(additions).filter(([, value]) => value !== undefined))
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
function routerExecutionError(error, telemetry, stage, timeoutKind) {
|
|
371
|
+
const source = error instanceof Error ? error : new Error(String(error));
|
|
372
|
+
const wrapped = new Error(source.message);
|
|
373
|
+
wrapped.name = source.name;
|
|
374
|
+
wrapped.routerTelemetry = normalizeRouterTelemetry(telemetry);
|
|
375
|
+
wrapped.routerFailureStage = stage;
|
|
376
|
+
wrapped.routerTimeoutKind = timeoutKind;
|
|
377
|
+
return wrapped;
|
|
378
|
+
}
|
|
379
|
+
function routerExecutionErrorContext(error) {
|
|
380
|
+
if (!(error instanceof Error)) {
|
|
381
|
+
return {};
|
|
382
|
+
}
|
|
383
|
+
const executionError = error;
|
|
384
|
+
return {
|
|
385
|
+
...(executionError.routerTelemetry ? { telemetry: executionError.routerTelemetry } : {}),
|
|
386
|
+
...(executionError.routerFailureStage ? { stage: executionError.routerFailureStage } : {}),
|
|
387
|
+
...(executionError.routerTimeoutKind ? { timeoutKind: executionError.routerTimeoutKind } : {})
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function routerEnvironment(configured, env = process.env) {
|
|
391
|
+
return Object.fromEntries(Object.entries(configured).map(([name, value]) => [
|
|
392
|
+
name,
|
|
393
|
+
value.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, variable) => env[variable] ?? "")
|
|
394
|
+
]));
|
|
395
|
+
}
|
|
396
|
+
export function routerProxyConfigured(configured, env = process.env) {
|
|
397
|
+
return routerProxyContext(configured, env).configured;
|
|
398
|
+
}
|
|
399
|
+
export function routerProxyContext(configured, env = process.env) {
|
|
400
|
+
const configuredEnvironment = routerEnvironment(configured, env);
|
|
401
|
+
const configuredProxy = preferredProxyEntry(configuredEnvironment);
|
|
402
|
+
if (configuredProxy) {
|
|
403
|
+
return {
|
|
404
|
+
configured: true,
|
|
405
|
+
source: "router-config",
|
|
406
|
+
variable: configuredProxy.name,
|
|
407
|
+
endpoint: safeProxyEndpoint(configuredProxy.value)
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const inheritedProxy = preferredProxyEntry({
|
|
411
|
+
...env,
|
|
412
|
+
...configuredEnvironment
|
|
75
413
|
});
|
|
414
|
+
if (!inheritedProxy) {
|
|
415
|
+
return { configured: false };
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
configured: true,
|
|
419
|
+
source: "environment",
|
|
420
|
+
variable: inheritedProxy.name,
|
|
421
|
+
endpoint: safeProxyEndpoint(inheritedProxy.value)
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function hasConfiguredProxy(env) {
|
|
425
|
+
return Object.entries(env).some(([name, value]) => (/^(?:HTTP|HTTPS|ALL)_PROXY$/i.test(name) && Boolean(value?.trim())));
|
|
426
|
+
}
|
|
427
|
+
function preferredProxyEntry(env) {
|
|
428
|
+
const candidates = Object.entries(env)
|
|
429
|
+
.filter((entry) => (/^(?:HTTP|HTTPS|ALL)_PROXY$/i.test(entry[0]) && Boolean(entry[1]?.trim())))
|
|
430
|
+
.sort((left, right) => {
|
|
431
|
+
const priority = proxyVariablePriority(left[0]) - proxyVariablePriority(right[0]);
|
|
432
|
+
return priority || left[0].localeCompare(right[0]);
|
|
433
|
+
});
|
|
434
|
+
const selected = candidates[0];
|
|
435
|
+
return selected ? { name: selected[0], value: selected[1].trim() } : null;
|
|
436
|
+
}
|
|
437
|
+
function proxyVariablePriority(name) {
|
|
438
|
+
const normalized = name.toUpperCase();
|
|
439
|
+
return normalized === "HTTPS_PROXY" ? 0 : normalized === "ALL_PROXY" ? 1 : 2;
|
|
440
|
+
}
|
|
441
|
+
function safeProxyEndpoint(value) {
|
|
442
|
+
try {
|
|
443
|
+
const normalized = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(value)
|
|
444
|
+
? value
|
|
445
|
+
: `http://${value}`;
|
|
446
|
+
const parsed = new URL(normalized);
|
|
447
|
+
return parsed.host || "custom";
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return "custom";
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function routerProxyRouteFields(proxyContext) {
|
|
454
|
+
if (!proxyContext) {
|
|
455
|
+
return {};
|
|
456
|
+
}
|
|
457
|
+
if (!proxyContext.configured) {
|
|
458
|
+
return { proxy_configured: false };
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
proxy_configured: true,
|
|
462
|
+
proxy_source: proxyContext.source,
|
|
463
|
+
proxy_variable: proxyContext.variable,
|
|
464
|
+
proxy_endpoint: proxyContext.endpoint
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function emitRouterProgress(listener, phase) {
|
|
468
|
+
try {
|
|
469
|
+
listener?.({ phase });
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
// UI progress listeners must never change Router execution semantics.
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function cancellationError() {
|
|
476
|
+
const error = new Error("Request cancelled.");
|
|
477
|
+
error.name = "AbortError";
|
|
478
|
+
return error;
|
|
479
|
+
}
|
|
480
|
+
function isAbortError(error) {
|
|
481
|
+
return error instanceof Error && error.name === "AbortError";
|
|
76
482
|
}
|
|
77
483
|
function buildCodexRouterPrompt(request, config) {
|
|
78
484
|
return [
|
|
@@ -94,12 +500,22 @@ function buildCodexRouterPrompt(request, config) {
|
|
|
94
500
|
].join("\n");
|
|
95
501
|
}
|
|
96
502
|
function parseCodexRoute(output, config) {
|
|
503
|
+
const routerName = routerDisplayName(routerCommandLabel(config.router.codex.command));
|
|
97
504
|
const jsonText = extractJsonObject(output);
|
|
98
505
|
const parsed = JSON.parse(jsonText);
|
|
506
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
507
|
+
throw new Error(`Invalid ${routerName} response object`);
|
|
508
|
+
}
|
|
509
|
+
const record = parsed;
|
|
510
|
+
const mode = typeof record.mode === "string" ? record.mode.trim().toLowerCase() : "";
|
|
511
|
+
if (mode !== "simple" && mode !== "complex") {
|
|
512
|
+
throw new Error(`Invalid ${routerName} mode`);
|
|
513
|
+
}
|
|
514
|
+
const reason = typeof record.reason === "string" ? record.reason.trim() : "";
|
|
99
515
|
return {
|
|
100
|
-
mode
|
|
101
|
-
reason:
|
|
102
|
-
suggested_roles:
|
|
516
|
+
mode,
|
|
517
|
+
reason: reason ? sanitizeRouterText(reason) : `${routerName} decision.`,
|
|
518
|
+
suggested_roles: mode === "complex" ? ["judge", "actor", "critic"] : [],
|
|
103
519
|
judge_engine: config.pairing.judge,
|
|
104
520
|
actor_engine: config.pairing.actor,
|
|
105
521
|
critic_engine: config.pairing.critic
|
|
@@ -116,14 +532,14 @@ function extractJsonObject(output) {
|
|
|
116
532
|
}
|
|
117
533
|
return match[0];
|
|
118
534
|
}
|
|
119
|
-
function fallbackRoute(config) {
|
|
535
|
+
function fallbackRoute(config, routerName) {
|
|
120
536
|
if (config.router.codex.fallback === "simple") {
|
|
121
|
-
return simpleRoute(
|
|
537
|
+
return simpleRoute(`${routerName} fallback forced simple.`, config);
|
|
122
538
|
}
|
|
123
539
|
if (config.router.codex.fallback === "complex") {
|
|
124
|
-
return complexRoute(
|
|
540
|
+
return complexRoute(`${routerName} fallback forced complex.`, config);
|
|
125
541
|
}
|
|
126
|
-
return complexRoute(
|
|
542
|
+
return complexRoute(`${routerName} fallback forced complex.`, config);
|
|
127
543
|
}
|
|
128
544
|
function summarizeRouterError(error) {
|
|
129
545
|
const raw = error instanceof Error ? error.message : String(error);
|
|
@@ -131,7 +547,18 @@ function summarizeRouterError(error) {
|
|
|
131
547
|
.split(/\r?\n/)
|
|
132
548
|
.map((line) => line.trim())
|
|
133
549
|
.find((line) => line && !line.toLowerCase().startsWith("tip:") && !line.toLowerCase().startsWith("usage:"));
|
|
134
|
-
return (meaningful || "unknown router error").replace(/[.。]+$/u, "");
|
|
550
|
+
return sanitizeRouterText(meaningful || "unknown router error").replace(/[.。]+$/u, "");
|
|
551
|
+
}
|
|
552
|
+
function summarizeRouterProcessDetail(output) {
|
|
553
|
+
const lines = output
|
|
554
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
555
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
556
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ")
|
|
557
|
+
.split(/\r?\n/)
|
|
558
|
+
.map((line) => line.trim())
|
|
559
|
+
.filter(Boolean);
|
|
560
|
+
const latest = lines.at(-1) ?? "";
|
|
561
|
+
return sanitizeRouterText(latest).slice(0, 240);
|
|
135
562
|
}
|
|
136
563
|
function simpleRoute(reason, config) {
|
|
137
564
|
return {
|