@nowcrew/daemon 0.6.77-beta.8 → 0.6.77-beta.9

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
@@ -486,6 +486,11 @@ Workspace persistence root is still selected independently by `NOWWORK_HOME`; it
486
486
  directory or an ECS-specific NAS directory. Runtime packages, locks, sockets, PIDs and temporary
487
487
  files must remain on the machine running the daemon.
488
488
 
489
+ The same local root also owns shared daemon control state:
490
+ `daemon/managed-services/registry.json`, `daemon/managed-services/installation-leases/`,
491
+ `daemon/compute-execution/`, and `logs/sls-spool/`. This keeps service ownership, upgrade leases,
492
+ host-wide execution admission, and offline log delivery on the daemon host rather than on NAS.
493
+
489
494
  The selector is controlled by `NOWWORK_LOCAL_HOME_MODE`:
490
495
 
491
496
  - `auto` (default): use an existing `.nowwork` installation, otherwise preserve an existing `.crew`
@@ -498,6 +503,8 @@ local root and should point to a local filesystem, never to the NAS Workspace ro
498
503
  check remains a doctor/migration gate. If the same profile
499
504
  is found in both roots, startup/install is refused until the duplicate is resolved explicitly. The
500
505
  old `.crew` tree is not deleted by this compatibility layer.
506
+ For shared machine control state, `auto` prefers an existing `~/.nowwork` root; during a mixed
507
+ migration window, set `NOWWORK_LOCAL_HOME_MODE=legacy` on services that still use the old registry.
501
508
 
502
509
  - `src/serve.ts`: connection, negotiation, routing, sync, and legacy boundary.
503
510
  - `src/daemon-update-eligibility.ts`, `src/daemon-updater.ts`, and `src/daemon-update-controller.ts`:
@@ -13,6 +13,7 @@ import { daemonReleaseChannel, isDaemonReleaseVersion } from "./daemon-release-v
13
13
  import { daemonRegistryCandidates } from "./daemon-registry.js";
14
14
  import { npmInvocation } from "./npm-command.js";
15
15
  import { discoverExactLegacyService, } from "./automatic-service-migration.js";
16
+ import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
16
17
  function profileStorage(platform, userHome, runner) {
17
18
  return {
18
19
  platform,
@@ -31,6 +32,7 @@ function sameHostname(left, right) {
31
32
  function defaultDependencies() {
32
33
  const platform = process.platform;
33
34
  const userHome = homedir();
35
+ const localHome = resolveDaemonLocalStateRootFromEnv({ userHome, platform, environment: process.env });
34
36
  const runner = systemCommandRunner;
35
37
  const storage = profileStorage(platform, userHome, runner);
36
38
  let managedRecords = [];
@@ -59,7 +61,7 @@ function defaultDependencies() {
59
61
  },
60
62
  captureService: async (spec, target) => {
61
63
  if (platform !== "win32") {
62
- managedRecords = await readManagedServiceRegistry({ userHome });
64
+ managedRecords = await readManagedServiceRegistry({ userHome, localHome });
63
65
  const entryPath = spec.daemonCommand[1];
64
66
  const installation = daemonGlobalInstallation(entryPath, platform);
65
67
  if (installation === null) {
@@ -93,7 +95,7 @@ function defaultDependencies() {
93
95
  managedServices: managedRecords,
94
96
  }),
95
97
  stopService: (spec) => serviceAction(spec, "stop", runner),
96
- acquireInstallationLease: (npmPrefix) => acquireDaemonInstallationLease(npmPrefix, { userHome }),
98
+ acquireInstallationLease: (npmPrefix) => acquireDaemonInstallationLease(npmPrefix, { userHome, localHome }),
97
99
  installPackage: installExactDaemonPackage,
98
100
  saveTargetProfile: async (profile, daemonHome) => {
99
101
  await saveProfile(profile, daemonHome, storage);
@@ -120,7 +122,7 @@ function defaultDependencies() {
120
122
  spec,
121
123
  record,
122
124
  runner,
123
- registry: { userHome },
125
+ registry: { userHome, localHome },
124
126
  });
125
127
  },
126
128
  removeLegacyService: async (legacy) => {
@@ -147,13 +149,13 @@ function defaultDependencies() {
147
149
  await restoreExactService(spec, previous, runner);
148
150
  return;
149
151
  }
150
- const current = (await readManagedServiceRegistry({ userHome })).find((record) => record.serviceId === spec.id);
152
+ const current = (await readManagedServiceRegistry({ userHome, localHome })).find((record) => record.serviceId === spec.id);
151
153
  if (current !== undefined && JSON.stringify(current) !== JSON.stringify(previousManagedRecord)) {
152
154
  await uninstallManagedService({
153
155
  spec,
154
156
  record: current,
155
157
  runner,
156
- registry: { userHome },
158
+ registry: { userHome, localHome },
157
159
  });
158
160
  }
159
161
  if (previousManagedRecord !== undefined) {
@@ -161,7 +163,7 @@ function defaultDependencies() {
161
163
  spec,
162
164
  record: previousManagedRecord,
163
165
  runner,
164
- registry: { userHome },
166
+ registry: { userHome, localHome },
165
167
  });
166
168
  if (previous.running)
167
169
  await serviceAction(spec, "start", runner);
@@ -18,7 +18,7 @@ import { assertManagedServiceIdentity, buildManagedServiceRecord, installManaged
18
18
  import { inspectManagedServiceDiagnostics } from "./managed-service-diagnostics.js";
19
19
  import { readManagedServiceRegistry } from "./managed-service-registry.js";
20
20
  import { installDaemonAutomatically, } from "./automatic-daemon-installer.js";
21
- import { resolveDaemonLocalHome } from "./daemon-local-home.js";
21
+ import { resolveDaemonLocalHome, resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
22
22
  const COMPUTER_COMMANDS = new Set(["profile", "doctor", "install", "uninstall", "start", "stop", "restart", "status", "upgrade"]);
23
23
  export function builtDaemonEntry(moduleUrl = import.meta.url) {
24
24
  return moduleUrl.endsWith(".js")
@@ -187,6 +187,12 @@ export async function runComputerCommand(argv, overrides = {}) {
187
187
  ...(configuredLocalHomeMode === undefined ? {} : { mode: configuredLocalHomeMode }),
188
188
  }).daemonHome
189
189
  : daemonHome(deps.env);
190
+ const localHome = resolveDaemonLocalStateRoot({
191
+ userHome: deps.userHome,
192
+ platform: deps.platform,
193
+ explicit: deps.env.NOWWORK_LOCAL_HOME,
194
+ mode: configuredLocalHomeMode,
195
+ });
190
196
  const profileStorage = {
191
197
  platform: deps.platform,
192
198
  userHome: deps.userHome,
@@ -298,7 +304,7 @@ export async function runComputerCommand(argv, overrides = {}) {
298
304
  try {
299
305
  installationLease = installation === null
300
306
  ? null
301
- : await deps.acquireInstallationLease(installation.npmPrefix, { userHome: deps.userHome });
307
+ : await deps.acquireInstallationLease(installation.npmPrefix, { userHome: deps.userHome, localHome });
302
308
  await upgradeDaemon(deps.platform, deps.runner, installation?.npmPrefix, releaseTag, configuredRegistryUrl === undefined
303
309
  ? restartProfile?.registryUrl
304
310
  ?? configuredRegistryUrl
@@ -380,7 +386,7 @@ export async function runComputerCommand(argv, overrides = {}) {
380
386
  spec,
381
387
  record: managedRecord,
382
388
  runner: deps.runner,
383
- registry: { userHome: deps.userHome },
389
+ registry: { userHome: deps.userHome, localHome },
384
390
  });
385
391
  deps.stdout(`${td("Installed '{{id}}'. Use status to confirm runtime state.", { id: spec.id })}\n`);
386
392
  return 0;
@@ -389,7 +395,7 @@ export async function runComputerCommand(argv, overrides = {}) {
389
395
  if (managedRecord === null)
390
396
  await uninstallService(spec, deps.runner);
391
397
  else {
392
- const current = (await readManagedServiceRegistry({ userHome: deps.userHome })).find((record) => record.serviceId === spec.id
398
+ const current = (await readManagedServiceRegistry({ userHome: deps.userHome, localHome })).find((record) => record.serviceId === spec.id
393
399
  && record.daemonHome === managedRecord.daemonHome
394
400
  && record.profile === name);
395
401
  if (current === undefined) {
@@ -400,7 +406,7 @@ export async function runComputerCommand(argv, overrides = {}) {
400
406
  daemonHome: home,
401
407
  npmPrefix: installation.npmPrefix,
402
408
  runner: deps.runner,
403
- registry: { userHome: deps.userHome },
409
+ registry: { userHome: deps.userHome, localHome },
404
410
  acquireInstallationLease: deps.acquireInstallationLease,
405
411
  });
406
412
  }
@@ -420,7 +426,7 @@ export async function runComputerCommand(argv, overrides = {}) {
420
426
  daemonHome: home,
421
427
  npmPrefix: installation.npmPrefix,
422
428
  runner: deps.runner,
423
- registry: { userHome: deps.userHome },
429
+ registry: { userHome: deps.userHome, localHome },
424
430
  acquireInstallationLease: deps.acquireInstallationLease,
425
431
  });
426
432
  }
@@ -430,7 +436,7 @@ export async function runComputerCommand(argv, overrides = {}) {
430
436
  spec,
431
437
  record: current,
432
438
  runner: deps.runner,
433
- registry: { userHome: deps.userHome },
439
+ registry: { userHome: deps.userHome, localHome },
434
440
  });
435
441
  }
436
442
  }
@@ -442,7 +448,7 @@ export async function runComputerCommand(argv, overrides = {}) {
442
448
  await assertManagedServiceIdentity({
443
449
  spec,
444
450
  record: managedRecord,
445
- registry: { userHome: deps.userHome },
451
+ registry: { userHome: deps.userHome, localHome },
446
452
  });
447
453
  }
448
454
  await serviceAction(spec, command, deps.runner);
@@ -473,7 +479,7 @@ export async function runComputerCommand(argv, overrides = {}) {
473
479
  : await inspectManagedServiceDiagnostics({
474
480
  spec,
475
481
  expectedRecord: managedRecord,
476
- registry: { userHome: deps.userHome },
482
+ registry: { userHome: deps.userHome, localHome },
477
483
  });
478
484
  const checks = [
479
485
  ...serviceChecks,
@@ -3,6 +3,7 @@ import { realpathSync } from "node:fs";
3
3
  import { mkdir, open } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { resolve } from "node:path";
6
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
6
7
  import { defaultProcessController } from "./execution-journal.js";
7
8
  import { createJournalLease, createJournalLeaseRegistry, inspectJournalLock, } from "./execution-journal-lock.js";
8
9
  const registry = createJournalLeaseRegistry();
@@ -18,9 +19,10 @@ function canonicalPrefix(npmPrefix) {
18
19
  return absolute;
19
20
  }
20
21
  }
21
- function leaseDirectory(npmPrefix, userHome) {
22
+ function leaseDirectory(npmPrefix, userHome, localHome) {
22
23
  const fingerprint = createHash("sha256").update(resolve(npmPrefix)).digest("hex");
23
- return resolve(userHome, ".crew", "daemon", "managed-services", "installation-leases", fingerprint);
24
+ const root = localHome ?? resolveDaemonLocalStateRoot({ userHome, platform: process.platform });
25
+ return resolve(root, "daemon", "managed-services", "installation-leases", fingerprint);
24
26
  }
25
27
  async function syncDirectory(path) {
26
28
  if (process.platform === "win32")
@@ -35,7 +37,8 @@ async function syncDirectory(path) {
35
37
  }
36
38
  export async function acquireDaemonInstallationLease(npmPrefix, options = {}) {
37
39
  const normalizedPrefix = canonicalPrefix(npmPrefix);
38
- const directory = leaseDirectory(normalizedPrefix, options.userHome ?? homedir());
40
+ const userHome = options.userHome ?? homedir();
41
+ const directory = leaseDirectory(normalizedPrefix, userHome, options.localHome);
39
42
  await mkdir(directory, { recursive: true, mode: 0o700 });
40
43
  const lease = createJournalLease({
41
44
  directory,
@@ -75,7 +78,7 @@ export function daemonInstallationLeaseHeld(npmPrefix) {
75
78
  export async function inspectDaemonInstallationLease(npmPrefix, options = {}) {
76
79
  const normalizedPrefix = canonicalPrefix(npmPrefix);
77
80
  return inspectJournalLock({
78
- directory: leaseDirectory(normalizedPrefix, options.userHome ?? homedir()),
81
+ directory: leaseDirectory(normalizedPrefix, options.userHome ?? homedir(), options.localHome),
79
82
  inspectIdentity: options.inspectIdentity ?? defaultProcessController.inspectIdentity,
80
83
  });
81
84
  }
@@ -1,4 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import { posix, win32 } from "node:path";
3
4
  const PROFILE = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
4
5
  const CONTROL = /[\0\r\n]/u;
@@ -71,3 +72,37 @@ export function resolveDaemonLocalHome(input) {
71
72
  return legacy;
72
73
  return resolution("nowwork", nowworkRoot, input.profile, input.platform, "auto_new");
73
74
  }
75
+ /**
76
+ * Resolve the local machine state root shared by service metadata, leases,
77
+ * host coordination and offline logs. It intentionally stays separate from
78
+ * NOWWORK_HOME, which may point at a NAS persistent data root.
79
+ */
80
+ export function resolveDaemonLocalStateRoot(input) {
81
+ const paths = pathsFor(input.platform);
82
+ const mode = parseDaemonLocalHomeMode(input.mode);
83
+ const nowworkRoot = absoluteRoot(input.explicit ?? paths.join(input.userHome, ".nowwork"), input.userHome, input.platform);
84
+ const legacyRoot = absoluteRoot(paths.join(input.userHome, ".crew"), input.userHome, input.platform);
85
+ if (mode === "legacy")
86
+ return legacyRoot;
87
+ if (input.explicit !== undefined || mode === "nowwork")
88
+ return nowworkRoot;
89
+ const exists = input.exists ?? existsSync;
90
+ // Once the new root exists, shared state must follow it even while the
91
+ // legacy root is retained as a rollback source.
92
+ if (exists(nowworkRoot))
93
+ return nowworkRoot;
94
+ if (exists(legacyRoot))
95
+ return legacyRoot;
96
+ return nowworkRoot;
97
+ }
98
+ export function resolveDaemonLocalStateRootFromEnv(input = {}) {
99
+ const environment = input.environment ?? process.env;
100
+ return resolveDaemonLocalStateRoot({
101
+ userHome: input.userHome ?? homedir(),
102
+ platform: input.platform ?? process.platform,
103
+ ...(environment.NOWWORK_LOCAL_HOME === undefined ? {} : { explicit: environment.NOWWORK_LOCAL_HOME }),
104
+ ...(environment.NOWWORK_LOCAL_HOME_MODE === undefined && environment.NOWWORK_STORAGE_MODE === undefined
105
+ ? {}
106
+ : { mode: environment.NOWWORK_LOCAL_HOME_MODE ?? environment.NOWWORK_STORAGE_MODE }),
107
+ });
108
+ }
@@ -21,6 +21,9 @@ const MigrationMessageSchema = z.object({
21
21
  function migrationBackupRoot(input, id) {
22
22
  return resolve(input.backupRoot ?? resolve(input.userHome ?? homedir(), ".crew", "migration-backups"), id);
23
23
  }
24
+ function legacyLocalHome(input) {
25
+ return resolve(input.userHome ?? homedir(), ".crew");
26
+ }
24
27
  async function assertEmptyOrMissing(path) {
25
28
  try {
26
29
  const entries = await readdir(path);
@@ -77,10 +80,10 @@ async function backupAndCopy(input, id, targetHome, targetAgents, service) {
77
80
  }
78
81
  async function cleanupCurrentServiceDefault(spec, input) {
79
82
  const userHome = input.userHome ?? homedir();
80
- const records = await readManagedServiceRegistry({ userHome });
83
+ const records = await readManagedServiceRegistry({ userHome, localHome: legacyLocalHome(input) });
81
84
  const record = records.find((candidate) => candidate.profile === spec.profile && candidate.daemonHome === input.currentDaemonHome);
82
85
  if (record !== undefined && (spec.platform === "darwin" || spec.platform === "linux")) {
83
- await uninstallManagedService({ spec, record, runner: systemCommandRunner, registry: { userHome } });
86
+ await uninstallManagedService({ spec, record, runner: systemCommandRunner, registry: { userHome, localHome: legacyLocalHome(input) } });
84
87
  return;
85
88
  }
86
89
  // Windows and legacy descriptors are handled by the normal CLI/service lifecycle.
@@ -148,7 +151,7 @@ export function createDaemonMigrationController(input) {
148
151
  throw new Error("current managed service identity is not exact and running");
149
152
  currentService = { installed: exact.installed, running: exact.running };
150
153
  if (platform === "darwin" || platform === "linux") {
151
- const records = await (input.readManagedRecords ?? (() => readManagedServiceRegistry({ userHome })))();
154
+ const records = await (input.readManagedRecords ?? (() => readManagedServiceRegistry({ userHome, localHome: legacyLocalHome(input) })))();
152
155
  managedRecord = records.find((record) => record.serviceId === currentSpec.id && record.profile === input.profileName && record.daemonHome === input.currentDaemonHome) ?? null;
153
156
  if (managedRecord === null)
154
157
  throw new Error("current managed service registry record is missing");
@@ -9,6 +9,7 @@ import { daemonGlobalInstallation } from "./daemon-installation.js";
9
9
  import { daemonInstallationLeaseHeld } from "./daemon-installation-lease.js";
10
10
  import { inspectLegacyServiceInstallation } from "./legacy-service-installation.js";
11
11
  import { inspectWindowsServiceUpdateScope } from "./windows-scheduled-task.js";
12
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
12
13
  import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, serviceDescriptorSha256, } from "./managed-service-registry.js";
13
14
  export async function managedDaemonCapabilities(eligibility) {
14
15
  try {
@@ -20,24 +21,31 @@ export async function managedDaemonCapabilities(eligibility) {
20
21
  return [];
21
22
  }
22
23
  }
23
- function defaults() {
24
+ function defaults(profileName) {
25
+ const userHome = homedir();
26
+ const localHome = resolveDaemonLocalStateRoot({
27
+ userHome,
28
+ platform: process.platform,
29
+ explicit: process.env.NOWWORK_LOCAL_HOME,
30
+ mode: process.env.NOWWORK_LOCAL_HOME_MODE ?? process.env.NOWWORK_STORAGE_MODE,
31
+ });
24
32
  return {
25
33
  platform: process.platform,
26
- profileHome: daemonHome(),
27
- userHome: homedir(),
34
+ profileHome: daemonHome(process.env, profileName),
35
+ userHome,
28
36
  uid: process.getuid?.(),
29
37
  nodePath: process.execPath,
30
38
  entryPath: builtDaemonEntry(),
31
39
  loadProfile,
32
- readManagedServices: () => readManagedServiceRegistry(),
33
- listManagedDescriptorPaths: () => listManagedServiceDescriptorPaths(process.platform, homedir()),
40
+ readManagedServices: () => readManagedServiceRegistry({ userHome, localHome }),
41
+ listManagedDescriptorPaths: () => listManagedServiceDescriptorPaths(process.platform, userHome),
34
42
  readServiceDescriptor,
35
43
  serviceStatus,
36
44
  assertWritable: (path) => access(path, constants.W_OK),
37
45
  installationLeaseHeld: daemonInstallationLeaseHeld,
38
46
  installationRealpath: realpathSync.native,
39
47
  inspectWindowsServiceUpdateScope,
40
- inspectLegacyDescriptorInstallation: (descriptorPath) => inspectLegacyServiceInstallation(descriptorPath, process.platform, homedir(), process.getuid?.()),
48
+ inspectLegacyDescriptorInstallation: (descriptorPath) => inspectLegacyServiceInstallation(descriptorPath, process.platform, userHome, process.getuid?.()),
41
49
  };
42
50
  }
43
51
  async function registryCoversUpdateScope(services, descriptorPaths, targetDescriptorPath, npmPrefix, inspectLegacyDescriptorInstallation) {
@@ -57,7 +65,7 @@ async function registryCoversUpdateScope(services, descriptorPaths, targetDescri
57
65
  return true;
58
66
  }
59
67
  export async function detectDaemonUpdateEligibility(profileName, overrides = {}) {
60
- const deps = { ...defaults(), ...overrides };
68
+ const deps = { ...defaults(profileName), ...overrides };
61
69
  if (deps.platform !== "darwin" && deps.platform !== "linux" && deps.platform !== "win32") {
62
70
  return { eligible: false, reason: "unsupported_platform" };
63
71
  }
@@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto";
5
5
  import { defaultProcessController } from "./execution-journal.js";
6
6
  import { createJournalLease, createJournalLeaseRegistry, JournalLockedError, } from "./execution-journal-lock.js";
7
7
  import { dslog } from "./slog.js";
8
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
8
9
  const HOST_EXECUTION_SLOT_COUNT = 10;
9
10
  const HOST_STARTUP_SLOT_COUNT = 10;
10
11
  const HOST_STARTUP_GAP_MS = 500;
@@ -16,10 +17,10 @@ export class HostReservationCancelledError extends Error {
16
17
  super(message);
17
18
  }
18
19
  }
19
- export function defaultHostExecutionCoordinatorRoot(userHome = homedir()) {
20
+ export function defaultHostExecutionCoordinatorRoot(userHome = homedir(), localHome) {
20
21
  // Deliberately independent of CREW_DAEMON_HOME and agentsRoot: every profile
21
22
  // owned by this OS user must share the same physical-compute safety boundary.
22
- return resolve(userHome, ".crew", "daemon", "compute-execution");
23
+ return resolve(localHome ?? resolveDaemonLocalStateRoot({ userHome, platform: process.platform }), "daemon", "compute-execution");
23
24
  }
24
25
  async function syncDirectory(path) {
25
26
  if (process.platform === "win32")
@@ -132,7 +133,7 @@ async function writeLaunchTimestamp(path, value) {
132
133
  }
133
134
  }
134
135
  export function createHostExecutionCoordinator(options = {}) {
135
- const root = resolve(options.root ?? defaultHostExecutionCoordinatorRoot());
136
+ const root = resolve(options.root ?? defaultHostExecutionCoordinatorRoot(options.userHome, options.localHome));
136
137
  const executionSlots = options.executionSlots ?? HOST_EXECUTION_SLOT_COUNT;
137
138
  const startupSlots = options.startupSlots ?? HOST_STARTUP_SLOT_COUNT;
138
139
  const startupGapMs = options.startupGapMs ?? HOST_STARTUP_GAP_MS;
package/dist/main.js CHANGED
@@ -30,6 +30,7 @@ import { storageMigrationStatusReplay } from "./storage-status-replay.js";
30
30
  import { runStorageWorkerCommand } from "./storage-worker-command.js";
31
31
  import { probeStorageMigrationRuntime } from "./storage-runtime-readiness.js";
32
32
  import { createStorageTargetConfirmationHandler } from "./storage-target-confirmation.js";
33
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
33
34
  async function main() {
34
35
  const workerResult = await runStorageWorkerCommand(process.argv.slice(2));
35
36
  if (workerResult !== null) {
@@ -73,6 +74,14 @@ async function main() {
73
74
  if (values["daemon-home"])
74
75
  process.env.CREW_DAEMON_HOME = values["daemon-home"];
75
76
  let activeDaemonHome;
77
+ const localHome = resolveDaemonLocalStateRoot({
78
+ userHome: homedir(),
79
+ platform: process.platform,
80
+ explicit: process.env.NOWWORK_LOCAL_HOME,
81
+ mode: process.env.NOWWORK_LOCAL_HOME_MODE
82
+ ?? process.env.NOWWORK_STORAGE_MODE
83
+ ?? (values["daemon-home"] === undefined ? undefined : "legacy"),
84
+ });
76
85
  if (values.profile) {
77
86
  const home = daemonHome(process.env, values.profile);
78
87
  activeDaemonHome = home;
@@ -83,6 +92,7 @@ async function main() {
83
92
  platform: process.platform,
84
93
  profile: profile.name,
85
94
  userHome: homedir(),
95
+ localHome,
86
96
  uid: process.getuid?.(),
87
97
  daemonHome: home,
88
98
  agentsRoot: resolveAgentsRoot(profile.agentsRoot, homedir(), process.platform),
@@ -141,7 +151,7 @@ async function main() {
141
151
  : daemonGlobalInstallation(builtDaemonEntry(), process.platform);
142
152
  const installationLease = installation === null
143
153
  ? null
144
- : await acquireDaemonInstallationLease(installation.npmPrefix);
154
+ : await acquireDaemonInstallationLease(installation.npmPrefix, { localHome });
145
155
  const migrationRuntime = values.profile === undefined || activeDaemonHome === undefined || storageRuntime !== null
146
156
  ? { ready: false, reason: "not_legacy_source" }
147
157
  : await probeStorageMigrationRuntime({
@@ -198,6 +208,7 @@ async function main() {
198
208
  await runWithDaemonInstallationLease(installationLease, async () => {
199
209
  const service = serve(runtimeConfig, {
200
210
  ...(values.profile === undefined ? {} : { profileName: values.profile }),
211
+ localHome,
201
212
  ...(storageMigration === undefined ? {} : { storageMigration }),
202
213
  });
203
214
  await runServeLifecycle(service);
@@ -16,6 +16,7 @@ export async function inspectManagedServiceDiagnostics(input) {
16
16
  const inspectLease = input.inspectInstallationLease
17
17
  ?? (() => inspectDaemonInstallationLease(input.expectedRecord.npmPrefix, {
18
18
  ...(userHome === undefined ? {} : { userHome }),
19
+ ...(registry.localHome === undefined ? {} : { localHome: registry.localHome }),
19
20
  }));
20
21
  let services = null;
21
22
  let registryCheck;
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
5
5
  import { dirname, isAbsolute, resolve } from "node:path";
6
6
  import { z } from "zod";
7
7
  import { acquireProfileSaveLock } from "./computer-profile-lock.js";
8
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
8
9
  const AbsolutePathSchema = z.string().min(1).refine(isAbsolute, "must be an absolute path");
9
10
  const ManagedServiceRecordSchema = z.object({
10
11
  version: z.literal(1),
@@ -42,11 +43,15 @@ export class ManagedServiceConflictError extends ManagedServiceRegistryError {
42
43
  this.owner = owner;
43
44
  }
44
45
  }
45
- function registryDirectory(userHome) {
46
- return resolve(userHome, ".crew", "daemon", "managed-services");
46
+ function registryDirectory(userHome, localHome) {
47
+ const root = localHome ?? resolveDaemonLocalStateRoot({
48
+ userHome,
49
+ platform: process.platform,
50
+ });
51
+ return resolve(root, "daemon", "managed-services");
47
52
  }
48
- export function managedServiceRegistryPath(userHome = homedir()) {
49
- return resolve(registryDirectory(userHome), "registry.json");
53
+ export function managedServiceRegistryPath(userHome = homedir(), localHome) {
54
+ return resolve(registryDirectory(userHome, localHome), "registry.json");
50
55
  }
51
56
  export async function listManagedServiceDescriptorPaths(platform, userHome = homedir()) {
52
57
  const directory = platform === "darwin"
@@ -219,14 +224,14 @@ export function managedServiceConflict(services, candidate) {
219
224
  }
220
225
  export async function withManagedServiceRegistryLock(options, operation) {
221
226
  const userHome = options.userHome ?? homedir();
222
- const directory = registryDirectory(userHome);
227
+ const directory = registryDirectory(userHome, options.localHome);
223
228
  const release = await acquireProfileSaveLock(directory, {
224
229
  ...(options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs }),
225
230
  ...(options.lockRetryMs === undefined ? {} : { retryMs: options.lockRetryMs }),
226
231
  });
227
232
  let operationError;
228
233
  try {
229
- const path = managedServiceRegistryPath(userHome);
234
+ const path = managedServiceRegistryPath(userHome, options.localHome);
230
235
  const services = await readRegistryFile(path);
231
236
  return await operation({
232
237
  services,
@@ -258,7 +263,7 @@ export async function withManagedServiceRegistryLock(options, operation) {
258
263
  }
259
264
  }
260
265
  export async function readManagedServiceRegistry(options = {}) {
261
- return readRegistryFile(managedServiceRegistryPath(options.userHome ?? homedir()));
266
+ return readRegistryFile(managedServiceRegistryPath(options.userHome ?? homedir(), options.localHome));
262
267
  }
263
268
  export async function registerManagedService(input, options = {}) {
264
269
  const candidate = normalizeManagedServiceRecord(input);
@@ -17,7 +17,10 @@ export async function assertManagedServiceStartup(input) {
17
17
  ...(input.environment === undefined ? {} : { environment: input.environment }),
18
18
  });
19
19
  const readRegistry = input.readManagedServices
20
- ?? (() => readManagedServiceRegistry({ userHome: input.userHome }));
20
+ ?? (() => readManagedServiceRegistry({
21
+ userHome: input.userHome,
22
+ ...(input.localHome === undefined ? {} : { localHome: input.localHome }),
23
+ }));
21
24
  const listDescriptors = input.listManagedDescriptorPaths
22
25
  ?? (() => listManagedServiceDescriptorPaths(input.platform, input.userHome));
23
26
  const readDescriptor = input.readServiceDescriptor ?? readServiceDescriptor;
package/dist/serve.js CHANGED
@@ -53,6 +53,7 @@ import { handleRuntimeHealthProbeFrame } from "./runtime-health-handler.js";
53
53
  import { buildControlPlaneUrl } from "./control-plane-url.js";
54
54
  import { createServeMigrationController, handleMigrationControlMessage } from "./daemon-migration-wiring.js";
55
55
  import { createCodexHomeModeController } from "./codex-home-mode-controller.js";
56
+ import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
56
57
  import { attachStorageMigrationRuntime, createServeStorageStatusDelivery, handleStorageMigrationControl, replayStorageMigrationStatus, storageMigrationCapabilities, } from "./storage-serve-adapter.js";
57
58
  export { buildControlPlaneUrl } from "./control-plane-url.js";
58
59
  // normalize.ts 的活动种类 → activity 枚举
@@ -83,8 +84,10 @@ export function serve(config, opts = {}) {
83
84
  let detectedExecutionRuntimes = [], executionRuntimesKnown = false;
84
85
  let runtimeFacts = null;
85
86
  const runtimeProbe = createRuntimeProbeCoordinator();
87
+ const localHome = opts.localHome ?? resolveDaemonLocalStateRootFromEnv();
86
88
  const hostCoordinator = opts.execution?.hostCoordinator
87
89
  ?? createHostExecutionCoordinator({
90
+ localHome,
88
91
  executionSlots: config.executionLimits.maxParallelTotal,
89
92
  startupSlots: config.executionLimits.maxStartingTotal,
90
93
  startupGapMs: config.executionLimits.startupGapMs,
package/dist/slog.js CHANGED
@@ -15,6 +15,7 @@ import { homedir } from "node:os";
15
15
  import { join, resolve } from "node:path";
16
16
  import { createHash } from "node:crypto";
17
17
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
18
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
18
19
  const FLUSH_INTERVAL_MS = 1500;
19
20
  const BATCH_SIZE = 50;
20
21
  const SPOOL_MAX_FILES = 50; // spool 目录文件数上限,超出丢最旧(日志非关键数据,防无限膨胀)
@@ -27,6 +28,7 @@ let flushPromise = null;
27
28
  let inFlightBatch = [];
28
29
  let lastErrorAt = 0;
29
30
  let exitHookInstalled = false;
31
+ let localStateRoot;
30
32
  function deploymentEnvironment(serverHost) {
31
33
  if (serverHost === "nowwork-in.nowcoder.com")
32
34
  return "prod";
@@ -40,12 +42,23 @@ function deploymentEnvironment(serverHost) {
40
42
  return "unknown";
41
43
  }
42
44
  function spoolDir() {
43
- return process.env.CREW_SLS_SPOOL_DIR ?? join(homedir(), ".crew", "logs", "sls-spool");
45
+ return process.env.CREW_SLS_SPOOL_DIR ?? join(localStateRoot ?? resolveDaemonLocalStateRoot({
46
+ userHome: homedir(),
47
+ platform: process.platform,
48
+ explicit: process.env.NOWWORK_LOCAL_HOME,
49
+ mode: process.env.NOWWORK_LOCAL_HOME_MODE ?? process.env.NOWWORK_STORAGE_MODE,
50
+ }), "logs", "sls-spool");
44
51
  }
45
52
  /** serve/run 启动时调用一次;disabled(CREW_SLS_LOG=off)则保持 no-op。 */
46
53
  export function initSlog(serverUrl, machineToken, identity = {}) {
47
54
  if (process.env.CREW_SLS_LOG === "off" || process.env.CREW_SLS_LOG === "0")
48
55
  return;
56
+ localStateRoot = resolveDaemonLocalStateRoot({
57
+ userHome: homedir(),
58
+ platform: process.platform,
59
+ explicit: process.env.NOWWORK_LOCAL_HOME,
60
+ mode: process.env.NOWWORK_LOCAL_HOME_MODE ?? process.env.NOWWORK_STORAGE_MODE,
61
+ });
49
62
  cfg = { serverUrl: serverUrl.replace(/\/+$/, ""), token: machineToken };
50
63
  let serverHost;
51
64
  try {
@@ -10,6 +10,7 @@ import { readStorageMigrationPolicy } from "./storage-migration-policy.js";
10
10
  import { createStorageMigrationRuntimeController, } from "./storage-runtime-controller.js";
11
11
  import { resolveEnvTag, resolveNowworkHome } from "./storage-layout.js";
12
12
  import { readStorageOwnershipClaim } from "./storage-ownership.js";
13
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
13
14
  const notReady = (reason) => Object.freeze({ ready: false, reason });
14
15
  function canonicalPath(value) {
15
16
  const absolute = resolve(value);
@@ -74,6 +75,12 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
74
75
  serverUrl: z.string().url().parse(rawInput.serverUrl),
75
76
  platform: rawInput.platform,
76
77
  };
78
+ const localHome = resolveDaemonLocalStateRoot({
79
+ userHome: rawInput.userHome,
80
+ platform: rawInput.platform,
81
+ explicit: rawInput.environment.NOWWORK_LOCAL_HOME,
82
+ mode: rawInput.environment.NOWWORK_LOCAL_HOME_MODE ?? rawInput.environment.NOWWORK_STORAGE_MODE,
83
+ });
77
84
  let nowworkHome;
78
85
  let environment;
79
86
  try {
@@ -112,7 +119,7 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
112
119
  };
113
120
  let sourceRecord;
114
121
  try {
115
- const records = await dependencies.readManagedServices({ userHome: input.userHome });
122
+ const records = await dependencies.readManagedServices({ userHome: input.userHome, localHome });
116
123
  const found = findSourceRecord(records, input);
117
124
  if (found === null)
118
125
  return notReady("source_service_record_missing");
@@ -134,7 +141,7 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
134
141
  const assertSourceIdentity = () => dependencies.assertManagedServiceIdentity({
135
142
  spec: sourceSpec,
136
143
  record: sourceRecord,
137
- registry: { userHome: input.userHome },
144
+ registry: { userHome: input.userHome, localHome },
138
145
  });
139
146
  try {
140
147
  await assertSourceIdentity();
@@ -191,7 +198,7 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
191
198
  backupArtifacts: [
192
199
  { name: "profile.json", path: profilePath(input.profile, input.currentDaemonHome) },
193
200
  { name: "service-descriptor", path: sourceRecord.descriptorPath },
194
- { name: "managed-service-registry.json", path: managedServiceRegistryPath(input.userHome) },
201
+ { name: "managed-service-registry.json", path: managedServiceRegistryPath(input.userHome, localHome) },
195
202
  { name: "installations.json", path: installationsPath(local.configRoot) },
196
203
  ],
197
204
  }, { assertSourceIdentity });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.77-beta.8",
3
+ "version": "0.6.77-beta.9",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -16,14 +16,6 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
- "scripts": {
20
- "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
21
- "build": "tsc -p tsconfig.json",
22
- "codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
23
- "prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
24
- "test": "vitest run",
25
- "typecheck": "tsc --noEmit"
26
- },
27
19
  "dependencies": {
28
20
  "@agentclientprotocol/sdk": "1.2.1",
29
21
  "@nowcrew/cli": "0.4.13",
@@ -44,5 +36,12 @@
44
36
  "tsx": "^4.19.0",
45
37
  "typescript": "^5.6.0",
46
38
  "vitest": "^2.1.0"
39
+ },
40
+ "scripts": {
41
+ "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
42
+ "build": "tsc -p tsconfig.json",
43
+ "codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
44
+ "test": "vitest run",
45
+ "typecheck": "tsc --noEmit"
47
46
  }
48
- }
47
+ }