@nowcrew/daemon 0.5.41 → 0.5.42

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/README.md CHANGED
@@ -105,8 +105,10 @@ CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon doctor --profile dev
105
105
  CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon status --profile dev
106
106
  ```
107
107
 
108
- If the descriptor has drifted, the profile is stopped, or another host descriptor is not registered, the
109
- daemon fails closed and remains manually upgradeable.
108
+ If the target descriptor has drifted or the profile is stopped, the daemon fails closed and remains
109
+ manually upgradeable. On macOS, another unregistered legacy LaunchAgent may coexist only when its plist
110
+ exactly matches a generated NowCrew service and resolves to a different npm prefix. An unverifiable or
111
+ same-prefix descriptor still fails closed.
110
112
 
111
113
  The daemon advertises `daemon_update_v1` only when all of these conditions hold:
112
114
 
@@ -115,7 +117,7 @@ The daemon advertises `daemon_update_v1` only when all of these conditions hold:
115
117
  | Platform | macOS LaunchAgent or Linux systemd user service |
116
118
  | Entrypoint | global `@nowcrew/daemon/dist/main.js`, not npx or source/tsx |
117
119
  | Startup | `crew-daemon serve --profile <name>` through the installed service |
118
- | Registry | every other NowCrew descriptor on the OS user account is registered and conflict-free; the exact target legacy descriptor may be adopted once |
120
+ | Registry | registered descriptors remain conflict-free; the exact target legacy descriptor may be adopted once; an exact macOS legacy descriptor on another npm prefix may coexist |
119
121
  | Identity | service ID, descriptor, daemon home, Agent root, entrypoint, package root, and npm prefix match |
120
122
  | Service | the selected profile is running and holds the installation lease |
121
123
  | Install root | a standard writable Unix global npm prefix can be derived from the running entrypoint |
@@ -6,6 +6,7 @@ import { builtDaemonEntry } from "./computer-cli.js";
6
6
  import { buildServiceSpec, readServiceDescriptor, serviceStatus, } from "./computer-service.js";
7
7
  import { daemonGlobalInstallation } from "./daemon-installation.js";
8
8
  import { daemonInstallationLeaseHeld } from "./daemon-installation-lease.js";
9
+ import { inspectLegacyServiceInstallation } from "./legacy-service-installation.js";
9
10
  import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, serviceDescriptorSha256, } from "./managed-service-registry.js";
10
11
  function defaults() {
11
12
  return {
@@ -22,8 +23,25 @@ function defaults() {
22
23
  serviceStatus,
23
24
  assertWritable: (path) => access(path, constants.W_OK),
24
25
  installationLeaseHeld: daemonInstallationLeaseHeld,
26
+ inspectLegacyDescriptorInstallation: (descriptorPath) => inspectLegacyServiceInstallation(descriptorPath, process.platform, homedir(), process.getuid?.()),
25
27
  };
26
28
  }
29
+ async function registryCoversUpdateScope(services, descriptorPaths, targetDescriptorPath, npmPrefix, inspectLegacyDescriptorInstallation) {
30
+ for (const descriptorPath of descriptorPaths) {
31
+ if (descriptorPath === targetDescriptorPath)
32
+ continue;
33
+ try {
34
+ assertRegistryCoversDescriptors(services, [descriptorPath]);
35
+ continue;
36
+ }
37
+ catch {
38
+ const legacy = await inspectLegacyDescriptorInstallation(descriptorPath).catch(() => null);
39
+ if (legacy === null || legacy.npmPrefix === npmPrefix)
40
+ return false;
41
+ }
42
+ }
43
+ return true;
44
+ }
27
45
  export async function detectDaemonUpdateEligibility(profileName, overrides = {}) {
28
46
  const deps = { ...defaults(), ...overrides };
29
47
  if (deps.platform !== "darwin" && deps.platform !== "linux") {
@@ -65,10 +83,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
65
83
  const registered = services.find((record) => record.serviceId === spec.id
66
84
  || (record.daemonHome === deps.profileHome && record.profile === profileName));
67
85
  if (registered !== undefined) {
68
- try {
69
- assertRegistryCoversDescriptors(services, await deps.listManagedDescriptorPaths());
70
- }
71
- catch {
86
+ if (!await registryCoversUpdateScope(services, await deps.listManagedDescriptorPaths(), spec.descriptorPath, installation.npmPrefix, deps.inspectLegacyDescriptorInstallation)) {
72
87
  return { eligible: false, reason: "managed_service_registry_incomplete" };
73
88
  }
74
89
  const descriptor = await deps.readServiceDescriptor(spec).catch(() => null);
@@ -109,12 +124,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
109
124
  if (legacyDescriptor !== legacySpec.descriptor) {
110
125
  return { eligible: false, reason: "managed_service_not_registered" };
111
126
  }
112
- try {
113
- const descriptorPaths = await deps.listManagedDescriptorPaths();
114
- const targetPath = legacySpec.descriptorPath;
115
- assertRegistryCoversDescriptors(services, descriptorPaths.filter((descriptorPath) => descriptorPath !== targetPath));
116
- }
117
- catch {
127
+ if (!await registryCoversUpdateScope(services, await deps.listManagedDescriptorPaths(), legacySpec.descriptorPath, installation.npmPrefix, deps.inspectLegacyDescriptorInstallation)) {
118
128
  return { eligible: false, reason: "managed_service_registry_incomplete" };
119
129
  }
120
130
  status = await deps.serviceStatus(legacySpec);
@@ -0,0 +1,56 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute } from "node:path";
4
+ import { z } from "zod";
5
+ import { buildServiceSpec, systemCommandRunner, } from "./computer-service.js";
6
+ import { daemonGlobalInstallation } from "./daemon-installation.js";
7
+ const LegacyLaunchAgentSchema = z.object({
8
+ Label: z.string().min(1),
9
+ ProgramArguments: z.tuple([
10
+ z.string().refine(isAbsolute),
11
+ z.string().refine(isAbsolute),
12
+ z.literal("serve"),
13
+ z.literal("--profile"),
14
+ z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/),
15
+ z.literal("--daemon-home"),
16
+ z.string().refine(isAbsolute),
17
+ ]),
18
+ KeepAlive: z.literal(true),
19
+ ProcessType: z.literal("Background"),
20
+ }).strict();
21
+ export async function inspectLegacyServiceInstallation(descriptorPath, platform, userHome = homedir(), uid = process.getuid?.(), runner = systemCommandRunner) {
22
+ if (platform !== "darwin" || uid === undefined)
23
+ return null;
24
+ const result = await runner("/usr/bin/plutil", ["-convert", "json", "-o", "-", descriptorPath]);
25
+ if (result.exitCode !== 0)
26
+ return null;
27
+ let decoded;
28
+ try {
29
+ decoded = JSON.parse(result.stdout);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ const parsed = LegacyLaunchAgentSchema.safeParse(decoded);
35
+ if (!parsed.success)
36
+ return null;
37
+ const [nodePath, entryPath, , , profile, , profileHome] = parsed.data.ProgramArguments;
38
+ const installation = daemonGlobalInstallation(entryPath, platform);
39
+ if (installation === null)
40
+ return null;
41
+ const spec = buildServiceSpec({
42
+ platform,
43
+ profile,
44
+ userHome,
45
+ uid,
46
+ nodePath,
47
+ entryPath,
48
+ profileHome,
49
+ legacyServiceId: true,
50
+ });
51
+ if (spec.id !== parsed.data.Label || spec.descriptorPath !== descriptorPath)
52
+ return null;
53
+ if (await readFile(descriptorPath, "utf8").catch(() => null) !== spec.descriptor)
54
+ return null;
55
+ return { npmPrefix: installation.npmPrefix };
56
+ }
@@ -18,7 +18,7 @@ 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";
21
+ import { evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
22
22
  function memoryPruneSnapshotFields(snapshot) {
23
23
  const fields = {};
24
24
  for (const [label, fact] of Object.entries(snapshot)) {
@@ -28,9 +28,25 @@ function memoryPruneSnapshotFields(snapshot) {
28
28
  fields[`${label}_sha256`] = fact.sha256;
29
29
  fields[`${label}_hash_skipped_reason`] = fact.hash_skipped_reason;
30
30
  fields[`${label}_error`] = fact.error;
31
+ fields[`${label}_markdown_heading_count`] = fact.markdown_heading_count;
32
+ fields[`${label}_lessons_reference_count`] = fact.lessons_reference_count;
33
+ fields[`${label}_same_as_shared_path`] = fact.same_as_shared_path;
31
34
  }
32
35
  return fields;
33
36
  }
37
+ function logMemoryPruneDiagnosticsFailure(input, traceId, phase, error) {
38
+ const errorCode = error?.code;
39
+ dslog("memory_prune.diagnostics_failed", "长期记忆收尾诊断失败", {
40
+ level: "WARN",
41
+ execution_id: input.executionId,
42
+ agent_handle: input.handle,
43
+ task_key: input.taskKey,
44
+ prune_trace_id: traceId,
45
+ diagnostics_phase: phase,
46
+ error_name: error instanceof Error ? error.name : "unknown",
47
+ error_code: errorCode,
48
+ });
49
+ }
34
50
  function truncateUtf8(value, maxBytes) {
35
51
  if (maxBytes <= 0)
36
52
  return "";
@@ -62,6 +78,7 @@ export function withLocalExecutionFacts(serverPrompt, maxBytes) {
62
78
  };
63
79
  }
64
80
  const STDERR_TAIL_CAP = 2_000;
81
+ const MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS = 2_000;
65
82
  const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
66
83
  const RESERVED_ENV = new Set([
67
84
  "PATH",
@@ -158,6 +175,7 @@ function resolvePrompt(prompt, context) {
158
175
  return typeof prompt === "string" ? prompt : prompt(context);
159
176
  }
160
177
  const nativeSessionLeaseTails = new Map();
178
+ const activeMemoryPrunes = new Map();
161
179
  async function withKeyedLease(key, operation, cancellation) {
162
180
  const predecessor = nativeSessionLeaseTails.get(key) ?? Promise.resolve();
163
181
  let release;
@@ -221,6 +239,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
221
239
  let memoryPruneRuntimeExitCode;
222
240
  let memoryPruneExecutorCompleted = false;
223
241
  let memoryPruneFailurePhase = "diagnostics_before";
242
+ let memoryPruneBeforeSnapshot = null;
243
+ let memoryPruneSharedWriteKey = null;
224
244
  let executionWorkspace = workspace;
225
245
  try {
226
246
  if (isDeepSeekCodex && !providerConfig.providerApiKey) {
@@ -326,14 +346,36 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
326
346
  const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
327
347
  memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
328
348
  if (memoryPruneTraceId !== null) {
329
- const snapshot = await inspectMemoryPruneFiles(executionWorkspace.dir, executionWorkspace.workLogPath);
330
- dslog("memory_prune.files_before", "长期记忆收尾执行前文件指纹", {
349
+ memoryPruneSharedWriteKey = JSON.stringify([input.launch.agentsRoot, input.handle]);
350
+ const activePruneCount = (activeMemoryPrunes.get(memoryPruneSharedWriteKey) ?? 0) + 1;
351
+ activeMemoryPrunes.set(memoryPruneSharedWriteKey, activePruneCount);
352
+ dslog(activePruneCount > 1
353
+ ? "memory_prune.shared_write_overlap_detected"
354
+ : "memory_prune.shared_write_started", activePruneCount > 1
355
+ ? "同一 Agent 存在重叠的共享记忆收尾"
356
+ : "Agent 共享记忆收尾已开始", {
357
+ ...(activePruneCount > 1 ? { level: "WARN" } : {}),
331
358
  execution_id: input.executionId,
332
359
  agent_handle: input.handle,
333
360
  task_key: input.taskKey,
334
361
  prune_trace_id: memoryPruneTraceId,
335
- ...memoryPruneSnapshotFields(snapshot),
362
+ active_prune_count: activePruneCount,
336
363
  });
364
+ try {
365
+ const snapshot = await inspectMemoryPruneFilesWithinDeadline(executionWorkspace.dir, executionWorkspace.runDir, executionWorkspace.workLogPath, dependencies.memoryPruneDiagnosticsTimeoutMs
366
+ ?? MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS);
367
+ memoryPruneBeforeSnapshot = snapshot;
368
+ dslog("memory_prune.files_before", "长期记忆收尾执行前文件指纹", {
369
+ execution_id: input.executionId,
370
+ agent_handle: input.handle,
371
+ task_key: input.taskKey,
372
+ prune_trace_id: memoryPruneTraceId,
373
+ ...memoryPruneSnapshotFields(snapshot),
374
+ });
375
+ }
376
+ catch (error) {
377
+ logMemoryPruneDiagnosticsFailure(input, memoryPruneTraceId, "before", error);
378
+ }
337
379
  }
338
380
  memoryPruneFailurePhase = "prompt_write";
339
381
  await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
@@ -583,24 +625,60 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
583
625
  finally {
584
626
  startupReservation?.release();
585
627
  if (memoryPruneTraceId !== null) {
586
- const snapshot = await inspectMemoryPruneFiles(executionWorkspace.dir, executionWorkspace.workLogPath);
587
- dslog("memory_prune.files_after", "长期记忆收尾执行后文件指纹", {
628
+ try {
629
+ const snapshot = await inspectMemoryPruneFilesWithinDeadline(executionWorkspace.dir, executionWorkspace.runDir, executionWorkspace.workLogPath, dependencies.memoryPruneDiagnosticsTimeoutMs
630
+ ?? MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS);
631
+ dslog("memory_prune.files_after", "长期记忆收尾执行后文件指纹", {
632
+ execution_id: input.executionId,
633
+ agent_handle: input.handle,
634
+ task_key: input.taskKey,
635
+ prune_trace_id: memoryPruneTraceId,
636
+ runtime_exit_code: memoryPruneRuntimeExitCode,
637
+ executor_outcome: memoryPruneExecutorCompleted ? "succeeded" : "failed",
638
+ ...memoryPruneSnapshotFields(snapshot),
639
+ });
640
+ if (memoryPruneBeforeSnapshot !== null) {
641
+ const postcondition = evaluateMemoryPrunePostcondition(memoryPruneBeforeSnapshot, snapshot, {
642
+ executorCompleted: memoryPruneExecutorCompleted,
643
+ ...(memoryPruneRuntimeExitCode === undefined
644
+ ? {}
645
+ : { runtimeExitCode: memoryPruneRuntimeExitCode }),
646
+ });
647
+ dslog("memory_prune.postcondition_evaluated", "长期记忆收尾后置条件已评估", {
648
+ execution_id: input.executionId,
649
+ agent_handle: input.handle,
650
+ task_key: input.taskKey,
651
+ prune_trace_id: memoryPruneTraceId,
652
+ postcondition_outcome: postcondition.outcome,
653
+ ...Object.fromEntries(Object.entries(postcondition).filter(([key]) => key !== "outcome")),
654
+ });
655
+ }
656
+ }
657
+ catch (error) {
658
+ logMemoryPruneDiagnosticsFailure(input, memoryPruneTraceId, "after", error);
659
+ }
660
+ dslog("memory_prune.execution_completed", "长期记忆收尾执行结束", {
588
661
  execution_id: input.executionId,
589
662
  agent_handle: input.handle,
590
663
  task_key: input.taskKey,
591
664
  prune_trace_id: memoryPruneTraceId,
592
- runtime_exit_code: memoryPruneRuntimeExitCode,
593
665
  executor_outcome: memoryPruneExecutorCompleted ? "succeeded" : "failed",
594
- ...memoryPruneSnapshotFields(snapshot),
666
+ runtime_exit_code: memoryPruneRuntimeExitCode,
667
+ ...(memoryPruneExecutorCompleted ? {} : { failure_phase: memoryPruneFailurePhase }),
595
668
  });
596
- dslog("memory_prune.execution_completed", "长期记忆收尾执行结束", {
669
+ }
670
+ if (memoryPruneSharedWriteKey !== null) {
671
+ const activePruneCount = Math.max(0, (activeMemoryPrunes.get(memoryPruneSharedWriteKey) ?? 1) - 1);
672
+ if (activePruneCount === 0)
673
+ activeMemoryPrunes.delete(memoryPruneSharedWriteKey);
674
+ else
675
+ activeMemoryPrunes.set(memoryPruneSharedWriteKey, activePruneCount);
676
+ dslog("memory_prune.shared_write_finished", "Agent 共享记忆收尾已结束", {
597
677
  execution_id: input.executionId,
598
678
  agent_handle: input.handle,
599
679
  task_key: input.taskKey,
600
680
  prune_trace_id: memoryPruneTraceId,
601
- executor_outcome: memoryPruneExecutorCompleted ? "succeeded" : "failed",
602
- runtime_exit_code: memoryPruneRuntimeExitCode,
603
- ...(memoryPruneExecutorCompleted ? {} : { failure_phase: memoryPruneFailurePhase }),
681
+ active_prune_count: activePruneCount,
604
682
  });
605
683
  }
606
684
  const attachmentDirectories = new Set([
@@ -2,23 +2,36 @@ import { createHash } from "node:crypto";
2
2
  import { createReadStream } from "node:fs";
3
3
  import { stat } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
+ import { StringDecoder } from "node:string_decoder";
5
6
  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
7
  export const MEMORY_PRUNE_HASH_LIMIT_BYTES = 10 * 1024 * 1024;
7
8
  export function parseMemoryPruneTraceId(wakePrompt) {
8
9
  return TRACE_PATTERN.exec(wakePrompt)?.[1] ?? null;
9
10
  }
10
- async function inspectFile(path) {
11
+ async function inspectFile(path, markdownFacts = false, sameAsSharedPath, signal) {
11
12
  let metadata;
12
13
  try {
13
14
  metadata = await stat(path);
14
15
  }
15
16
  catch (error) {
16
17
  const code = error.code;
18
+ const pathFact = sameAsSharedPath === undefined
19
+ ? {}
20
+ : { same_as_shared_path: sameAsSharedPath };
17
21
  if (code === "ENOENT")
18
- return { exists: false };
19
- return { exists: false, error: code ?? (error instanceof Error ? error.name : "unknown") };
22
+ return { exists: false, ...pathFact };
23
+ return {
24
+ exists: false,
25
+ ...pathFact,
26
+ error: code ?? (error instanceof Error ? error.name : "unknown"),
27
+ };
20
28
  }
21
- const fact = { exists: true, size: metadata.size, mtime_ms: metadata.mtimeMs };
29
+ const fact = {
30
+ exists: true,
31
+ size: metadata.size,
32
+ mtime_ms: metadata.mtimeMs,
33
+ ...(sameAsSharedPath === undefined ? {} : { same_as_shared_path: sameAsSharedPath }),
34
+ };
22
35
  if (!metadata.isFile())
23
36
  return { ...fact, hash_skipped_reason: "not_regular_file" };
24
37
  if (metadata.size > MEMORY_PRUNE_HASH_LIMIT_BYTES) {
@@ -27,7 +40,29 @@ async function inspectFile(path) {
27
40
  try {
28
41
  const hash = createHash("sha256");
29
42
  let bytesRead = 0;
30
- const stream = createReadStream(path);
43
+ let markdownHeadingCount = 0;
44
+ let lessonsReferenceCount = 0;
45
+ let pendingText = "";
46
+ const decoder = new StringDecoder("utf8");
47
+ const inspectText = (text, final) => {
48
+ if (!markdownFacts)
49
+ return;
50
+ pendingText += text;
51
+ const lines = pendingText.split(/\r?\n/);
52
+ pendingText = final ? "" : (lines.pop() ?? "");
53
+ for (const line of lines) {
54
+ if (/^\s{0,3}#{1,6}(?:\s|$)/.test(line))
55
+ markdownHeadingCount += 1;
56
+ lessonsReferenceCount += line.match(/notes\/lessons\.md/gi)?.length ?? 0;
57
+ }
58
+ if (final && pendingText !== "") {
59
+ if (/^\s{0,3}#{1,6}(?:\s|$)/.test(pendingText))
60
+ markdownHeadingCount += 1;
61
+ lessonsReferenceCount += pendingText.match(/notes\/lessons\.md/gi)?.length ?? 0;
62
+ pendingText = "";
63
+ }
64
+ };
65
+ const stream = createReadStream(path, signal === undefined ? {} : { signal });
31
66
  try {
32
67
  for await (const chunk of stream) {
33
68
  bytesRead += chunk.length;
@@ -35,23 +70,116 @@ async function inspectFile(path) {
35
70
  return { ...fact, hash_skipped_reason: "grew_too_large" };
36
71
  }
37
72
  hash.update(chunk);
73
+ inspectText(decoder.write(chunk), false);
38
74
  }
75
+ inspectText(decoder.end(), true);
39
76
  }
40
77
  finally {
41
78
  stream.destroy();
42
79
  }
43
- return { ...fact, sha256: hash.digest("hex") };
80
+ return {
81
+ ...fact,
82
+ sha256: hash.digest("hex"),
83
+ ...(markdownFacts ? {
84
+ markdown_heading_count: markdownHeadingCount,
85
+ lessons_reference_count: lessonsReferenceCount,
86
+ } : {}),
87
+ };
44
88
  }
45
89
  catch (error) {
46
90
  const code = error.code;
47
91
  return { ...fact, error: code ?? (error instanceof Error ? error.name : "unknown") };
48
92
  }
49
93
  }
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),
94
+ export async function inspectMemoryPruneFiles(homeDir, runDir, workLogPath, signal) {
95
+ const cwdIsHome = runDir === homeDir;
96
+ const [memory, lessons, cwdMemory, cwdLessons, workLog] = await Promise.all([
97
+ inspectFile(join(homeDir, "MEMORY.md"), true, undefined, signal),
98
+ inspectFile(join(homeDir, "notes", "lessons.md"), true, undefined, signal),
99
+ inspectFile(join(runDir, "MEMORY.md"), true, cwdIsHome, signal),
100
+ inspectFile(join(runDir, "notes", "lessons.md"), true, cwdIsHome, signal),
101
+ inspectFile(workLogPath, false, undefined, signal),
55
102
  ]);
56
- return { memory, lessons, work_log: workLog };
103
+ return {
104
+ memory,
105
+ lessons,
106
+ cwd_memory: cwdMemory,
107
+ cwd_lessons: cwdLessons,
108
+ work_log: workLog,
109
+ };
110
+ }
111
+ export async function inspectMemoryPruneFilesWithinDeadline(homeDir, runDir, workLogPath, timeoutMs) {
112
+ return withinMemoryPruneDiagnosticsDeadline((signal) => inspectMemoryPruneFiles(homeDir, runDir, workLogPath, signal), timeoutMs);
113
+ }
114
+ export async function withinMemoryPruneDiagnosticsDeadline(operation, timeoutMs) {
115
+ const controller = new AbortController();
116
+ let timer;
117
+ const deadline = new Promise((_resolve, reject) => {
118
+ timer = setTimeout(() => {
119
+ const error = Object.assign(new Error("Memory prune diagnostics timed out"), {
120
+ code: "diagnostics_timeout",
121
+ });
122
+ reject(error);
123
+ controller.abort(error);
124
+ }, timeoutMs);
125
+ timer.unref?.();
126
+ });
127
+ try {
128
+ return await Promise.race([
129
+ operation(controller.signal),
130
+ deadline,
131
+ ]);
132
+ }
133
+ finally {
134
+ if (timer !== undefined)
135
+ clearTimeout(timer);
136
+ }
137
+ }
138
+ function fileChanged(before, after) {
139
+ if (before.exists !== after.exists)
140
+ return true;
141
+ if (!before.exists && !after.exists)
142
+ return false;
143
+ if (before.sha256 !== undefined && after.sha256 !== undefined) {
144
+ return before.sha256 !== after.sha256;
145
+ }
146
+ return before.size !== after.size || before.mtime_ms !== after.mtime_ms;
147
+ }
148
+ export function evaluateMemoryPrunePostcondition(before, after, execution) {
149
+ const facts = {
150
+ work_log_before_nonempty: before.work_log.exists && (before.work_log.size ?? 0) > 0,
151
+ work_log_cleared: !after.work_log.exists || after.work_log.size === 0,
152
+ lessons_changed: fileChanged(before.lessons, after.lessons),
153
+ shared_memory_changed: fileChanged(before.memory, after.memory),
154
+ shared_memory_has_lessons_reference: (after.memory.lessons_reference_count ?? 0) > 0,
155
+ cwd_memory_changed: !after.cwd_memory.same_as_shared_path
156
+ && fileChanged(before.cwd_memory, after.cwd_memory),
157
+ cwd_memory_has_lessons_reference: !after.cwd_memory.same_as_shared_path
158
+ && (after.cwd_memory.lessons_reference_count ?? 0) > 0,
159
+ cwd_lessons_changed: !after.cwd_lessons.same_as_shared_path
160
+ && fileChanged(before.cwd_lessons, after.cwd_lessons),
161
+ };
162
+ let outcome;
163
+ if (!execution.executorCompleted)
164
+ outcome = "executor_failed";
165
+ else if (execution.runtimeExitCode !== 0)
166
+ outcome = "runtime_failed";
167
+ else if (!before.work_log.exists)
168
+ outcome = "source_work_log_missing";
169
+ else if (!facts.work_log_before_nonempty)
170
+ outcome = "source_work_log_empty";
171
+ else if (!facts.work_log_cleared)
172
+ outcome = "work_log_not_cleared";
173
+ else if (facts.lessons_changed && facts.shared_memory_has_lessons_reference) {
174
+ outcome = "promoted_and_indexed";
175
+ }
176
+ else if ((!facts.shared_memory_changed && facts.cwd_memory_changed)
177
+ || (!facts.lessons_changed && facts.cwd_lessons_changed)) {
178
+ outcome = "possible_wrong_memory_path";
179
+ }
180
+ else if (facts.lessons_changed)
181
+ outcome = "promoted_without_index_change";
182
+ else
183
+ outcome = "no_files_changed";
184
+ return { outcome, ...facts };
57
185
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.5.41",
3
+ "version": "0.5.42",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",