parallel-codex-tui 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.
Files changed (78) hide show
  1. package/.parallel-codex/config.example.toml +136 -3
  2. package/README.md +299 -21
  3. package/dist/bootstrap.js +37 -17
  4. package/dist/cli-args.js +34 -3
  5. package/dist/cli-help.js +20 -0
  6. package/dist/cli-startup-preflight.js +18 -0
  7. package/dist/cli-startup-recovery.js +82 -0
  8. package/dist/cli-workspace-picker.js +330 -0
  9. package/dist/cli-workspace-transition.js +33 -0
  10. package/dist/cli-workspace.js +7 -71
  11. package/dist/cli.js +234 -24
  12. package/dist/core/collaboration-timeline.js +268 -0
  13. package/dist/core/config-errors.js +14 -0
  14. package/dist/core/config.js +297 -109
  15. package/dist/core/file-store.js +119 -6
  16. package/dist/core/lease-finalization.js +22 -0
  17. package/dist/core/paths.js +7 -0
  18. package/dist/core/process-identity.js +48 -0
  19. package/dist/core/process-mutation-turn.js +128 -0
  20. package/dist/core/process-ownership.js +276 -0
  21. package/dist/core/process-tree.js +90 -0
  22. package/dist/core/router-audit.js +155 -0
  23. package/dist/core/router-redaction.js +31 -0
  24. package/dist/core/router.js +462 -35
  25. package/dist/core/session-index.js +412 -88
  26. package/dist/core/session-manager.js +1110 -40
  27. package/dist/core/task-session-details.js +175 -0
  28. package/dist/core/task-state-machine.js +18 -0
  29. package/dist/core/workspace-commit-recovery.js +118 -0
  30. package/dist/core/workspace.js +19 -11
  31. package/dist/doctor.js +373 -34
  32. package/dist/domain/schemas.js +142 -7
  33. package/dist/orchestrator/collaboration-channel.js +289 -6
  34. package/dist/orchestrator/feature-plan.js +70 -0
  35. package/dist/orchestrator/final-acceptance.js +86 -0
  36. package/dist/orchestrator/judge-artifacts.js +236 -0
  37. package/dist/orchestrator/orchestrator.js +2086 -203
  38. package/dist/orchestrator/prompts.js +168 -5
  39. package/dist/orchestrator/supervisor-summary.js +56 -2
  40. package/dist/orchestrator/workspace-sandbox.js +927 -0
  41. package/dist/tui/App.js +3187 -161
  42. package/dist/tui/AppShell.js +196 -25
  43. package/dist/tui/CollaborationTimelineView.js +327 -0
  44. package/dist/tui/FeatureBoardView.js +232 -0
  45. package/dist/tui/InputBar.js +581 -57
  46. package/dist/tui/RouterDiagnosticsView.js +469 -0
  47. package/dist/tui/StatusBar.js +610 -57
  48. package/dist/tui/StatusDetailView.js +164 -0
  49. package/dist/tui/TaskSessionDetailView.js +222 -0
  50. package/dist/tui/TaskSessionsView.js +210 -0
  51. package/dist/tui/TerminalOutput.js +53 -9
  52. package/dist/tui/WorkerOutputView.js +1404 -161
  53. package/dist/tui/WorkerOverviewView.js +269 -0
  54. package/dist/tui/chat-history.js +25 -0
  55. package/dist/tui/chat-input.js +67 -19
  56. package/dist/tui/chat-paste.js +76 -0
  57. package/dist/tui/display-width.js +41 -3
  58. package/dist/tui/incremental-text-file.js +101 -0
  59. package/dist/tui/keyboard.js +49 -0
  60. package/dist/tui/markdown-text.js +14 -0
  61. package/dist/tui/raw-input-decoder.js +3 -0
  62. package/dist/tui/scrolling.js +2 -1
  63. package/dist/tui/status-line.js +360 -11
  64. package/dist/tui/task-memory.js +13 -1
  65. package/dist/tui/task-result.js +105 -0
  66. package/dist/tui/terminal-screen.js +13 -1
  67. package/dist/tui/theme-contrast.js +144 -0
  68. package/dist/tui/theme-preview.js +109 -0
  69. package/dist/tui/theme.js +158 -0
  70. package/dist/version.js +1 -1
  71. package/dist/workers/capabilities.js +213 -0
  72. package/dist/workers/live-probe.js +177 -0
  73. package/dist/workers/mock-adapter.js +73 -8
  74. package/dist/workers/native-attach.js +106 -16
  75. package/dist/workers/process-adapter.js +572 -77
  76. package/dist/workers/provider.js +26 -0
  77. package/dist/workers/registry.js +12 -20
  78. package/package.json +11 -2
@@ -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
- export async function routeRequestWithCodex(request, config, runner = runCodexRouterProcess, cwd = config.projectRoot) {
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 output = await runner(buildCodexRouterPrompt(request, config), config, cwd);
12
- const route = parseCodexRoute(output, config);
13
- return RouteDecisionSchema.parse(route);
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
- const fallback = fallbackRoute(config);
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
- reason: `Codex router failed: ${summarizeRouterError(error)}. ${fallback.reason}`
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
- export async function runCodexRouterProcess(prompt, config, cwd = config.projectRoot) {
24
- const { command, args, timeoutMs } = config.router.codex;
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: source === "forced" ? 0 : 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: process.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 timeout;
35
- const finish = (error, output = stdout) => {
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
- if (timeout) {
41
- clearTimeout(timeout);
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
- timeout = setTimeout(() => {
52
- child.kill("SIGTERM");
53
- finish(new Error(`Codex router timed out after ${timeoutMs}ms`));
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
- stdout += chunk.toString("utf8");
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
- stderr += chunk.toString("utf8");
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
- finish(error);
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(`Codex router exited with ${signal ? `signal ${signal}` : `code ${code ?? 1}`}${detail ? `: ${detail}` : ""}`));
319
+ finish(new Error(`${routerName} exited with ${signal ? `signal ${signal}` : `code ${code ?? 1}`}${detail ? `: ${detail}` : ""}`), stdout, "exit");
72
320
  });
73
- child.stdin.write(prompt);
74
- child.stdin.end();
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: parsed.mode === "complex" ? "complex" : "simple",
101
- reason: parsed.reason || "Codex router decision.",
102
- suggested_roles: parsed.mode === "complex" ? ["judge", "actor", "critic"] : [],
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("Codex router fallback forced simple.", config);
537
+ return simpleRoute(`${routerName} fallback forced simple.`, config);
122
538
  }
123
539
  if (config.router.codex.fallback === "complex") {
124
- return complexRoute("Codex router fallback forced complex.", config);
540
+ return complexRoute(`${routerName} fallback forced complex.`, config);
125
541
  }
126
- return complexRoute("Codex router fallback forced complex.", config);
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 {