@nowcrew/daemon 0.5.19 → 0.5.21

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,17 +1,19 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, ExecutionRejectedSchema, ExecutionStartSchema, } from "./execution-protocol.js";
5
4
  import { JournalConflictError } from "./execution-journal.js";
6
5
  import { boundExecutionFrame } from "./execution-event-limit.js";
7
6
  import { mintAgentToken } from "./token.js";
8
7
  import { executeLocal, withLocalExecutionFacts, } from "./local-executor.js";
9
8
  import { startDormantSupervisor, } from "./execution-supervisor.js";
10
- import { buildClaudeArgs, CLAUDE_EFFORT_LEVELS } from "./runtimes/claude.js";
9
+ import { CLAUDE_EFFORT_LEVELS } from "./runtimes/claude.js";
11
10
  import { CODEX_EFFORT_LEVELS } from "./runtimes/codex.js";
12
11
  import { KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
13
12
  import { executionBackendCapability } from "./execution-backend.js";
14
13
  import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
14
+ import { RuntimeCancelledError } from "./runtime-cancellation.js";
15
+ import { supervisorLaunch } from "./supervised-runtime.js";
16
+ export { supervisorLaunch } from "./supervised-runtime.js";
15
17
  const ACTIVITY_KIND = {
16
18
  init: "working",
17
19
  text: "thinking",
@@ -183,70 +185,6 @@ function launchProviderConfig(config) {
183
185
  ...(config.description === undefined ? {} : { description: config.description }),
184
186
  };
185
187
  }
186
- export function supervisorLaunch(request) {
187
- const common = {
188
- wakePrompt: request.wakePrompt,
189
- dangerous: request.effectivePermission === "full_access",
190
- effectivePermission: request.effectivePermission,
191
- ...(request.model === undefined ? {} : { model: request.model }),
192
- ...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
193
- };
194
- if (request.runtime === "claude") {
195
- return {
196
- command: request.bin,
197
- args: buildClaudeArgs({
198
- ...common,
199
- bin: request.bin,
200
- cwd: request.cwd,
201
- env: request.env,
202
- systemPromptPath: request.systemPromptPath,
203
- ...(request.sessionId === undefined ? {} : {
204
- sessionId: request.sessionId,
205
- resume: request.resume,
206
- }),
207
- }),
208
- cwd: request.cwd,
209
- env: request.env,
210
- };
211
- }
212
- if (request.runtime === "codex") {
213
- return {
214
- command: process.execPath,
215
- args: [
216
- fileURLToPath(new URL("./runtimes/codex-app-server-runner.js", import.meta.url)),
217
- "--bin", request.bin,
218
- ],
219
- cwd: request.cwd,
220
- env: request.env,
221
- stdinText: JSON.stringify({
222
- systemPrompt: request.systemPrompt,
223
- wakePrompt: request.wakePrompt,
224
- effectivePermission: request.effectivePermission,
225
- ...(request.model === undefined ? {} : { model: request.model }),
226
- ...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
227
- ...(request.sessionId === undefined ? {} : { sessionId: request.sessionId }),
228
- ...(request.imagePaths === undefined ? {} : { imagePaths: request.imagePaths }),
229
- resume: request.resume,
230
- }),
231
- };
232
- }
233
- if (request.effectivePermission !== "full_access") {
234
- throw new Error(`Kimi ACP cannot enforce ${request.effectivePermission} permission`);
235
- }
236
- return {
237
- command: process.execPath,
238
- args: [
239
- fileURLToPath(new URL("./runtimes/kimi-acp-runner.js", import.meta.url)),
240
- "--bin", request.bin,
241
- ...(request.model === undefined ? [] : ["--model", request.model]),
242
- ...(request.sessionId === undefined ? [] : ["--session", request.sessionId]),
243
- ...(request.resume ? ["--resume"] : []),
244
- ],
245
- cwd: request.cwd,
246
- env: request.env,
247
- stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
248
- };
249
- }
250
188
  function rejection(executionId, reason, message, at) {
251
189
  return ExecutionRejectedSchema.parse({
252
190
  type: "execution:rejected",
@@ -494,7 +432,7 @@ export async function runExecution(config, input, dependencies) {
494
432
  catch { /* best-effort console omitted when its envelope cannot fit */ }
495
433
  },
496
434
  } : {}),
497
- ...(spec.context.externalResponseSessionId ? {
435
+ ...(spec.context.externalResponseSessionId || spec.context.answerStream ? {
498
436
  onExternalOutput: (text) => {
499
437
  const frame = DaemonToServerExecutionFrameSchema.parse({
500
438
  type: "execution:output",
@@ -513,6 +451,7 @@ export async function runExecution(config, input, dependencies) {
513
451
  } : {}),
514
452
  };
515
453
  const localDependencies = {
454
+ ...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
516
455
  launchRuntime: async (request) => {
517
456
  if (launchClosed || dependencies.cancellation?.isRequested())
518
457
  throw new ExecutionCancelledError();
@@ -521,27 +460,42 @@ export async function runExecution(config, input, dependencies) {
521
460
  launchAttempts.add(launchSettled);
522
461
  try {
523
462
  const processStartedAt = now().toISOString();
524
- const guarded = await dependencies.journal.startGuarded(spec.executionId, processStartedAt, () => startSupervisor(supervisorLaunch(request)));
463
+ const launchControl = { cancel: null };
464
+ const guarded = await dependencies.journal.startGuarded(spec.executionId, processStartedAt, () => startSupervisor(supervisorLaunch(request)), {
465
+ beforeRelease: ({ entry, handle, abort }) => {
466
+ supervisorState.active = handle;
467
+ let stopPromise = null;
468
+ let releaseStarted = false;
469
+ const stopOnce = (operation) => {
470
+ if (stopPromise === null) {
471
+ try {
472
+ stopPromise = Promise.resolve(operation());
473
+ }
474
+ catch (error) {
475
+ stopPromise = Promise.reject(error);
476
+ }
477
+ }
478
+ return stopPromise;
479
+ };
480
+ launchControl.cancel = () => stopOnce(releaseStarted ? handle.cancel : abort);
481
+ supervisorState.abortOnce = () => stopOnce(abort);
482
+ dependencies.cancellation?.register(launchControl.cancel);
483
+ startedAt = entry.processStartedAt ?? processStartedAt;
484
+ if (dependencies.cancellation?.isRequested()) {
485
+ return dependencies.cancellation.waitForStop().then(() => {
486
+ throw new ExecutionCancelledError();
487
+ });
488
+ }
489
+ releaseStarted = true;
490
+ },
491
+ });
525
492
  if (guarded.kind !== "started") {
526
493
  throw new Error(`Execution became ${guarded.entry.state} before local launch`);
527
494
  }
528
- supervisorState.active = guarded.handle;
529
- let stopPromise = null;
530
- const stopOnce = (operation) => {
531
- if (stopPromise === null) {
532
- try {
533
- stopPromise = Promise.resolve(operation());
534
- }
535
- catch (error) {
536
- stopPromise = Promise.reject(error);
537
- }
538
- }
539
- return stopPromise;
540
- };
541
- const cancelOnce = () => stopOnce(guarded.handle.cancel);
542
- supervisorState.abortOnce = () => stopOnce(guarded.handle.abort);
543
- dependencies.cancellation?.register(cancelOnce);
544
- startedAt = guarded.entry.processStartedAt ?? processStartedAt;
495
+ if (launchControl.cancel === null) {
496
+ throw new Error("Execution launch cancellation gate was not installed");
497
+ }
498
+ const installedCancel = launchControl.cancel;
545
499
  await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
546
500
  type: "execution:started",
547
501
  protocolVersion: 1,
@@ -552,14 +506,14 @@ export async function runExecution(config, input, dependencies) {
552
506
  timeout = setTimeout(() => {
553
507
  timedOut = true;
554
508
  try {
555
- void cancelOnce().catch(rejectCancellationFailure);
509
+ void installedCancel().catch(rejectCancellationFailure);
556
510
  }
557
511
  catch (error) {
558
512
  rejectCancellationFailure(error);
559
513
  }
560
514
  }, effectiveTimeoutMs);
561
515
  }
562
- return guarded.handle;
516
+ return { ...guarded.handle, cancel: installedCancel };
563
517
  }
564
518
  finally {
565
519
  launchAttempts.delete(launchSettled);
@@ -608,10 +562,10 @@ export async function runExecution(config, input, dependencies) {
608
562
  maxTurns: config.sessionMaxTurns,
609
563
  },
610
564
  };
611
- const result = await cancellable(Promise.race([
565
+ const result = await Promise.race([
612
566
  execute(localInput, callbacks, localDependencies),
613
567
  cancellationFailure,
614
- ]), dependencies.cancellation);
568
+ ]);
615
569
  if (timeout !== undefined)
616
570
  clearTimeout(timeout);
617
571
  const finishedAt = now().toISOString();
@@ -652,6 +606,9 @@ export async function runExecution(config, input, dependencies) {
652
606
  ...(!spec.reporting.captureFinal || result.finalText === null
653
607
  ? {}
654
608
  : { finalText: result.finalText }),
609
+ ...(spec.context.answerStream && result.exitCode === 0 && result.externalAnswer
610
+ ? { externalAnswer: result.externalAnswer }
611
+ : {}),
655
612
  ...(result.usage === undefined ? {} : { usage: result.usage }),
656
613
  startedAt,
657
614
  finishedAt,
@@ -660,7 +617,8 @@ export async function runExecution(config, input, dependencies) {
660
617
  catch (error) {
661
618
  if (timeout !== undefined)
662
619
  clearTimeout(timeout);
663
- if (error instanceof ExecutionCancelledError) {
620
+ const cancelled = error instanceof ExecutionCancelledError || error instanceof RuntimeCancelledError;
621
+ if (cancelled) {
664
622
  await closeLaunchGate();
665
623
  await dependencies.cancellation?.waitForStop();
666
624
  }
@@ -673,7 +631,7 @@ export async function runExecution(config, input, dependencies) {
673
631
  throw new AggregateError([error, abortError], `Failed to stop the execution supervisor: ${detail}`);
674
632
  }
675
633
  }
676
- completion = error instanceof ExecutionCancelledError
634
+ completion = cancelled
677
635
  ? ExecutionCompletedSchema.parse({
678
636
  type: "execution:completed",
679
637
  protocolVersion: 1,
@@ -1,6 +1,7 @@
1
1
  import spawn from "cross-spawn";
2
2
  import { z } from "zod";
3
3
  import { pathToFileURL } from "node:url";
4
+ import { assignProcessToJob, createKillOnCloseJob, terminateJob, } from "./win32-job-object.js";
4
5
  const LaunchSchema = z.object({
5
6
  command: z.string().min(1),
6
7
  args: z.array(z.string()),
@@ -8,6 +9,9 @@ const LaunchSchema = z.object({
8
9
  env: z.record(z.string()),
9
10
  stdinText: z.string().optional(),
10
11
  }).strict();
12
+ function messageOf(error) {
13
+ return error instanceof Error ? error.message : String(error);
14
+ }
11
15
  function send(message, callback) {
12
16
  if (!process.connected) {
13
17
  callback?.();
@@ -56,10 +60,17 @@ function forwardWhileWritable(source, destination) {
56
60
  }
57
61
  export function runExecutionSupervisorChild() {
58
62
  let launch = null;
63
+ // 是否用 Job Object 拥有 runtime 树。由 parent 依据「durable + win32 + Job Object 后端已选中」决定并下发;
64
+ // legacy(process-lifetime)路径恒为 false —— 关键:不能仅凭 process.platform 就建 Job,否则灰度关时
65
+ // legacy Windows 执行会被强行套 Job,且 koffi 加载失败会误杀 legacy runtime(退回 fail-closed 承诺被破坏)。
66
+ let useJobObject = false;
59
67
  let released = false;
60
68
  let runtime = null;
61
69
  let settled = false;
62
70
  let cleaningTree = false;
71
+ // win32:本 child 创建并持有的 Job(KILL_ON_JOB_CLOSE)。持有它 = 拥有整棵 runtime 进程树:
72
+ // 本进程一死(含崩溃)内核即回收 Job 内全部进程,等价于 POSIX killpg 且覆盖 supervisor 自身崩溃。
73
+ let jobHandle = null;
63
74
  const outputForwarders = [];
64
75
  const terminateOwnedTree = () => {
65
76
  if (cleaningTree)
@@ -68,6 +79,15 @@ export function runExecutionSupervisorChild() {
68
79
  for (const forwarder of outputForwarders)
69
80
  forwarder.discard();
70
81
  if (process.platform === "win32") {
82
+ // 显式一次性杀光 Job 内进程;即便这里失败,process.exit 关闭 job handle 也会触发内核回收。
83
+ if (jobHandle !== null) {
84
+ try {
85
+ terminateJob(jobHandle);
86
+ }
87
+ catch {
88
+ // handle 随进程退出自动关闭,KILL_ON_JOB_CLOSE 兜底。
89
+ }
90
+ }
71
91
  process.exit(1);
72
92
  return;
73
93
  }
@@ -126,6 +146,7 @@ export function runExecutionSupervisorChild() {
126
146
  return;
127
147
  }
128
148
  launch = parsed.data;
149
+ useJobObject = raw.useJobObject === true;
129
150
  send({ type: "ready" });
130
151
  return;
131
152
  }
@@ -139,6 +160,18 @@ export function runExecutionSupervisorChild() {
139
160
  if (raw.type !== "release" || released || launch === null)
140
161
  return;
141
162
  released = true;
163
+ if (useJobObject) {
164
+ // 所有权链条落地(仅 durable win32):先建 Job(带 KILL_ON_JOB_CLOSE),再 spawn,spawn 后立即 assign。
165
+ // 建 Job 失败即无法保证所有权 → 宁可不 spawn,报 spawn-error 让上层 fail-closed。
166
+ try {
167
+ jobHandle = createKillOnCloseJob();
168
+ }
169
+ catch (error) {
170
+ send({ type: "runtime-spawn-error", message: `Job Object creation failed: ${messageOf(error)}` });
171
+ process.exit(2);
172
+ return;
173
+ }
174
+ }
142
175
  const child = spawn(launch.command, launch.args, {
143
176
  cwd: launch.cwd,
144
177
  env: launch.env,
@@ -146,6 +179,24 @@ export function runExecutionSupervisorChild() {
146
179
  stdio: [launch.stdinText === undefined ? "ignore" : "pipe", "pipe", "pipe"],
147
180
  });
148
181
  runtime = child;
182
+ if (jobHandle !== null && child.pid !== undefined) {
183
+ // spawn 返回后 child.pid 同步可用,此刻立即 assign,把「assign 前已 fork 孙进程」的窗口压到最小。
184
+ try {
185
+ assignProcessToJob(jobHandle, child.pid);
186
+ }
187
+ catch (error) {
188
+ // 无法建立所有权:杀掉刚起的 runtime 并拆除,不让它脱离 Job 裸奔。
189
+ send({ type: "runtime-spawn-error", message: `AssignProcessToJobObject failed: ${messageOf(error)}` });
190
+ try {
191
+ child.kill("SIGKILL");
192
+ }
193
+ catch {
194
+ // 已退出。
195
+ }
196
+ terminateOwnedTree();
197
+ return;
198
+ }
199
+ }
149
200
  outputForwarders.push(forwardWhileWritable(child.stdout, process.stdout), forwardWhileWritable(child.stderr, process.stderr));
150
201
  child.once("spawn", () => {
151
202
  send({ type: "runtime-started" });
@@ -1,6 +1,6 @@
1
1
  import { fork } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
- import { executionBackendCapability } from "./execution-backend.js";
3
+ import { executionBackendCapability, ownsRuntimeViaJobObject } from "./execution-backend.js";
4
4
  const DEFAULT_ABORT_TIMEOUT_MS = 5_000;
5
5
  const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5_000;
6
6
  const DEFAULT_TASKKILL_TIMEOUT_MS = 5_000;
@@ -44,6 +44,55 @@ async function waitForProcessGroupExit(pid, timeoutMs) {
44
44
  await new Promise((resolve) => setTimeout(resolve, PROCESS_GROUP_POLL_MS));
45
45
  }
46
46
  }
47
+ async function processGroupExists(pid) {
48
+ try {
49
+ process.kill(-pid, 0);
50
+ return true;
51
+ }
52
+ catch (error) {
53
+ const code = error.code;
54
+ if (code === "ESRCH")
55
+ return false;
56
+ if (code === "EPERM")
57
+ return true;
58
+ throw error;
59
+ }
60
+ }
61
+ async function signalOwnedTreeIfPresent(pid, signal, platform, signalTree) {
62
+ try {
63
+ await signalTree(pid, signal, platform);
64
+ }
65
+ catch (error) {
66
+ if (error.code !== "ESRCH")
67
+ throw error;
68
+ }
69
+ }
70
+ async function terminateAndConfirmOwnedTree(pid, platform, timeoutMs, supervisorClosed, signalTree) {
71
+ const waitUntilStopped = () => platform === "win32"
72
+ ? waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), timeoutMs, pid)
73
+ : waitForProcessGroupExit(pid, timeoutMs);
74
+ let termError;
75
+ try {
76
+ await signalOwnedTreeIfPresent(pid, "SIGTERM", platform, signalTree);
77
+ await waitUntilStopped();
78
+ return;
79
+ }
80
+ catch (error) {
81
+ termError = error;
82
+ }
83
+ try {
84
+ await signalOwnedTreeIfPresent(pid, "SIGKILL", platform, signalTree);
85
+ await waitUntilStopped();
86
+ }
87
+ catch (killError) {
88
+ throw new AggregateError([termError, killError], "Supervisor process-tree termination failed");
89
+ }
90
+ }
91
+ async function confirmOrTerminateOwnedTree(pid, platform, timeoutMs, supervisorClosed, signalTree) {
92
+ if (platform !== "win32" && !(await processGroupExists(pid)))
93
+ return;
94
+ await terminateAndConfirmOwnedTree(pid, platform, timeoutMs, supervisorClosed, signalTree);
95
+ }
47
96
  async function withTimeout(promise, timeoutMs, phase) {
48
97
  let timer;
49
98
  try {
@@ -86,9 +135,15 @@ export async function signalSupervisorTree(pid, signal, platform = process.platf
86
135
  }
87
136
  export async function startDormantSupervisor(launch, options = {}) {
88
137
  const platform = options.platform ?? process.platform;
89
- const backend = executionBackendCapability(platform);
90
- if (!backend.supported)
91
- throw new Error(backend.reason);
138
+ const ownershipMode = options.ownershipMode ?? "durable";
139
+ if (ownershipMode === "durable") {
140
+ const backend = executionBackendCapability(platform);
141
+ if (!backend.supported)
142
+ throw new Error(backend.reason);
143
+ }
144
+ // 仅 durable + win32 + Job Object 后端已选中才让 child 套 Job;legacy 恒 false(见 ownsRuntimeViaJobObject)。
145
+ const useJobObject = ownsRuntimeViaJobObject(ownershipMode, platform);
146
+ const signalTree = options.signalTree ?? signalSupervisorTree;
92
147
  const childEntry = options.childEntry
93
148
  ?? fileURLToPath(new URL("./execution-supervisor-child.js", import.meta.url));
94
149
  const abortTimeoutMs = options.abortTimeoutMs ?? DEFAULT_ABORT_TIMEOUT_MS;
@@ -118,12 +173,16 @@ export async function startDormantSupervisor(launch, options = {}) {
118
173
  ...(supervisorSpawnError === undefined && signal !== null ? { terminationSignal: signal } : {}),
119
174
  }));
120
175
  });
176
+ const supervisorClosed = new Promise((resolve) => child.once("close", () => resolve()));
177
+ let treeStopPromise = null;
178
+ const ensureTreeStopped = () => {
179
+ treeStopPromise ??= confirmOrTerminateOwnedTree(pid, platform, abortTimeoutMs, supervisorClosed, signalTree);
180
+ return treeStopPromise;
181
+ };
121
182
  const exit = supervisorExit.then(async (result) => {
122
- if (platform !== "win32")
123
- await waitForProcessGroupExit(pid, abortTimeoutMs);
183
+ await ensureTreeStopped();
124
184
  return result;
125
185
  });
126
- const supervisorClosed = new Promise((resolve) => child.once("close", () => resolve()));
127
186
  let readyResolve;
128
187
  let readyReject;
129
188
  const ready = new Promise((resolve, reject) => {
@@ -161,41 +220,30 @@ export async function startDormantSupervisor(launch, options = {}) {
161
220
  const abort = async () => {
162
221
  if (child.exitCode !== null || child.signalCode !== null) {
163
222
  await supervisorClosed;
223
+ await ensureTreeStopped();
164
224
  return;
165
225
  }
166
- try {
167
- await signalSupervisorTree(pid, "SIGTERM", platform);
168
- }
169
- catch (error) {
170
- const code = error instanceof Error && "code" in error ? error.code : undefined;
171
- if (code !== "ESRCH")
172
- throw error;
173
- }
174
- try {
175
- await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
176
- }
177
- catch (error) {
178
- try {
179
- await signalSupervisorTree(pid, "SIGKILL", platform);
180
- }
181
- catch (killError) {
182
- const code = killError instanceof Error && "code" in killError ? killError.code : undefined;
183
- if (code !== "ESRCH")
184
- throw new AggregateError([error, killError], "Supervisor abort failed");
185
- }
186
- await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
187
- }
226
+ await ensureTreeStopped();
227
+ await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
228
+ await ensureTreeStopped();
188
229
  };
189
230
  const cancel = async () => {
190
231
  if (child.exitCode !== null || child.signalCode !== null) {
191
232
  await supervisorClosed;
233
+ await ensureTreeStopped();
192
234
  return;
193
235
  }
194
236
  try {
195
237
  await new Promise((resolve, reject) => {
196
238
  child.send({ type: "abort" }, (error) => error === null ? resolve() : reject(error));
197
239
  });
198
- await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
240
+ if (platform === "win32") {
241
+ await ensureTreeStopped();
242
+ }
243
+ else {
244
+ await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
245
+ await ensureTreeStopped();
246
+ }
199
247
  }
200
248
  catch {
201
249
  await abort();
@@ -203,7 +251,7 @@ export async function startDormantSupervisor(launch, options = {}) {
203
251
  };
204
252
  try {
205
253
  await new Promise((resolve, reject) => {
206
- child.send({ type: "launch", launch: { ...launch, args: [...launch.args], env } }, (error) => {
254
+ child.send({ type: "launch", launch: { ...launch, args: [...launch.args], env }, useJobObject }, (error) => {
207
255
  if (error === null)
208
256
  resolve();
209
257
  else
@@ -218,9 +266,8 @@ export async function startDormantSupervisor(launch, options = {}) {
218
266
  });
219
267
  throw error;
220
268
  }
221
- return {
269
+ const handle = {
222
270
  pid,
223
- parentExitGuard: "pipe-eof",
224
271
  stdout: child.stdout,
225
272
  stderr: child.stderr,
226
273
  exit,
@@ -249,4 +296,7 @@ export async function startDormantSupervisor(launch, options = {}) {
249
296
  abort,
250
297
  cancel,
251
298
  };
299
+ return ownershipMode === "durable"
300
+ ? { ...handle, ownershipMode, parentExitGuard: "pipe-eof" }
301
+ : { ...handle, ownershipMode };
252
302
  }
@@ -84,3 +84,31 @@ export function stripExternalAnswerMarkers(value) {
84
84
  }
85
85
  return (sections.length > 0 ? sections.join("") : value).trim();
86
86
  }
87
+ /**
88
+ * 提取 marker 内容:有 marker 返回拼接内容(trim,空→null),无 marker 返回 null。
89
+ * 与 stripExternalAnswerMarkers 的区别:strip 在无 marker 时回退整段原文(finalText 展示用),
90
+ * 本函数用于判定"agent 是否给出了频道直接回复"——必须能区分有无 marker。
91
+ */
92
+ export function extractExternalAnswer(value) {
93
+ const parts = [];
94
+ let found = false;
95
+ let rest = value;
96
+ for (;;) {
97
+ const open = rest.indexOf(EXTERNAL_ANSWER_OPEN);
98
+ if (open < 0)
99
+ break;
100
+ found = true;
101
+ const afterOpen = rest.slice(open + EXTERNAL_ANSWER_OPEN.length);
102
+ const close = afterOpen.indexOf(EXTERNAL_ANSWER_CLOSE);
103
+ if (close < 0) {
104
+ parts.push(afterOpen);
105
+ break;
106
+ }
107
+ parts.push(afterOpen.slice(0, close));
108
+ rest = afterOpen.slice(close + EXTERNAL_ANSWER_CLOSE.length);
109
+ }
110
+ if (!found)
111
+ return null;
112
+ const answer = parts.join("").trim();
113
+ return answer.length > 0 ? answer : null;
114
+ }
package/dist/i18n.js CHANGED
@@ -17,6 +17,8 @@ export function detectDaemonLang(env = process.env) {
17
17
  }
18
18
  const zh = {
19
19
  "Claude session started": "Claude 会话启动",
20
+ "Codex session started": "Codex 会话启动",
21
+ "Files changed": "文件变更",
20
22
  "Run failed": "运行出错",
21
23
  "Run finished": "本轮结束",
22
24
  "Missing CREW_MACHINE_TOKEN (sk_machine_*, printed by seed)": "缺少 CREW_MACHINE_TOKEN(sk_machine_*,由 seed 打印)",
@@ -40,6 +42,7 @@ const zh = {
40
42
  "Saved profile '{{name}}' with private credentials.": "已保存配置 '{{name}}',凭证仅私有可读。",
41
43
  "Service '{{id}}' is not installed": "服务 '{{id}}' 尚未安装",
42
44
  "Upgraded daemon and restart request accepted for '{{name}}'. Verify with status.": "daemon 已升级,并已请求重启 '{{name}}';请用 status 确认。",
45
+ "Upgraded daemon but skipped restart for '{{name}}': {{reason}}": "daemon 已升级,但已跳过 '{{name}}' 的重启:{{reason}}",
43
46
  "Upgraded daemon. Installed services were not restarted; pass --profile to restart one.": "daemon 已升级;已安装服务尚未重启,可传入 --profile 重启指定服务。",
44
47
  "Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry": "服务生命周期必须使用已构建的 daemon 入口(.js),不能使用 TypeScript 开发入口",
45
48
  "Installed '{{id}}'. Use status to confirm runtime state.": "已安装 '{{id}}';请用 status 确认运行状态。",
@@ -47,6 +50,9 @@ const zh = {
47
50
  "{{action}} request accepted for '{{id}}'. Verify with status.": "已接受对 '{{id}}' 的 {{action}} 请求;请用 status 确认。",
48
51
  "--token-stdin requires a token on standard input": "--token-stdin 需要从标准输入读取令牌",
49
52
  "Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@latest": "服务生命周期需要全局安装 @nowcrew/daemon;请运行 npm install --global @nowcrew/daemon@latest",
53
+ "Profile '{{profile}}' conflicts with profile '{{conflict}}': both resolve to agents root '{{agentsRoot}}'. Save it with a unique root, for example: {{command}}": "配置 '{{profile}}' 与配置 '{{conflict}}' 解析到了同一个 agents root '{{agentsRoot}}'。请保存为唯一目录,例如:{{command}}",
54
+ "Daemon is already running (PID={{ownerPid}}) for agents root '{{agentsRoot}}'. Journal: '{{journalPath}}'. Do not delete the active journal lock. Stop the existing daemon before retrying. If this profile is managed as a service, use 'crew-daemon stop --profile {{profileName}}' or 'crew-daemon restart --profile {{profileName}}'.": "daemon 已在运行(PID={{ownerPid}}),agents root 为 '{{agentsRoot}}'。日志目录:'{{journalPath}}'。不要删除活跃进程持有的 journal 锁。请先停止现有 daemon 再重试;如果此 profile 由系统服务管理,请使用 'crew-daemon stop --profile {{profileName}}' 或 'crew-daemon restart --profile {{profileName}}'。",
55
+ "Daemon is already running (PID={{ownerPid}}) for agents root '{{agentsRoot}}'. Journal: '{{journalPath}}'. Do not delete the active journal lock. Stop the existing daemon before retrying. If it is managed as a service, use 'crew-daemon stop --profile <name>' or 'crew-daemon restart --profile <name>'.": "daemon 已在运行(PID={{ownerPid}}),agents root 为 '{{agentsRoot}}'。日志目录:'{{journalPath}}'。不要删除活跃进程持有的 journal 锁。请先停止现有 daemon 再重试;如果它由系统服务管理,请使用 'crew-daemon stop --profile <name>' 或 'crew-daemon restart --profile <name>'。",
50
56
  };
51
57
  export function translateDaemon(lang, message) {
52
58
  if (lang === "zh")
@@ -54,9 +60,5 @@ export function translateDaemon(lang, message) {
54
60
  return message;
55
61
  }
56
62
  export function formatDaemonText(lang, message, values = {}) {
57
- let rendered = translateDaemon(lang, message);
58
- for (const [key, value] of Object.entries(values)) {
59
- rendered = rendered.replaceAll(`{{${key}}}`, String(value));
60
- }
61
- return rendered;
63
+ return translateDaemon(lang, message).replace(/\{\{([^{}]+)\}\}/g, (token, key) => Object.hasOwn(values, key) ? String(values[key]) : token);
62
64
  }