@nowcrew/daemon 0.5.44 → 0.5.46

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/serve.js CHANGED
@@ -8,7 +8,8 @@ import { randomUUID } from "node:crypto";
8
8
  import { initSlog, dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
9
9
  import { mergeRunAgentResults, reportScheduledStartFailure, runAgent } from "./runner.js";
10
10
  import { buildOriginDecisionRetryPrompt, buildScheduledPrompt } from "./prompt.js";
11
- import { collectMachineHello, daemonCapabilities, EXECUTION_PROTOCOL, } from "./machine-info.js";
11
+ import { collectMachineHello, cliVersion, daemonVersion, daemonCapabilities, detectExecutionRuntimesWithSignal, EXECUTION_PROTOCOL, } from "./machine-info.js";
12
+ import { conservativeExecutionRuntimes, createRuntimeProbeCoordinator, } from "./runtime-probe.js";
12
13
  import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
13
14
  import { listSkills } from "./skills.js";
14
15
  import { inspectRaftWorkspace, importRaftWorkspace } from "./workspace-import.js";
@@ -79,6 +80,7 @@ export function serve(config, opts = {}) {
79
80
  const executeProtocol = opts.execution?.runExecution ?? runExecution;
80
81
  let detectedExecutionRuntimes = [];
81
82
  let runtimeFacts = null;
83
+ const runtimeProbe = createRuntimeProbeCoordinator();
82
84
  const hostCoordinator = opts.execution?.hostCoordinator ?? createHostExecutionCoordinator();
83
85
  const localSlots = createSharedSlotManager(config.executionLimits);
84
86
  const sharedSlots = hostCoordinatedSlotManager(localSlots, hostCoordinator);
@@ -280,7 +282,7 @@ export function serve(config, opts = {}) {
280
282
  reservation.release();
281
283
  };
282
284
  let connectedAt = 0; // 本次 WS 连接建立时刻(断开日志算在线时长用)
283
- initSlog(config.serverUrl, config.machineToken);
285
+ initSlog(config.serverUrl, config.machineToken, { daemonVersion: daemonVersion(), cliVersion: cliVersion(), ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }), agentsRoot: config.agentsRoot });
284
286
  dslog("daemon.start", "daemon 常驻模式启动", { server_url: config.serverUrl, runtime: config.runtimeBin });
285
287
  // 并行调度:同一 agent 可并行处理多个【不同任务】(线程/频道),每任务隔离 cwd+work-log。
286
288
  // scheduled 重复 run 仍由 running 去重;普通同线程 wake 用 legacyTaskTails 串成 FIFO。
@@ -303,7 +305,11 @@ export function serve(config, opts = {}) {
303
305
  // 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
304
306
  void drainSpool();
305
307
  // 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
308
+ const { detectInstalled, detectExecutable = detectExecutionRuntimesWithSignal } = opts.machineInfo ?? {};
306
309
  const helloPromise = collectMachineHello(config.agentsRoot, config.executionLimits, process.platform, {
310
+ ...(detectInstalled ? { detectInstalled } : {}),
311
+ // First hello must not wait for third-party handshakes; optional transports arrive in the refresh.
312
+ detectExecutable: async (installed) => conservativeExecutionRuntimes(installed),
307
313
  additionalCapabilities: async () => {
308
314
  try {
309
315
  return (await updateEligibility()).eligible ? ["daemon_update_v1"] : [];
@@ -313,7 +319,21 @@ export function serve(config, opts = {}) {
313
319
  }
314
320
  },
315
321
  });
316
- runtimeFacts = helloPromise.then((hello) => hello.executionRuntimes, (error) => {
322
+ runtimeFacts = helloPromise
323
+ .then(async (hello) => {
324
+ if (hello.executionProtocol === undefined)
325
+ return [];
326
+ const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable);
327
+ if (ws === openedSocket && openedSocket.readyState === WebSocket.OPEN
328
+ && detected.some((runtime) => !hello.executionRuntimes.includes(runtime))) {
329
+ detectedExecutionRuntimes = detected;
330
+ latestMachineHello = { ...hello, executionRuntimes: [...detected] };
331
+ latestHelloSocket = openedSocket;
332
+ sendEffectiveMachineHello();
333
+ }
334
+ return detected;
335
+ })
336
+ .catch((error) => {
317
337
  dslog("execution.runtime_detection_failed", "runtime 探测失败", {
318
338
  level: "ERROR", error_message: error.message,
319
339
  });
@@ -552,7 +572,9 @@ export function serve(config, opts = {}) {
552
572
  let availableRuntimes;
553
573
  try {
554
574
  availableRuntimes = opts.execution?.availableRuntimes?.()
555
- ?? await (runtimeFacts ?? Promise.resolve(detectedExecutionRuntimes));
575
+ ?? (detectedExecutionRuntimes.includes(spec.runtime.name)
576
+ ? detectedExecutionRuntimes
577
+ : await (runtimeFacts ?? Promise.resolve(detectedExecutionRuntimes)));
556
578
  }
557
579
  catch (error) {
558
580
  cleanupExecutionReservation();
@@ -1111,6 +1133,7 @@ export function serve(config, opts = {}) {
1111
1133
  return stopPromise;
1112
1134
  stopPromise = (async () => {
1113
1135
  stopped = true;
1136
+ runtimeProbe.stop();
1114
1137
  completionRetransmitter.stop();
1115
1138
  const deadline = createShutdownDeadline(shutdownTimeoutMs);
1116
1139
  if (reconnectTimer !== null) {
package/dist/slog.js CHANGED
@@ -12,7 +12,8 @@
12
12
  */
13
13
  import { hostname } from "node:os";
14
14
  import { homedir } from "node:os";
15
- import { join } from "node:path";
15
+ import { join, resolve } from "node:path";
16
+ import { createHash } from "node:crypto";
16
17
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
17
18
  const FLUSH_INTERVAL_MS = 1500;
18
19
  const BATCH_SIZE = 50;
@@ -30,11 +31,28 @@ function spoolDir() {
30
31
  return process.env.CREW_SLS_SPOOL_DIR ?? join(homedir(), ".crew", "logs", "sls-spool");
31
32
  }
32
33
  /** serve/run 启动时调用一次;disabled(CREW_SLS_LOG=off)则保持 no-op。 */
33
- export function initSlog(serverUrl, machineToken) {
34
+ export function initSlog(serverUrl, machineToken, identity = {}) {
34
35
  if (process.env.CREW_SLS_LOG === "off" || process.env.CREW_SLS_LOG === "0")
35
36
  return;
36
37
  cfg = { serverUrl: serverUrl.replace(/\/+$/, ""), token: machineToken };
37
- defaults = { host: hostname(), pid: process.pid };
38
+ let serverHost;
39
+ try {
40
+ serverHost = new URL(serverUrl).hostname || undefined;
41
+ }
42
+ catch {
43
+ serverHost = undefined;
44
+ }
45
+ defaults = {
46
+ host: hostname(),
47
+ pid: process.pid,
48
+ daemon_version: identity.daemonVersion,
49
+ cli_version: identity.cliVersion,
50
+ daemon_profile: identity.profileName,
51
+ server_host: serverHost,
52
+ agents_root_fingerprint: identity.agentsRoot === undefined
53
+ ? undefined
54
+ : createHash("sha256").update(resolve(identity.agentsRoot), "utf8").digest("hex"),
55
+ };
38
56
  // 退出兜底:残余队列同步落 spool(exit 回调只能做同步工作,append 正合适)
39
57
  if (!exitHookInstalled) {
40
58
  exitHookInstalled = true;
@@ -56,15 +56,36 @@ export function supervisorLaunch(request) {
56
56
  }),
57
57
  };
58
58
  }
59
+ if (request.runtime === "opencode") {
60
+ if (request.effectivePermission !== "full_access") {
61
+ throw new Error(`OpenCode cannot enforce ${request.effectivePermission} permission`);
62
+ }
63
+ return {
64
+ command: process.execPath,
65
+ args: [
66
+ fileURLToPath(new URL("./runtimes/opencode-runner.js", import.meta.url)),
67
+ "--bin", request.bin,
68
+ ...(request.model === undefined ? [] : ["--model", request.model]),
69
+ ...(request.reasoning === undefined ? [] : ["--reasoning", request.reasoning]),
70
+ ...(request.sessionId === undefined ? [] : ["--session", request.sessionId]),
71
+ ],
72
+ cwd: request.cwd,
73
+ env: { ...request.env, PWD: request.cwd },
74
+ stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
75
+ };
76
+ }
59
77
  if (request.effectivePermission !== "full_access") {
60
- throw new Error(`Kimi ACP cannot enforce ${request.effectivePermission} permission`);
78
+ const displayName = request.runtime === "hermes" ? "Hermes" : "Kimi";
79
+ throw new Error(`${displayName} ACP cannot enforce ${request.effectivePermission} permission`);
61
80
  }
62
81
  return {
63
82
  command: process.execPath,
64
83
  args: [
65
84
  fileURLToPath(new URL("./runtimes/kimi-acp-runner.js", import.meta.url)),
85
+ "--provider", request.runtime,
66
86
  "--bin", request.bin,
67
87
  ...(request.model === undefined ? [] : ["--model", request.model]),
88
+ ...(request.reasoning === undefined ? [] : ["--reasoning", request.reasoning]),
68
89
  ...(request.sessionId === undefined ? [] : ["--session", request.sessionId]),
69
90
  ...(request.resume ? ["--resume"] : []),
70
91
  ],
@@ -0,0 +1,64 @@
1
+ import { systemCommandRunner, windowsCommandArg, } from "./computer-service.js";
2
+ import { win32 } from "node:path";
3
+ function encoded(value) {
4
+ return Buffer.from(value, "utf16le").toString("base64");
5
+ }
6
+ function sameWindowsPath(left, right) {
7
+ if (left === undefined)
8
+ return false;
9
+ return left.replaceAll("/", "\\").toLowerCase()
10
+ === right.replaceAll("/", "\\").toLowerCase();
11
+ }
12
+ export async function inspectWindowsServiceUpdateScope(spec, npmPrefix, daemonPid = process.pid, runner = systemCommandRunner) {
13
+ if (spec.platform !== "win32")
14
+ return { valid: false };
15
+ const expectedArguments = spec.daemonCommand.slice(1).map(windowsCommandArg).join(" ");
16
+ const entryMarker = windowsCommandArg(win32.resolve(npmPrefix, "node_modules", "@nowcrew", "daemon", "dist", "main.js"));
17
+ const script = [
18
+ "$ErrorActionPreference='Stop';",
19
+ "$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
20
+ "$entry=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[1]));",
21
+ "$pidValue=[int]$args[2];",
22
+ "$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
23
+ "if($tasks.Count -ne 1){[pscustomobject]@{count=$tasks.Count}|ConvertTo-Json -Compress;exit 0};",
24
+ "$task=$tasks[0];",
25
+ "$scheduler=New-Object -ComObject 'Schedule.Service';$scheduler.Connect();",
26
+ "$registered=$scheduler.GetFolder('\\').GetTask($n);$instances=@($registered.GetInstances(0));",
27
+ "$current=Get-CimInstance Win32_Process -Filter ('ProcessId='+$pidValue) -ErrorAction Stop;",
28
+ "$ancestors=@();$cursor=$current;while($cursor.ParentProcessId -gt 0){$cursor=Get-CimInstance Win32_Process -Filter ('ProcessId='+$cursor.ParentProcessId) -ErrorAction SilentlyContinue;if($null -eq $cursor){break};$ancestors+=[int64]$cursor.ProcessId};",
29
+ "$processIdentity=(Get-Process -Id $pidValue -ErrorAction Stop).StartTime.ToUniversalTime().ToFileTimeUtc().ToString();",
30
+ "$actions=@($task.Actions);",
31
+ "$others=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -like 'NowCrew Daemon (*)' -and $_.TaskName -cne $n});",
32
+ "$conflicts=@($others|Where-Object{@($_.Actions|Where-Object{$_.Arguments.IndexOf($entry,[StringComparison]::OrdinalIgnoreCase) -ge 0}).Count -gt 0});",
33
+ "$enginePid=if($instances.Count -eq 1){[int64]$instances[0].EnginePID}else{0};",
34
+ "[pscustomobject]@{count=1;state=[int]$task.State;execute=if($actions.Count -eq 1){$actions[0].Execute}else{$null};arguments=if($actions.Count -eq 1){$actions[0].Arguments}else{$null};instanceCount=$instances.Count;enginePid=$enginePid;engineOwned=($enginePid -eq $pidValue -or $ancestors -contains $enginePid);processIdentity=$processIdentity;conflictingPrefixes=$conflicts.Count}|ConvertTo-Json -Compress;",
35
+ ].join("");
36
+ const result = await runner("powershell.exe", [
37
+ "-NoProfile", "-NonInteractive", "-Command", script,
38
+ encoded(spec.id), encoded(entryMarker), String(daemonPid),
39
+ ]);
40
+ if (result.exitCode !== 0)
41
+ return { valid: false };
42
+ let inspection;
43
+ try {
44
+ inspection = JSON.parse(result.stdout);
45
+ }
46
+ catch {
47
+ return { valid: false };
48
+ }
49
+ const running = inspection.state === 4;
50
+ if (inspection.count !== 1
51
+ || (running && (inspection.instanceCount !== 1 || inspection.engineOwned !== true
52
+ || typeof inspection.processIdentity !== "string" || inspection.processIdentity.length === 0))
53
+ || (!running && inspection.instanceCount !== 0)
54
+ || inspection.conflictingPrefixes !== 0
55
+ || !sameWindowsPath(inspection.execute, spec.daemonCommand[0])
56
+ || inspection.arguments !== expectedArguments) {
57
+ return { valid: false };
58
+ }
59
+ return {
60
+ valid: true,
61
+ running,
62
+ ...(running ? { processIdentity: inspection.processIdentity } : {}),
63
+ };
64
+ }
package/dist/workspace.js CHANGED
@@ -10,6 +10,7 @@
10
10
  import { mkdir, writeFile, readFile, chmod, access } from "node:fs/promises";
11
11
  import { createHash, randomUUID } from "node:crypto";
12
12
  import { join } from "node:path";
13
+ import { dslog } from "./slog.js";
13
14
  /** 文件系统安全的 taskKey:仅留 [\w.-],其余转 _,截断,避免路径穿越/超长。 */
14
15
  export function safeKey(key) {
15
16
  return key.replace(/[^\w.-]+/g, "_").replace(/^[-.]+/, "").slice(0, 80) || "default";
@@ -38,10 +39,24 @@ export async function prepareWorkspace(input) {
38
39
  await mkdir(crewDir, { recursive: true });
39
40
  // MEMORY.md:首次创建"索引 + Active Context"骨架,之后由 agent 自己维护
40
41
  const memoryPath = join(dir, "MEMORY.md");
41
- if (!(await exists(memoryPath))) {
42
- await writeFile(memoryPath, memorySeed(input.handle, input.description), "utf8");
42
+ const memorySeedCreated = !(await exists(memoryPath));
43
+ if (memorySeedCreated) {
44
+ try {
45
+ await writeFile(memoryPath, memorySeed(input.handle, input.description), "utf8");
46
+ }
47
+ catch (error) {
48
+ logMemoryPrepareFailure(input, "memory_seed_write", error);
49
+ throw error;
50
+ }
51
+ }
52
+ let memory;
53
+ try {
54
+ memory = await readFile(memoryPath, "utf8");
55
+ }
56
+ catch (error) {
57
+ logMemoryPrepareFailure(input, "memory_read", error);
58
+ throw error;
43
59
  }
44
- const memory = await readFile(memoryPath, "utf8");
45
60
  // 系统提示词按 execution 隔离;省略 identity 的兼容调用每次使用随机路径。
46
61
  const promptDir = join(crewDir, "prompts");
47
62
  await mkdir(promptDir, { recursive: true });
@@ -93,6 +108,7 @@ export async function prepareWorkspace(input) {
93
108
  crewDir,
94
109
  systemPromptPath,
95
110
  memory,
111
+ memorySeedCreated,
96
112
  homeDir,
97
113
  runDir,
98
114
  workLogPath,
@@ -102,6 +118,21 @@ export async function prepareWorkspace(input) {
102
118
  sessionDir,
103
119
  };
104
120
  }
121
+ function logMemoryPrepareFailure(input, phase, error) {
122
+ try {
123
+ dslog("local_memory.context_prepare_failed", "本地记忆上下文准备失败", {
124
+ level: "WARN",
125
+ execution_id: input.executionId,
126
+ agent_handle: input.handle,
127
+ failure_phase: phase,
128
+ error_name: error instanceof Error ? error.name : "unknown",
129
+ error_code: error?.code,
130
+ });
131
+ }
132
+ catch {
133
+ // Diagnostics cannot replace the original workspace error.
134
+ }
135
+ }
105
136
  /**
106
137
  * 冷启动时轮换会话 id:写入新 uuid 到 <runDir>/.session 并返回。
107
138
  * 已有会话但本轮决定不续用(warm 窗口过期 / CREW_RESUME=off)时调用——起一个全新会话,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.5.44",
3
+ "version": "0.5.46",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -16,13 +16,6 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
- "scripts": {
20
- "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
21
- "build": "tsc -p tsconfig.json",
22
- "prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
23
- "test": "vitest run",
24
- "typecheck": "tsc --noEmit"
25
- },
26
19
  "dependencies": {
27
20
  "@agentclientprotocol/sdk": "1.2.1",
28
21
  "@nowcrew/cli": "^0.4.13",
@@ -40,5 +33,11 @@
40
33
  "tsx": "^4.19.0",
41
34
  "typescript": "^5.6.0",
42
35
  "vitest": "^2.1.0"
36
+ },
37
+ "scripts": {
38
+ "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
39
+ "build": "tsc -p tsconfig.json",
40
+ "test": "vitest run",
41
+ "typecheck": "tsc --noEmit"
43
42
  }
44
- }
43
+ }