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.
- package/.parallel-codex/config.example.toml +136 -3
- package/README.md +299 -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-preflight.js +18 -0
- package/dist/cli-startup-recovery.js +82 -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 +234 -24
- package/dist/core/collaboration-timeline.js +268 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +297 -109
- 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 +412 -88
- package/dist/core/session-manager.js +1110 -40
- package/dist/core/task-session-details.js +175 -0
- package/dist/core/task-state-machine.js +18 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +19 -11
- package/dist/doctor.js +373 -34
- package/dist/domain/schemas.js +142 -7
- package/dist/orchestrator/collaboration-channel.js +289 -6
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/final-acceptance.js +86 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +2086 -203
- package/dist/orchestrator/prompts.js +168 -5
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +927 -0
- package/dist/tui/App.js +3187 -161
- package/dist/tui/AppShell.js +196 -25
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +232 -0
- package/dist/tui/InputBar.js +581 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/StatusDetailView.js +164 -0
- package/dist/tui/TaskSessionDetailView.js +222 -0
- package/dist/tui/TaskSessionsView.js +210 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1404 -161
- package/dist/tui/WorkerOverviewView.js +269 -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 +49 -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 +360 -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 +213 -0
- package/dist/workers/live-probe.js +177 -0
- package/dist/workers/mock-adapter.js +73 -8
- package/dist/workers/native-attach.js +106 -16
- package/dist/workers/process-adapter.js +572 -77
- package/dist/workers/provider.js +26 -0
- package/dist/workers/registry.js +12 -20
- package/package.json +11 -2
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export class ProcessTreeCleanupError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "ProcessTreeCleanupError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export async function terminateProcessTree(child, options) {
|
|
8
|
+
const pid = child.pid;
|
|
9
|
+
if (!pid || !processTreeIsAlive(child, options.processGroup)) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const termGraceMs = options.termGraceMs ?? 250;
|
|
13
|
+
const killWaitMs = options.killWaitMs ?? 500;
|
|
14
|
+
const pollMs = options.pollMs ?? 20;
|
|
15
|
+
try {
|
|
16
|
+
if (processGroupLeaderWasReused(child, options.processGroup)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
signalProcessTree(pid, "SIGTERM", options.processGroup);
|
|
20
|
+
if (await waitForProcessTreeExit(child, options.processGroup, termGraceMs, pollMs)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
signalProcessTree(pid, "SIGKILL", options.processGroup);
|
|
24
|
+
if (await waitForProcessTreeExit(child, options.processGroup, killWaitMs, pollMs)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
throw new ProcessTreeCleanupError(`Could not terminate ${options.label} ${pid}: ${errorMessage(error)}`);
|
|
30
|
+
}
|
|
31
|
+
throw new ProcessTreeCleanupError(`${options.label} group ${pid} remained alive after SIGKILL.`);
|
|
32
|
+
}
|
|
33
|
+
export function processTreeIsAlive(child, processGroup) {
|
|
34
|
+
const pid = child.pid;
|
|
35
|
+
if (!pid) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
39
|
+
if (processGroupLeaderWasReused(child, processGroup)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return processGroup && signalTargetIsAlive(-pid);
|
|
43
|
+
}
|
|
44
|
+
if (processGroup && signalTargetIsAlive(-pid)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return signalTargetIsAlive(pid);
|
|
48
|
+
}
|
|
49
|
+
function processGroupLeaderWasReused(child, processGroup) {
|
|
50
|
+
// POSIX keeps the numeric PID reserved while the original process group still exists.
|
|
51
|
+
return processGroup
|
|
52
|
+
&& (child.exitCode !== null || child.signalCode !== null)
|
|
53
|
+
&& typeof child.pid === "number"
|
|
54
|
+
&& signalTargetIsAlive(child.pid);
|
|
55
|
+
}
|
|
56
|
+
async function waitForProcessTreeExit(child, processGroup, timeoutMs, pollMs) {
|
|
57
|
+
const deadline = Date.now() + timeoutMs;
|
|
58
|
+
while (Date.now() < deadline) {
|
|
59
|
+
if (!processTreeIsAlive(child, processGroup)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
await delay(pollMs);
|
|
63
|
+
}
|
|
64
|
+
return !processTreeIsAlive(child, processGroup);
|
|
65
|
+
}
|
|
66
|
+
function signalProcessTree(pid, signal, processGroup) {
|
|
67
|
+
try {
|
|
68
|
+
process.kill(processGroup ? -pid : pid, signal);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.code !== "ESRCH") {
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function signalTargetIsAlive(pid) {
|
|
77
|
+
try {
|
|
78
|
+
process.kill(pid, 0);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return error.code === "EPERM";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function delay(milliseconds) {
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
87
|
+
}
|
|
88
|
+
function errorMessage(error) {
|
|
89
|
+
return error instanceof Error ? error.message : String(error);
|
|
90
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RouteDecisionSchema, RouterFailureKindSchema } from "../domain/schemas.js";
|
|
3
|
+
import { readRecentJsonLines } from "./file-store.js";
|
|
4
|
+
import { sanitizeRouterText } from "./router-redaction.js";
|
|
5
|
+
export { RouterFailureKindSchema };
|
|
6
|
+
export const RouterAuditRecordSchema = RouteDecisionSchema.extend({
|
|
7
|
+
time: z.string().datetime(),
|
|
8
|
+
request: z.string(),
|
|
9
|
+
workspace: z.string().min(1),
|
|
10
|
+
scope: z.enum(["initial", "follow-up"]).default("initial"),
|
|
11
|
+
router_timeout_ms: z.number().int().positive().optional(),
|
|
12
|
+
router_first_output_timeout_ms: z.number().int().positive().optional(),
|
|
13
|
+
router_idle_timeout_ms: z.number().int().positive().optional(),
|
|
14
|
+
router_max_output_bytes: z.number().int().min(1024).max(16 * 1024 * 1024).optional(),
|
|
15
|
+
router_max_attempts: z.number().int().min(1).max(3).optional(),
|
|
16
|
+
router_retry_delay_ms: z.number().int().nonnegative().max(10000).optional(),
|
|
17
|
+
proxy_configured: z.boolean().optional(),
|
|
18
|
+
failure_kind: RouterFailureKindSchema.optional()
|
|
19
|
+
});
|
|
20
|
+
export function classifyRouterFailure(reason) {
|
|
21
|
+
const timedOut = /\b(?:timed out|timeout|ETIMEDOUT)\b/i.test(reason);
|
|
22
|
+
const proxyMentioned = /\bproxy\b|代理/i.test(reason);
|
|
23
|
+
const proxyFailure = (/\bproxy(?:\s+(?:connection|connect|handshake|authentication|request|server|transport|tunnel))?\s+(?:authentication|required|failed|failure|error|refused|unreachable|invalid|closed|reset)\b/i.test(reason)
|
|
24
|
+
|| /\b(?:failed|unable|cannot|could not)\s+(?:to\s+)?(?:connect|reach|use|negotiate).{0,30}\bproxy\b/i.test(reason)
|
|
25
|
+
|| /代理.{0,20}(?:认证|失败|错误|拒绝|无法|不可达)/i.test(reason));
|
|
26
|
+
if (proxyMentioned && timedOut) {
|
|
27
|
+
return "timeout";
|
|
28
|
+
}
|
|
29
|
+
if (proxyFailure) {
|
|
30
|
+
return "proxy";
|
|
31
|
+
}
|
|
32
|
+
if (/\b(?:401|403)\b|\b(?:unauthori[sz]ed|forbidden|authentication|api[-_\s]?key|login required|not logged in|sign in)\b/i.test(reason)) {
|
|
33
|
+
return "auth";
|
|
34
|
+
}
|
|
35
|
+
if (/\b429\b|\b(?:rate[ -]?limit|too many requests|quota (?:exceeded|exhausted)|usage limit)\b/i.test(reason)) {
|
|
36
|
+
return "rate-limit";
|
|
37
|
+
}
|
|
38
|
+
if (/\b(?:ECONNREFUSED|ECONNRESET|ENETUNREACH|EHOSTUNREACH|ENOTFOUND|EAI_AGAIN)\b|\b(?:network|websocket|https transport|fetch failed|certificate|tls|ssl)\b/i.test(reason)) {
|
|
39
|
+
return "network";
|
|
40
|
+
}
|
|
41
|
+
if (timedOut) {
|
|
42
|
+
return "timeout";
|
|
43
|
+
}
|
|
44
|
+
if (/\b(?:ENOENT|command not found|spawn error)\b/i.test(reason)) {
|
|
45
|
+
return "unavailable";
|
|
46
|
+
}
|
|
47
|
+
if (/\b(?:no json object|invalid json|invalid codex router (?:mode|response object)|failed to parse json|unexpected token)\b/i.test(reason)) {
|
|
48
|
+
return "invalid-output";
|
|
49
|
+
}
|
|
50
|
+
if (/\b(?:exited with (?:code|signal)|process exited)\b/i.test(reason)) {
|
|
51
|
+
return "exit";
|
|
52
|
+
}
|
|
53
|
+
if (/\b(?:router input failed|stdin|EPIPE)\b/i.test(reason)) {
|
|
54
|
+
return "input";
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
export function routerFallbackIsTransient(route) {
|
|
59
|
+
if (route.source !== "fallback") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (route.router_timeout_kind === "total") {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (route.router_timeout_kind === "first-output" || route.router_timeout_kind === "idle") {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
const kind = route.router_failure_kind ?? classifyRouterFailure(route.reason);
|
|
69
|
+
if (kind === "network" || kind === "proxy") {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (kind === "timeout") {
|
|
73
|
+
return route.router_failure_stage !== "response";
|
|
74
|
+
}
|
|
75
|
+
if (kind === "input") {
|
|
76
|
+
return /\b(?:EPIPE|ECONNRESET|temporar(?:y|ily))\b/i.test(route.reason);
|
|
77
|
+
}
|
|
78
|
+
if (kind === "exit") {
|
|
79
|
+
return /\b(?:ECONNRESET|ETIMEDOUT|EAI_AGAIN|temporar(?:y|ily)|try again|connection reset)\b/i.test(route.reason);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
export function diagnoseRouterFailure(evidence) {
|
|
84
|
+
const kind = evidence.failure_kind ?? classifyRouterFailure(evidence.reason) ?? "unknown";
|
|
85
|
+
if (kind === "auth") {
|
|
86
|
+
return diagnosis(kind, "Codex authentication failed", "run codex login, then retry Router");
|
|
87
|
+
}
|
|
88
|
+
if (kind === "rate-limit") {
|
|
89
|
+
return diagnosis(kind, "The provider rate limit blocked routing", "wait for quota or change the Router model/provider");
|
|
90
|
+
}
|
|
91
|
+
if (kind === "proxy") {
|
|
92
|
+
return diagnosis(kind, "Router reported a proxy-path failure", "run parallel-codex-tui --doctor --probe-router and verify the proxy upstream");
|
|
93
|
+
}
|
|
94
|
+
if (kind === "network") {
|
|
95
|
+
return diagnosis(kind, "Router reported a network-path failure", "run parallel-codex-tui --doctor --probe-router and verify DNS, TLS, and API reachability");
|
|
96
|
+
}
|
|
97
|
+
if (kind === "unavailable") {
|
|
98
|
+
return diagnosis(kind, evidence.router_failure_stage === "spawn" ? "Router process could not start" : "Router command is unavailable", "run parallel-codex-tui --doctor and fix router.codex.command");
|
|
99
|
+
}
|
|
100
|
+
if (kind === "invalid-output") {
|
|
101
|
+
return diagnosis(kind, "Router returned output that was not valid route JSON", "retry Router; if it repeats, inspect the Router model/provider output");
|
|
102
|
+
}
|
|
103
|
+
if (kind === "input") {
|
|
104
|
+
return diagnosis(kind, "Router process rejected the request input", "check that router.codex.args accepts a prompt on stdin");
|
|
105
|
+
}
|
|
106
|
+
if (kind === "timeout") {
|
|
107
|
+
return diagnoseRouterTimeout(evidence);
|
|
108
|
+
}
|
|
109
|
+
if (kind === "exit") {
|
|
110
|
+
return diagnosis(kind, "Router process exited before a valid route response", "run the configured Codex command directly and inspect its exit output");
|
|
111
|
+
}
|
|
112
|
+
return diagnosis("unknown", "Router failed before a valid route response", "run parallel-codex-tui --doctor --probe-router and inspect the recorded reason");
|
|
113
|
+
}
|
|
114
|
+
function diagnoseRouterTimeout(evidence) {
|
|
115
|
+
if (evidence.router_timeout_kind === "first-output") {
|
|
116
|
+
return diagnosis("timeout", "Router produced no output before the first-output deadline", evidence.proxy_configured
|
|
117
|
+
? "run parallel-codex-tui --doctor --probe-router; verify Codex login and proxy upstream, or raise router.codex.firstOutputTimeoutMs"
|
|
118
|
+
: "run parallel-codex-tui --doctor --probe-router; verify Codex login and API network path, or raise router.codex.firstOutputTimeoutMs");
|
|
119
|
+
}
|
|
120
|
+
if (evidence.router_timeout_kind === "idle") {
|
|
121
|
+
return diagnosis("timeout", (evidence.router_stdout_bytes ?? 0) > 0
|
|
122
|
+
? "Router response stopped before valid route JSON completed"
|
|
123
|
+
: "Router diagnostics stopped before a route response", "inspect the reason; retry Router or raise router.codex.idleTimeoutMs");
|
|
124
|
+
}
|
|
125
|
+
if ((evidence.router_stdout_bytes ?? 0) > 0) {
|
|
126
|
+
return diagnosis("timeout", "Router began a route response but did not finish", "retry Router or raise router.codex.timeoutMs");
|
|
127
|
+
}
|
|
128
|
+
if ((evidence.router_stderr_bytes ?? 0) > 0) {
|
|
129
|
+
return diagnosis("timeout", "Router emitted diagnostics but no route response", "inspect the reason, then run parallel-codex-tui --doctor --probe-router");
|
|
130
|
+
}
|
|
131
|
+
if (evidence.router_failure_stage === "waiting-output") {
|
|
132
|
+
return diagnosis("timeout", "Router produced no output before the timeout", evidence.proxy_configured
|
|
133
|
+
? "run parallel-codex-tui --doctor --probe-router; verify Codex login and proxy upstream"
|
|
134
|
+
: "run parallel-codex-tui --doctor --probe-router; verify Codex login and API network path");
|
|
135
|
+
}
|
|
136
|
+
return diagnosis("timeout", "Router timed out before a valid route response", evidence.proxy_configured
|
|
137
|
+
? "run parallel-codex-tui --doctor --probe-router; verify Codex login and proxy upstream"
|
|
138
|
+
: "run parallel-codex-tui --doctor --probe-router; verify Codex login and API network path");
|
|
139
|
+
}
|
|
140
|
+
function diagnosis(kind, summary, action) {
|
|
141
|
+
return { kind, summary, action };
|
|
142
|
+
}
|
|
143
|
+
export async function readRouterAudit(path, limit = 100) {
|
|
144
|
+
const boundedLimit = Number.isFinite(limit)
|
|
145
|
+
? Math.min(500, Math.max(0, Math.trunc(limit)))
|
|
146
|
+
: 100;
|
|
147
|
+
if (boundedLimit === 0) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
return (await readRecentJsonLines(path, RouterAuditRecordSchema, boundedLimit)).map((record) => ({
|
|
151
|
+
...record,
|
|
152
|
+
request: sanitizeRouterText(record.request),
|
|
153
|
+
reason: sanitizeRouterText(record.reason)
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const ROUTER_URL_PATTERN = /\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"'`]+/giu;
|
|
2
|
+
const ROUTER_SECRET_ASSIGNMENT_PATTERN = /\b((?:(?:[a-z][a-z0-9]*_)*(?:api_?key|access_?token|auth_?token|token|password|passwd|secret|client_?secret))\s*(?:=|:)\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;]+)/giu;
|
|
3
|
+
const ROUTER_AUTHORIZATION_PATTERN = /\b((?:authorization\s*:\s*)?(?:bearer|basic)\s+)[^\s,;]+/giu;
|
|
4
|
+
const ROUTER_RAW_TOKEN_PATTERN = /\b(?:sk-[a-z0-9_-]{8,}|npm_[a-z0-9]{20,}|gh[pousr]_[a-z0-9]{20,}|github_pat_[a-z0-9_]{20,})\b/giu;
|
|
5
|
+
const ROUTER_URL_TRAILING_PUNCTUATION = /[),.;!?,。;!?、]+$/u;
|
|
6
|
+
export function sanitizeRouterText(value) {
|
|
7
|
+
return value
|
|
8
|
+
.replace(ROUTER_URL_PATTERN, sanitizeRouterUrl)
|
|
9
|
+
.replace(ROUTER_AUTHORIZATION_PATTERN, "$1***")
|
|
10
|
+
.replace(ROUTER_SECRET_ASSIGNMENT_PATTERN, "$1***")
|
|
11
|
+
.replace(ROUTER_RAW_TOKEN_PATTERN, "***");
|
|
12
|
+
}
|
|
13
|
+
function sanitizeRouterUrl(value) {
|
|
14
|
+
const trailing = value.match(ROUTER_URL_TRAILING_PUNCTUATION)?.[0] ?? "";
|
|
15
|
+
const candidate = trailing ? value.slice(0, -trailing.length) : value;
|
|
16
|
+
try {
|
|
17
|
+
const parsed = new URL(candidate);
|
|
18
|
+
const credentials = parsed.username || parsed.password ? "***@" : "";
|
|
19
|
+
return `${parsed.protocol}//${credentials}${parsed.host}${trailing}`;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
const authority = candidate.match(/^([a-z][a-z0-9+.-]*:\/\/)([^/?#]*)/iu);
|
|
23
|
+
if (!authority) {
|
|
24
|
+
return value.replace(/\b([a-z][a-z0-9+.-]*:\/\/)([^@\s/]+)@/iu, "$1***@");
|
|
25
|
+
}
|
|
26
|
+
const safeAuthority = authority[2]?.includes("@")
|
|
27
|
+
? `***@${authority[2].slice(authority[2].lastIndexOf("@") + 1)}`
|
|
28
|
+
: authority[2] ?? "";
|
|
29
|
+
return `${authority[1]}${safeAuthority}${trailing}`;
|
|
30
|
+
}
|
|
31
|
+
}
|