@cjhyy/code-shell-capability-coding 0.9.44 → 0.9.47
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/dist/capability-runtime.js +4 -1
- package/dist/cc-orchestrator/codex-session-history.d.ts +3 -1
- package/dist/cc-orchestrator/codex-session-history.js +11 -4
- package/dist/cc-orchestrator/external-agent-changes.js +7 -65
- package/dist/cc-orchestrator/external-agent-driver.js +61 -3
- package/dist/cc-orchestrator/session-discovery.d.ts +4 -2
- package/dist/cc-orchestrator/session-discovery.js +63 -6
- package/dist/cc-orchestrator/session-history.js +5 -0
- package/dist/external-runtimes/claude-code/event-translator.d.ts +7 -0
- package/dist/external-runtimes/claude-code/event-translator.js +58 -9
- package/dist/external-runtimes/claude-code/mcp-config.d.ts +8 -0
- package/dist/external-runtimes/claude-code/mcp-config.js +16 -3
- package/dist/external-runtimes/claude-code/permissions.d.ts +69 -0
- package/dist/external-runtimes/claude-code/permissions.js +120 -0
- package/dist/external-runtimes/claude-code/runtime.d.ts +43 -4
- package/dist/external-runtimes/claude-code/runtime.js +155 -39
- package/dist/external-runtimes/claude-code/text-inference.d.ts +22 -0
- package/dist/external-runtimes/claude-code/text-inference.js +240 -0
- package/dist/external-runtimes/codex/app-server-client.d.ts +15 -0
- package/dist/external-runtimes/codex/app-server-client.js +69 -6
- package/dist/external-runtimes/codex/event-translator.js +16 -2
- package/dist/external-runtimes/codex/runtime.d.ts +14 -0
- package/dist/external-runtimes/codex/runtime.js +152 -16
- package/dist/external-runtimes/index.d.ts +4 -0
- package/dist/external-runtimes/index.js +2 -0
- package/dist/external-runtimes/session-factory.d.ts +16 -0
- package/dist/external-runtimes/session-factory.js +43 -1
- package/dist/external-runtimes/shared/mcp-bridge.d.ts +36 -1
- package/dist/external-runtimes/shared/mcp-bridge.js +93 -13
- package/dist/git/parse-log.d.ts +6 -2
- package/dist/git/parse-log.js +7 -3
- package/dist/git/utils.d.ts +4 -0
- package/dist/git/utils.js +29 -10
- package/dist/git/worktree/crud.d.ts +2 -0
- package/dist/git/worktree/crud.js +29 -1
- package/dist/git/worktree/diff.js +57 -29
- package/dist/index.capability.js +14 -1
- package/dist/index.git.d.ts +1 -0
- package/dist/index.git.js +1 -0
- package/dist/lsp/client.d.ts +3 -0
- package/dist/lsp/client.js +96 -25
- package/dist/lsp/manager.d.ts +2 -0
- package/dist/lsp/manager.js +56 -15
- package/dist/prompt/coding.md +1 -0
- package/dist/quota/index.d.ts +2 -1
- package/dist/quota/index.js +27 -7
- package/dist/quota/types.d.ts +5 -2
- package/dist/review/review-prompt.d.ts +2 -0
- package/dist/review/review-prompt.js +70 -29
- package/dist/review/review-target.d.ts +29 -0
- package/dist/review/review-target.js +353 -0
- package/dist/tools/apply-patch/applier.js +64 -39
- package/dist/tools/apply-patch/backup-targets.d.ts +8 -0
- package/dist/tools/apply-patch/backup-targets.js +24 -0
- package/dist/tools/apply-patch/parser.js +4 -2
- package/dist/tools/check-quota.js +3 -1
- package/dist/tools/lsp.js +36 -5
- package/package.json +2 -2
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { killProcessGroup } from "@cjhyy/code-shell-core/extension";
|
|
6
|
+
const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
const MAX_STDERR_BYTES = 64 * 1024;
|
|
8
|
+
const MAX_INPUT_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
function usageFromResult(value) {
|
|
10
|
+
if (!value || typeof value !== "object")
|
|
11
|
+
return undefined;
|
|
12
|
+
const raw = value;
|
|
13
|
+
const input = raw.input_tokens;
|
|
14
|
+
const output = raw.output_tokens;
|
|
15
|
+
const read = raw.cache_read_input_tokens ?? 0;
|
|
16
|
+
const write = raw.cache_creation_input_tokens ?? 0;
|
|
17
|
+
if (![input, output, read, write].every((count) => Number.isSafeInteger(count) && Number(count) >= 0))
|
|
18
|
+
return undefined;
|
|
19
|
+
const promptTokens = Number(input) + Number(read) + Number(write);
|
|
20
|
+
const totalTokens = promptTokens + Number(output);
|
|
21
|
+
if (!Number.isSafeInteger(totalTokens))
|
|
22
|
+
return undefined;
|
|
23
|
+
return {
|
|
24
|
+
promptTokens,
|
|
25
|
+
completionTokens: Number(output),
|
|
26
|
+
totalTokens,
|
|
27
|
+
...(raw.cache_read_input_tokens !== undefined ? { cacheReadTokens: Number(read) } : {}),
|
|
28
|
+
...(raw.cache_creation_input_tokens !== undefined
|
|
29
|
+
? { cacheCreationTokens: Number(write) }
|
|
30
|
+
: {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Fresh CLI inference, with no conversation, Host tools or user customizations. */
|
|
34
|
+
export async function runClaudeTextInference(options, dependencies = {}) {
|
|
35
|
+
options.signal?.throwIfAborted();
|
|
36
|
+
if (!options.systemPrompt?.trim() || !options.userMessage?.trim())
|
|
37
|
+
throw new Error("Claude text inference requires a system prompt and user message");
|
|
38
|
+
if (Buffer.byteLength(options.systemPrompt) + Buffer.byteLength(options.userMessage) >
|
|
39
|
+
MAX_INPUT_BYTES)
|
|
40
|
+
throw new Error("Claude text inference input exceeds its byte limit");
|
|
41
|
+
const maxTokens = options.maxTokens ?? 3072;
|
|
42
|
+
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
43
|
+
if (!Number.isSafeInteger(maxTokens) || maxTokens < 1 || maxTokens > 128_000)
|
|
44
|
+
throw new Error("Invalid Claude text inference output budget");
|
|
45
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3_600_000)
|
|
46
|
+
throw new Error("Invalid Claude text inference timeout");
|
|
47
|
+
const privateDir = await mkdtemp(join(tmpdir(), "codeshell-claude-text-"));
|
|
48
|
+
try {
|
|
49
|
+
await chmod(privateDir, 0o700);
|
|
50
|
+
const systemPromptFile = join(privateDir, "system-prompt.txt");
|
|
51
|
+
await writeFile(systemPromptFile, options.systemPrompt, { mode: 0o600 });
|
|
52
|
+
await chmod(systemPromptFile, 0o600);
|
|
53
|
+
options.signal?.throwIfAborted();
|
|
54
|
+
const args = [
|
|
55
|
+
"-p",
|
|
56
|
+
"--output-format",
|
|
57
|
+
"stream-json",
|
|
58
|
+
"--verbose",
|
|
59
|
+
"--include-partial-messages",
|
|
60
|
+
"--safe-mode",
|
|
61
|
+
"--tools",
|
|
62
|
+
"",
|
|
63
|
+
"--disallowedTools",
|
|
64
|
+
"*",
|
|
65
|
+
"--strict-mcp-config",
|
|
66
|
+
"--disable-slash-commands",
|
|
67
|
+
"--no-session-persistence",
|
|
68
|
+
"--permission-mode",
|
|
69
|
+
"dontAsk",
|
|
70
|
+
"--permission-prompts",
|
|
71
|
+
"none",
|
|
72
|
+
"--max-turns",
|
|
73
|
+
"1",
|
|
74
|
+
"--system-prompt-file",
|
|
75
|
+
systemPromptFile,
|
|
76
|
+
...(options.model ? ["--model", options.model] : []),
|
|
77
|
+
];
|
|
78
|
+
// Safe mode keeps the CLI's auth/provider selection, unlike --bare. Managed
|
|
79
|
+
// policy (including managed hooks) remains authoritative inside the CLI.
|
|
80
|
+
const env = {
|
|
81
|
+
...(options.env ?? process.env),
|
|
82
|
+
CLAUDE_CODE_SAFE_MODE: "1",
|
|
83
|
+
CLAUDE_CODE_MAX_OUTPUT_TOKENS: String(maxTokens),
|
|
84
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
85
|
+
CLAUDE_CODE_DISABLE_TERMINAL_TITLE: "1",
|
|
86
|
+
CLAUDE_CODE_DISABLE_FILE_MENTIONS: "1",
|
|
87
|
+
};
|
|
88
|
+
for (const name of [
|
|
89
|
+
"CLAUDECODE",
|
|
90
|
+
"CLAUDE_CODE_ENTRYPOINT",
|
|
91
|
+
"CLAUDE_CODE_SIMPLE",
|
|
92
|
+
"CODESHELL_CODEX_MCP_TOKEN",
|
|
93
|
+
"CODE_SHELL_HOST_MCP_SERVERS",
|
|
94
|
+
"CODESHELL_MODEL_TRACE_FD",
|
|
95
|
+
])
|
|
96
|
+
delete env[name];
|
|
97
|
+
return await new Promise((resolve, reject) => {
|
|
98
|
+
const launch = dependencies.spawn ?? spawn;
|
|
99
|
+
const child = launch(options.command ?? "claude", args, {
|
|
100
|
+
cwd: options.cwd ?? privateDir,
|
|
101
|
+
env,
|
|
102
|
+
shell: false,
|
|
103
|
+
detached: process.platform !== "win32",
|
|
104
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
105
|
+
});
|
|
106
|
+
let outputBytes = 0;
|
|
107
|
+
let stderrBytes = 0;
|
|
108
|
+
let pending = "";
|
|
109
|
+
let terminal;
|
|
110
|
+
let stopReason;
|
|
111
|
+
let failure;
|
|
112
|
+
let settling = false;
|
|
113
|
+
let stopping;
|
|
114
|
+
const stop = () => {
|
|
115
|
+
stopping ??= child.pid ? killProcessGroup(child.pid, { graceMs: 250 }) : Promise.resolve();
|
|
116
|
+
return stopping;
|
|
117
|
+
};
|
|
118
|
+
const finish = (code, signal) => {
|
|
119
|
+
if (settling)
|
|
120
|
+
return;
|
|
121
|
+
settling = true;
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
124
|
+
void stop().then(() => {
|
|
125
|
+
child.stdin.destroy();
|
|
126
|
+
child.stdout.destroy();
|
|
127
|
+
child.stderr.destroy();
|
|
128
|
+
if (failure)
|
|
129
|
+
return reject(failure);
|
|
130
|
+
if (code !== 0 || signal)
|
|
131
|
+
return reject(new Error(`Claude text inference exited without success (${signal ?? code})`));
|
|
132
|
+
try {
|
|
133
|
+
if (pending.trim())
|
|
134
|
+
accept(pending);
|
|
135
|
+
if (!terminal)
|
|
136
|
+
throw new Error("Claude text inference produced no terminal result");
|
|
137
|
+
resolve({
|
|
138
|
+
text: terminal.result,
|
|
139
|
+
usage: usageFromResult(terminal.usage),
|
|
140
|
+
stopReason,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
reject(error);
|
|
145
|
+
}
|
|
146
|
+
}, reject);
|
|
147
|
+
};
|
|
148
|
+
const fail = (error) => {
|
|
149
|
+
if (settling || failure)
|
|
150
|
+
return;
|
|
151
|
+
failure = error;
|
|
152
|
+
void stop().then(() => finish(), reject);
|
|
153
|
+
};
|
|
154
|
+
const acceptStopReason = (value) => {
|
|
155
|
+
if (typeof value !== "string")
|
|
156
|
+
return;
|
|
157
|
+
if (!["end_turn", "stop", "stop_sequence"].includes(value))
|
|
158
|
+
throw new Error(`Claude text inference stopped without complete text (${value})`);
|
|
159
|
+
stopReason = value;
|
|
160
|
+
};
|
|
161
|
+
const accept = (line) => {
|
|
162
|
+
if (!line.trim())
|
|
163
|
+
return;
|
|
164
|
+
let item;
|
|
165
|
+
try {
|
|
166
|
+
item = JSON.parse(line);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
throw new Error("Claude text inference emitted malformed JSON");
|
|
170
|
+
}
|
|
171
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
172
|
+
throw new Error("Claude text inference emitted a malformed event");
|
|
173
|
+
const content = item.message?.content;
|
|
174
|
+
const block = item.event?.content_block;
|
|
175
|
+
if (["tool_progress", "tool_use_summary"].includes(item.type) ||
|
|
176
|
+
(item.type === "control_request" && item.request?.subtype === "can_use_tool") ||
|
|
177
|
+
["tool_use", "server_tool_use"].includes(block?.type) ||
|
|
178
|
+
(Array.isArray(content) &&
|
|
179
|
+
content.some((part) => ["tool_use", "server_tool_use", "tool_result"].includes(part?.type))) ||
|
|
180
|
+
(item.type === "system" && item.subtype === "init" && item.tools?.length > 0))
|
|
181
|
+
throw new Error("Claude text inference attempted to use tools");
|
|
182
|
+
if (item.type === "assistant")
|
|
183
|
+
acceptStopReason(item.message?.stop_reason);
|
|
184
|
+
if (item.type === "stream_event" && item.event?.type === "message_delta")
|
|
185
|
+
acceptStopReason(item.event.delta?.stop_reason);
|
|
186
|
+
if (item.type !== "result")
|
|
187
|
+
return;
|
|
188
|
+
if (terminal)
|
|
189
|
+
throw new Error("Claude text inference emitted multiple terminal results");
|
|
190
|
+
if (item.is_error !== false ||
|
|
191
|
+
item.subtype !== "success" ||
|
|
192
|
+
typeof item.result !== "string" ||
|
|
193
|
+
!item.result.trim() ||
|
|
194
|
+
(Array.isArray(item.errors) && item.errors.length > 0))
|
|
195
|
+
throw new Error("Claude text inference returned an incomplete or failed result");
|
|
196
|
+
acceptStopReason(item.stop_reason);
|
|
197
|
+
terminal = item;
|
|
198
|
+
};
|
|
199
|
+
const onAbort = () => fail(Object.assign(new Error("Claude text inference aborted"), { name: "AbortError" }));
|
|
200
|
+
const timer = setTimeout(() => fail(new Error(`Claude text inference timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
201
|
+
child.stdout.setEncoding("utf8");
|
|
202
|
+
child.stdout.on("data", (chunk) => {
|
|
203
|
+
if (settling || failure)
|
|
204
|
+
return;
|
|
205
|
+
outputBytes += Buffer.byteLength(chunk);
|
|
206
|
+
if (outputBytes > MAX_OUTPUT_BYTES)
|
|
207
|
+
return fail(new Error("Claude text inference output exceeds its byte limit"));
|
|
208
|
+
pending += chunk;
|
|
209
|
+
try {
|
|
210
|
+
for (let newline; (newline = pending.indexOf("\n")) >= 0;) {
|
|
211
|
+
const line = pending.slice(0, newline);
|
|
212
|
+
pending = pending.slice(newline + 1);
|
|
213
|
+
accept(line);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
fail(error);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
child.stderr.on("data", (chunk) => {
|
|
221
|
+
stderrBytes += chunk.byteLength;
|
|
222
|
+
if (stderrBytes > MAX_STDERR_BYTES)
|
|
223
|
+
fail(new Error("Claude text inference diagnostics exceed their byte limit"));
|
|
224
|
+
});
|
|
225
|
+
child.stdin.on("error", (error) => fail(error));
|
|
226
|
+
child.stdout.on("error", (error) => fail(error));
|
|
227
|
+
child.stderr.on("error", (error) => fail(error));
|
|
228
|
+
child.once("error", (error) => fail(error));
|
|
229
|
+
child.once("close", finish);
|
|
230
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
231
|
+
if (options.signal?.aborted)
|
|
232
|
+
onAbort();
|
|
233
|
+
else
|
|
234
|
+
child.stdin.end(options.userMessage);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
await rm(privateDir, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -21,6 +21,14 @@ export type NotificationHandler = (method: string, params: unknown) => void;
|
|
|
21
21
|
* waiting either way.
|
|
22
22
|
*/
|
|
23
23
|
export type ServerRequestHandler = (method: string, params: unknown) => Promise<unknown> | unknown;
|
|
24
|
+
/**
|
|
25
|
+
* Told once when the transport is gone for good — the process exited or failed
|
|
26
|
+
* to spawn, the stream broke, or {@link CodexAppServerClient.close} ran. Pending
|
|
27
|
+
* requests are rejected by then; this is for state that is NOT a request, such
|
|
28
|
+
* as a turn whose RPC was already answered and now waits on notifications that
|
|
29
|
+
* can never arrive.
|
|
30
|
+
*/
|
|
31
|
+
export type CloseHandler = (reason: string) => void;
|
|
24
32
|
export declare class CodexAppServerClient {
|
|
25
33
|
private readonly options;
|
|
26
34
|
private child?;
|
|
@@ -29,7 +37,9 @@ export declare class CodexAppServerClient {
|
|
|
29
37
|
private readonly pending;
|
|
30
38
|
private notificationHandler?;
|
|
31
39
|
private requestHandler?;
|
|
40
|
+
private closeHandler?;
|
|
32
41
|
private closed;
|
|
42
|
+
private closing?;
|
|
33
43
|
private closeReason?;
|
|
34
44
|
/** Buffer lines that arrive before a handler exists, then drain in order. */
|
|
35
45
|
private readonly preHandlerLines;
|
|
@@ -42,6 +52,7 @@ export declare class CodexAppServerClient {
|
|
|
42
52
|
*/
|
|
43
53
|
onNotification(handler: NotificationHandler): void;
|
|
44
54
|
onServerRequest(handler: ServerRequestHandler): void;
|
|
55
|
+
onClose(handler: CloseHandler): void;
|
|
45
56
|
/** Spawn the app-server. Handlers must already be registered. */
|
|
46
57
|
start(): void;
|
|
47
58
|
private acceptLine;
|
|
@@ -59,8 +70,12 @@ export declare class CodexAppServerClient {
|
|
|
59
70
|
request(method: string, params?: unknown, timeoutMs?: number): Promise<unknown>;
|
|
60
71
|
notify(method: string, params?: unknown): void;
|
|
61
72
|
private write;
|
|
73
|
+
private failInput;
|
|
62
74
|
private failAll;
|
|
75
|
+
/** Why the transport closed, once it has. */
|
|
76
|
+
get closedReason(): string | undefined;
|
|
63
77
|
get isClosed(): boolean;
|
|
64
78
|
/** Stop via stdin EOF, then SIGTERM and SIGKILL if the server does not exit. */
|
|
65
79
|
close(): Promise<void>;
|
|
80
|
+
private stop;
|
|
66
81
|
}
|
|
@@ -36,7 +36,9 @@ export class CodexAppServerClient {
|
|
|
36
36
|
pending = new Map();
|
|
37
37
|
notificationHandler;
|
|
38
38
|
requestHandler;
|
|
39
|
+
closeHandler;
|
|
39
40
|
closed = false;
|
|
41
|
+
closing;
|
|
40
42
|
closeReason;
|
|
41
43
|
/** Buffer lines that arrive before a handler exists, then drain in order. */
|
|
42
44
|
preHandlerLines = [];
|
|
@@ -56,8 +58,13 @@ export class CodexAppServerClient {
|
|
|
56
58
|
onServerRequest(handler) {
|
|
57
59
|
this.requestHandler = handler;
|
|
58
60
|
}
|
|
61
|
+
onClose(handler) {
|
|
62
|
+
this.closeHandler = handler;
|
|
63
|
+
}
|
|
59
64
|
/** Spawn the app-server. Handlers must already be registered. */
|
|
60
65
|
start() {
|
|
66
|
+
if (this.closed)
|
|
67
|
+
throw new Error(this.closeReason ?? "app-server client is closed");
|
|
61
68
|
if (this.child)
|
|
62
69
|
return;
|
|
63
70
|
const command = this.options.command ?? "codex";
|
|
@@ -70,6 +77,12 @@ export class CodexAppServerClient {
|
|
|
70
77
|
stdio: ["pipe", "pipe", "pipe"],
|
|
71
78
|
shell: false,
|
|
72
79
|
});
|
|
80
|
+
this.child.stdin.on("error", (error) => {
|
|
81
|
+
// Failed spawns have no pid; ChildProcess.error owns their OS diagnostic.
|
|
82
|
+
if (this.child?.pid === undefined)
|
|
83
|
+
return;
|
|
84
|
+
this.failInput(error);
|
|
85
|
+
});
|
|
73
86
|
this.child.stdout.setEncoding("utf8");
|
|
74
87
|
this.lines = createInterface({ input: this.child.stdout, crlfDelay: Infinity });
|
|
75
88
|
this.lines.on("line", (line) => this.acceptLine(line));
|
|
@@ -114,6 +127,8 @@ export class CodexAppServerClient {
|
|
|
114
127
|
this.handleLine(line);
|
|
115
128
|
}
|
|
116
129
|
handleLine(line) {
|
|
130
|
+
if (this.closed)
|
|
131
|
+
return;
|
|
117
132
|
if (line.length > MAX_LINE_BYTES) {
|
|
118
133
|
// Oversized is a transport failure: parsing it would stall the event loop.
|
|
119
134
|
this.failAll("app-server sent an oversized line");
|
|
@@ -208,7 +223,17 @@ export class CodexAppServerClient {
|
|
|
208
223
|
}, timeoutMs);
|
|
209
224
|
}
|
|
210
225
|
this.pending.set(id, entry);
|
|
211
|
-
|
|
226
|
+
try {
|
|
227
|
+
this.write({ id, method, params: params ?? {} });
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
// Invalid caller data was never written. Release only this request;
|
|
231
|
+
// serialization failures say nothing about the transport's health.
|
|
232
|
+
this.pending.delete(id);
|
|
233
|
+
if (entry.timer)
|
|
234
|
+
clearTimeout(entry.timer);
|
|
235
|
+
reject(error);
|
|
236
|
+
}
|
|
212
237
|
});
|
|
213
238
|
}
|
|
214
239
|
notify(method, params) {
|
|
@@ -218,9 +243,27 @@ export class CodexAppServerClient {
|
|
|
218
243
|
}
|
|
219
244
|
write(payload) {
|
|
220
245
|
const child = this.child;
|
|
221
|
-
if (
|
|
246
|
+
if (this.closed)
|
|
222
247
|
return;
|
|
223
|
-
child.
|
|
248
|
+
if (child && child.pid === undefined)
|
|
249
|
+
return;
|
|
250
|
+
if (!child || child.stdin.destroyed || !child.stdin.writable) {
|
|
251
|
+
this.failInput();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const line = `${JSON.stringify(payload)}\n`;
|
|
255
|
+
try {
|
|
256
|
+
child.stdin.write(line);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
this.failInput(error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
failInput(error) {
|
|
263
|
+
this.failAll(`app-server input pipe closed${error?.code ? ` (${diagnosticText(error.code)})` : ""}`);
|
|
264
|
+
// Reject outstanding RPCs now and terminate even when the process keeps
|
|
265
|
+
// running after closing fd 0. close() owns one bounded cleanup sequence.
|
|
266
|
+
void this.close();
|
|
224
267
|
}
|
|
225
268
|
failAll(reason, details = {}) {
|
|
226
269
|
if (this.closed)
|
|
@@ -234,15 +277,35 @@ export class CodexAppServerClient {
|
|
|
234
277
|
entry.reject(new Error(reason));
|
|
235
278
|
}
|
|
236
279
|
this.pending.clear();
|
|
280
|
+
try {
|
|
281
|
+
this.closeHandler?.(reason);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
this.log("appserver.close_handler_failed", {
|
|
285
|
+
error: error instanceof Error ? error.name : "unknown",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** Why the transport closed, once it has. */
|
|
290
|
+
get closedReason() {
|
|
291
|
+
return this.closeReason;
|
|
237
292
|
}
|
|
238
293
|
get isClosed() {
|
|
239
294
|
return this.closed;
|
|
240
295
|
}
|
|
241
296
|
/** Stop via stdin EOF, then SIGTERM and SIGKILL if the server does not exit. */
|
|
242
|
-
|
|
297
|
+
close() {
|
|
298
|
+
if (!this.closing) {
|
|
299
|
+
// Publish cleanup ownership before notifying callers, whose onClose may
|
|
300
|
+
// itself call close(). One process must have only one escalation timer.
|
|
301
|
+
this.closing = Promise.resolve().then(() => this.stop());
|
|
302
|
+
this.failAll("app-server client closed");
|
|
303
|
+
this.lines?.close();
|
|
304
|
+
}
|
|
305
|
+
return this.closing;
|
|
306
|
+
}
|
|
307
|
+
async stop() {
|
|
243
308
|
const child = this.child;
|
|
244
|
-
this.failAll("app-server client closed");
|
|
245
|
-
this.lines?.close();
|
|
246
309
|
// Failed spawns have no pid and never emit `exit`. A process already killed
|
|
247
310
|
// by a signal likewise has no numeric exitCode and will not emit it again.
|
|
248
311
|
if (!child || !child.pid || child.exitCode !== null || child.signalCode !== null)
|
|
@@ -48,6 +48,21 @@ function toolArgsOf(item) {
|
|
|
48
48
|
}
|
|
49
49
|
return args;
|
|
50
50
|
}
|
|
51
|
+
function toolErrorOf(item, type) {
|
|
52
|
+
const detail = str(asRecord(item.error)?.message) ?? str(item.error);
|
|
53
|
+
if (detail)
|
|
54
|
+
return detail;
|
|
55
|
+
// A completed app-server item means the operation settled, not that its
|
|
56
|
+
// command succeeded. In particular, nonzero exits often have no error object.
|
|
57
|
+
if (type === "commandExecution" && Number.isInteger(item.exitCode) && item.exitCode !== 0) {
|
|
58
|
+
return `Command exited with code ${item.exitCode}.`;
|
|
59
|
+
}
|
|
60
|
+
if (item.status === "failed")
|
|
61
|
+
return `${type} failed.`;
|
|
62
|
+
if (item.status === "declined")
|
|
63
|
+
return `${type} was declined.`;
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
51
66
|
/**
|
|
52
67
|
* Map a Codex turn status onto a CodeShell `TerminalReason`.
|
|
53
68
|
*
|
|
@@ -350,8 +365,7 @@ export class CodexEventTranslator {
|
|
|
350
365
|
output = String(rawOutput);
|
|
351
366
|
}
|
|
352
367
|
}
|
|
353
|
-
const
|
|
354
|
-
const errorMessage = str(error?.message) ?? str(item.error);
|
|
368
|
+
const errorMessage = toolErrorOf(item, type);
|
|
355
369
|
const events = [];
|
|
356
370
|
// Correct the arguments first when they only materialized now, so the
|
|
357
371
|
// recorded call shows what actually ran before its result.
|
|
@@ -77,6 +77,7 @@ export declare class CodexRuntime {
|
|
|
77
77
|
private readonly log;
|
|
78
78
|
private translator?;
|
|
79
79
|
private threadId?;
|
|
80
|
+
private reportedModel?;
|
|
80
81
|
private started;
|
|
81
82
|
private resumed;
|
|
82
83
|
private firstTurn;
|
|
@@ -85,6 +86,8 @@ export declare class CodexRuntime {
|
|
|
85
86
|
/** Codex thread id, once the thread exists. Protocol routing only — never the
|
|
86
87
|
* authorization subject (§8.1). */
|
|
87
88
|
get runtimeSessionId(): string | undefined;
|
|
89
|
+
/** CLI-reported model, falling back to the requested model until reported. */
|
|
90
|
+
get resolvedModel(): string | undefined;
|
|
88
91
|
/**
|
|
89
92
|
* Spawn the app-server, handshake, and open a thread.
|
|
90
93
|
*
|
|
@@ -111,7 +114,18 @@ export declare class CodexRuntime {
|
|
|
111
114
|
* that no further step starts (§13.3).
|
|
112
115
|
*/
|
|
113
116
|
interrupt(): Promise<void>;
|
|
117
|
+
private requestInterrupt;
|
|
114
118
|
close(): Promise<void>;
|
|
119
|
+
/** Learn the active turn's id; deliver a Stop that was waiting for it, once. */
|
|
120
|
+
private bindTurnId;
|
|
121
|
+
/**
|
|
122
|
+
* The app-server is gone (exited, failed, broken stream). A turn Codex already
|
|
123
|
+
* accepted would otherwise wait forever for a `turn/completed` that can never
|
|
124
|
+
* arrive, so report it terminal now as a model error. A turn whose
|
|
125
|
+
* `turn/start` is still unanswered is NOT reported here: that RPC rejects, and
|
|
126
|
+
* the rejected `send()` is its caller's terminal signal.
|
|
127
|
+
*/
|
|
128
|
+
private onTransportClosed;
|
|
115
129
|
private onNotification;
|
|
116
130
|
private emitEvent;
|
|
117
131
|
/**
|