@nowcrew/daemon 0.5.35 → 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.
@@ -18,6 +18,19 @@ import { routeRuntimeAttachments, runtimeCapability, } from "./runtime-capabilit
18
18
  import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancellation.js";
19
19
  import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
20
20
  import { dslog } from "./slog.js";
21
+ import { inspectMemoryPruneFiles, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
22
+ function memoryPruneSnapshotFields(snapshot) {
23
+ const fields = {};
24
+ for (const [label, fact] of Object.entries(snapshot)) {
25
+ fields[`${label}_exists`] = fact.exists;
26
+ fields[`${label}_size`] = fact.size;
27
+ fields[`${label}_mtime_ms`] = fact.mtime_ms;
28
+ fields[`${label}_sha256`] = fact.sha256;
29
+ fields[`${label}_hash_skipped_reason`] = fact.hash_skipped_reason;
30
+ fields[`${label}_error`] = fact.error;
31
+ }
32
+ return fields;
33
+ }
21
34
  function truncateUtf8(value, maxBytes) {
22
35
  if (maxBytes <= 0)
23
36
  return "";
@@ -204,6 +217,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
204
217
  let materialized = null;
205
218
  let knownAttachmentDirectory = null;
206
219
  let startupReservation = null;
220
+ let memoryPruneTraceId = null;
221
+ let memoryPruneRuntimeExitCode;
222
+ let memoryPruneExecutorCompleted = false;
223
+ let memoryPruneFailurePhase = "diagnostics_before";
207
224
  try {
208
225
  if (isDeepSeekCodex && !providerConfig.providerApiKey) {
209
226
  throw new Error("DeepSeek API key is not configured for this Agent");
@@ -268,7 +285,20 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
268
285
  }
269
286
  const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
270
287
  const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
288
+ memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
289
+ if (memoryPruneTraceId !== null) {
290
+ const snapshot = await inspectMemoryPruneFiles(workspace.dir, workspace.workLogPath);
291
+ dslog("memory_prune.files_before", "长期记忆收尾执行前文件指纹", {
292
+ execution_id: input.executionId,
293
+ agent_handle: input.handle,
294
+ task_key: input.taskKey,
295
+ prune_trace_id: memoryPruneTraceId,
296
+ ...memoryPruneSnapshotFields(snapshot),
297
+ });
298
+ }
299
+ memoryPruneFailurePhase = "prompt_write";
271
300
  await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
301
+ memoryPruneFailurePhase = "runtime_prepare";
272
302
  const inheritedEnv = { ...process.env };
273
303
  for (const key of Object.keys(inheritedEnv)) {
274
304
  if (key.startsWith("CREW_AGENT_MEMORY_"))
@@ -321,6 +351,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
321
351
  }
322
352
  }
323
353
  const runtimeLaunchAt = Date.now();
354
+ memoryPruneFailurePhase = "runtime_launch";
324
355
  const launchRequest = {
325
356
  runtime: runtime.name,
326
357
  bin: runtime.name,
@@ -342,6 +373,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
342
373
  const child = input.projectSkills !== undefined && dependencies.projectSkills !== undefined
343
374
  ? await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, () => launchRuntime(launchRequest))
344
375
  : await launchRuntime(launchRequest);
376
+ memoryPruneFailurePhase = "runtime_execution";
345
377
  if (child.cancel !== undefined) {
346
378
  dependencies.cancellation?.register(child.cancel);
347
379
  }
@@ -456,6 +488,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
456
488
  throw error;
457
489
  }
458
490
  const { exitCode, spawnError, terminationSignal } = runtimeExit;
491
+ memoryPruneRuntimeExitCode = exitCode;
459
492
  const errorTail = [
460
493
  stderrTail.trim(),
461
494
  spawnError,
@@ -470,6 +503,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
470
503
  }
471
504
  }
472
505
  if (input.session.enabled && supportsNativeResume && sessionId) {
506
+ memoryPruneFailurePhase = "session_finalize";
473
507
  const contextTokens = usage
474
508
  ? usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens
475
509
  : (resuming ? prior?.contextTokens : undefined);
@@ -483,6 +517,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
483
517
  ...(contextTokens === undefined ? {} : { contextTokens }),
484
518
  });
485
519
  }
520
+ memoryPruneExecutorCompleted = true;
486
521
  return {
487
522
  workspaceRunDir: workspace.runDir,
488
523
  exitCode,
@@ -505,6 +540,27 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
505
540
  }
506
541
  finally {
507
542
  startupReservation?.release();
543
+ if (memoryPruneTraceId !== null) {
544
+ const snapshot = await inspectMemoryPruneFiles(workspace.dir, workspace.workLogPath);
545
+ dslog("memory_prune.files_after", "长期记忆收尾执行后文件指纹", {
546
+ execution_id: input.executionId,
547
+ agent_handle: input.handle,
548
+ task_key: input.taskKey,
549
+ prune_trace_id: memoryPruneTraceId,
550
+ runtime_exit_code: memoryPruneRuntimeExitCode,
551
+ executor_outcome: memoryPruneExecutorCompleted ? "succeeded" : "failed",
552
+ ...memoryPruneSnapshotFields(snapshot),
553
+ });
554
+ dslog("memory_prune.execution_completed", "长期记忆收尾执行结束", {
555
+ execution_id: input.executionId,
556
+ agent_handle: input.handle,
557
+ task_key: input.taskKey,
558
+ prune_trace_id: memoryPruneTraceId,
559
+ executor_outcome: memoryPruneExecutorCompleted ? "succeeded" : "failed",
560
+ runtime_exit_code: memoryPruneRuntimeExitCode,
561
+ ...(memoryPruneExecutorCompleted ? {} : { failure_phase: memoryPruneFailurePhase }),
562
+ });
563
+ }
508
564
  const attachmentDirectories = new Set([
509
565
  ...(knownAttachmentDirectory === null ? [] : [knownAttachmentDirectory]),
510
566
  ...(materialized === null ? [] : [materialized.directory]),
@@ -0,0 +1,57 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { stat } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ const TRACE_PATTERN = /\[memory-prune trace_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\]/i;
6
+ export const MEMORY_PRUNE_HASH_LIMIT_BYTES = 10 * 1024 * 1024;
7
+ export function parseMemoryPruneTraceId(wakePrompt) {
8
+ return TRACE_PATTERN.exec(wakePrompt)?.[1] ?? null;
9
+ }
10
+ async function inspectFile(path) {
11
+ let metadata;
12
+ try {
13
+ metadata = await stat(path);
14
+ }
15
+ catch (error) {
16
+ const code = error.code;
17
+ if (code === "ENOENT")
18
+ return { exists: false };
19
+ return { exists: false, error: code ?? (error instanceof Error ? error.name : "unknown") };
20
+ }
21
+ const fact = { exists: true, size: metadata.size, mtime_ms: metadata.mtimeMs };
22
+ if (!metadata.isFile())
23
+ return { ...fact, hash_skipped_reason: "not_regular_file" };
24
+ if (metadata.size > MEMORY_PRUNE_HASH_LIMIT_BYTES) {
25
+ return { ...fact, hash_skipped_reason: "file_too_large" };
26
+ }
27
+ try {
28
+ const hash = createHash("sha256");
29
+ let bytesRead = 0;
30
+ const stream = createReadStream(path);
31
+ try {
32
+ for await (const chunk of stream) {
33
+ bytesRead += chunk.length;
34
+ if (bytesRead > MEMORY_PRUNE_HASH_LIMIT_BYTES) {
35
+ return { ...fact, hash_skipped_reason: "grew_too_large" };
36
+ }
37
+ hash.update(chunk);
38
+ }
39
+ }
40
+ finally {
41
+ stream.destroy();
42
+ }
43
+ return { ...fact, sha256: hash.digest("hex") };
44
+ }
45
+ catch (error) {
46
+ const code = error.code;
47
+ return { ...fact, error: code ?? (error instanceof Error ? error.name : "unknown") };
48
+ }
49
+ }
50
+ export async function inspectMemoryPruneFiles(homeDir, workLogPath) {
51
+ const [memory, lessons, workLog] = await Promise.all([
52
+ inspectFile(join(homeDir, "MEMORY.md")),
53
+ inspectFile(join(homeDir, "notes", "lessons.md")),
54
+ inspectFile(workLogPath),
55
+ ]);
56
+ return { memory, lessons, work_log: workLog };
57
+ }
@@ -32,13 +32,46 @@ const ProjectCommandSchema = z.discriminatedUnion("type", [
32
32
  }).strict()).max(MAX_AGENT_PROJECT_SKILL_BINDINGS),
33
33
  }).strict(),
34
34
  ]);
35
+ const DEFAULT_PROJECT_SCAN_TIMEOUT_MS = 10_000;
36
+ class ProjectScanTimeoutError extends Error {
37
+ code = "project_scan_timeout";
38
+ constructor() {
39
+ super("project_scan_timeout");
40
+ this.name = "ProjectScanTimeoutError";
41
+ }
42
+ }
43
+ const withinScanDeadline = async (operation, timeoutMs) => {
44
+ let timer;
45
+ try {
46
+ return await Promise.race([
47
+ operation,
48
+ new Promise((_resolve, reject) => {
49
+ timer = setTimeout(() => reject(new ProjectScanTimeoutError()), timeoutMs);
50
+ timer.unref?.();
51
+ }),
52
+ ]);
53
+ }
54
+ finally {
55
+ if (timer !== undefined)
56
+ clearTimeout(timer);
57
+ }
58
+ };
35
59
  export function createProjectSkillsController(deps) {
36
60
  let scanned = Object.freeze([]);
37
61
  const operationTail = createPromiseTail();
38
62
  let initialization = null;
39
63
  let initializationState = "idle";
40
- const refresh = async () => {
41
- scanned = await scanProjects(await deps.registry.list());
64
+ const scan = deps.scan ?? scanProjects;
65
+ const scanTimeoutMs = deps.scanTimeoutMs ?? DEFAULT_PROJECT_SCAN_TIMEOUT_MS;
66
+ const scanCurrent = async (deferredProjectId) => withinScanDeadline(scan(await deps.registry.list(), undefined, [
67
+ ...scanned
68
+ .map((project) => project.inventory.projectId)
69
+ .filter((projectId) => projectId !== deferredProjectId),
70
+ ...(deferredProjectId === undefined ? [] : [deferredProjectId]),
71
+ ]), scanTimeoutMs);
72
+ const refresh = async (deferredProjectId) => {
73
+ const next = await scanCurrent(deferredProjectId);
74
+ scanned = next;
42
75
  };
43
76
  const publishCurrent = async () => {
44
77
  await deps.publish(Object.freeze({
@@ -47,27 +80,38 @@ export function createProjectSkillsController(deps) {
47
80
  }));
48
81
  };
49
82
  const enqueue = (operation) => operationTail.enqueue(operation);
50
- return {
51
- initialize() {
52
- if (initialization !== null)
53
- return initialization;
54
- initializationState = "initializing";
55
- initialization = enqueue(refresh).then(() => { initializationState = "ready"; }, (error) => {
56
- initializationState = "failed";
57
- throw error;
58
- });
83
+ const initialize = () => {
84
+ if (initializationState === "ready" && initialization !== null)
59
85
  return initialization;
60
- },
86
+ if (initializationState === "initializing" && initialization !== null)
87
+ return initialization;
88
+ initializationState = "initializing";
89
+ const attempt = enqueue(refresh).then(() => { initializationState = "ready"; }, (error) => {
90
+ initializationState = "failed";
91
+ if (initialization === attempt)
92
+ initialization = null;
93
+ throw error;
94
+ });
95
+ initialization = attempt;
96
+ return attempt;
97
+ };
98
+ return {
99
+ initialize,
61
100
  publishCurrent: () => enqueue(publishCurrent),
62
101
  scannedProjects: () => scanned,
63
- handle(input) {
64
- return enqueue(async () => {
65
- const parsed = ProjectCommandSchema.safeParse(input);
66
- if (!parsed.success)
67
- return { ok: false, error: "invalid_project_command" };
68
- if (initializationState !== "ready") {
102
+ async handle(input) {
103
+ const parsed = ProjectCommandSchema.safeParse(input);
104
+ if (!parsed.success)
105
+ return { ok: false, error: "invalid_project_command" };
106
+ if (parsed.data.type === "agent:skills:sync") {
107
+ try {
108
+ await initialize();
109
+ }
110
+ catch {
69
111
  return { ok: false, error: "project_skills_unavailable" };
70
112
  }
113
+ }
114
+ return enqueue(async () => {
71
115
  const command = parsed.data;
72
116
  try {
73
117
  if (command.type === "agent:skills:sync") {
@@ -79,7 +123,16 @@ export function createProjectSkillsController(deps) {
79
123
  };
80
124
  }
81
125
  if (command.type === "project:add") {
126
+ const existed = (await deps.registry.list())
127
+ .some((project) => project.projectId === command.projectId);
82
128
  await deps.registry.add(command.projectId, command.root);
129
+ const next = await scanCurrent();
130
+ const added = next.find((project) => project.inventory.projectId === command.projectId);
131
+ if (!existed && added?.inventory.errorCode === "machine_project_skill_limit_exceeded") {
132
+ await deps.registry.remove(command.projectId);
133
+ return { ok: false, error: "machine_project_skill_limit_exceeded" };
134
+ }
135
+ scanned = next;
83
136
  }
84
137
  else if (command.type === "project:remove") {
85
138
  await deps.registry.remove(command.projectId);
@@ -90,7 +143,9 @@ export function createProjectSkillsController(deps) {
90
143
  return { ok: false, error: "project_not_found" };
91
144
  }
92
145
  }
93
- await refresh();
146
+ if (command.type !== "project:add") {
147
+ await refresh(command.type === "project:rescan" ? command.projectId : undefined);
148
+ }
94
149
  await publishCurrent();
95
150
  return { ok: true, data: { projectId: command.projectId } };
96
151
  }
@@ -6,8 +6,7 @@ import { createPromiseTail } from "../promise-tail.js";
6
6
  import { atomicPrivateWrite } from "../atomic-private-write.js";
7
7
  const RegistryFileSchema = z.object({
8
8
  version: z.literal(1),
9
- projects: z.record(z.object({ root: z.string().min(1).max(4_096) }).strict())
10
- .refine((projects) => Object.keys(projects).length <= MAX_MACHINE_PROJECTS),
9
+ projects: z.record(z.object({ root: z.string().min(1).max(4_096) }).strict()),
11
10
  }).strict();
12
11
  export class ProjectRegistryError extends Error {
13
12
  code;
@@ -17,9 +16,22 @@ export class ProjectRegistryError extends Error {
17
16
  this.name = "ProjectRegistryError";
18
17
  }
19
18
  }
20
- const immutableList = (projects) => Object.freeze(Object.entries(projects)
21
- .map(([projectId, project]) => Object.freeze({ projectId, root: project.root }))
22
- .sort((left, right) => left.projectId.localeCompare(right.projectId)));
19
+ const immutableList = (projects) => {
20
+ const sorted = Object.entries(projects)
21
+ .map(([projectId, project]) => Object.freeze({ projectId, root: project.root }))
22
+ .sort((left, right) => left.projectId.localeCompare(right.projectId));
23
+ if (sorted.length <= MAX_MACHINE_PROJECTS)
24
+ return Object.freeze(sorted);
25
+ const visible = sorted.slice(0, MAX_MACHINE_PROJECTS);
26
+ const boundary = visible.at(-1);
27
+ if (boundary !== undefined) {
28
+ visible[visible.length - 1] = Object.freeze({
29
+ ...boundary,
30
+ errorCode: "project_registry_limit_exceeded",
31
+ });
32
+ }
33
+ return Object.freeze(visible);
34
+ };
23
35
  export function createProjectRegistry(agentsRoot) {
24
36
  const path = join(agentsRoot, ".crew", "projects.json");
25
37
  const writeTail = createPromiseTail();
@@ -15,6 +15,8 @@ const unavailable = (projectId, scannedAt, errorCode) => Object.freeze({
15
15
  });
16
16
  export async function scanProject(project, now = () => new Date()) {
17
17
  const scannedAt = now().toISOString();
18
+ if (project.errorCode !== undefined)
19
+ return unavailable(project.projectId, scannedAt, project.errorCode);
18
20
  const projectStat = await stat(project.root).catch(() => null);
19
21
  if (!projectStat?.isDirectory())
20
22
  return unavailable(project.projectId, scannedAt, "project_path_unavailable");
@@ -51,8 +53,12 @@ export async function scanProject(project, now = () => new Date()) {
51
53
  continue;
52
54
  }
53
55
  const frontmatter = parseSkillFrontmatter(markdown);
54
- const name = frontmatter?.name ?? entry.name;
55
- const description = frontmatter?.description ?? "";
56
+ const name = frontmatter.name;
57
+ const description = frontmatter.description;
58
+ if (name === undefined || description === undefined) {
59
+ invalidSkillCount += 1;
60
+ continue;
61
+ }
56
62
  if (!isProjectSkillName(name) || description.length > MAX_PROJECT_SKILL_DESCRIPTION_LENGTH) {
57
63
  invalidSkillCount += 1;
58
64
  continue;
@@ -84,20 +90,37 @@ export async function scanProject(project, now = () => new Date()) {
84
90
  resolved: Object.freeze(resolved),
85
91
  });
86
92
  }
87
- export function boundScannedProjects(projects) {
93
+ export function boundScannedProjects(projects, retainedProjectIds = []) {
94
+ const retainedOrder = new Map(retainedProjectIds.map((projectId, index) => [projectId, index]));
95
+ const allocationOrder = [...projects].sort((left, right) => {
96
+ const leftOrder = retainedOrder.get(left.inventory.projectId);
97
+ const rightOrder = retainedOrder.get(right.inventory.projectId);
98
+ if (leftOrder !== undefined && rightOrder !== undefined)
99
+ return leftOrder - rightOrder;
100
+ if (leftOrder !== undefined)
101
+ return -1;
102
+ if (rightOrder !== undefined)
103
+ return 1;
104
+ return left.inventory.projectId.localeCompare(right.inventory.projectId);
105
+ });
88
106
  let skillCount = 0;
89
- return Object.freeze(projects.map((project) => {
90
- if (project.inventory.status !== "available")
91
- return project;
107
+ const bounded = new Map();
108
+ for (const project of allocationOrder) {
109
+ if (project.inventory.status !== "available") {
110
+ bounded.set(project.inventory.projectId, project);
111
+ continue;
112
+ }
92
113
  const nextCount = skillCount + project.inventory.skills.length;
93
114
  if (nextCount <= MAX_MACHINE_PROJECT_SKILLS) {
94
115
  skillCount = nextCount;
95
- return project;
116
+ bounded.set(project.inventory.projectId, project);
117
+ continue;
96
118
  }
97
- return unavailable(project.inventory.projectId, project.inventory.scannedAt, "machine_project_skill_limit_exceeded");
98
- }));
119
+ bounded.set(project.inventory.projectId, unavailable(project.inventory.projectId, project.inventory.scannedAt, "machine_project_skill_limit_exceeded"));
120
+ }
121
+ return Object.freeze(projects.map((project) => bounded.get(project.inventory.projectId) ?? project));
99
122
  }
100
- export async function scanProjects(projects, now = () => new Date()) {
123
+ export async function scanProjects(projects, now = () => new Date(), retainedProjectIds = []) {
101
124
  const sorted = [...projects].sort((left, right) => left.projectId.localeCompare(right.projectId));
102
- return boundScannedProjects(await Promise.all(sorted.map((project) => scanProject(project, now))));
125
+ return boundScannedProjects(await Promise.all(sorted.map((project) => scanProject(project, now))), retainedProjectIds);
103
126
  }
package/dist/serve.js CHANGED
@@ -40,6 +40,7 @@ import { createProjectSkillsController, } from "./project-skills/controller.js";
40
40
  import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
41
41
  import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
42
42
  import { createProjectSkillsReconciler, ProjectProjectionError, } from "./project-skills/reconciler.js";
43
+ import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
43
44
  // normalize.ts 的活动种类 → activity 枚举
44
45
  const ACTIVITY_MAP = {
45
46
  init: "working", text: "thinking", reading: "reading", sending: "sending",
@@ -126,21 +127,58 @@ export function serve(config, opts = {}) {
126
127
  },
127
128
  reconcile: (handle, bindings) => projectSkillsReconciler.reconcile(handle, bindings),
128
129
  });
129
- const projectSkillsInitialized = projectSkillsController.initialize().then(() => true, (error) => {
130
- dslog("project_skills.registry_failed", "本机项目注册表读取失败", {
131
- level: "ERROR", error_code: error.code ?? "project_registry_corrupt",
132
- });
133
- return false;
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),
134
140
  });
135
- let projectSkillsAvailable = false;
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
+ };
136
174
  const initializedProjectSkillsReconciler = {
137
- reconcile(handle, bindings) {
138
- return projectSkillsAvailable
175
+ async reconcile(handle, bindings) {
176
+ return await ensureProjectSkillsInitialized()
139
177
  ? projectSkillsReconciler.reconcile(handle, bindings)
140
178
  : Promise.reject(new ProjectProjectionError("skill_projection_failed"));
141
179
  },
142
- prepareAndLaunch(agentsRoot, handle, bindings, launch) {
143
- return projectSkillsAvailable
180
+ async prepareAndLaunch(agentsRoot, handle, bindings, launch) {
181
+ return await ensureProjectSkillsInitialized()
144
182
  ? projectSkillsReconciler.prepareAndLaunch(agentsRoot, handle, bindings, launch)
145
183
  : Promise.reject(new ProjectProjectionError("skill_projection_failed"));
146
184
  },
@@ -248,12 +286,13 @@ export function serve(config, opts = {}) {
248
286
  // scheduled 重复 run 仍由 running 去重;普通同线程 wake 用 legacyTaskTails 串成 FIFO。
249
287
  // 每 agent 超过并行上限的任务继续进入 sharedQueues(不丢)。
250
288
  const running = new Set();
289
+ const activeExecutionTaskKeys = new Map();
251
290
  const legacyTaskTails = new Map();
252
291
  const log = (s) => process.stdout.write(formatDaemonLogLine(s) + "\n");
253
292
  function connect() {
254
293
  if (stopped)
255
294
  return;
256
- const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken, process.platform, undefined, projectSkillsAvailable);
295
+ const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken, process.platform, undefined, projectSkillsStatus === "ready");
257
296
  ws = createWebSocket(wsUrl);
258
297
  ws.on("open", () => {
259
298
  const openedSocket = ws;
@@ -282,18 +321,12 @@ export function serve(config, opts = {}) {
282
321
  });
283
322
  void helloPromise
284
323
  .then((hello) => {
324
+ if (ws !== openedSocket || openedSocket.readyState !== WebSocket.OPEN)
325
+ return;
285
326
  detectedExecutionRuntimes = hello.executionRuntimes;
286
- const effectiveHello = projectSkillsAvailable
287
- ? hello
288
- : {
289
- ...hello,
290
- capabilities: hello.capabilities.filter((capability) => capability !== PROJECT_SKILLS_CAPABILITY),
291
- };
292
- try {
293
- ws?.send(JSON.stringify(effectiveHello));
294
- log(`📤 已上报机器信息: ${effectiveHello.hostname} · ${effectiveHello.os} · installed=[${effectiveHello.runtimes.join(",")}] · executable=[${effectiveHello.executionRuntimes.join(",")}] · agents=[${effectiveHello.agentHandles.join(",")}]`);
295
- }
296
- catch { /* 非 OPEN,忽略 */ }
327
+ latestMachineHello = hello;
328
+ latestHelloSocket = openedSocket;
329
+ sendEffectiveMachineHello();
297
330
  })
298
331
  .catch(() => { });
299
332
  opts.onOpen?.(ws);
@@ -440,6 +473,18 @@ export function serve(config, opts = {}) {
440
473
  }
441
474
  return;
442
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
+ }
443
488
  const reservation = sharedSlots.reserve(spec.agent.handle, "execution");
444
489
  if (!reservation.accepted) {
445
490
  dslog("execution.machine_queue_rejected", "机器执行队列已满", {
@@ -465,9 +510,41 @@ export function serve(config, opts = {}) {
465
510
  });
466
511
  }
467
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
+ };
468
531
  const cancellation = cancellationFor(spec.executionId);
469
532
  const cleanupExecutionReservation = () => {
533
+ executionTaskKeyFinished = true;
470
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
+ }
471
548
  cancellations.delete(spec.executionId);
472
549
  executionReservations.delete(spec.executionId);
473
550
  knownExecutionHashes.delete(spec.executionId);
@@ -506,6 +583,7 @@ export function serve(config, opts = {}) {
506
583
  slot: {
507
584
  state: reservation.state,
508
585
  ready: reservation.ready.then(() => {
586
+ markExecutionTaskKeyActive();
509
587
  const snapshot = sharedSlots.snapshot();
510
588
  dslog("execution.machine_slot_ready", "execution 获得机器执行名额", {
511
589
  execution_id: spec.executionId,
@@ -567,7 +645,7 @@ export function serve(config, opts = {}) {
567
645
  ? r.serverCapabilities.filter((c) => typeof c === "string")
568
646
  : []);
569
647
  if (serverCapabilities.has(PROJECT_SKILLS_CAPABILITY)) {
570
- if (await projectSkillsInitialized)
648
+ if (await ensureProjectSkillsInitialized())
571
649
  await projectSkillsController.publishCurrent();
572
650
  }
573
651
  return;
@@ -591,6 +669,10 @@ export function serve(config, opts = {}) {
591
669
  const result = serverCapabilities.has(PROJECT_SKILLS_CAPABILITY)
592
670
  ? await projectSkillsController.handle(msg)
593
671
  : { ok: false, error: "capability_unavailable" };
672
+ if (result.ok && (msg.type === "project:remove"
673
+ || msg.type === "project:rescan")) {
674
+ void ensureProjectSkillsInitialized();
675
+ }
594
676
  try {
595
677
  ws?.send(JSON.stringify({ type: "fs:result", reqId: request.reqId, ...result }));
596
678
  }
@@ -671,6 +753,14 @@ export function serve(config, opts = {}) {
671
753
  run_id: runId, agent_handle: msg.agentHandle, channel_id: msg.channelId,
672
754
  thread_id: threadId ?? null, task_key: taskKey,
673
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
+ }
674
764
  dslog("run.wake_received", `收到唤醒 ${msg.agentHandle}`, {
675
765
  ...runKeys, reason: msg.reason ?? "", sender: msg.wake?.senderHandle,
676
766
  wake_origin: msg.wake?.origin ?? null,
@@ -1013,7 +1103,7 @@ export function serve(config, opts = {}) {
1013
1103
  flush: flushSlog,
1014
1104
  writeStderr: (line) => process.stderr.write(line),
1015
1105
  });
1016
- projectSkillsAvailable = await projectSkillsInitialized;
1106
+ void ensureProjectSkillsInitialized();
1017
1107
  connect();
1018
1108
  })();
1019
1109
  const stop = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.5.35",
3
+ "version": "0.5.36",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",