@nowcrew/daemon 0.5.34 → 0.5.36

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,7 @@ 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, DAEMON_CAPABILITIES, EXECUTION_PROTOCOL, } from "./machine-info.js";
11
+ import { collectMachineHello, daemonCapabilities, EXECUTION_PROTOCOL, } from "./machine-info.js";
12
12
  import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
13
13
  import { listSkills } from "./skills.js";
14
14
  import { inspectRaftWorkspace, importRaftWorkspace } from "./workspace-import.js";
@@ -35,26 +35,36 @@ import { detectDaemonUpdateEligibility, } from "./daemon-update-eligibility.js";
35
35
  import { createDaemonUpdateController } from "./daemon-update-controller.js";
36
36
  import { installExactDaemonUpdate } from "./daemon-updater.js";
37
37
  import { scheduleServiceRestart } from "./computer-service.js";
38
+ import { createProjectRegistry } from "./project-skills/registry.js";
39
+ import { createProjectSkillsController, } from "./project-skills/controller.js";
40
+ import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
41
+ import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
42
+ import { createProjectSkillsReconciler, ProjectProjectionError, } from "./project-skills/reconciler.js";
43
+ import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
38
44
  // normalize.ts 的活动种类 → activity 枚举
39
45
  const ACTIVITY_MAP = {
40
46
  init: "working", text: "thinking", reading: "reading", sending: "sending",
41
47
  checking: "checking", claiming: "claiming", crew: "working", tool: "working",
42
48
  tool_result: "working", done: "done", error: "error",
43
49
  };
44
- export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform, jobObjectProbe) {
50
+ export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform, jobObjectProbe, projectSkillsAvailable = true) {
45
51
  const query = new URLSearchParams({ key: machineToken });
46
52
  if (executionBackendCapability(runtimePlatform, jobObjectProbe).supported) {
47
53
  query.set("execution_min", String(EXECUTION_PROTOCOL.min));
48
54
  query.set("execution_max", String(EXECUTION_PROTOCOL.max));
49
55
  }
50
- for (const capability of DAEMON_CAPABILITIES)
51
- query.append("capability", capability);
56
+ for (const capability of daemonCapabilities(runtimePlatform)) {
57
+ if (capability !== PROJECT_SKILLS_CAPABILITY || projectSkillsAvailable) {
58
+ query.append("capability", capability);
59
+ }
60
+ }
52
61
  return `${serverUrl.replace(/^http/, "ws").replace(/\/+$/, "")}/daemon/connect?${query.toString()}`;
53
62
  }
54
63
  export function serve(config, opts = {}) {
55
- const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken);
56
64
  let stopped = false;
57
65
  let ws = null;
66
+ // 当前连接的 server 能力(ready 帧下发;重连后由新 ready 帧刷新)。
67
+ let serverCapabilities = new Set();
58
68
  let backoff = 1000;
59
69
  const maxBackoff = opts.maxBackoffMs ?? 30_000;
60
70
  const testShutdown = readTestShutdownConfiguration(process.env);
@@ -97,6 +107,82 @@ export function serve(config, opts = {}) {
97
107
  catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
98
108
  },
99
109
  });
110
+ const projectionCoordinator = createAgentProjectionCoordinator();
111
+ let projectSkillsController;
112
+ const projectSkillsReconciler = opts.projectSkills?.reconciler ?? createProjectSkillsReconciler({
113
+ agentsRoot: config.agentsRoot,
114
+ coordinator: projectionCoordinator,
115
+ scannedProjects: () => projectSkillsController.scannedProjects(),
116
+ });
117
+ projectSkillsController = opts.projectSkills?.controller ?? createProjectSkillsController({
118
+ registry: createProjectRegistry(config.agentsRoot),
119
+ publish: (frame) => {
120
+ if (!serverCapabilities.has(PROJECT_SKILLS_CAPABILITY))
121
+ return;
122
+ try {
123
+ if (ws?.readyState === WebSocket.OPEN)
124
+ ws.send(JSON.stringify(frame));
125
+ }
126
+ catch { /* 下一次 ready 或项目操作会重新发送完整快照 */ }
127
+ },
128
+ reconcile: (handle, bindings) => projectSkillsReconciler.reconcile(handle, bindings),
129
+ });
130
+ let projectSkillsStatus = "initializing";
131
+ let projectSkillsInitialization = null;
132
+ let latestMachineHello = null;
133
+ let latestHelloSocket = null;
134
+ const effectiveMachineHello = (hello) => ({
135
+ ...hello,
136
+ projectSkillsStatus,
137
+ capabilities: projectSkillsStatus === "ready"
138
+ ? hello.capabilities
139
+ : hello.capabilities.filter((capability) => capability !== PROJECT_SKILLS_CAPABILITY),
140
+ });
141
+ const sendEffectiveMachineHello = () => {
142
+ if (latestMachineHello === null || latestHelloSocket?.readyState !== WebSocket.OPEN)
143
+ return;
144
+ const hello = effectiveMachineHello(latestMachineHello);
145
+ try {
146
+ latestHelloSocket.send(JSON.stringify(hello));
147
+ log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} · installed=[${hello.runtimes.join(",")}] · executable=[${hello.executionRuntimes.join(",")}] · agents=[${hello.agentHandles.join(",")}]`);
148
+ }
149
+ catch { /* 非 OPEN,忽略 */ }
150
+ };
151
+ const ensureProjectSkillsInitialized = () => {
152
+ if (projectSkillsStatus === "ready")
153
+ return Promise.resolve(true);
154
+ if (projectSkillsInitialization !== null)
155
+ return projectSkillsInitialization;
156
+ projectSkillsStatus = "initializing";
157
+ const attempt = projectSkillsController.initialize().then(() => {
158
+ projectSkillsStatus = "ready";
159
+ return true;
160
+ }, (error) => {
161
+ projectSkillsStatus = "unavailable";
162
+ dslog("project_skills.registry_failed", "本机项目注册表读取失败", {
163
+ level: "ERROR", error_code: error.code ?? "project_registry_corrupt",
164
+ });
165
+ return false;
166
+ }).finally(() => {
167
+ if (projectSkillsInitialization === attempt)
168
+ projectSkillsInitialization = null;
169
+ sendEffectiveMachineHello();
170
+ });
171
+ projectSkillsInitialization = attempt;
172
+ return attempt;
173
+ };
174
+ const initializedProjectSkillsReconciler = {
175
+ async reconcile(handle, bindings) {
176
+ return await ensureProjectSkillsInitialized()
177
+ ? projectSkillsReconciler.reconcile(handle, bindings)
178
+ : Promise.reject(new ProjectProjectionError("skill_projection_failed"));
179
+ },
180
+ async prepareAndLaunch(agentsRoot, handle, bindings, launch) {
181
+ return await ensureProjectSkillsInitialized()
182
+ ? projectSkillsReconciler.prepareAndLaunch(agentsRoot, handle, bindings, launch)
183
+ : Promise.reject(new ProjectProjectionError("skill_projection_failed"));
184
+ },
185
+ };
100
186
  const safeExecutionSend = (frame) => {
101
187
  try {
102
188
  if (ws?.readyState !== WebSocket.OPEN)
@@ -200,11 +286,13 @@ export function serve(config, opts = {}) {
200
286
  // scheduled 重复 run 仍由 running 去重;普通同线程 wake 用 legacyTaskTails 串成 FIFO。
201
287
  // 每 agent 超过并行上限的任务继续进入 sharedQueues(不丢)。
202
288
  const running = new Set();
289
+ const activeExecutionTaskKeys = new Map();
203
290
  const legacyTaskTails = new Map();
204
291
  const log = (s) => process.stdout.write(formatDaemonLogLine(s) + "\n");
205
292
  function connect() {
206
293
  if (stopped)
207
294
  return;
295
+ const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken, process.platform, undefined, projectSkillsStatus === "ready");
208
296
  ws = createWebSocket(wsUrl);
209
297
  ws.on("open", () => {
210
298
  const openedSocket = ws;
@@ -233,12 +321,12 @@ export function serve(config, opts = {}) {
233
321
  });
234
322
  void helloPromise
235
323
  .then((hello) => {
324
+ if (ws !== openedSocket || openedSocket.readyState !== WebSocket.OPEN)
325
+ return;
236
326
  detectedExecutionRuntimes = hello.executionRuntimes;
237
- try {
238
- ws?.send(JSON.stringify(hello));
239
- log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} · installed=[${hello.runtimes.join(",")}] · executable=[${hello.executionRuntimes.join(",")}] · agents=[${hello.agentHandles.join(",")}]`);
240
- }
241
- catch { /* 非 OPEN,忽略 */ }
327
+ latestMachineHello = hello;
328
+ latestHelloSocket = openedSocket;
329
+ sendEffectiveMachineHello();
242
330
  })
243
331
  .catch(() => { });
244
332
  opts.onOpen?.(ws);
@@ -385,6 +473,18 @@ export function serve(config, opts = {}) {
385
473
  }
386
474
  return;
387
475
  }
476
+ const pruneTraceId = parseMemoryPruneTraceId(spec.instructions.wakePrompt);
477
+ if (pruneTraceId !== null) {
478
+ dslog("memory_prune.received", "Daemon 已收到长期记忆收尾 execution", {
479
+ protocol: "execution_v1",
480
+ prune_trace_id: pruneTraceId,
481
+ execution_id: spec.executionId,
482
+ agent_handle: spec.agent.handle,
483
+ channel_id: spec.context.channelId,
484
+ thread_id: spec.context.threadId ?? null,
485
+ task_key: spec.workspace.taskKey,
486
+ });
487
+ }
388
488
  const reservation = sharedSlots.reserve(spec.agent.handle, "execution");
389
489
  if (!reservation.accepted) {
390
490
  dslog("execution.machine_queue_rejected", "机器执行队列已满", {
@@ -410,9 +510,41 @@ export function serve(config, opts = {}) {
410
510
  });
411
511
  }
412
512
  executionReservations.set(spec.executionId, reservation);
513
+ const executionTaskKey = `${spec.agent.handle}:${spec.workspace.taskKey}`;
514
+ let executionTaskKeyActive = false;
515
+ let executionTaskKeyFinished = false;
516
+ const markExecutionTaskKeyActive = () => {
517
+ if (executionTaskKeyActive || executionTaskKeyFinished)
518
+ return;
519
+ executionTaskKeyActive = true;
520
+ const activeSameTask = (activeExecutionTaskKeys.get(executionTaskKey) ?? 0) + 1;
521
+ activeExecutionTaskKeys.set(executionTaskKey, activeSameTask);
522
+ dslog(activeSameTask > 1 ? "execution.task_key_overlap_detected" : "execution.task_key_started", activeSameTask > 1 ? "同一 Agent 任务键存在重叠 execution" : "Agent 任务键 execution 已开始", {
523
+ ...(activeSameTask > 1 ? { level: "WARN" } : {}),
524
+ execution_id: spec.executionId,
525
+ agent_handle: spec.agent.handle,
526
+ task_key: spec.workspace.taskKey,
527
+ active_same_task: activeSameTask,
528
+ ...reservation.facts,
529
+ });
530
+ };
413
531
  const cancellation = cancellationFor(spec.executionId);
414
532
  const cleanupExecutionReservation = () => {
533
+ executionTaskKeyFinished = true;
415
534
  reservation.release();
535
+ if (executionTaskKeyActive) {
536
+ const remainingSameTask = Math.max(0, (activeExecutionTaskKeys.get(executionTaskKey) ?? 1) - 1);
537
+ if (remainingSameTask === 0)
538
+ activeExecutionTaskKeys.delete(executionTaskKey);
539
+ else
540
+ activeExecutionTaskKeys.set(executionTaskKey, remainingSameTask);
541
+ dslog("execution.task_key_finished", "Agent 任务键 execution 已结束", {
542
+ execution_id: spec.executionId,
543
+ agent_handle: spec.agent.handle,
544
+ task_key: spec.workspace.taskKey,
545
+ active_same_task: remainingSameTask,
546
+ });
547
+ }
416
548
  cancellations.delete(spec.executionId);
417
549
  executionReservations.delete(spec.executionId);
418
550
  knownExecutionHashes.delete(spec.executionId);
@@ -437,6 +569,7 @@ export function serve(config, opts = {}) {
437
569
  }
438
570
  const execution = executeProtocol(config, spec, {
439
571
  ...opts.execution?.dependencies,
572
+ projectSkills: opts.execution?.dependencies?.projectSkills ?? initializedProjectSkillsReconciler,
440
573
  ...(agentMemory === undefined ? {} : { agentMemory }),
441
574
  journal: executionJournal,
442
575
  facts: {
@@ -450,6 +583,7 @@ export function serve(config, opts = {}) {
450
583
  slot: {
451
584
  state: reservation.state,
452
585
  ready: reservation.ready.then(() => {
586
+ markExecutionTaskKeyActive();
453
587
  const snapshot = sharedSlots.snapshot();
454
588
  dslog("execution.machine_slot_ready", "execution 获得机器执行名额", {
455
589
  execution_id: spec.executionId,
@@ -506,6 +640,14 @@ export function serve(config, opts = {}) {
506
640
  // ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
507
641
  const r = msg;
508
642
  setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
643
+ // 旧 server 无此字段 → 空集合(daemon 保持全部旧行为)。
644
+ serverCapabilities = new Set(Array.isArray(r.serverCapabilities)
645
+ ? r.serverCapabilities.filter((c) => typeof c === "string")
646
+ : []);
647
+ if (serverCapabilities.has(PROJECT_SKILLS_CAPABILITY)) {
648
+ if (await ensureProjectSkillsInitialized())
649
+ await projectSkillsController.publishCurrent();
650
+ }
509
651
  return;
510
652
  }
511
653
  if (msg.type === "error") {
@@ -517,6 +659,26 @@ export function serve(config, opts = {}) {
517
659
  }
518
660
  return;
519
661
  }
662
+ if (msg.type === "project:add"
663
+ || msg.type === "project:remove"
664
+ || msg.type === "project:rescan"
665
+ || msg.type === "agent:skills:sync") {
666
+ const request = msg;
667
+ if (typeof request.reqId !== "string")
668
+ return;
669
+ const result = serverCapabilities.has(PROJECT_SKILLS_CAPABILITY)
670
+ ? await projectSkillsController.handle(msg)
671
+ : { ok: false, error: "capability_unavailable" };
672
+ if (result.ok && (msg.type === "project:remove"
673
+ || msg.type === "project:rescan")) {
674
+ void ensureProjectSkillsInitialized();
675
+ }
676
+ try {
677
+ ws?.send(JSON.stringify({ type: "fs:result", reqId: request.reqId, ...result }));
678
+ }
679
+ catch { /* server 会按请求超时处理 */ }
680
+ return;
681
+ }
520
682
  // 导入 raft agent 工作区:inspect 反填 name/description;import 复制用户内容
521
683
  if (msg.type === "raft:inspect" || msg.type === "raft:import") {
522
684
  const req = msg;
@@ -579,6 +741,9 @@ export function serve(config, opts = {}) {
579
741
  : {}),
580
742
  })
581
743
  : null;
744
+ // 活取值:发报告的时刻按"当前连接"的 server 能力判定(滚动发布中 run 可能跨重连;
745
+ // 断线时集合被清空 → 回退为 daemon 自行发通知,方向安全)。
746
+ const serverOwnsFailureNotice = () => serverCapabilities.has("scheduled_failure_notice_v1");
582
747
  const threadId = msg.wake?.threadId;
583
748
  const taskKey = scheduled ? scheduled.runId : (threadId ?? msg.channelId);
584
749
  const key = `${msg.agentHandle}:${taskKey}`;
@@ -588,6 +753,14 @@ export function serve(config, opts = {}) {
588
753
  run_id: runId, agent_handle: msg.agentHandle, channel_id: msg.channelId,
589
754
  thread_id: threadId ?? null, task_key: taskKey,
590
755
  };
756
+ const pruneTraceId = parseMemoryPruneTraceId(msg.wake?.content ?? "");
757
+ if (pruneTraceId !== null) {
758
+ dslog("memory_prune.received", "Daemon 已收到长期记忆收尾唤醒", {
759
+ ...runKeys,
760
+ protocol: "legacy",
761
+ prune_trace_id: pruneTraceId,
762
+ });
763
+ }
591
764
  dslog("run.wake_received", `收到唤醒 ${msg.agentHandle}`, {
592
765
  ...runKeys, reason: msg.reason ?? "", sender: msg.wake?.senderHandle,
593
766
  wake_origin: msg.wake?.origin ?? null,
@@ -744,6 +917,7 @@ export function serve(config, opts = {}) {
744
917
  title: scheduled.title,
745
918
  outputPolicy: scheduled.outputPolicy,
746
919
  externalNotificationPolicy: scheduled.externalNotificationPolicy,
920
+ serverOwnsFailureNotice,
747
921
  },
748
922
  } : {}),
749
923
  ...(wakeOrigin ? { wakeOrigin, originDecisionAttempt: attempt } : {}),
@@ -852,6 +1026,7 @@ export function serve(config, opts = {}) {
852
1026
  channelId: msg.channelId,
853
1027
  scheduled,
854
1028
  errorMessage: e.message,
1029
+ serverOwnsFailureNotice,
855
1030
  });
856
1031
  }
857
1032
  catch { /* token 也不可用时只能让 server 按 runtime failure 终态化 */ }
@@ -889,6 +1064,8 @@ export function serve(config, opts = {}) {
889
1064
  ws.on("close", (code) => {
890
1065
  if (stopped)
891
1066
  return;
1067
+ // server 能力随连接失效;下个连接的 ready 帧重新声明(防降级重连后沿用过期能力)。
1068
+ serverCapabilities = new Set();
892
1069
  completionRetransmitter.pause();
893
1070
  // 4001 = 控制面应用级「鉴权失败」关闭码(见 server control-plane.ts)。即使上面的
894
1071
  // error 帧因 close 抢先而丢失,也能据关闭码识别这是凭证失效——退避拉满,不再每秒热循环。
@@ -926,6 +1103,7 @@ export function serve(config, opts = {}) {
926
1103
  flush: flushSlog,
927
1104
  writeStderr: (line) => process.stderr.write(line),
928
1105
  });
1106
+ void ensureProjectSkillsInitialized();
929
1107
  connect();
930
1108
  })();
931
1109
  const stop = () => {
@@ -0,0 +1,23 @@
1
+ export function parseSkillFrontmatter(markdown) {
2
+ const match = markdown.match(/^---[\t ]*\r?\n([\s\S]*?)\r?\n---(?:[\t ]*\r?\n|$)/u);
3
+ if (!match)
4
+ return Object.freeze({});
5
+ let name;
6
+ let description;
7
+ for (const line of match[1]?.split(/\r?\n/u) ?? []) {
8
+ const field = line.match(/^(name|description)[\t ]*:[\t ]*(.*?)[\t ]*$/u);
9
+ if (!field)
10
+ continue;
11
+ const value = (field[2] ?? "")
12
+ .replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u, "$1$2")
13
+ .trim();
14
+ if (field[1] === "name")
15
+ name = value;
16
+ if (field[1] === "description")
17
+ description = value;
18
+ }
19
+ return Object.freeze({
20
+ ...(name === undefined ? {} : { name }),
21
+ ...(description === undefined ? {} : { description }),
22
+ });
23
+ }
package/dist/skills.js CHANGED
@@ -9,25 +9,8 @@
9
9
  import { readdir, readFile, stat } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
11
  import { homedir } from "node:os";
12
+ import { parseSkillFrontmatter } from "./skill-frontmatter.js";
12
13
  const globalSkillsDir = () => process.env.CREW_GLOBAL_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
13
- /** 从 SKILL.md 顶部 YAML frontmatter 取 name / description (简易解析,够用)。 */
14
- function parseFrontmatter(md) {
15
- const m = md.match(/^---\s*\n([\s\S]*?)\n---/);
16
- if (!m)
17
- return {};
18
- const out = {};
19
- for (const line of m[1].split("\n")) {
20
- const kv = line.match(/^(name|description)\s*:\s*(.+?)\s*$/);
21
- if (kv) {
22
- const val = kv[2].replace(/^["']|["']$/g, "");
23
- if (kv[1] === "name")
24
- out.name = val;
25
- else
26
- out.description = val;
27
- }
28
- }
29
- return out;
30
- }
31
14
  async function readSkillsFrom(dir, scope) {
32
15
  const names = await readdir(dir).catch(() => []);
33
16
  const skills = [];
@@ -39,7 +22,7 @@ async function readSkillsFrom(dir, scope) {
39
22
  if (!s || !s.isFile())
40
23
  continue;
41
24
  const md = await readFile(skillMd, "utf8").catch(() => "");
42
- const fm = parseFrontmatter(md);
25
+ const fm = parseSkillFrontmatter(md);
43
26
  skills.push({ scope, name: fm.name || name, description: fm.description ?? "" });
44
27
  }
45
28
  return skills;
@@ -1,4 +1,5 @@
1
1
  import { fileURLToPath } from "node:url";
2
+ import { join } from "node:path";
2
3
  import { startDormantSupervisor, } from "./execution-supervisor.js";
3
4
  import { buildClaudeArgs } from "./runtimes/claude.js";
4
5
  import { RuntimeCancelledError } from "./runtime-cancellation.js";
@@ -19,6 +20,9 @@ export function supervisorLaunch(request) {
19
20
  cwd: request.cwd,
20
21
  env: request.env,
21
22
  systemPromptPath: request.systemPromptPath,
23
+ ...(request.agentRoot === undefined ? {} : {
24
+ projectSkillsDirectory: join(request.agentRoot, ".crew", "claude-skills"),
25
+ }),
22
26
  ...(request.sessionId === undefined ? {} : {
23
27
  sessionId: request.sessionId,
24
28
  resume: request.resume,
@@ -45,6 +49,9 @@ export function supervisorLaunch(request) {
45
49
  ...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
46
50
  ...(request.sessionId === undefined ? {} : { sessionId: request.sessionId }),
47
51
  ...(request.imagePaths === undefined ? {} : { imagePaths: request.imagePaths }),
52
+ ...(request.agentRoot === undefined ? {} : {
53
+ projectRootMarkers: [".git", ".nowwork-root"],
54
+ }),
48
55
  resume: request.resume,
49
56
  }),
50
57
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.5.34",
3
+ "version": "0.5.36",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",