@nowcrew/daemon 0.5.18 → 0.5.20

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 (41) hide show
  1. package/README.md +23 -0
  2. package/dist/attachments.js +196 -0
  3. package/dist/computer-cli.js +72 -12
  4. package/dist/computer-profile-lock.js +395 -0
  5. package/dist/computer-profile.js +189 -20
  6. package/dist/config.js +2 -1
  7. package/dist/console.js +175 -9
  8. package/dist/execution-event-limit.js +1 -1
  9. package/dist/execution-journal-lock.js +199 -40
  10. package/dist/execution-journal.js +42 -4
  11. package/dist/execution-protocol.js +21 -1
  12. package/dist/execution-recovery.js +71 -0
  13. package/dist/execution-runner.js +68 -77
  14. package/dist/execution-supervisor.js +79 -31
  15. package/dist/external-output.js +114 -0
  16. package/dist/i18n.js +5 -5
  17. package/dist/list-models.js +41 -5
  18. package/dist/local-executor.js +103 -14
  19. package/dist/machine-info.js +6 -1
  20. package/dist/main.js +23 -8
  21. package/dist/origin-decision.js +3 -1
  22. package/dist/prompt.js +4 -1
  23. package/dist/runner.js +14 -9
  24. package/dist/runtime-cancellation.js +74 -0
  25. package/dist/runtime-capabilities.js +38 -0
  26. package/dist/runtime-path.js +60 -0
  27. package/dist/runtimes/claude.js +9 -4
  28. package/dist/runtimes/codex-app-server-runner.js +340 -0
  29. package/dist/runtimes/codex.js +10 -4
  30. package/dist/runtimes/kimi-acp-runner.js +117 -17
  31. package/dist/runtimes/kimi.js +2 -0
  32. package/dist/runtimes/progress-watchdog.js +26 -0
  33. package/dist/serve-lifecycle.js +82 -0
  34. package/dist/serve.js +212 -212
  35. package/dist/session.js +1 -1
  36. package/dist/shared-execution-slots.js +68 -0
  37. package/dist/shutdown-deadline.js +32 -0
  38. package/dist/slog.js +34 -20
  39. package/dist/supervised-runtime.js +104 -0
  40. package/dist/websocket-shutdown.js +53 -0
  41. package/package.json +3 -3
@@ -0,0 +1,74 @@
1
+ export class RuntimeCancelledError extends Error {
2
+ constructor(message = "Runtime launch cancelled") {
3
+ super(message);
4
+ this.name = "RuntimeCancelledError";
5
+ }
6
+ }
7
+ export async function awaitWithCancellation(promise, cancellation) {
8
+ if (cancellation === undefined)
9
+ return promise;
10
+ if (cancellation.isRequested())
11
+ throw new RuntimeCancelledError();
12
+ const result = await Promise.race([
13
+ promise,
14
+ cancellation.requested.then(() => { throw new RuntimeCancelledError(); }),
15
+ ]);
16
+ if (cancellation.isRequested())
17
+ throw new RuntimeCancelledError();
18
+ return result;
19
+ }
20
+ export function createRuntimeCancellation() {
21
+ let requested = false;
22
+ let resolveRequested;
23
+ const requestedPromise = new Promise((resolve) => { resolveRequested = resolve; });
24
+ const registrations = new Map();
25
+ const startRegisteredStops = () => {
26
+ if (!requested)
27
+ return;
28
+ for (const [cancel, stopPromise] of registrations) {
29
+ if (stopPromise !== null)
30
+ continue;
31
+ const started = Promise.resolve().then(cancel);
32
+ registrations.set(cancel, started);
33
+ void started.catch(() => undefined);
34
+ }
35
+ };
36
+ const waitForStop = async () => {
37
+ if (!requested)
38
+ return;
39
+ while (true) {
40
+ startRegisteredStops();
41
+ const registrationCount = registrations.size;
42
+ const results = await Promise.allSettled([...registrations.values()].filter((value) => value !== null));
43
+ if (registrations.size !== registrationCount)
44
+ continue;
45
+ const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
46
+ if (failures.length === 1)
47
+ throw failures[0];
48
+ if (failures.length > 1)
49
+ throw new AggregateError(failures, "Runtime cancellation failed");
50
+ return;
51
+ }
52
+ };
53
+ const cancellation = {
54
+ isRequested: () => requested,
55
+ requested: requestedPromise,
56
+ register: (next) => {
57
+ if (registrations.has(next))
58
+ return;
59
+ registrations.set(next, null);
60
+ startRegisteredStops();
61
+ },
62
+ waitForStop,
63
+ };
64
+ return {
65
+ cancellation,
66
+ request: () => {
67
+ if (requested)
68
+ return;
69
+ requested = true;
70
+ resolveRequested();
71
+ startRegisteredStops();
72
+ },
73
+ };
74
+ }
@@ -1,5 +1,43 @@
1
1
  export const LOCAL_EXECUTION_RUNTIMES = ["claude", "codex", "kimi"];
2
+ export const LOCAL_RUNTIME_CAPABILITIES = Object.freeze({
3
+ claude: Object.freeze({
4
+ transport: "claude-stream-json",
5
+ nativeResume: true,
6
+ systemPromptTransport: "file",
7
+ }),
8
+ codex: Object.freeze({
9
+ transport: "codex-app-server",
10
+ nativeResume: true,
11
+ systemPromptTransport: "protocol",
12
+ }),
13
+ kimi: Object.freeze({
14
+ transport: "kimi-acp",
15
+ nativeResume: true,
16
+ systemPromptTransport: "protocol",
17
+ }),
18
+ });
19
+ export function runtimeCapability(runtime) {
20
+ return LOCAL_RUNTIME_CAPABILITIES[runtime];
21
+ }
2
22
  const LOCAL_EXECUTION_RUNTIME_SET = new Set(LOCAL_EXECUTION_RUNTIMES);
3
23
  export function executableRuntimes(installed) {
4
24
  return [...new Set(installed)].filter((runtime) => LOCAL_EXECUTION_RUNTIME_SET.has(runtime));
5
25
  }
26
+ export function routeRuntimeAttachments(runtime, attachments) {
27
+ if (attachments.length === 0)
28
+ return { nativeImagePaths: [], promptSuffix: "" };
29
+ const lines = attachments.map((attachment) => `- ${JSON.stringify(attachment.path)} (${attachment.mime}, ${attachment.sizeBytes} bytes)`);
30
+ return {
31
+ nativeImagePaths: runtime === "codex"
32
+ ? attachments.filter((attachment) => attachment.mime.startsWith("image/"))
33
+ .map((attachment) => attachment.path)
34
+ : [],
35
+ promptSuffix: [
36
+ "",
37
+ "",
38
+ "## Files attached to the triggering message",
39
+ "These files were downloaded into the local execution workspace. Open/read the relevant files before describing their contents; do not infer contents from filenames alone.",
40
+ ...lines,
41
+ ].join("\n"),
42
+ };
43
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * daemon 被非登录/非交互进程(sh -c / pnpm script / tmux / 后台转发)拉起时,继承的 PATH 常退化为系统
3
+ * 默认,缺用户级 CLI 目录——尤其 Claude 官方原生安装器默认的 `~/.local/bin`。结果 `which claude` 探测
4
+ * 落空、真正 spawn 也 ENOENT。Kimi 官方安装器同样只把 `~/.kimi-code/bin` 写入 shell rc。
5
+ * 这里在探测与启动前把常见安装目录补进 PATH,保证「扫得到 = 起得来」。
6
+ *
7
+ * codex 装在 `/usr/local/bin`(系统默认 PATH 本就含之)所以不受影响;本模块对已在 PATH 中的目录是无操作。
8
+ */
9
+ import { existsSync } from "node:fs";
10
+ import { win32, posix } from "node:path";
11
+ const pathApi = (platform) => (platform === "win32" ? win32 : posix);
12
+ /** 该平台常见 CLI 安装目录候选(是否真实存在稍后由 augmentedPath 校验)。 */
13
+ export function commonBinDirs(env = process.env, platform = process.platform) {
14
+ const p = pathApi(platform);
15
+ if (platform === "win32") {
16
+ const dirs = [];
17
+ if (env.USERPROFILE) {
18
+ dirs.push(p.join(env.USERPROFILE, ".kimi-code", "bin"), // Kimi 官方安装器默认落点
19
+ p.join(env.USERPROFILE, ".local", "bin"));
20
+ }
21
+ if (env.APPDATA)
22
+ dirs.push(p.join(env.APPDATA, "npm"));
23
+ if (env.LOCALAPPDATA)
24
+ dirs.push(p.join(env.LOCALAPPDATA, "Microsoft", "WindowsApps"));
25
+ return dirs;
26
+ }
27
+ const dirs = [];
28
+ const home = env.HOME;
29
+ if (home) {
30
+ dirs.push(p.join(home, ".kimi-code", "bin"), // Kimi 官方安装器默认落点
31
+ p.join(home, ".local", "bin"), // Claude 官方原生安装器默认落点
32
+ p.join(home, ".claude", "local"));
33
+ }
34
+ dirs.push("/opt/homebrew/bin", "/usr/local/bin"); // Apple Silicon brew / Intel brew & npm 全局
35
+ if (home) {
36
+ dirs.push(p.join(home, ".npm-global", "bin"), p.join(home, ".bun", "bin"), p.join(home, ".deno", "bin"));
37
+ }
38
+ return dirs;
39
+ }
40
+ /**
41
+ * 在 `env.PATH` 基础上,把候选目录里「真实存在且尚未在 PATH 中」的去重后 prepend,返回新的 PATH 字符串。
42
+ * 用户装的目录优先命中(prepend);全部已存在或都不在磁盘上时,原样返回 `env.PATH`。
43
+ */
44
+ export function augmentedPath(env = process.env, deps = {}, platform = process.platform) {
45
+ const dirExists = deps.dirExists ?? existsSync;
46
+ const sep = pathApi(platform).delimiter;
47
+ const current = env.PATH ?? "";
48
+ const existing = new Set(current.split(sep).filter((entry) => entry.length > 0));
49
+ const extra = [];
50
+ for (const dir of commonBinDirs(env, platform)) {
51
+ if (existing.has(dir) || extra.includes(dir))
52
+ continue;
53
+ if (!dirExists(dir))
54
+ continue;
55
+ extra.push(dir);
56
+ }
57
+ if (extra.length === 0)
58
+ return current;
59
+ return current.length > 0 ? `${extra.join(sep)}${sep}${current}` : extra.join(sep);
60
+ }
@@ -4,22 +4,27 @@
4
4
  // cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
5
5
  import spawn from "cross-spawn";
6
6
  // Claude Code 原生 --effort 档位(claude 2.1.196 实测:--help 与非法值告警均枚举这五档)。
7
- // 白名单外的值(含 "default" 与 codex 专属档)不传参 → 用 claude 自身默认,脏数据不影响启动。
7
+ // 白名单外的值(含 "default" 与 codex 专属档)回落 CLAUDE_DEFAULT_EFFORT,脏数据不影响启动。
8
8
  export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
9
+ // 未配置/非法档位时的默认思考强度:medium 开启原生 thinking(终端透传要展示思考过程),
10
+ // 又不至于 high/max 的 token 开销;agent 配置白名单档位可覆盖。
11
+ export const CLAUDE_DEFAULT_EFFORT = "medium";
9
12
  export function buildClaudeArgs(input) {
10
13
  const args = [
11
14
  "--print",
12
15
  "--verbose",
13
16
  "--output-format",
14
17
  "stream-json",
18
+ "--include-partial-messages",
15
19
  "--append-system-prompt-file",
16
20
  input.systemPromptPath,
17
21
  ];
18
22
  if (input.model)
19
23
  args.push("--model", input.model);
20
- if (input.reasoning && CLAUDE_EFFORT_LEVELS.includes(input.reasoning)) {
21
- args.push("--effort", input.reasoning);
22
- }
24
+ const effort = input.reasoning && CLAUDE_EFFORT_LEVELS.includes(input.reasoning)
25
+ ? input.reasoning
26
+ : CLAUDE_DEFAULT_EFFORT;
27
+ args.push("--effort", effort);
23
28
  if (input.sessionId) {
24
29
  args.push(input.resume ? "--resume" : "--session-id", input.sessionId);
25
30
  }
@@ -0,0 +1,340 @@
1
+ import { once } from "node:events";
2
+ import { createInterface } from "node:readline";
3
+ import { parseArgs } from "node:util";
4
+ import { pathToFileURL } from "node:url";
5
+ import spawn from "cross-spawn";
6
+ import { z } from "zod";
7
+ import { startFirstProgressWatchdog } from "./progress-watchdog.js";
8
+ const RunnerInputSchema = z.object({
9
+ systemPrompt: z.string().min(1),
10
+ wakePrompt: z.string().min(1),
11
+ effectivePermission: z.enum(["sandboxed", "workspace_write", "full_access"]),
12
+ model: z.string().min(1).optional(),
13
+ reasoning: z.string().min(1).optional(),
14
+ sessionId: z.string().min(1).optional(),
15
+ imagePaths: z.array(z.string().min(1)).optional(),
16
+ resume: z.boolean(),
17
+ }).strict();
18
+ const RPC_TIMEOUT_MS = 30_000;
19
+ const ERROR_MESSAGE_CAP = 2_000;
20
+ function safeErrorMessage(error, secrets) {
21
+ let message = error instanceof Error ? error.message : String(error);
22
+ for (const secret of secrets) {
23
+ if (secret)
24
+ message = message.replaceAll(secret, "[prompt redacted]");
25
+ }
26
+ return message.slice(0, ERROR_MESSAGE_CAP);
27
+ }
28
+ function jsonLine(event) {
29
+ if (process.stdout.write(`${JSON.stringify(event)}\n`))
30
+ return Promise.resolve();
31
+ return once(process.stdout, "drain").then(() => undefined);
32
+ }
33
+ function sandboxMode(permission) {
34
+ if (permission === "sandboxed")
35
+ return "read-only";
36
+ if (permission === "workspace_write")
37
+ return "workspace-write";
38
+ return "danger-full-access";
39
+ }
40
+ /** Unexpected approval requests fail closed; only explicit full access may approve operations. */
41
+ export function codexApprovalResponse(method, permission) {
42
+ if (method === "item/commandExecution/requestApproval"
43
+ || method === "item/fileChange/requestApproval") {
44
+ return { decision: permission === "full_access" ? "accept" : "decline" };
45
+ }
46
+ if (method === "item/tool/requestUserInput")
47
+ return { answers: {} };
48
+ return null;
49
+ }
50
+ /** Translate app-server v2 notifications into the daemon's existing runtime event contract. */
51
+ export function mapCodexNotification(method, params) {
52
+ const value = params;
53
+ if (method === "thread/started" && value.thread?.id) {
54
+ return [{ type: "thread.started", thread_id: value.thread.id }];
55
+ }
56
+ if (method !== "item/completed" || value.item === undefined)
57
+ return [];
58
+ if (value.item.type === "agentMessage" && value.item.text) {
59
+ return [{ type: "item.completed", item: { type: "agent_message", text: value.item.text } }];
60
+ }
61
+ if (value.item.type === "commandExecution" && value.item.command) {
62
+ return [{
63
+ type: "item.completed",
64
+ item: {
65
+ type: "command_execution",
66
+ command: value.item.command,
67
+ ...(value.item.status === undefined ? {} : { status: value.item.status }),
68
+ ...(value.item.aggregatedOutput == null ? {} : { aggregated_output: value.item.aggregatedOutput }),
69
+ },
70
+ }];
71
+ }
72
+ return [];
73
+ }
74
+ async function readRunnerInput() {
75
+ process.stdin.setEncoding("utf8");
76
+ let raw = "";
77
+ for await (const chunk of process.stdin)
78
+ raw += String(chunk);
79
+ return RunnerInputSchema.parse(JSON.parse(raw));
80
+ }
81
+ class CodexRpcClient {
82
+ child;
83
+ permission;
84
+ onNotification;
85
+ onFatal;
86
+ nextId = 1;
87
+ pending = new Map();
88
+ closedError = null;
89
+ constructor(child, permission, onNotification, onFatal) {
90
+ this.child = child;
91
+ this.permission = permission;
92
+ this.onNotification = onNotification;
93
+ this.onFatal = onFatal;
94
+ if (child.stdin === null || child.stdout === null) {
95
+ throw new Error("Codex app-server did not expose stdio");
96
+ }
97
+ const lines = createInterface({ input: child.stdout });
98
+ child.stdin.on("error", (error) => this.close(error));
99
+ lines.on("line", (line) => {
100
+ void this.receive(line).catch((error) => {
101
+ this.close(error instanceof Error ? error : new Error(String(error)));
102
+ });
103
+ });
104
+ child.once("error", (error) => this.close(error));
105
+ child.once("close", (code, signal) => {
106
+ this.close(new Error(`Codex app-server exited: code=${code} signal=${signal}`));
107
+ });
108
+ }
109
+ request(method, params, timeoutMs = RPC_TIMEOUT_MS) {
110
+ if (this.closedError !== null)
111
+ return Promise.reject(this.closedError);
112
+ const id = this.nextId++;
113
+ return new Promise((resolve, reject) => {
114
+ const timer = setTimeout(() => {
115
+ this.pending.delete(id);
116
+ reject(new Error(`Codex app-server ${method} timed out after ${timeoutMs}ms`));
117
+ }, timeoutMs);
118
+ this.pending.set(id, { resolve, reject, timer });
119
+ this.write({ jsonrpc: "2.0", id, method, params });
120
+ });
121
+ }
122
+ notify(method, params) {
123
+ this.write({ jsonrpc: "2.0", method, ...(params === undefined ? {} : { params }) });
124
+ }
125
+ write(message) {
126
+ if (this.child.stdin === null || this.child.stdin.destroyed) {
127
+ throw this.closedError ?? new Error("Codex app-server stdin is closed");
128
+ }
129
+ this.child.stdin.write(`${JSON.stringify(message)}\n`);
130
+ }
131
+ async receive(line) {
132
+ let message;
133
+ try {
134
+ message = JSON.parse(line);
135
+ }
136
+ catch {
137
+ process.stderr.write(`Codex app-server emitted invalid JSON: ${line.slice(0, 500)}\n`);
138
+ return;
139
+ }
140
+ if (message.id !== undefined && message.method !== undefined) {
141
+ const result = codexApprovalResponse(message.method, this.permission);
142
+ if (result === null) {
143
+ this.write({
144
+ jsonrpc: "2.0",
145
+ id: message.id,
146
+ error: { code: -32001, message: `NowCrew denied unsupported request ${message.method}` },
147
+ });
148
+ }
149
+ else {
150
+ this.write({ jsonrpc: "2.0", id: message.id, result });
151
+ }
152
+ return;
153
+ }
154
+ if (message.id !== undefined) {
155
+ const pending = this.pending.get(message.id);
156
+ if (pending === undefined)
157
+ return;
158
+ this.pending.delete(message.id);
159
+ clearTimeout(pending.timer);
160
+ if (message.error !== undefined) {
161
+ pending.reject(new Error(`Codex app-server RPC failed: ${message.error.message ?? "unknown error"}`));
162
+ }
163
+ else {
164
+ pending.resolve(message.result);
165
+ }
166
+ return;
167
+ }
168
+ if (message.method !== undefined)
169
+ await this.onNotification(message.method, message.params);
170
+ }
171
+ close(error) {
172
+ if (this.closedError !== null)
173
+ return;
174
+ this.closedError = error;
175
+ this.onFatal(error);
176
+ for (const pending of this.pending.values()) {
177
+ clearTimeout(pending.timer);
178
+ pending.reject(error);
179
+ }
180
+ this.pending.clear();
181
+ }
182
+ }
183
+ async function stopChild(child) {
184
+ if (child.exitCode !== null || child.signalCode !== null)
185
+ return;
186
+ const closed = once(child, "close").then(() => undefined);
187
+ child.kill("SIGTERM");
188
+ let timer;
189
+ const graceful = await Promise.race([
190
+ closed.then(() => true),
191
+ new Promise((resolve) => { timer = setTimeout(() => resolve(false), 1_000); }),
192
+ ]);
193
+ if (timer !== undefined)
194
+ clearTimeout(timer);
195
+ if (!graceful && child.exitCode === null && child.signalCode === null) {
196
+ child.kill("SIGKILL");
197
+ await closed;
198
+ }
199
+ }
200
+ export async function runCodexAppServer(bin) {
201
+ const input = await readRunnerInput();
202
+ const child = spawn(bin, ["app-server", "--listen", "stdio://"], {
203
+ cwd: process.cwd(),
204
+ env: process.env,
205
+ stdio: ["pipe", "pipe", "pipe"],
206
+ });
207
+ child.stderr?.pipe(process.stderr, { end: false });
208
+ let threadId = null;
209
+ let announcedThreadId = null;
210
+ let turnId = null;
211
+ let lastUsage;
212
+ let completionResolve;
213
+ let completionReject;
214
+ const completion = new Promise((resolve, reject) => {
215
+ completionResolve = resolve;
216
+ completionReject = reject;
217
+ });
218
+ void completion.catch(() => undefined);
219
+ let firstProgress = startFirstProgressWatchdog(() => undefined);
220
+ firstProgress.stop();
221
+ const rpc = new CodexRpcClient(child, input.effectivePermission, async (method, params) => {
222
+ if (method === "thread/tokenUsage/updated") {
223
+ lastUsage = params.tokenUsage;
224
+ }
225
+ if (method === "turn/started" || method === "item/started"
226
+ || method === "item/agentMessage/delta" || method === "item/completed") {
227
+ firstProgress.observe();
228
+ }
229
+ for (const event of mapCodexNotification(method, params)) {
230
+ const eventThreadId = typeof event.thread_id === "string" ? event.thread_id : null;
231
+ if (event.type === "thread.started" && eventThreadId === announcedThreadId)
232
+ continue;
233
+ if (event.type === "thread.started")
234
+ announcedThreadId = eventThreadId;
235
+ await jsonLine(event);
236
+ }
237
+ if (method === "turn/completed")
238
+ completionResolve(params);
239
+ if (method === "error" && params.willRetry === false) {
240
+ const detail = params.error?.message;
241
+ if (detail)
242
+ process.stderr.write(`Codex turn error: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n`);
243
+ }
244
+ }, completionReject);
245
+ let cancelling = false;
246
+ const cancel = async () => {
247
+ if (cancelling)
248
+ return;
249
+ cancelling = true;
250
+ if (threadId !== null && turnId !== null) {
251
+ await rpc.request("turn/interrupt", { threadId, turnId }, 5_000).catch(() => undefined);
252
+ }
253
+ await stopChild(child);
254
+ };
255
+ const onSignal = () => { void cancel().finally(() => process.exit(130)); };
256
+ process.once("SIGTERM", onSignal);
257
+ process.once("SIGINT", onSignal);
258
+ try {
259
+ await rpc.request("initialize", {
260
+ clientInfo: { name: "nowcrew-daemon", version: "1" },
261
+ capabilities: { experimentalApi: true, requestAttestation: false },
262
+ });
263
+ rpc.notify("initialized");
264
+ const threadParams = {
265
+ cwd: process.cwd(),
266
+ approvalPolicy: "never",
267
+ sandbox: sandboxMode(input.effectivePermission),
268
+ developerInstructions: input.systemPrompt,
269
+ ...(input.model === undefined ? {} : { model: input.model }),
270
+ };
271
+ const thread = input.resume && input.sessionId !== undefined
272
+ ? await rpc.request("thread/resume", { threadId: input.sessionId, ...threadParams })
273
+ : await rpc.request("thread/start", threadParams);
274
+ threadId = thread.thread.id;
275
+ if (announcedThreadId !== threadId) {
276
+ announcedThreadId = threadId;
277
+ await jsonLine({ type: "thread.started", thread_id: threadId });
278
+ }
279
+ firstProgress = startFirstProgressWatchdog(() => {
280
+ completionReject(new Error("Codex produced no semantic progress within the startup window"));
281
+ void cancel();
282
+ });
283
+ const started = await rpc.request("turn/start", {
284
+ threadId,
285
+ input: [
286
+ { type: "text", text: input.wakePrompt, text_elements: [] },
287
+ ...(input.imagePaths ?? []).map((path) => ({ type: "localImage", path })),
288
+ ],
289
+ ...(input.reasoning === undefined || input.reasoning === "default"
290
+ ? {}
291
+ : { effort: input.reasoning }),
292
+ });
293
+ turnId = started.turn.id;
294
+ const completed = await completion;
295
+ firstProgress.stop();
296
+ const usage = lastUsage?.last;
297
+ await jsonLine({
298
+ type: "turn.completed",
299
+ ...(usage === undefined ? {} : {
300
+ usage: {
301
+ input_tokens: usage.inputTokens ?? 0,
302
+ output_tokens: usage.outputTokens ?? 0,
303
+ cached_input_tokens: usage.cachedInputTokens ?? 0,
304
+ },
305
+ }),
306
+ });
307
+ if (completed.turn?.status === "completed")
308
+ return 0;
309
+ const detail = completed.turn?.error?.message ?? `turn status ${completed.turn?.status ?? "unknown"}`;
310
+ process.stderr.write(`Codex turn failed: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n`);
311
+ return completed.turn?.status === "interrupted" ? 130 : 1;
312
+ }
313
+ catch (error) {
314
+ process.stderr.write(`Codex app-server execution failed: ${safeErrorMessage(error, [input.systemPrompt, input.wakePrompt])}\n`);
315
+ return 1;
316
+ }
317
+ finally {
318
+ firstProgress.stop();
319
+ process.off("SIGTERM", onSignal);
320
+ process.off("SIGINT", onSignal);
321
+ await stopChild(child);
322
+ }
323
+ }
324
+ function binFromArgv(argv) {
325
+ const { values } = parseArgs({
326
+ args: [...argv],
327
+ options: { bin: { type: "string" } },
328
+ });
329
+ if (!values.bin)
330
+ throw new Error("--bin is required");
331
+ return values.bin;
332
+ }
333
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
334
+ runCodexAppServer(binFromArgv(process.argv.slice(2)))
335
+ .then((code) => { process.exitCode = code; })
336
+ .catch((error) => {
337
+ process.stderr.write(`Codex app-server runner failed: ${safeErrorMessage(error, [])}\n`);
338
+ process.exitCode = 1;
339
+ });
340
+ }
@@ -5,17 +5,21 @@
5
5
  import spawn from "cross-spawn";
6
6
  // Codex CLI 原生 model_reasoning_effort 档位(codex 0.135.0 实测:非法值时 config 解析报错枚举这六档)。
7
7
  // 注意:codex 对非法值是硬失败(进程直接退出),所以必须白名单过滤;白名单外(含 "default"、
8
- // claude 专属的 "max")不传 → 用 codex 自身默认。
8
+ // claude 专属的 "max")回落 CODEX_DEFAULT_EFFORT。
9
9
  export const CODEX_EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh"];
10
+ // 未配置/非法档位时的默认思考强度:medium 开启 reasoning(终端透传要展示思考过程);
11
+ // 配置白名单档位(含显式 none 关思考)可覆盖。
12
+ export const CODEX_DEFAULT_EFFORT = "medium";
10
13
  export function buildCodexArgs(input) {
11
14
  // agent 运行目录由 daemon 管理,不是 git 仓库;不带 --skip-git-repo-check 时 codex exec
12
15
  // 会以 "Not inside a trusted directory" 秒退(且只报在本地 stderr),表现为 agent 静默不回复。
13
16
  const args = ["exec", "--json", "--skip-git-repo-check"];
14
17
  if (input.model)
15
18
  args.push("--model", input.model);
16
- if (input.reasoning && CODEX_EFFORT_LEVELS.includes(input.reasoning)) {
17
- args.push("-c", `model_reasoning_effort=${input.reasoning}`);
18
- }
19
+ const effort = input.reasoning && CODEX_EFFORT_LEVELS.includes(input.reasoning)
20
+ ? input.reasoning
21
+ : CODEX_DEFAULT_EFFORT;
22
+ args.push("-c", `model_reasoning_effort=${effort}`);
19
23
  if (input.effectivePermission === "sandboxed")
20
24
  args.push("--sandbox", "read-only");
21
25
  else if (input.effectivePermission === "workspace_write")
@@ -23,6 +27,8 @@ export function buildCodexArgs(input) {
23
27
  else if (input.effectivePermission === "full_access" || (input.effectivePermission === undefined && input.dangerous)) {
24
28
  args.push("--dangerously-bypass-approvals-and-sandbox");
25
29
  }
30
+ for (const imagePath of input.imagePaths ?? [])
31
+ args.push("--image", imagePath);
26
32
  // `-` instructs codex exec to read the prompt from stdin. Keeping the complete prompt out of argv
27
33
  // avoids Windows' command-line length limit when a thread carries a large wake context.
28
34
  args.push("-");