@nowcrew/daemon 0.5.45 → 0.5.47

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";
@@ -31,9 +32,9 @@ import { createCompletionRetransmitter } from "./completion-retransmitter.js";
31
32
  import { createAgentMemoryBridge } from "./agent-memory/bridge.js";
32
33
  import { createRuntimeStartupGate } from "./runtime-startup-gate.js";
33
34
  import { createHostExecutionCoordinator, hostCoordinatedSlotManager, hostCoordinatedStartupGate, } from "./host-execution-coordinator.js";
34
- import { detectDaemonUpdateEligibility, } from "./daemon-update-eligibility.js";
35
+ import { detectDaemonUpdateEligibility, managedDaemonCapabilities, } from "./daemon-update-eligibility.js";
35
36
  import { createDaemonUpdateController } from "./daemon-update-controller.js";
36
- import { installExactDaemonUpdate } from "./daemon-updater.js";
37
+ import { installExactDaemonUpdate, prepareDaemonRestart } from "./daemon-updater.js";
37
38
  import { scheduleServiceRestart } from "./computer-service.js";
38
39
  import { createProjectRegistry } from "./project-skills/registry.js";
39
40
  import { createProjectSkillsController, } from "./project-skills/controller.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);
@@ -98,6 +100,10 @@ export function serve(config, opts = {}) {
98
100
  localSlots,
99
101
  hostCoordinator,
100
102
  })),
103
+ prepareRestart: opts.update?.prepareRestart ?? (() => prepareDaemonRestart({
104
+ localSlots,
105
+ hostCoordinator,
106
+ })),
101
107
  scheduleRestart: opts.update?.scheduleRestart ?? scheduleServiceRestart,
102
108
  sendStatus: (frame) => {
103
109
  try {
@@ -280,7 +286,7 @@ export function serve(config, opts = {}) {
280
286
  reservation.release();
281
287
  };
282
288
  let connectedAt = 0; // 本次 WS 连接建立时刻(断开日志算在线时长用)
283
- initSlog(config.serverUrl, config.machineToken);
289
+ initSlog(config.serverUrl, config.machineToken, { daemonVersion: daemonVersion(), cliVersion: cliVersion(), ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }), agentsRoot: config.agentsRoot });
284
290
  dslog("daemon.start", "daemon 常驻模式启动", { server_url: config.serverUrl, runtime: config.runtimeBin });
285
291
  // 并行调度:同一 agent 可并行处理多个【不同任务】(线程/频道),每任务隔离 cwd+work-log。
286
292
  // scheduled 重复 run 仍由 running 去重;普通同线程 wake 用 legacyTaskTails 串成 FIFO。
@@ -303,17 +309,28 @@ export function serve(config, opts = {}) {
303
309
  // 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
304
310
  void drainSpool();
305
311
  // 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
312
+ const { detectInstalled, detectExecutable = detectExecutionRuntimesWithSignal } = opts.machineInfo ?? {};
306
313
  const helloPromise = collectMachineHello(config.agentsRoot, config.executionLimits, process.platform, {
307
- additionalCapabilities: async () => {
308
- try {
309
- return (await updateEligibility()).eligible ? ["daemon_update_v1"] : [];
310
- }
311
- catch {
312
- return [];
313
- }
314
- },
314
+ ...(detectInstalled ? { detectInstalled } : {}),
315
+ // First hello must not wait for third-party handshakes; optional transports arrive in the refresh.
316
+ detectExecutable: async (installed) => conservativeExecutionRuntimes(installed),
317
+ additionalCapabilities: () => managedDaemonCapabilities(updateEligibility),
315
318
  });
316
- runtimeFacts = helloPromise.then((hello) => hello.executionRuntimes, (error) => {
319
+ runtimeFacts = helloPromise
320
+ .then(async (hello) => {
321
+ if (hello.executionProtocol === undefined)
322
+ return [];
323
+ const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable);
324
+ if (ws === openedSocket && openedSocket.readyState === WebSocket.OPEN
325
+ && detected.some((runtime) => !hello.executionRuntimes.includes(runtime))) {
326
+ detectedExecutionRuntimes = detected;
327
+ latestMachineHello = { ...hello, executionRuntimes: [...detected] };
328
+ latestHelloSocket = openedSocket;
329
+ sendEffectiveMachineHello();
330
+ }
331
+ return detected;
332
+ })
333
+ .catch((error) => {
317
334
  dslog("execution.runtime_detection_failed", "runtime 探测失败", {
318
335
  level: "ERROR", error_message: error.message,
319
336
  });
@@ -370,9 +387,9 @@ export function serve(config, opts = {}) {
370
387
  if (typeof decoded !== "object" || decoded === null)
371
388
  return;
372
389
  const rawType = "type" in decoded && typeof decoded.type === "string" ? decoded.type : "";
373
- if (rawType === "daemon:update") {
390
+ if (rawType === "daemon:update" || rawType === "daemon:restart") {
374
391
  void updateController.handle(decoded).catch((error) => {
375
- dslog("daemon.update_failed", "daemon 更新处理失败", {
392
+ dslog("daemon.control_failed", "daemon 控制操作处理失败", {
376
393
  level: "ERROR", error_message: error.message,
377
394
  });
378
395
  });
@@ -552,7 +569,9 @@ export function serve(config, opts = {}) {
552
569
  let availableRuntimes;
553
570
  try {
554
571
  availableRuntimes = opts.execution?.availableRuntimes?.()
555
- ?? await (runtimeFacts ?? Promise.resolve(detectedExecutionRuntimes));
572
+ ?? (detectedExecutionRuntimes.includes(spec.runtime.name)
573
+ ? detectedExecutionRuntimes
574
+ : await (runtimeFacts ?? Promise.resolve(detectedExecutionRuntimes)));
556
575
  }
557
576
  catch (error) {
558
577
  cleanupExecutionReservation();
@@ -1111,6 +1130,7 @@ export function serve(config, opts = {}) {
1111
1130
  return stopPromise;
1112
1131
  stopPromise = (async () => {
1113
1132
  stopped = true;
1133
+ runtimeProbe.stop();
1114
1134
  completionRetransmitter.stop();
1115
1135
  const deadline = createShutdownDeadline(shutdownTimeoutMs);
1116
1136
  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
  ],
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.45",
3
+ "version": "0.5.47",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",