@koreanhole/opencodex 2.97.0 → 2.98.0

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.
@@ -1,307 +0,0 @@
1
- import { execFileSync, spawn as nodeSpawn, type ChildProcess } from "node:child_process";
2
- import { closeSync, openSync } from "node:fs";
3
- import type { AdapterEvent, IncomingMeta } from "../../types";
4
- import { commandInvocation } from "../../lib/win-exec";
5
- import { readJsonLines } from "./protocol";
6
- import type { CliAgentTurnDeps, CliAgentTurnPlan } from "./types";
7
-
8
- const DEFAULT_TIMEOUT_MS = 300_000;
9
- const DEFAULT_KILL_GRACE_MS = 2_000;
10
- const DEFAULT_TOOL_BATCH_IDLE_MS = 250;
11
- const MAX_STDERR_BYTES = 8 * 1024;
12
- const MAX_TOOL_CALLS = 16;
13
-
14
- function killWindowsProcessTree(pid: number): void {
15
- const taskkill = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\taskkill.exe`;
16
- execFileSync(taskkill, ["/PID", String(pid), "/T", "/F"], {
17
- stdio: "pipe",
18
- windowsHide: true,
19
- });
20
- }
21
-
22
- /** Run one bounded local-CLI turn. The driver's parser and cleanup stay outside this lifecycle. */
23
- export async function runCliAgentTurn(
24
- plan: CliAgentTurnPlan,
25
- incoming: IncomingMeta,
26
- emit: (event: AdapterEvent) => void,
27
- deps: CliAgentTurnDeps = {},
28
- ): Promise<void> {
29
- const spawnFn = deps.spawn ?? nodeSpawn;
30
- const platform = deps.platform ?? process.platform;
31
- const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
32
- const killGraceMs = deps.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
33
- const reapTimeoutMs = deps.reapTimeoutMs ?? killGraceMs * 2 + 250;
34
- const toolBatchIdleMs = plan.toolBatchIdleMs ?? DEFAULT_TOOL_BATCH_IDLE_MS;
35
-
36
- if (incoming.abortSignal?.aborted) {
37
- await plan.cleanup();
38
- emit({ type: "error", message: `${plan.label} turn was aborted before start.`, status: 499, errorType: "invalid_request_error", code: "client_cancelled", retryable: false });
39
- return;
40
- }
41
-
42
- let child: ChildProcess | undefined;
43
- let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
44
- let killTimer: ReturnType<typeof setTimeout> | undefined;
45
- let toolIdleTimer: ReturnType<typeof setTimeout> | undefined;
46
- let terminalEmitted = false;
47
- let aborted = false;
48
- let timedOut = false;
49
- let toolBatchStopped = false;
50
- let childProcessError: Error | undefined;
51
- let processLifecycle: Promise<void> | undefined;
52
- const stderrChunks: string[] = [];
53
-
54
- const emitOnce = (event: AdapterEvent): void => {
55
- const terminal = event.type === "done" && plan.requireToolCall && plan.state.toolCalls === 0
56
- ? {
57
- type: "error" as const,
58
- message: `${plan.label} returned no tool call for a request that requires one.`,
59
- status: 502,
60
- errorType: "upstream_error",
61
- code: "tool_call_required",
62
- retryable: false,
63
- }
64
- : event;
65
- if (terminal.type === "done" || terminal.type === "error" || terminal.type === "incomplete") {
66
- if (terminalEmitted) return;
67
- terminalEmitted = true;
68
- }
69
- emit(terminal);
70
- };
71
-
72
- const kill = (): void => {
73
- if (!child || child.killed) return;
74
- if (platform === "win32" && child.pid !== undefined) {
75
- try {
76
- (deps.killWindowsProcessTree ?? killWindowsProcessTree)(child.pid);
77
- return;
78
- } catch {
79
- // Fall through to the direct-child termination ladder.
80
- }
81
- }
82
- try { child.kill("SIGTERM"); } catch { /* already gone */ }
83
- killTimer = setTimeout(() => {
84
- try { child?.kill("SIGKILL"); } catch { /* already gone */ }
85
- }, killGraceMs);
86
- };
87
-
88
- const stopStream = (): void => {
89
- try { child?.stdout?.destroy(); } catch { /* already closed */ }
90
- };
91
-
92
- const onAbort = (): void => {
93
- aborted = true;
94
- kill();
95
- stopStream();
96
- };
97
-
98
- const armToolIdle = (): void => {
99
- if (terminalEmitted || toolBatchStopped || plan.state.toolCalls === 0) return;
100
- if (toolIdleTimer) clearTimeout(toolIdleTimer);
101
- toolIdleTimer = setTimeout(() => {
102
- toolBatchStopped = true;
103
- kill();
104
- stopStream();
105
- }, toolBatchIdleMs);
106
- };
107
-
108
- try {
109
- const invocation = commandInvocation(plan.binary, [...plan.args], platform, { env: plan.env });
110
- let stdinDescriptor: number | undefined;
111
- if (plan.stdinFile) {
112
- try {
113
- stdinDescriptor = openSync(plan.stdinFile, "r");
114
- } catch (error) {
115
- emitOnce({
116
- type: "error",
117
- message: `${plan.label} prompt could not be opened: ${error instanceof Error ? error.message : String(error)}`,
118
- status: 500,
119
- errorType: "upstream_error",
120
- code: "cli_prompt_staging_failed",
121
- retryable: false,
122
- });
123
- return;
124
- }
125
- }
126
- try {
127
- child = spawnFn(invocation.file, invocation.args, {
128
- ...invocation.options,
129
- cwd: plan.cwd,
130
- env: plan.env,
131
- stdio: [stdinDescriptor ?? "pipe", "pipe", "pipe"],
132
- windowsHide: true,
133
- });
134
- } catch (error) {
135
- if (stdinDescriptor !== undefined) closeSync(stdinDescriptor);
136
- emitOnce({
137
- type: "error",
138
- message: `${plan.label} could not start: ${error instanceof Error ? error.message : String(error)}`,
139
- status: 500,
140
- errorType: "upstream_error",
141
- code: "cli_spawn_failed",
142
- retryable: false,
143
- });
144
- return;
145
- }
146
- if (stdinDescriptor !== undefined) closeSync(stdinDescriptor);
147
-
148
- processLifecycle = new Promise<void>((resolve) => {
149
- let settled = false;
150
- const settle = (): void => {
151
- if (settled) return;
152
- settled = true;
153
- resolve();
154
- };
155
- child?.once("error", (error) => {
156
- childProcessError = error;
157
- if (child?.pid === undefined) settle();
158
- });
159
- child?.once("close", settle);
160
- if (child?.exitCode !== null) settle();
161
- });
162
-
163
- incoming.abortSignal?.addEventListener("abort", onAbort, { once: true });
164
- timeoutTimer = setTimeout(() => {
165
- timedOut = true;
166
- kill();
167
- stopStream();
168
- emitOnce({
169
- type: "error",
170
- message: `${plan.label} turn timed out.`,
171
- status: 504,
172
- errorType: "upstream_error",
173
- code: "timeout",
174
- retryable: true,
175
- });
176
- }, timeoutMs);
177
-
178
- child.stderr?.setEncoding("utf8");
179
- child.stderr?.on("data", (chunk: string) => {
180
- if (stderrChunks.join("").length < MAX_STDERR_BYTES) stderrChunks.push(chunk);
181
- });
182
-
183
- if (!child.stdout) throw new Error(`${plan.label} produced no stdout stream`);
184
- if (!plan.stdinFile) {
185
- if (!child.stdin) throw new Error(`${plan.label} produced no stdin stream`);
186
- child.stdin.on("error", () => { /* EPIPE when the CLI exits early */ });
187
- child.stdin.end(plan.stdin);
188
- }
189
-
190
- for await (const message of readJsonLines(child.stdout)) {
191
- if (aborted || terminalEmitted) break;
192
- const events = plan.parseLine(message, plan.state);
193
- for (const event of events) emitOnce(event);
194
- if (!terminalEmitted) armToolIdle();
195
- }
196
-
197
- if (plan.state.toolCalls > MAX_TOOL_CALLS && !terminalEmitted) {
198
- emitOnce({
199
- type: "error",
200
- message: `${plan.label} exceeded the local-CLI tool-call limit.`,
201
- status: 502,
202
- errorType: "upstream_error",
203
- code: "tool_call_limit_exceeded",
204
- retryable: false,
205
- });
206
- }
207
-
208
- if (!terminalEmitted && !aborted) {
209
- if (plan.state.toolCalls > 0) {
210
- const exitedCleanly = !childProcessError && !timedOut && child.exitCode === 0;
211
- if (toolBatchStopped || exitedCleanly) {
212
- emitOnce({
213
- type: "done",
214
- stopReason: "tool_use",
215
- ...(plan.state.usage ? { usage: plan.state.usage } : {}),
216
- });
217
- } else {
218
- const failure = plan.classifyFailure?.({
219
- exitCode: child.exitCode ?? 1,
220
- stderr: stderrChunks.join("").slice(0, MAX_STDERR_BYTES),
221
- }) ?? {
222
- type: "error" as const,
223
- message: `${plan.label} ended after emitting a tool call.`,
224
- status: 502,
225
- errorType: "upstream_error",
226
- code: "cli_process_failed",
227
- retryable: false,
228
- };
229
- emitOnce(failure);
230
- }
231
- } else if (plan.requireToolCall) {
232
- emitOnce({
233
- type: "error",
234
- message: `${plan.label} returned no tool call for a request that requires one.`,
235
- status: 502,
236
- errorType: "upstream_error",
237
- code: "tool_call_required",
238
- retryable: false,
239
- });
240
- } else if (childProcessError) {
241
- emitOnce({
242
- type: "error",
243
- message: `${plan.label} failed to start: ${childProcessError.message}`,
244
- status: 500,
245
- errorType: "upstream_error",
246
- code: "cli_spawn_failed",
247
- retryable: false,
248
- });
249
- } else {
250
- const exitCode = child.exitCode ?? 1;
251
- const stderr = stderrChunks.join("").slice(0, MAX_STDERR_BYTES);
252
- const failure = plan.classifyFailure?.({ exitCode, stderr }) ?? {
253
- type: "error" as const,
254
- message: `${plan.label} ended without a terminal frame.`,
255
- status: exitCode === 0 ? 502 : 500,
256
- errorType: "upstream_error",
257
- code: exitCode === 0 ? "cli_protocol_error" : "cli_process_failed",
258
- retryable: false,
259
- };
260
- emitOnce(failure);
261
- }
262
- }
263
- } catch (error) {
264
- if (toolBatchStopped && plan.state.toolCalls > 0) {
265
- emitOnce({
266
- type: "done",
267
- stopReason: "tool_use",
268
- ...(plan.state.usage ? { usage: plan.state.usage } : {}),
269
- });
270
- } else if (!terminalEmitted && !aborted) {
271
- emitOnce({
272
- type: "error",
273
- message: `${plan.label} failed: ${error instanceof Error ? error.message : String(error)}`,
274
- status: 502,
275
- errorType: "upstream_error",
276
- code: "cli_protocol_error",
277
- retryable: false,
278
- });
279
- }
280
- } finally {
281
- if (timeoutTimer) clearTimeout(timeoutTimer);
282
- if (toolIdleTimer) clearTimeout(toolIdleTimer);
283
- incoming.abortSignal?.removeEventListener("abort", onAbort);
284
- try { child?.stdin?.destroy(); } catch { /* ignore */ }
285
-
286
- if (child && child.exitCode === null && !child.killed) kill();
287
- if (child && child.exitCode === null) {
288
- await Promise.race([
289
- processLifecycle ?? new Promise<void>((resolve) => child?.once("close", () => resolve())),
290
- new Promise<void>((resolve) => setTimeout(resolve, reapTimeoutMs)),
291
- ]);
292
- }
293
- if (killTimer) clearTimeout(killTimer);
294
- await plan.cleanup();
295
- }
296
-
297
- if (aborted && !terminalEmitted && !timedOut) {
298
- emitOnce({
299
- type: "error",
300
- message: `${plan.label} turn was cancelled.`,
301
- status: 499,
302
- errorType: "invalid_request_error",
303
- code: "client_cancelled",
304
- retryable: false,
305
- });
306
- }
307
- }