@nowcrew/daemon 0.5.19 → 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.
package/dist/slog.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * 所以补传的日志仍落在正确时间点,时间线不乱;补传条目带 spooled=true + reported_at 归因。
7
7
  * - 上报失败(server 不可达/断网)→ 落本地 spool(~/.crew/logs/sls-spool/*.jsonl);
8
8
  * 下次 WS 重连成功(drainSpool)时补传。「daemon 为什么断开」的现场就靠这批日志还原。
9
- * - 进程退出(SIGINT/SIGTERM/exit)→ 残余队列同步落 spool,下次启动补传。
9
+ * - 进程退出(exit)→ 残余队列同步落 spool,下次启动补传。异步信号由 main.ts 统一收口。
10
10
  *
11
11
  * 永不抛错、永不阻塞业务;未 init 时所有调用是 no-op(兼容单测/一次性 run 模式无凭证场景)。
12
12
  */
@@ -22,8 +22,10 @@ let cfg = null;
22
22
  let defaults = {};
23
23
  let queue = [];
24
24
  let timer = null;
25
- let flushing = false;
25
+ let flushPromise = null;
26
+ let inFlightBatch = [];
26
27
  let lastErrorAt = 0;
28
+ let exitHookInstalled = false;
27
29
  function spoolDir() {
28
30
  return process.env.CREW_SLS_SPOOL_DIR ?? join(homedir(), ".crew", "logs", "sls-spool");
29
31
  }
@@ -34,12 +36,9 @@ export function initSlog(serverUrl, machineToken) {
34
36
  cfg = { serverUrl: serverUrl.replace(/\/+$/, ""), token: machineToken };
35
37
  defaults = { host: hostname(), pid: process.pid };
36
38
  // 退出兜底:残余队列同步落 spool(exit 回调只能做同步工作,append 正合适)
37
- process.once("exit", () => spoolRemainingSync());
38
- for (const sig of ["SIGINT", "SIGTERM"]) {
39
- process.once(sig, () => {
40
- spoolRemainingSync();
41
- process.exit(sig === "SIGINT" ? 130 : 143);
42
- });
39
+ if (!exitHookInstalled) {
40
+ exitHookInstalled = true;
41
+ process.once("exit", () => spoolRemainingSync());
43
42
  }
44
43
  }
45
44
  /** 追加默认关联字段(如 ready 帧下发的 machine_id),之后每条日志自动带上。 */
@@ -73,20 +72,34 @@ export async function flushSlog() {
73
72
  clearTimeout(timer);
74
73
  timer = null;
75
74
  }
76
- if (!cfg || flushing || queue.length === 0)
75
+ if (!cfg)
77
76
  return;
78
- flushing = true;
79
- const batch = queue;
80
- queue = [];
77
+ if (flushPromise !== null)
78
+ return flushPromise;
79
+ const current = (async () => {
80
+ while (queue.length > 0) {
81
+ const batch = queue;
82
+ queue = [];
83
+ inFlightBatch = batch;
84
+ try {
85
+ await post(batch);
86
+ }
87
+ catch (e) {
88
+ appendSpool(batch);
89
+ warnThrottled(`SLS 日志上报失败,已落本地 spool(${batch.length} 条): ${e.message}`);
90
+ }
91
+ finally {
92
+ inFlightBatch = [];
93
+ }
94
+ }
95
+ })();
96
+ flushPromise = current;
81
97
  try {
82
- await post(batch);
83
- }
84
- catch (e) {
85
- appendSpool(batch);
86
- warnThrottled(`SLS 日志上报失败,已落本地 spool(${batch.length} 条): ${e.message}`);
98
+ await current;
87
99
  }
88
100
  finally {
89
- flushing = false;
101
+ if (flushPromise === current)
102
+ flushPromise = null;
90
103
  }
91
104
  }
92
105
  /**
@@ -190,9 +203,10 @@ function rotateSpool(dir) {
190
203
  catch { /* 忽略 */ }
191
204
  }
192
205
  function spoolRemainingSync() {
193
- if (queue.length === 0)
206
+ if (inFlightBatch.length === 0 && queue.length === 0)
194
207
  return;
195
- const batch = queue;
208
+ const batch = [...inFlightBatch, ...queue];
209
+ inFlightBatch = [];
196
210
  queue = [];
197
211
  appendSpool(batch);
198
212
  }
@@ -0,0 +1,104 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { startDormantSupervisor, } from "./execution-supervisor.js";
3
+ import { buildClaudeArgs } from "./runtimes/claude.js";
4
+ import { RuntimeCancelledError } from "./runtime-cancellation.js";
5
+ export function supervisorLaunch(request) {
6
+ const common = {
7
+ wakePrompt: request.wakePrompt,
8
+ dangerous: request.effectivePermission === "full_access",
9
+ effectivePermission: request.effectivePermission,
10
+ ...(request.model === undefined ? {} : { model: request.model }),
11
+ ...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
12
+ };
13
+ if (request.runtime === "claude") {
14
+ return {
15
+ command: request.bin,
16
+ args: buildClaudeArgs({
17
+ ...common,
18
+ bin: request.bin,
19
+ cwd: request.cwd,
20
+ env: request.env,
21
+ systemPromptPath: request.systemPromptPath,
22
+ ...(request.sessionId === undefined ? {} : {
23
+ sessionId: request.sessionId,
24
+ resume: request.resume,
25
+ }),
26
+ }),
27
+ cwd: request.cwd,
28
+ env: request.env,
29
+ };
30
+ }
31
+ if (request.runtime === "codex") {
32
+ return {
33
+ command: process.execPath,
34
+ args: [
35
+ fileURLToPath(new URL("./runtimes/codex-app-server-runner.js", import.meta.url)),
36
+ "--bin", request.bin,
37
+ ],
38
+ cwd: request.cwd,
39
+ env: request.env,
40
+ stdinText: JSON.stringify({
41
+ systemPrompt: request.systemPrompt,
42
+ wakePrompt: request.wakePrompt,
43
+ effectivePermission: request.effectivePermission,
44
+ ...(request.model === undefined ? {} : { model: request.model }),
45
+ ...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
46
+ ...(request.sessionId === undefined ? {} : { sessionId: request.sessionId }),
47
+ ...(request.imagePaths === undefined ? {} : { imagePaths: request.imagePaths }),
48
+ resume: request.resume,
49
+ }),
50
+ };
51
+ }
52
+ if (request.effectivePermission !== "full_access") {
53
+ throw new Error(`Kimi ACP cannot enforce ${request.effectivePermission} permission`);
54
+ }
55
+ return {
56
+ command: process.execPath,
57
+ args: [
58
+ fileURLToPath(new URL("./runtimes/kimi-acp-runner.js", import.meta.url)),
59
+ "--bin", request.bin,
60
+ ...(request.model === undefined ? [] : ["--model", request.model]),
61
+ ...(request.sessionId === undefined ? [] : ["--session", request.sessionId]),
62
+ ...(request.resume ? ["--resume"] : []),
63
+ ],
64
+ cwd: request.cwd,
65
+ env: request.env,
66
+ stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
67
+ };
68
+ }
69
+ export async function launchSupervisedRuntime(request, startSupervisor = startDormantSupervisor, cancellation, platform) {
70
+ if (cancellation?.isRequested())
71
+ throw new RuntimeCancelledError();
72
+ const supervisor = await startSupervisor(supervisorLaunch(request), {
73
+ ownershipMode: "process-lifetime",
74
+ ...(platform === undefined ? {} : { platform }),
75
+ });
76
+ let stopPromise = null;
77
+ const cancelOnce = () => {
78
+ stopPromise ??= Promise.resolve().then(supervisor.cancel);
79
+ return stopPromise;
80
+ };
81
+ cancellation?.register(cancelOnce);
82
+ if (cancellation?.isRequested()) {
83
+ await cancellation.waitForStop();
84
+ throw new RuntimeCancelledError();
85
+ }
86
+ const release = supervisor.release();
87
+ if (cancellation === undefined) {
88
+ await release;
89
+ }
90
+ else {
91
+ await Promise.race([
92
+ release,
93
+ cancellation.requested.then(async () => {
94
+ await cancellation.waitForStop();
95
+ throw new RuntimeCancelledError();
96
+ }),
97
+ ]);
98
+ }
99
+ if (cancellation?.isRequested()) {
100
+ await cancellation.waitForStop();
101
+ throw new RuntimeCancelledError();
102
+ }
103
+ return { ...supervisor, cancel: cancelOnce };
104
+ }
@@ -0,0 +1,53 @@
1
+ import { WebSocket } from "ws";
2
+ export function closeWebSocketWithinDeadline(socket, signal) {
3
+ if (socket === null || socket.readyState === WebSocket.CLOSED)
4
+ return Promise.resolve();
5
+ return new Promise((resolve, reject) => {
6
+ let settled = false;
7
+ const cleanup = () => {
8
+ socket.removeListener("close", onClose);
9
+ socket.removeListener("error", onError);
10
+ signal.removeEventListener("abort", onAbort);
11
+ };
12
+ const settle = (outcome) => {
13
+ if (settled)
14
+ return;
15
+ settled = true;
16
+ cleanup();
17
+ outcome();
18
+ };
19
+ const onClose = () => settle(resolve);
20
+ const onError = () => {
21
+ if (socket.readyState === WebSocket.CLOSED)
22
+ onClose();
23
+ };
24
+ const onAbort = () => {
25
+ settle(() => {
26
+ if (socket.readyState !== WebSocket.CLOSED) {
27
+ try {
28
+ socket.terminate();
29
+ }
30
+ catch { /* deadline error remains authoritative */ }
31
+ }
32
+ reject(signal.reason);
33
+ });
34
+ };
35
+ socket.on("close", onClose);
36
+ socket.on("error", onError);
37
+ signal.addEventListener("abort", onAbort, { once: true });
38
+ if (signal.aborted) {
39
+ onAbort();
40
+ }
41
+ else if (socket.readyState === WebSocket.CLOSED) {
42
+ onClose();
43
+ }
44
+ else if (socket.readyState !== WebSocket.CLOSING) {
45
+ try {
46
+ socket.close();
47
+ }
48
+ catch (error) {
49
+ settle(() => reject(error));
50
+ }
51
+ }
52
+ });
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.5.19",
3
+ "version": "0.5.20",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -21,7 +21,7 @@
21
21
  "cross-spawn": "^7.0.6",
22
22
  "ws": "^8",
23
23
  "zod": "^3.23.0",
24
- "@nowcrew/cli": "^0.4.11"
24
+ "@nowcrew/cli": "^0.4.6"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/cross-spawn": "^6.0.6",