@nowcrew/daemon 0.5.35 → 0.5.37

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.
@@ -0,0 +1,38 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { basename, dirname, resolve } from "node:path";
3
+ function canonicalPath(path, realpath) {
4
+ try {
5
+ return realpath(path);
6
+ }
7
+ catch (error) {
8
+ const code = error.code;
9
+ return code === "ENOENT" || code === "ENOTDIR" ? resolve(path) : null;
10
+ }
11
+ }
12
+ export function daemonGlobalInstallation(entryPath, platform, realpath = realpathSync.native) {
13
+ if (platform !== "darwin" && platform !== "linux")
14
+ return null;
15
+ const packageRoot = resolve(dirname(entryPath), "..");
16
+ const globalNodeModules = resolve(packageRoot, "..", "..");
17
+ const libDirectory = dirname(globalNodeModules);
18
+ if (basename(globalNodeModules) !== "node_modules" || basename(libDirectory) !== "lib")
19
+ return null;
20
+ if (resolve(entryPath) !== resolve(packageRoot, "dist", "main.js"))
21
+ return null;
22
+ if (resolve(packageRoot) !== resolve(globalNodeModules, "@nowcrew", "daemon"))
23
+ return null;
24
+ const canonicalPackageRoot = canonicalPath(packageRoot, realpath);
25
+ const canonicalGlobalNodeModules = canonicalPath(globalNodeModules, realpath);
26
+ const canonicalNpmPrefix = canonicalPath(dirname(libDirectory), realpath);
27
+ if (canonicalPackageRoot === null || canonicalGlobalNodeModules === null || canonicalNpmPrefix === null)
28
+ return null;
29
+ if (canonicalPackageRoot !== resolve(canonicalGlobalNodeModules, "@nowcrew", "daemon"))
30
+ return null;
31
+ if (canonicalGlobalNodeModules !== resolve(canonicalNpmPrefix, "lib", "node_modules"))
32
+ return null;
33
+ return {
34
+ packageRoot: canonicalPackageRoot,
35
+ globalNodeModules: canonicalGlobalNodeModules,
36
+ npmPrefix: canonicalNpmPrefix,
37
+ };
38
+ }
@@ -7,6 +7,7 @@ const DaemonUpdateMessageSchema = z.object({
7
7
  export function createDaemonUpdateController(deps) {
8
8
  const handled = new Set();
9
9
  let running = null;
10
+ let restartHandoff = null;
10
11
  const failed = (updateId, errorCode) => {
11
12
  deps.sendStatus({ type: "daemon:update-status", updateId, status: "failed", errorCode });
12
13
  };
@@ -19,6 +20,7 @@ export function createDaemonUpdateController(deps) {
19
20
  const installed = await deps.install({
20
21
  targetVersion: message.targetVersion,
21
22
  packageRoot: eligibility.packageRoot,
23
+ npmPrefix: eligibility.npmPrefix,
22
24
  onInstalling: () => {
23
25
  deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "installing" });
24
26
  },
@@ -27,16 +29,42 @@ export function createDaemonUpdateController(deps) {
27
29
  failed(message.updateId, installed.errorCode);
28
30
  return;
29
31
  }
32
+ let released = false;
33
+ const releaseOnce = async () => {
34
+ if (released)
35
+ return;
36
+ released = true;
37
+ await installed.release();
38
+ };
30
39
  deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "restarting" });
31
40
  try {
32
- deps.scheduleRestart(eligibility.serviceSpec);
41
+ const handoff = deps.scheduleRestart(eligibility.serviceSpec);
42
+ const state = {
43
+ release: releaseOnce,
44
+ shutdownBegun: false,
45
+ failureTask: Promise.resolve(),
46
+ };
47
+ state.failureTask = handoff.failed.then(async () => {
48
+ if (state.shutdownBegun)
49
+ return;
50
+ await state.release();
51
+ failed(message.updateId, "restart_failed");
52
+ });
53
+ restartHandoff = state;
33
54
  }
34
55
  catch {
35
- await installed.release();
56
+ await releaseOnce();
36
57
  failed(message.updateId, "restart_failed");
37
58
  }
38
59
  };
39
60
  return {
61
+ drain: async () => {
62
+ await running?.done;
63
+ if (restartHandoff !== null) {
64
+ restartHandoff.shutdownBegun = true;
65
+ await restartHandoff.release();
66
+ }
67
+ },
40
68
  handle: async (input) => {
41
69
  const parsed = DaemonUpdateMessageSchema.safeParse(input);
42
70
  if (!parsed.success)
@@ -1,12 +1,13 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import { constants } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { resolve } from "node:path";
5
- import { daemonHome, listProfiles } from "./computer-profile.js";
6
- import { builtDaemonEntry, isGlobalDaemonEntry } from "./computer-cli.js";
7
- import { buildServiceSpec, serviceStatus, systemCommandRunner, } from "./computer-service.js";
4
+ import { daemonHome, loadProfile, resolveAgentsRoot } from "./computer-profile.js";
5
+ import { builtDaemonEntry } from "./computer-cli.js";
6
+ import { buildServiceSpec, readServiceDescriptor, serviceStatus, } from "./computer-service.js";
7
+ import { daemonGlobalInstallation } from "./daemon-installation.js";
8
+ import { daemonInstallationLeaseHeld } from "./daemon-installation-lease.js";
9
+ import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, serviceDescriptorSha256, } from "./managed-service-registry.js";
8
10
  function defaults() {
9
- const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
10
11
  return {
11
12
  platform: process.platform,
12
13
  profileHome: daemonHome(),
@@ -14,16 +15,13 @@ function defaults() {
14
15
  uid: process.getuid?.(),
15
16
  nodePath: process.execPath,
16
17
  entryPath: builtDaemonEntry(),
17
- resolveGlobalNodeModules: async () => {
18
- const result = await systemCommandRunner(npmCommand, ["root", "--global"]);
19
- if (result.exitCode !== 0 || !result.stdout.trim()) {
20
- throw new Error(result.stderr.trim() || "global npm root unavailable");
21
- }
22
- return result.stdout.trim();
23
- },
24
- listProfiles,
18
+ loadProfile,
19
+ readManagedServices: () => readManagedServiceRegistry(),
20
+ listManagedDescriptorPaths: () => listManagedServiceDescriptorPaths(process.platform, homedir()),
21
+ readServiceDescriptor,
25
22
  serviceStatus,
26
23
  assertWritable: (path) => access(path, constants.W_OK),
24
+ installationLeaseHeld: daemonInstallationLeaseHeld,
27
25
  };
28
26
  }
29
27
  export async function detectDaemonUpdateEligibility(profileName, overrides = {}) {
@@ -33,39 +31,73 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
33
31
  }
34
32
  if (!profileName)
35
33
  return { eligible: false, reason: "profile_required" };
36
- let globalNodeModules;
34
+ const installation = daemonGlobalInstallation(deps.entryPath, deps.platform);
35
+ if (installation === null)
36
+ return { eligible: false, reason: "global_install_required" };
37
+ let profile;
37
38
  try {
38
- globalNodeModules = await deps.resolveGlobalNodeModules();
39
+ profile = await deps.loadProfile(profileName, deps.profileHome, {
40
+ platform: deps.platform,
41
+ userHome: deps.userHome,
42
+ });
39
43
  }
40
44
  catch {
41
- return { eligible: false, reason: "global_install_required" };
45
+ return { eligible: false, reason: "managed_service_identity_mismatch" };
42
46
  }
43
- if (!isGlobalDaemonEntry(deps.entryPath, globalNodeModules)) {
44
- return { eligible: false, reason: "global_install_required" };
47
+ const spec = buildServiceSpec({
48
+ platform: deps.platform,
49
+ profile: profileName,
50
+ userHome: deps.userHome,
51
+ uid: deps.uid,
52
+ nodePath: deps.nodePath,
53
+ entryPath: deps.entryPath,
54
+ profileHome: deps.profileHome,
55
+ });
56
+ const status = await deps.serviceStatus(spec);
57
+ if (!status.running)
58
+ return { eligible: false, reason: "service_not_running" };
59
+ let services;
60
+ try {
61
+ services = await deps.readManagedServices();
62
+ }
63
+ catch {
64
+ return { eligible: false, reason: "managed_service_registry_invalid" };
45
65
  }
46
- const profiles = await deps.listProfiles(deps.profileHome);
47
- const installed = [];
48
- for (const profile of profiles) {
49
- const spec = buildServiceSpec({
66
+ const registered = services.find((record) => record.serviceId === spec.id
67
+ || (record.daemonHome === deps.profileHome && record.profile === profileName));
68
+ if (registered === undefined) {
69
+ return { eligible: false, reason: "managed_service_not_registered" };
70
+ }
71
+ try {
72
+ assertRegistryCoversDescriptors(services, await deps.listManagedDescriptorPaths());
73
+ }
74
+ catch {
75
+ return { eligible: false, reason: "managed_service_registry_incomplete" };
76
+ }
77
+ const descriptor = await deps.readServiceDescriptor(spec).catch(() => null);
78
+ if (descriptor === null || spec.descriptorPath === null || spec.descriptor === null
79
+ || !managedServiceIdentityMatches(registered, {
80
+ version: 1,
50
81
  platform: deps.platform,
51
- profile,
52
- userHome: deps.userHome,
53
- uid: deps.uid,
82
+ serviceId: spec.id,
83
+ profile: profileName,
84
+ daemonHome: deps.profileHome,
85
+ agentsRoot: resolveAgentsRoot(profile.agentsRoot, deps.userHome, deps.platform),
54
86
  nodePath: deps.nodePath,
55
87
  entryPath: deps.entryPath,
56
- profileHome: deps.profileHome,
57
- });
58
- const status = await deps.serviceStatus(spec);
59
- if (status.installed)
60
- installed.push({ profile, spec, running: status.running });
88
+ packageRoot: installation.packageRoot,
89
+ npmPrefix: installation.npmPrefix,
90
+ descriptorPath: spec.descriptorPath,
91
+ descriptorSha256: serviceDescriptorSha256(descriptor),
92
+ })
93
+ || descriptor !== spec.descriptor) {
94
+ return { eligible: false, reason: "managed_service_identity_mismatch" };
95
+ }
96
+ if (!deps.installationLeaseHeld(installation.npmPrefix)) {
97
+ return { eligible: false, reason: "installation_lease_missing" };
61
98
  }
62
- const current = installed.find((entry) => entry.profile === profileName);
63
- if (!current?.running)
64
- return { eligible: false, reason: "service_not_running" };
65
- if (installed.length !== 1)
66
- return { eligible: false, reason: "multiple_managed_profiles" };
67
99
  try {
68
- await deps.assertWritable(globalNodeModules);
100
+ await deps.assertWritable(installation.globalNodeModules);
69
101
  }
70
102
  catch {
71
103
  return { eligible: false, reason: "global_root_not_writable" };
@@ -73,8 +105,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
73
105
  return {
74
106
  eligible: true,
75
107
  profileName,
76
- globalNodeModules,
77
- packageRoot: resolve(globalNodeModules, "@nowcrew", "daemon"),
78
- serviceSpec: current.spec,
108
+ ...installation,
109
+ serviceSpec: spec,
79
110
  };
80
111
  }
@@ -1,6 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import { systemCommandRunner } from "./computer-service.js";
4
+ import { daemonGlobalInstallation } from "./daemon-installation.js";
4
5
  const RELEASED_VERSION_RE = /^\d+\.\d+\.\d+$/;
5
6
  async function readPackageVersion(packageRoot) {
6
7
  const body = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
@@ -10,6 +11,12 @@ export async function installExactDaemonUpdate(input) {
10
11
  if (!RELEASED_VERSION_RE.test(input.targetVersion)) {
11
12
  return { ok: false, errorCode: "ineligible" };
12
13
  }
14
+ const installation = daemonGlobalInstallation(input.currentEntryPath ?? process.argv[1] ?? "", input.platform ?? process.platform);
15
+ if (installation === null
16
+ || installation.packageRoot !== input.packageRoot
17
+ || installation.npmPrefix !== input.npmPrefix) {
18
+ return { ok: false, errorCode: "ineligible" };
19
+ }
13
20
  const local = input.localSlots.tryAcquireExclusive();
14
21
  if (local === null)
15
22
  return { ok: false, errorCode: "runtime_busy" };
@@ -26,6 +33,13 @@ export async function installExactDaemonUpdate(input) {
26
33
  await host.release();
27
34
  local.release();
28
35
  };
36
+ const lockedInstallation = daemonGlobalInstallation(input.currentEntryPath ?? process.argv[1] ?? "", input.platform ?? process.platform);
37
+ if (lockedInstallation === null
38
+ || lockedInstallation.packageRoot !== input.packageRoot
39
+ || lockedInstallation.npmPrefix !== input.npmPrefix) {
40
+ await release();
41
+ return { ok: false, errorCode: "ineligible" };
42
+ }
29
43
  const runner = input.runner ?? systemCommandRunner;
30
44
  try {
31
45
  await input.onInstalling?.();
@@ -37,6 +51,8 @@ export async function installExactDaemonUpdate(input) {
37
51
  const result = await runner(process.platform === "win32" ? "npm.cmd" : "npm", [
38
52
  "install",
39
53
  "--global",
54
+ "--prefix",
55
+ input.npmPrefix,
40
56
  "--ignore-scripts",
41
57
  "--no-audit",
42
58
  "--no-fund",
package/dist/i18n.js CHANGED
@@ -42,7 +42,7 @@ const zh = {
42
42
  "Saved profile '{{name}}' with private credentials.": "已保存配置 '{{name}}',凭证仅私有可读。",
43
43
  "Service '{{id}}' is not installed": "服务 '{{id}}' 尚未安装",
44
44
  "Upgraded daemon and restart request accepted for '{{name}}'. Verify with status.": "daemon 已升级,并已请求重启 '{{name}}';请用 status 确认。",
45
- "Upgraded daemon but skipped restart for '{{name}}': {{reason}}": "daemon 已升级,但已跳过 '{{name}}' 的重启:{{reason}}",
45
+ "Refused daemon upgrade for '{{name}}': {{reason}}": "已拒绝升级 daemon '{{name}}'{{reason}}",
46
46
  "Upgraded daemon. Installed services were not restarted; pass --profile to restart one.": "daemon 已升级;已安装服务尚未重启,可传入 --profile 重启指定服务。",
47
47
  "Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry": "服务生命周期必须使用已构建的 daemon 入口(.js),不能使用 TypeScript 开发入口",
48
48
  "Installed '{{id}}'. Use status to confirm runtime state.": "已安装 '{{id}}';请用 status 确认运行状态。",
@@ -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]),
package/dist/main.js CHANGED
@@ -14,10 +14,13 @@ import { runAgent } from "./runner.js";
14
14
  import { serve } from "./serve.js";
15
15
  import { initSlog, flushSlog } from "./slog.js";
16
16
  import { formatDaemonLogLine } from "./log-format.js";
17
- import { applyProfileToEnv, assertProfileAgentsRootUnique, daemonHome, loadProfile, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, ProfileAgentsRootConflictError, } from "./computer-profile.js";
18
- import { runComputerCommand } from "./computer-cli.js";
17
+ import { applyProfileToEnv, assertProfileAgentsRootUnique, daemonHome, loadProfile, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, ProfileAgentsRootConflictError, resolveAgentsRoot, } from "./computer-profile.js";
18
+ import { builtDaemonEntry, runComputerCommand } from "./computer-cli.js";
19
19
  import { runServeLifecycle } from "./serve-lifecycle.js";
20
20
  import { formatDaemonStartupError } from "./daemon-startup-error.js";
21
+ import { assertManagedServiceStartup } from "./managed-service-startup.js";
22
+ import { daemonGlobalInstallation } from "./daemon-installation.js";
23
+ import { acquireDaemonInstallationLease, runWithDaemonInstallationLease, } from "./daemon-installation-lease.js";
21
24
  async function main() {
22
25
  const computerResult = await runComputerCommand(process.argv.slice(2));
23
26
  if (computerResult !== null) {
@@ -57,8 +60,19 @@ async function main() {
57
60
  if (values.profile) {
58
61
  const home = daemonHome();
59
62
  const profile = await loadProfile(values.profile, home);
60
- if (cmd === "serve")
63
+ if (cmd === "serve") {
61
64
  await assertProfileAgentsRootUnique(profile, home, homedir());
65
+ await assertManagedServiceStartup({
66
+ platform: process.platform,
67
+ profile: profile.name,
68
+ userHome: homedir(),
69
+ uid: process.getuid?.(),
70
+ daemonHome: home,
71
+ agentsRoot: resolveAgentsRoot(profile.agentsRoot, homedir(), process.platform),
72
+ nodePath: process.execPath,
73
+ entryPath: builtDaemonEntry(),
74
+ });
75
+ }
62
76
  applyProfileToEnv(profile, process.env);
63
77
  }
64
78
  // 命令行参数优先于环境变量,填回 env 供 loadConfig 读取
@@ -80,10 +94,18 @@ async function main() {
80
94
  }
81
95
  if (cmd === "serve") {
82
96
  process.stdout.write(formatDaemonLogLine(`🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...`) + "\n");
83
- const service = serve(config, {
84
- ...(values.profile === undefined ? {} : { profileName: values.profile }),
97
+ const installation = values.profile === undefined
98
+ ? null
99
+ : daemonGlobalInstallation(builtDaemonEntry(), process.platform);
100
+ const installationLease = installation === null
101
+ ? null
102
+ : await acquireDaemonInstallationLease(installation.npmPrefix);
103
+ await runWithDaemonInstallationLease(installationLease, async () => {
104
+ const service = serve(config, {
105
+ ...(values.profile === undefined ? {} : { profileName: values.profile }),
106
+ });
107
+ await runServeLifecycle(service);
85
108
  });
86
- await runServeLifecycle(service);
87
109
  return;
88
110
  }
89
111
  if (!values.agent || !values.channel) {
@@ -0,0 +1,92 @@
1
+ import { inspectDaemonInstallationLease } from "./daemon-installation-lease.js";
2
+ import { readServiceDescriptor } from "./computer-service.js";
3
+ import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, } from "./managed-service-registry.js";
4
+ function errorDetail(error) {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
7
+ export async function inspectManagedServiceDiagnostics(input) {
8
+ const registry = input.registry ?? {};
9
+ const userHome = registry.userHome;
10
+ const readServices = input.readManagedServices
11
+ ?? (() => readManagedServiceRegistry(registry));
12
+ const listDescriptors = input.listManagedDescriptorPaths
13
+ ?? (() => listManagedServiceDescriptorPaths(input.spec.platform, userHome));
14
+ const readDescriptor = input.readDescriptor
15
+ ?? (() => readServiceDescriptor(input.spec));
16
+ const inspectLease = input.inspectInstallationLease
17
+ ?? (() => inspectDaemonInstallationLease(input.expectedRecord.npmPrefix, {
18
+ ...(userHome === undefined ? {} : { userHome }),
19
+ }));
20
+ let services = null;
21
+ let registryCheck;
22
+ try {
23
+ services = await readServices();
24
+ assertRegistryCoversDescriptors(services, await listDescriptors());
25
+ registryCheck = {
26
+ name: "managed-service-registry",
27
+ ok: true,
28
+ detail: `${services.length} registered service(s); all descriptors are covered`,
29
+ };
30
+ }
31
+ catch (error) {
32
+ registryCheck = {
33
+ name: "managed-service-registry",
34
+ ok: false,
35
+ detail: errorDetail(error),
36
+ };
37
+ }
38
+ let identityCheck;
39
+ const registered = services?.find((record) => record.serviceId === input.expectedRecord.serviceId
40
+ || (record.daemonHome === input.expectedRecord.daemonHome
41
+ && record.profile === input.expectedRecord.profile));
42
+ if (registered === undefined) {
43
+ identityCheck = {
44
+ name: "managed-service-identity",
45
+ ok: false,
46
+ detail: services === null
47
+ ? "identity unavailable because the registry is invalid"
48
+ : `managed service '${input.expectedRecord.serviceId}' is not registered`,
49
+ };
50
+ }
51
+ else {
52
+ try {
53
+ const { generation: _generation, createdAt: _createdAt, ...expectedIdentity } = input.expectedRecord;
54
+ const descriptor = await readDescriptor();
55
+ const matches = descriptor === input.spec.descriptor
56
+ && managedServiceIdentityMatches(registered, expectedIdentity);
57
+ identityCheck = {
58
+ name: "managed-service-identity",
59
+ ok: matches,
60
+ detail: matches
61
+ ? `registry and descriptor match '${input.expectedRecord.serviceId}'`
62
+ : `registry or descriptor identity does not match '${input.expectedRecord.serviceId}'`,
63
+ };
64
+ }
65
+ catch (error) {
66
+ identityCheck = {
67
+ name: "managed-service-identity",
68
+ ok: false,
69
+ detail: errorDetail(error),
70
+ };
71
+ }
72
+ }
73
+ let installationLease;
74
+ try {
75
+ installationLease = await inspectLease();
76
+ }
77
+ catch (error) {
78
+ installationLease = { status: "corrupt", detail: errorDetail(error) };
79
+ }
80
+ const leaseCheck = {
81
+ name: "installation-lease",
82
+ ok: installationLease.status === "owned" && installationLease.ownerAlive === true,
83
+ detail: installationLease.detail,
84
+ };
85
+ const checks = [registryCheck, identityCheck, leaseCheck];
86
+ return {
87
+ healthy: checks.every((check) => check.ok),
88
+ npmPrefix: input.expectedRecord.npmPrefix,
89
+ checks,
90
+ installationLease,
91
+ };
92
+ }