@nowcrew/daemon 0.6.76 → 0.6.77-beta.10

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.
Files changed (50) hide show
  1. package/README.md +27 -0
  2. package/dist/agent-ability/resolver.js +1 -1
  3. package/dist/attachment-limits.js +4 -0
  4. package/dist/attachments.js +33 -27
  5. package/dist/automatic-daemon-installer.js +44 -24
  6. package/dist/automatic-install-target.js +34 -9
  7. package/dist/computer-cli.js +43 -14
  8. package/dist/computer-profile.js +15 -2
  9. package/dist/computer-service.js +26 -5
  10. package/dist/config.js +34 -6
  11. package/dist/daemon-installation-lease.js +7 -4
  12. package/dist/daemon-local-home.js +108 -0
  13. package/dist/daemon-migration-controller.js +15 -4
  14. package/dist/daemon-registry.js +20 -0
  15. package/dist/daemon-update-controller.js +1 -0
  16. package/dist/daemon-update-eligibility.js +17 -7
  17. package/dist/daemon-updater.js +56 -30
  18. package/dist/execution-protocol.js +5 -2
  19. package/dist/execution-runner.js +7 -3
  20. package/dist/host-execution-coordinator.js +4 -3
  21. package/dist/local-executor.js +7 -0
  22. package/dist/local-memory/runtime.js +5 -3
  23. package/dist/machine-info.js +2 -0
  24. package/dist/main.js +33 -14
  25. package/dist/managed-service-diagnostics.js +1 -0
  26. package/dist/managed-service-registry.js +12 -7
  27. package/dist/managed-service-startup.js +4 -1
  28. package/dist/npm-command.js +9 -0
  29. package/dist/project-dependencies.js +4 -4
  30. package/dist/project-skills/runtime-root-bootstrap.js +6 -0
  31. package/dist/project-skills/runtime-root-startup.js +2 -1
  32. package/dist/project-skills/serve-startup.js +23 -0
  33. package/dist/repository-identity.js +2 -0
  34. package/dist/runner.js +3 -0
  35. package/dist/runtime-capabilities.js +3 -1
  36. package/dist/serve.js +7 -6
  37. package/dist/slog.js +14 -1
  38. package/dist/storage-control.js +12 -1
  39. package/dist/storage-identity.js +15 -1
  40. package/dist/storage-layout.js +2 -1
  41. package/dist/storage-migration-controller.js +25 -4
  42. package/dist/storage-ownership.js +55 -0
  43. package/dist/storage-runtime-controller.js +29 -7
  44. package/dist/storage-runtime-readiness.js +26 -4
  45. package/dist/storage-runtime.js +18 -2
  46. package/dist/storage-status-delivery.js +4 -0
  47. package/dist/storage-status-replay.js +16 -1
  48. package/dist/storage-worker-executor.js +1 -0
  49. package/dist/storage-worker-operation.js +4 -0
  50. package/package.json +1 -1
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,8 +74,16 @@ 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
- const home = daemonHome();
86
+ const home = daemonHome(process.env, values.profile);
78
87
  activeDaemonHome = home;
79
88
  const profile = await loadProfile(values.profile, home);
80
89
  if (cmd === "serve") {
@@ -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),
@@ -117,6 +127,23 @@ async function main() {
117
127
  }
118
128
  const provenance = daemonReleaseProvenance(daemonVersion());
119
129
  assertReleaseTagForServerUrl(config.serverUrl, provenance.releaseTag);
130
+ const storageRuntime = values.profile === undefined
131
+ ? null
132
+ : await loadStorageRuntime({
133
+ profile: values.profile,
134
+ serverUrl: config.serverUrl,
135
+ userHome: homedir(),
136
+ platform: process.platform,
137
+ environment: process.env,
138
+ });
139
+ const runtimeConfig = storageRuntime === null
140
+ ? config
141
+ : Object.freeze({
142
+ ...config,
143
+ agentsRoot: storageRuntime.layout.agentItemsRoot,
144
+ reposRoot: storageRuntime.layout.repoRoot,
145
+ qaReportRoot: storageRuntime.layout.qaReportRoot,
146
+ });
120
147
  if (cmd === "serve") {
121
148
  process.stdout.write(formatDaemonLogLine(`🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}, ${provenance.releaseChannel}/${provenance.releaseTag}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...`) + "\n");
122
149
  const installation = values.profile === undefined
@@ -124,16 +151,7 @@ async function main() {
124
151
  : daemonGlobalInstallation(builtDaemonEntry(), process.platform);
125
152
  const installationLease = installation === null
126
153
  ? null
127
- : await acquireDaemonInstallationLease(installation.npmPrefix);
128
- const storageRuntime = values.profile === undefined
129
- ? null
130
- : await loadStorageRuntime({
131
- profile: values.profile,
132
- serverUrl: config.serverUrl,
133
- userHome: homedir(),
134
- platform: process.platform,
135
- environment: process.env,
136
- });
154
+ : await acquireDaemonInstallationLease(installation.npmPrefix, { localHome });
137
155
  const migrationRuntime = values.profile === undefined || activeDaemonHome === undefined || storageRuntime !== null
138
156
  ? { ready: false, reason: "not_legacy_source" }
139
157
  : await probeStorageMigrationRuntime({
@@ -188,8 +206,9 @@ async function main() {
188
206
  }
189
207
  : undefined;
190
208
  await runWithDaemonInstallationLease(installationLease, async () => {
191
- const service = serve(config, {
209
+ const service = serve(runtimeConfig, {
192
210
  ...(values.profile === undefined ? {} : { profileName: values.profile }),
211
+ localHome,
193
212
  ...(storageMigration === undefined ? {} : { storageMigration }),
194
213
  });
195
214
  await runServeLifecycle(service);
@@ -209,9 +228,9 @@ async function main() {
209
228
  gitSha: provenance.gitSha,
210
229
  artifactDigest: provenance.artifactDigest,
211
230
  ...(values.profile === undefined ? {} : { profileName: values.profile }),
212
- agentsRoot: config.agentsRoot,
231
+ agentsRoot: runtimeConfig.agentsRoot,
213
232
  }); // 一次性 run 模式也上报 SLS(runner 里的埋点生效)
214
- const result = await runAgent(config, {
233
+ const result = await runAgent(runtimeConfig, {
215
234
  handle: values.agent,
216
235
  channelId: values.channel,
217
236
  ...(values.wake ? { wake: values.wake } : {}),
@@ -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;
@@ -0,0 +1,9 @@
1
+ import { posix, win32 } from "node:path";
2
+ /** Execute POSIX npm through its owning Node binary so service PATH is irrelevant. */
3
+ export function npmInvocation(platform, nodePath = process.execPath) {
4
+ const paths = platform === "win32" ? win32 : posix;
5
+ const npmPath = paths.join(paths.dirname(nodePath), platform === "win32" ? "npm.cmd" : "npm");
6
+ return platform === "win32"
7
+ ? { command: npmPath, argsPrefix: [] }
8
+ : { command: nodePath, argsPrefix: [npmPath] };
9
+ }
@@ -111,8 +111,8 @@ async function ensureWorktreeExclude(mainRoot) {
111
111
  const suffix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
112
112
  await writeFile(excludePath, current + suffix + ".worktrees\n", "utf8");
113
113
  }
114
- async function ensureMainClone(git, repo, repositoryKey, reposRoot) {
115
- const mainRoot = resolve(reposRoot, repositoryKey);
114
+ async function ensureMainClone(git, repo, repositoryKey, directoryName, reposRoot) {
115
+ const mainRoot = resolve(reposRoot, directoryName);
116
116
  if (!isWithin(reposRoot, mainRoot) || await existingAncestorIsSymlink(reposRoot, mainRoot)) {
117
117
  throw new ProjectDependencyError("project_dependency_checkout_conflict", "repository path is unsafe", repositoryKey);
118
118
  }
@@ -123,7 +123,7 @@ async function ensureMainClone(git, repo, repositoryKey, reposRoot) {
123
123
  if (info === null) {
124
124
  await mkdir(dirname(mainRoot), { recursive: true });
125
125
  try {
126
- await git.run(["clone", "--no-checkout", repo, mainRoot]);
126
+ await git.run(["clone", repo, mainRoot]);
127
127
  }
128
128
  catch {
129
129
  await rm(mainRoot, { recursive: true, force: true }).catch(() => { });
@@ -243,7 +243,7 @@ export function createProjectDependencyProvisioner(options = {}) {
243
243
  for (const { identity, projects } of grouped.values()) {
244
244
  try {
245
245
  await withProjectDependencyLock(lockRoot, identity.repositoryKey, async () => {
246
- const mainRoot = await ensureMainClone(git, projects[0].repo, identity.repositoryKey, input.reposRoot);
246
+ const mainRoot = await ensureMainClone(git, projects[0].repo, identity.repositoryKey, identity.directoryName, input.reposRoot);
247
247
  const seenVersions = new Set();
248
248
  for (const project of projects) {
249
249
  const branchSource = project.branch === undefined ? "remote-default" : "manifest";
@@ -16,12 +16,18 @@ export async function initializeAfterProjectSkillLeaseProtection(input) {
16
16
  /**
17
17
  * Runs only after execution-journal restart reconciliation has settled. A lease without a terminal
18
18
  * journal fact remains durable and prevents the caller from marking Project Skill v2 ready.
19
+ *
20
+ * 重连时本进程仍在跑的 execution 合法持有 lease(`liveExecutionIds`):既不释放,也不算 unresolved。
21
+ * 否则一次网络抖动就会让 daemon 在整条连接上摘掉 Project Skills 能力(incident: 2026-09-02,
22
+ * 绑定项目的 Agent 全部 execution_spec_incompatible)。
19
23
  */
20
24
  export async function recoverProjectSkillRuntimeRoots(input) {
21
25
  for (const agentRoot of input.agentRoots) {
22
26
  await input.roots.recover(agentRoot);
23
27
  let unresolvedLease = false;
24
28
  await input.leases.recover(agentRoot, async (executionId) => {
29
+ if (input.liveExecutionIds?.has(executionId))
30
+ return false;
25
31
  const entry = await input.journal.get(executionId);
26
32
  const releasable = entry?.state === "completed" || entry?.state === "interrupted";
27
33
  if (!releasable)
@@ -22,13 +22,14 @@ export function createProjectSkillRuntimeRootStartup() {
22
22
  leases,
23
23
  initialize,
24
24
  }),
25
- recover: async (agentsRoot, journal, protectedExecutionIds) => {
25
+ recover: async (agentsRoot, journal, protectedExecutionIds, liveExecutionIds) => {
26
26
  await recoverProjectSkillRuntimeRoots({
27
27
  agentRoots: await agentRoots(agentsRoot),
28
28
  journal,
29
29
  roots,
30
30
  leases,
31
31
  gc,
32
+ ...(liveExecutionIds === undefined ? {} : { liveExecutionIds }),
32
33
  currentRootId: async (agentRoot) => (await readAppliedProjectSkillManifest(agentRoot))?.rootId ?? null,
33
34
  afterLeaseRecovery: async () => {
34
35
  const remaining = await initializeAfterProjectSkillLeaseProtection({
@@ -1,6 +1,29 @@
1
1
  import { createExecutionJournal } from "../execution-journal.js";
2
2
  import { reconcileExecutionJournal } from "../execution-recovery.js";
3
3
  import { dslog, flushSlog } from "../slog.js";
4
+ /**
5
+ * 每次连接 preflight 的 Runtime root / lease 恢复。重连时本进程仍在跑的 execution 合法持有 lease,
6
+ * 必须当作存活而非孤儿;失败也必须留痕,否则整条连接静默摘掉 Project Skills 能力
7
+ * (incident: 2026-09-02,绑定项目的 Agent 全部 execution_spec_incompatible)。
8
+ */
9
+ export function createProjectSkillReconnectRecovery(startup, agentsRoot, journal, protectedExecutionIds, liveExecutionIds) {
10
+ return async () => {
11
+ const live = liveExecutionIds();
12
+ try {
13
+ await startup.recover(agentsRoot, journal(), protectedExecutionIds, live);
14
+ return true;
15
+ }
16
+ catch (error) {
17
+ dslog("project_skills.preflight_recovery_failed", "Project Skill runtime root 恢复失败,本连接不宣告 Project Skills 能力", {
18
+ level: "ERROR",
19
+ error_code: error.code ?? "unknown",
20
+ error_message: error.message,
21
+ live_execution_count: live.size,
22
+ });
23
+ return false;
24
+ }
25
+ };
26
+ }
4
27
  export async function initializeProjectSkillProtectedExecutionJournal(input) {
5
28
  return input.startup.initializeAfterLeaseProtection(input.config.agentsRoot, async (snapshot) => {
6
29
  input.protectedExecutionIds.clear();
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { AbilityRepositoryUrlSchema } from "./agent-ability/types.js";
3
3
  const SCP_REPOSITORY = /^git@([^:]+):(.+)$/u;
4
4
  const COMMIT = /^[a-f0-9]{40}$/u;
5
+ const DIRECTORY_HASH_LENGTH = 12;
5
6
  function normalizePath(value) {
6
7
  const path = value.replace(/^\/+|\/+$/gu, "").replace(/\.git$/iu, "");
7
8
  if (!path || path.split("/").some((part) => part.length === 0 || part === "." || part === ".." || part.includes("\\"))) {
@@ -31,6 +32,7 @@ export function normalizeRepositoryIdentity(repository) {
31
32
  canonical,
32
33
  repositoryKey: `repo-${hash}`,
33
34
  displayName,
35
+ directoryName: `${displayName}--${hash.slice(0, DIRECTORY_HASH_LENGTH)}`,
34
36
  });
35
37
  }
36
38
  export function worktreeDirectoryName(branch, resolvedCommit) {
package/dist/runner.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /** Legacy agent orchestration around the business-neutral local executor. */
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { join } from "node:path";
4
+ import { resolveReposRoot } from "./config.js";
4
5
  import { mintAgentToken } from "./token.js";
5
6
  import { buildSystemPrompt, buildWakePrompt, capMemoryForInject, capWorkLogForInject } from "./prompt.js";
6
7
  import { deliverScheduledReport, } from "./scheduled-report.js";
@@ -84,6 +85,8 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
84
85
  token: credential.token,
85
86
  agentId: credential.agentId,
86
87
  agentsRoot: config.agentsRoot,
88
+ reposRoot: resolveReposRoot(config.agentsRoot, config.reposRoot),
89
+ ...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
87
90
  cliPath: config.cliPath,
88
91
  providerConfig,
89
92
  codexHomeMode: config.codexHomeMode,
@@ -1,3 +1,4 @@
1
+ import { ATTACHMENT_NATIVE_IMAGE_MAX_BYTES } from "./attachment-limits.js";
1
2
  export const LOCAL_EXECUTION_RUNTIMES = [
2
3
  "claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness",
3
4
  ];
@@ -46,7 +47,8 @@ export function routeRuntimeAttachments(runtime, attachments) {
46
47
  const lines = attachments.map((attachment) => `- ${JSON.stringify(attachment.path)} (${attachment.mime}, ${attachment.sizeBytes} bytes)`);
47
48
  return {
48
49
  nativeImagePaths: runtime === "codex"
49
- ? attachments.filter((attachment) => attachment.mime.startsWith("image/"))
50
+ ? attachments.filter((attachment) => attachment.mime.startsWith("image/")
51
+ && attachment.sizeBytes <= ATTACHMENT_NATIVE_IMAGE_MAX_BYTES)
50
52
  .map((attachment) => attachment.path)
51
53
  : [],
52
54
  promptSuffix: [
package/dist/serve.js CHANGED
@@ -42,7 +42,7 @@ import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
42
42
  import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
43
43
  import { createProjectSkillRuntimeRootStartup, } from "./project-skills/runtime-root-startup.js";
44
44
  import { createProjectSkillControllerReadiness, withProjectSkillCapabilityStatus } from "./project-skills/capability.js";
45
- import { initializeProjectSkillProtectedExecutionJournal } from "./project-skills/serve-startup.js";
45
+ import { createProjectSkillReconnectRecovery, initializeProjectSkillProtectedExecutionJournal } from "./project-skills/serve-startup.js";
46
46
  import { createProjectSkillConnectionCapabilityProvider, createProjectSkillMachineHelloPublisher } from "./project-skills/serve-capability.js";
47
47
  import { createAgentAbilityRuntime } from "./agent-ability/runtime.js";
48
48
  import { createProjectSkillsReconciler, } from "./project-skills/reconciler.js";
@@ -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,
@@ -215,9 +218,7 @@ export function serve(config, opts = {}) {
215
218
  additionalCapabilities: () => managedDaemonCapabilities(updateEligibility),
216
219
  probeRuntimeRootStorePrimitive: projectSkillRuntimeRootStartup.probeNativePublicationPrimitiveReadiness,
217
220
  initializeController: projectSkillControllerReadiness.ensure,
218
- recoverRuntimeRootsAndLeases: async () => projectSkillRuntimeRootStartup
219
- .recover(config.agentsRoot, executionJournal, protectedExecutionIds)
220
- .then(() => true, () => false),
221
+ recoverRuntimeRootsAndLeases: createProjectSkillReconnectRecovery(projectSkillRuntimeRootStartup, config.agentsRoot, () => executionJournal, protectedExecutionIds, () => new Set(executionRuns.keys())),
221
222
  applyReadiness: projectSkillControllerReadiness.applyPreflight,
222
223
  });
223
224
  const safeExecutionSend = (frame) => {
@@ -827,9 +828,9 @@ export function serve(config, opts = {}) {
827
828
  // ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
828
829
  const r = msg;
829
830
  setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
830
- if (opts.storageMigration?.ready === true && typeof r.workspaceId === "string") {
831
+ if (opts.storageMigration?.ready === true && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
831
832
  try {
832
- opts.storageMigration.bindServerIdentity?.({ workspaceId: r.workspaceId });
833
+ opts.storageMigration.bindServerIdentity?.({ workspaceId: r.workspaceId, machineId: r.machineId });
833
834
  machineHelloPublisher.republish();
834
835
  }
835
836
  catch (error) {
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 {
@@ -5,6 +5,8 @@ const IdentitySchema = z.object({
5
5
  migrationId: z.string().uuid(),
6
6
  environment: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,31}$/u),
7
7
  workspaceId: z.string().uuid(),
8
+ ownerEpoch: z.number().int().positive().safe().optional(),
9
+ ownershipClaimFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u).optional(),
8
10
  });
9
11
  const DoctorRequestSchema = IdentitySchema.extend({ type: z.literal("daemon:storage-doctor") }).strict();
10
12
  const MigrateRequestSchema = IdentitySchema.extend({
@@ -26,6 +28,11 @@ export function createStorageControlController(input) {
26
28
  const profile = z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/u).parse(input.profile);
27
29
  const sourceStorageFingerprint = z.string().regex(/^sha256:[0-9a-f]{64}$/u).parse(input.sourceStorageFingerprint);
28
30
  const targetStorageFingerprint = z.string().regex(/^sha256:[0-9a-f]{64}$/u).parse(input.targetStorageFingerprint);
31
+ const ownership = input.ownership === undefined ? undefined : Object.freeze({
32
+ ownerEpoch: z.number().int().positive().safe().parse(input.ownership.ownerEpoch),
33
+ ownershipClaimFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u)
34
+ .parse(input.ownership.ownershipClaimFingerprint),
35
+ });
29
36
  const preflights = new Map();
30
37
  const preflightRuns = new Map();
31
38
  const appliedMigrationIds = new Set();
@@ -39,6 +46,7 @@ export function createStorageControlController(input) {
39
46
  environment,
40
47
  workspaceId,
41
48
  profile,
49
+ ...(ownership === undefined ? {} : ownership),
42
50
  ...extra,
43
51
  });
44
52
  const failure = (identity, errorCode, error, reopenAdmission = false, extra = {}) => {
@@ -59,7 +67,10 @@ export function createStorageControlController(input) {
59
67
  input.onStatusError?.(error, statusFrame);
60
68
  }
61
69
  };
62
- const matchesIdentity = (identity) => identity.environment === environment && identity.workspaceId === workspaceId;
70
+ const matchesIdentity = (identity) => identity.environment === environment && identity.workspaceId === workspaceId
71
+ && (ownership === undefined
72
+ || (identity.ownerEpoch === ownership.ownerEpoch
73
+ && identity.ownershipClaimFingerprint === ownership.ownershipClaimFingerprint));
63
74
  const preflightKey = (identity) => `${identity.migrationId}\0${identity.environment}\0${identity.workspaceId}`;
64
75
  const runPreflight = (identity, failureFallback = "doctor_failed") => {
65
76
  const key = preflightKey(identity);
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { STORAGE_LAYOUT_VERSION } from "./storage-layout.js";
3
+ import { createStorageOwnershipClaim } from "./storage-ownership.js";
3
4
  function fingerprint(namespace, value) {
4
5
  return `sha256:${createHash("sha256").update(`${namespace}\0${value}`).digest("hex")}`;
5
6
  }
@@ -14,14 +15,27 @@ export function storageHelloIdentity(input) {
14
15
  || installation.runtimeRoot !== layout.runtimeRoot) {
15
16
  throw new Error("installation storage identity does not match resolved layout");
16
17
  }
18
+ const workspaceRootFingerprint = fingerprint("workspace-root", layout.workspaceRoot);
19
+ let ownership;
20
+ if (input.ownership !== undefined) {
21
+ const { claimFingerprint: reportedFingerprint, ...claimIdentity } = input.ownership;
22
+ ownership = createStorageOwnershipClaim(claimIdentity);
23
+ if (ownership.claimFingerprint !== reportedFingerprint
24
+ || ownership.workspaceId !== layout.workspaceId
25
+ || ownership.environment !== layout.environment
26
+ || ownership.workspaceRootFingerprint !== workspaceRootFingerprint) {
27
+ throw new Error("storage ownership identity does not match resolved layout");
28
+ }
29
+ }
17
30
  return Object.freeze({
18
31
  storageLayoutVersion: STORAGE_LAYOUT_VERSION,
19
32
  storageEnvironment: layout.environment,
20
33
  storageWorkspaceId: layout.workspaceId,
21
34
  storageIdFingerprint: storageIdFingerprint(installation.storageId),
22
- workspaceRootFingerprint: fingerprint("workspace-root", layout.workspaceRoot),
35
+ workspaceRootFingerprint,
23
36
  daemonRootFingerprint: fingerprint("daemon-root", layout.daemonRoot),
24
37
  agentRootFingerprint: fingerprint("agent-root", layout.agentItemsRoot),
38
+ ...(ownership === undefined ? {} : { storageOwnership: ownership }),
25
39
  ...(installation.storageMigrationId === undefined
26
40
  ? {}
27
41
  : { storageMigrationId: installation.storageMigrationId }),
@@ -121,6 +121,7 @@ export function storageLayout(input) {
121
121
  agentItemsRoot: paths.join(workspaceRoot, "agent-items"),
122
122
  daemonRoot: paths.join(workspaceRoot, "daemon"),
123
123
  repoRoot: paths.join(workspaceRoot, "repos"),
124
+ qaReportRoot: paths.join(workspaceRoot, "qa-report-server-v2"),
124
125
  suiteToolsRoot: paths.join(workspaceRoot, "suite-tools"),
125
126
  migrationsRoot: paths.join(persistentRoot, "migrations"),
126
127
  runtimeRoot,
@@ -132,7 +133,7 @@ export function agentStorageRoot(layout, agentId) {
132
133
  }
133
134
  const COMPARABLE_FIELDS = [
134
135
  "layoutVersion", "environment", "workspaceId", "persistentRoot", "environmentRoot", "workspaceRoot",
135
- "agentItemsRoot", "daemonRoot", "repoRoot", "suiteToolsRoot", "migrationsRoot", "runtimeRoot",
136
+ "agentItemsRoot", "daemonRoot", "repoRoot", "qaReportRoot", "suiteToolsRoot", "migrationsRoot", "runtimeRoot",
136
137
  ];
137
138
  export function compareStorageLayout(current, expected) {
138
139
  if (current === null || current.layoutVersion !== STORAGE_LAYOUT_VERSION)
@@ -27,6 +27,8 @@ const ReceiptSchema = z.object({
27
27
  environment: z.string().min(1),
28
28
  workspaceId: z.string().uuid(),
29
29
  profile: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/u),
30
+ ownerEpoch: z.number().int().positive().safe().optional(),
31
+ ownershipClaimFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u).optional(),
30
32
  manifestDigest: z.string().regex(/^[0-9a-f]{64}$/u),
31
33
  targetFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u),
32
34
  status: ReceiptStatusSchema,
@@ -37,13 +39,23 @@ const ReceiptSchema = z.object({
37
39
  sourceStopAttempted: z.boolean().optional(),
38
40
  sourceStopped: z.boolean().optional(),
39
41
  targetTouched: z.boolean().optional(),
40
- }).strict();
42
+ }).strict().superRefine((receipt, context) => {
43
+ if ((receipt.ownerEpoch === undefined) !== (receipt.ownershipClaimFingerprint === undefined)) {
44
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "storage receipt ownership identity must be complete" });
45
+ }
46
+ });
41
47
  const MigrationLockSchema = z.object({
42
48
  version: z.literal(1),
43
49
  migrationId: z.string().uuid(),
44
50
  pid: z.number().int().positive(),
45
51
  createdAt: z.string().datetime({ offset: true }),
46
52
  }).strict();
53
+ const REQUIRED_BACKUP_ARTIFACTS = Object.freeze([
54
+ "profile.json",
55
+ "service-descriptor",
56
+ "managed-service-registry.json",
57
+ "installations.json",
58
+ ]);
47
59
  export class StorageMigrationFailure extends Error {
48
60
  code;
49
61
  receipt;
@@ -204,6 +216,16 @@ function assertOwnedStaging(input) {
204
216
  }
205
217
  }
206
218
  async function backupArtifacts(input, receipt) {
219
+ const providedNames = new Set(input.backupArtifacts.map((artifact) => artifact.name));
220
+ const missing = REQUIRED_BACKUP_ARTIFACTS.filter((name) => !providedNames.has(name));
221
+ if (missing.length > 0) {
222
+ throw new StorageMigrationFailure("backup_artifact_missing", `required rollback artifacts are missing: ${missing.join(", ")}`);
223
+ }
224
+ for (const artifact of input.backupArtifacts) {
225
+ if (!(await lstat(artifact.path)).isFile()) {
226
+ throw new StorageMigrationFailure("backup_artifact_invalid", "backup artifact must be a file");
227
+ }
228
+ }
207
229
  const directory = join(input.backupsRoot, receipt.migrationId);
208
230
  const artifactsDirectory = join(directory, "artifacts");
209
231
  await mkdir(artifactsDirectory, { recursive: true, mode: 0o700 });
@@ -217,8 +239,6 @@ async function backupArtifacts(input, receipt) {
217
239
  throw new StorageMigrationFailure("backup_artifact_invalid", "backup artifact names must be unique safe file names");
218
240
  }
219
241
  names.add(artifact.name);
220
- if (!(await lstat(artifact.path)).isFile())
221
- throw new StorageMigrationFailure("backup_artifact_invalid", "backup artifact must be a file");
222
242
  const destination = join(artifactsDirectory, artifact.name);
223
243
  await copyFile(artifact.path, destination);
224
244
  await chmod(destination, 0o600);
@@ -271,6 +291,7 @@ export async function runStorageMigration(input, dependencies) {
271
291
  environment: input.doctor.environment,
272
292
  workspaceId: input.doctor.workspaceId,
273
293
  profile: input.doctor.profile,
294
+ ...(input.ownership === undefined ? {} : input.ownership),
274
295
  manifestDigest: input.doctor.manifestDigest,
275
296
  targetFingerprint: targetFingerprint(input.targetWorkspaceRoot),
276
297
  status: "preflight_passed",
@@ -301,7 +322,7 @@ export async function runStorageMigration(input, dependencies) {
301
322
  input.assertHealthy?.();
302
323
  await transition("backing_up");
303
324
  receipt = await backupArtifacts(input, receipt).catch((error) => {
304
- failureCode = "backup_failed";
325
+ failureCode = error instanceof StorageMigrationFailure ? error.code : "backup_failed";
305
326
  throw error;
306
327
  });
307
328
  await writeReceipt(path, receipt);