@nowcrew/daemon 0.6.77 → 0.6.78

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 (52) hide show
  1. package/README.md +27 -0
  2. package/dist/automatic-daemon-installer.js +44 -24
  3. package/dist/automatic-install-target.js +34 -9
  4. package/dist/computer-cli.js +43 -14
  5. package/dist/computer-profile.js +15 -2
  6. package/dist/computer-service.js +26 -5
  7. package/dist/config.js +28 -8
  8. package/dist/control-plane-url.js +3 -9
  9. package/dist/daemon-installation-lease.js +7 -4
  10. package/dist/daemon-local-home.js +108 -0
  11. package/dist/daemon-migration-controller.js +15 -4
  12. package/dist/daemon-registry.js +20 -0
  13. package/dist/daemon-update-controller.js +1 -0
  14. package/dist/daemon-update-eligibility.js +17 -7
  15. package/dist/daemon-updater.js +26 -26
  16. package/dist/execution-runner.js +7 -7
  17. package/dist/host-execution-coordinator.js +4 -3
  18. package/dist/local-executor.js +23 -17
  19. package/dist/local-memory/runtime.js +5 -3
  20. package/dist/machine-info.js +3 -0
  21. package/dist/main.js +13 -2
  22. package/dist/managed-service-diagnostics.js +1 -0
  23. package/dist/managed-service-registry.js +12 -7
  24. package/dist/managed-service-startup.js +4 -1
  25. package/dist/npm-command.js +9 -0
  26. package/dist/project-skills/controller.js +6 -71
  27. package/dist/project-skills/execution-adapter.js +7 -0
  28. package/dist/project-skills/execution-projection.js +163 -0
  29. package/dist/project-skills/initialized-reconciler.js +10 -8
  30. package/dist/project-skills/legacy-cleanup.js +224 -0
  31. package/dist/project-skills/machine-hello-publisher.js +20 -0
  32. package/dist/project-skills/reconciler.js +114 -500
  33. package/dist/project-skills/resolution-records.js +63 -0
  34. package/dist/project-skills/runtime-launch.js +0 -42
  35. package/dist/project-skills/runtime-warning.js +1 -3
  36. package/dist/project-skills/scanner.js +1 -17
  37. package/dist/project-skills/startup-cleanup.js +24 -0
  38. package/dist/runner.js +2 -1
  39. package/dist/serve.js +49 -49
  40. package/dist/slog.js +14 -1
  41. package/dist/storage-control.js +12 -1
  42. package/dist/storage-identity.js +15 -1
  43. package/dist/storage-migration-controller.js +25 -4
  44. package/dist/storage-ownership.js +55 -0
  45. package/dist/storage-runtime-controller.js +29 -7
  46. package/dist/storage-runtime-readiness.js +26 -4
  47. package/dist/storage-runtime.js +18 -2
  48. package/dist/storage-status-delivery.js +4 -0
  49. package/dist/storage-status-replay.js +16 -1
  50. package/dist/storage-worker-executor.js +1 -0
  51. package/dist/storage-worker-operation.js +4 -0
  52. package/package.json +1 -1
@@ -0,0 +1,63 @@
1
+ import { posix, win32 } from "node:path";
2
+ import { compareProjectSkillRefs, isProjectId, isProjectSkillName, MAX_AGENT_PROJECT_SKILL_BINDINGS, } from "./types.js";
3
+ const MAX_SOURCE_PATH_BYTES = 16 * 1024;
4
+ /** Thrown when resolution records fail the shape/normalization invariants below. */
5
+ export class ProjectSkillResolutionRecordsCorruptError extends Error {
6
+ constructor() {
7
+ super("skill_projection_state_corrupt");
8
+ this.name = "ProjectSkillResolutionRecordsCorruptError";
9
+ }
10
+ }
11
+ const corrupt = () => { throw new ProjectSkillResolutionRecordsCorruptError(); };
12
+ const normalizeWindowsPath = (sourcePath) => {
13
+ let value = sourcePath.replaceAll("/", "\\");
14
+ if (/^\\\\\?\\UNC\\/iu.test(value))
15
+ value = `\\\\${value.slice(8)}`;
16
+ else if (/^\\\\\?\\[A-Za-z]:\\/u.test(value))
17
+ value = value.slice(4);
18
+ if (!win32.isAbsolute(value))
19
+ return corrupt();
20
+ return win32.normalize(value).toLowerCase();
21
+ };
22
+ const normalizeSourcePath = (sourcePath, platform) => {
23
+ if (sourcePath.includes("\0") || Buffer.byteLength(sourcePath, "utf8") > MAX_SOURCE_PATH_BYTES) {
24
+ return corrupt();
25
+ }
26
+ if (platform === "win32")
27
+ return normalizeWindowsPath(sourcePath);
28
+ if (!posix.isAbsolute(sourcePath))
29
+ return corrupt();
30
+ return posix.normalize(sourcePath);
31
+ };
32
+ export function normalizeProjectSkillResolutionRecords(records, platform = process.platform) {
33
+ if (records.length > MAX_AGENT_PROJECT_SKILL_BINDINGS)
34
+ return corrupt();
35
+ const seen = new Set();
36
+ const normalized = records.map((record) => {
37
+ if (!isProjectId(record.projectId) || !isProjectSkillName(record.skillName))
38
+ return corrupt();
39
+ const key = `${record.projectId}\0${record.skillName}`;
40
+ if (seen.has(key))
41
+ return corrupt();
42
+ seen.add(key);
43
+ if (record.mode === "missing") {
44
+ if (record.sourcePath !== null)
45
+ return corrupt();
46
+ return Object.freeze({
47
+ projectId: record.projectId,
48
+ skillName: record.skillName,
49
+ sourcePath: null,
50
+ mode: "missing",
51
+ });
52
+ }
53
+ if (record.mode !== "resolved" || record.sourcePath === null)
54
+ return corrupt();
55
+ return Object.freeze({
56
+ projectId: record.projectId,
57
+ skillName: record.skillName,
58
+ sourcePath: normalizeSourcePath(record.sourcePath, platform),
59
+ mode: "resolved",
60
+ });
61
+ }).sort(compareProjectSkillRefs);
62
+ return Object.freeze(normalized);
63
+ }
@@ -1,48 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { resolveOrderedUniqueClaudeDirectories } from "../runtimes/claude.js";
3
3
  import { ProjectProjectionError, } from "./reconciler.js";
4
- const runtimeRootSecrets = (root) => Object.freeze([...new Set([
5
- root.rootDirectory,
6
- root.codexSkillRoot,
7
- root.claudeAdditionalDirectory,
8
- root.rootId,
9
- ].flatMap((value) => [
10
- value,
11
- value.replaceAll("\\", "/"),
12
- value.replaceAll("/", "\\"),
13
- ]))].sort((left, right) => right.length - left.length));
14
- export function redactProjectSkillRuntimeRootText(text, root) {
15
- if (root === undefined)
16
- return text;
17
- return runtimeRootSecrets(root)
18
- .reduce((redacted, secret) => redacted.replaceAll(secret, "[project-skill-root]"), text);
19
- }
20
- export function redactProjectSkillRuntimeRootError(error, root) {
21
- if (!(error instanceof Error)) {
22
- return typeof error === "string" ? redactProjectSkillRuntimeRootText(error, root) : error;
23
- }
24
- const message = redactProjectSkillRuntimeRootText(error.message, root);
25
- if (message === error.message)
26
- return error;
27
- const redacted = new Error(message);
28
- redacted.name = error.name;
29
- return redacted;
30
- }
31
- /** Recursively redacts exact v2 root identities before Runtime output crosses daemon boundaries. */
32
- export function redactProjectSkillRuntimeRootValue(value, root) {
33
- if (root === undefined)
34
- return value;
35
- if (typeof value === "string")
36
- return redactProjectSkillRuntimeRootText(value, root);
37
- if (Array.isArray(value)) {
38
- return value.map((item) => redactProjectSkillRuntimeRootValue(item, root));
39
- }
40
- if (typeof value === "object" && value !== null) {
41
- return Object.fromEntries(Object.entries(value)
42
- .map(([key, item]) => [key, redactProjectSkillRuntimeRootValue(item, root)]));
43
- }
44
- return value;
45
- }
46
4
  /** Maps one captured daemon-local root to the exact native Runtime registration seam. */
47
5
  export function projectSkillRuntimeLaunchFacts(runtime, root) {
48
6
  if (root === undefined)
@@ -1,6 +1,4 @@
1
1
  /** Stable, path-free console text; only logical IDs from the server snapshot are included. */
2
2
  export function formatProjectSkillRuntimeWarning(warning) {
3
- return warning.code === "skill_projection_advanced"
4
- ? "[runtime-warning] skill_projection_advanced"
5
- : `[runtime-warning] project_skill_unavailable project=${warning.projectId} skill=${warning.skillName}`;
3
+ return `[runtime-warning] project_skill_unavailable project=${warning.projectId} skill=${warning.skillName}`;
6
4
  }
@@ -2,7 +2,7 @@ import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { compareProjectSkillRefs, isProjectSkillName, MAX_MACHINE_PROJECT_SKILLS, MAX_PROJECT_SKILL_DESCRIPTION_LENGTH, MAX_PROJECT_SKILL_FILE_BYTES, MAX_PROJECT_SKILLS_PER_PROJECT, } from "./types.js";
4
4
  import { parseSkillFrontmatter } from "../skill-frontmatter.js";
5
- import { computeProjectSkillBindingDigest, computeProjectSkillResolutionDigest, normalizeProjectSkillResolutionRecords, } from "./projection-state.js";
5
+ import { normalizeProjectSkillResolutionRecords } from "./resolution-records.js";
6
6
  const unavailable = (projectId, scannedAt, errorCode) => Object.freeze({
7
7
  inventory: Object.freeze({
8
8
  projectId,
@@ -143,19 +143,3 @@ export function projectSkillResolutionRecords(bindings, projects, platform = pro
143
143
  });
144
144
  return normalizeProjectSkillResolutionRecords(records, platform);
145
145
  }
146
- /** Daemon-local expectation derived from one immutable scanner snapshot; no path crosses the wire. */
147
- export function projectSkillProjectionExpectation(snapshot, projects, platform = process.platform) {
148
- const bindings = Object.freeze(snapshot.bindings
149
- .map((binding) => Object.freeze({
150
- projectId: binding.projectId,
151
- skillName: binding.skillName,
152
- }))
153
- .sort(compareProjectSkillRefs));
154
- const resolutions = projectSkillResolutionRecords(bindings, projects, platform);
155
- return Object.freeze({
156
- bindings,
157
- generation: snapshot.generation,
158
- bindingDigest: computeProjectSkillBindingDigest(bindings),
159
- resolutionDigest: computeProjectSkillResolutionDigest(resolutions, platform),
160
- });
161
- }
@@ -0,0 +1,24 @@
1
+ import { cleanupLegacyProjectSkillState, sweepAllExecutionProjections } from "./legacy-cleanup.js";
2
+ export async function runProjectSkillStartupCleanup(agentsRoot, journal, log) {
3
+ try {
4
+ const swept = await sweepAllExecutionProjections(agentsRoot, async (executionId) => {
5
+ const entry = await journal.get(executionId);
6
+ return entry !== null && entry.state !== "completed" && entry.state !== "interrupted";
7
+ });
8
+ const legacy = await cleanupLegacyProjectSkillState({ agentsRoot, journal });
9
+ const degraded = swept.failed > 0 || legacy.failedAgents.length > 0 || legacy.keptStores.length > 0;
10
+ log("project_skills.startup_cleanup", "启动清扫执行投影与旧运行时状态", {
11
+ removed_projections: swept.removed, kept_projections: swept.kept,
12
+ failed_projection_sweeps: swept.failed,
13
+ removed_legacy_stores: legacy.removedStores.length, kept_legacy_stores: legacy.keptStores.length,
14
+ failed_legacy_agents: legacy.failedAgents.length,
15
+ removed_shared_entries: legacy.removedSharedEntries,
16
+ ...(degraded ? { level: "WARN" } : {}),
17
+ });
18
+ }
19
+ catch (error) {
20
+ log("project_skills.startup_cleanup_failed", "启动清扫失败,能力不受影响", {
21
+ level: "WARN", error_message: error.message,
22
+ });
23
+ }
24
+ }
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,7 +85,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
84
85
  token: credential.token,
85
86
  agentId: credential.agentId,
86
87
  agentsRoot: config.agentsRoot,
87
- ...(config.reposRoot === undefined ? {} : { reposRoot: config.reposRoot }),
88
+ reposRoot: resolveReposRoot(config.agentsRoot, config.reposRoot),
88
89
  ...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
89
90
  cliPath: config.cliPath,
90
91
  providerConfig,
package/dist/serve.js CHANGED
@@ -8,7 +8,8 @@ import { randomUUID } from "node:crypto";
8
8
  import { dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
9
9
  import { mergeRunAgentResults, reportScheduledStartFailure, runAgent } from "./runner.js";
10
10
  import { buildOriginDecisionRetryPrompt, buildScheduledPrompt } from "./prompt.js";
11
- import { collectMachineHello, daemonCapabilities, detectExecutionRuntimesWithSignal, } from "./machine-info.js";
11
+ import { collectMachineHello, daemonCapabilities, daemonCapabilityBindings, detectExecutionRuntimesWithSignal, } from "./machine-info.js";
12
+ import { reconcileExecutionJournal } from "./execution-recovery.js";
12
13
  import { initReleaseSlog, reportDaemonConnectionClose, reportReleaseError } from "./release-channel-guard.js";
13
14
  import { conservativeExecutionRuntimes, createRuntimeProbeCoordinator, } from "./runtime-probe.js";
14
15
  import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
@@ -19,6 +20,7 @@ import { normalizeScheduledContext } from "./scheduled-report.js";
19
20
  import { formatDaemonLogLine } from "./log-format.js";
20
21
  import { reportAgentRunComplete } from "./scheduled-run-report.js";
21
22
  import { runWithOriginDecisionGuard } from "./origin-decision.js";
23
+ import { createExecutionJournal } from "./execution-journal.js";
22
24
  import { createExecutionTelemetryJournal } from "./execution-telemetry-journal.js";
23
25
  import { ExecutionRejectedSchema, ExecutionSnapshotSchema, LegacyAgentStartSchema, ServerToDaemonExecutionFrameSchema, } from "./execution-protocol.js";
24
26
  import { hashExecutionSpec, projectWorkspaceCapabilityRejection, runExecution, } from "./execution-runner.js";
@@ -40,10 +42,8 @@ import { createProjectRegistryWiring } from "./project-registry-wiring.js";
40
42
  import { createProjectSkillsController, } from "./project-skills/controller.js";
41
43
  import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
42
44
  import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
43
- import { createProjectSkillRuntimeRootStartup, } from "./project-skills/runtime-root-startup.js";
44
- import { createProjectSkillControllerReadiness, withProjectSkillCapabilityStatus } from "./project-skills/capability.js";
45
- import { initializeProjectSkillProtectedExecutionJournal } from "./project-skills/serve-startup.js";
46
- import { createProjectSkillConnectionCapabilityProvider, createProjectSkillMachineHelloPublisher } from "./project-skills/serve-capability.js";
45
+ import { createMachineHelloPublisher } from "./project-skills/machine-hello-publisher.js";
46
+ import { runProjectSkillStartupCleanup } from "./project-skills/startup-cleanup.js";
47
47
  import { createAgentAbilityRuntime } from "./agent-ability/runtime.js";
48
48
  import { createProjectSkillsReconciler, } from "./project-skills/reconciler.js";
49
49
  import { initializedProjectSkillsReconciler } from "./project-skills/initialized-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 枚举
@@ -75,7 +76,6 @@ export function serve(config, opts = {}) {
75
76
  let stopPromise = null;
76
77
  let executionJournal;
77
78
  const injectedExecutionJournal = opts.execution?.journal;
78
- const protectedExecutionIds = new Set();
79
79
  const agentMemory = opts.execution?.dependencies?.agentMemory
80
80
  ?? (config.agentMemory === null ? undefined : createAgentMemoryBridge(config.agentMemory));
81
81
  const executionTelemetry = createExecutionTelemetryJournal(config.agentsRoot);
@@ -83,8 +83,10 @@ export function serve(config, opts = {}) {
83
83
  let detectedExecutionRuntimes = [], executionRuntimesKnown = false;
84
84
  let runtimeFacts = null;
85
85
  const runtimeProbe = createRuntimeProbeCoordinator();
86
+ const localHome = opts.localHome ?? resolveDaemonLocalStateRootFromEnv();
86
87
  const hostCoordinator = opts.execution?.hostCoordinator
87
88
  ?? createHostExecutionCoordinator({
89
+ localHome,
88
90
  executionSlots: config.executionLimits.maxParallelTotal,
89
91
  startupSlots: config.executionLimits.maxStartingTotal,
90
92
  startupGapMs: config.executionLimits.startupGapMs,
@@ -151,9 +153,10 @@ export function serve(config, opts = {}) {
151
153
  let projectSkillsController;
152
154
  const projectSkillsReconciler = opts.projectSkills?.reconciler ?? createProjectSkillsReconciler({
153
155
  agentsRoot: config.agentsRoot,
154
- coordinator: projectionCoordinator,
155
156
  scannedProjects: () => projectSkillsController.scannedProjects(),
156
- protectedExecutionIds,
157
+ onCleanupFailed: (executionId, error) => dslog("project_skills.cleanup_failed", "执行投影目录清理失败,留待下次启动清扫", {
158
+ level: "WARN", execution_id: executionId, error_code: error.code ?? "unknown",
159
+ }),
157
160
  });
158
161
  projectSkillsController = opts.projectSkills?.controller ?? createProjectSkillsController({
159
162
  registry: projectRegistry,
@@ -168,30 +171,21 @@ export function serve(config, opts = {}) {
168
171
  },
169
172
  isAdmissionClosed: () => storageMigrationAdmissionClosed(),
170
173
  reconcile: (handle, bindings) => projectSkillsReconciler.reconcile(handle, bindings),
171
- ensureSnapshot: (handle, snapshot, options) => projectSkillsReconciler.ensureSnapshot(handle, snapshot, options),
172
174
  });
173
- const projectSkillRuntimeRootStartup = opts.projectSkills?.runtimeRootStartup
174
- ?? createProjectSkillRuntimeRootStartup();
175
- const projectSkillControllerReadiness = createProjectSkillControllerReadiness({
176
- initialize: () => projectSkillsController.initialize(),
177
- onUnavailable: (error) => dslog("project_skills.registry_failed", "本机项目注册表读取失败", {
178
- level: "ERROR", error_code: error.code ?? "project_registry_corrupt",
179
- }),
180
- onSettled: () => machineHelloPublisher.republish(),
175
+ const logRegistryFailure = (error) => dslog("project_skills.registry_failed", "本机项目注册表读取失败,本次不上报项目清单(能力不受影响)", {
176
+ level: "ERROR", error_code: error.code ?? "project_registry_corrupt",
181
177
  });
182
- const machineHelloPublisher = createProjectSkillMachineHelloPublisher({
183
- status: projectSkillControllerReadiness.status,
184
- effective: (hello, status) => {
185
- const effective = withProjectSkillCapabilityStatus(hello, status);
178
+ const machineHelloPublisher = createMachineHelloPublisher({
179
+ effective: (hello) => {
186
180
  const configuredIdentity = opts.storageMigration?.identity;
187
181
  const currentIdentity = typeof configuredIdentity === "function"
188
182
  ? configuredIdentity()
189
183
  : configuredIdentity;
190
- return currentIdentity === undefined ? effective : { ...effective, ...currentIdentity };
184
+ return currentIdentity === undefined ? hello : { ...hello, ...currentIdentity };
191
185
  },
192
186
  onSent: (hello) => log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} · installed=[${hello.runtimes.join(",")}] · executable=[${hello.executionRuntimes.join(",")}] · agents=[${hello.agentHandles.join(",")}]`),
193
187
  });
194
- const initializedProjectSkills = initializedProjectSkillsReconciler(projectSkillControllerReadiness.ensure, projectSkillsReconciler);
188
+ const initializedProjectSkills = initializedProjectSkillsReconciler(() => projectSkillsController.ensureScanned(), projectSkillsReconciler);
195
189
  const configuredCapabilities = opts.machineInfo?.capabilities ?? daemonCapabilities(process.platform);
196
190
  const storageCapabilities = storageMigrationCapabilities(configuredCapabilities, opts.storageMigration);
197
191
  const storageStatusDelivery = createServeStorageStatusDelivery(opts.storageMigration, {
@@ -209,17 +203,7 @@ export function serve(config, opts = {}) {
209
203
  sendStatus: storageStatusDelivery.send,
210
204
  });
211
205
  storageMigrationAdmissionClosed = storageMigrationRuntime.isAdmissionClosed;
212
- const prepareProjectSkillCapabilities = createProjectSkillConnectionCapabilityProvider({
213
- platform: process.platform,
214
- baseCapabilities: storageCapabilities,
215
- additionalCapabilities: () => managedDaemonCapabilities(updateEligibility),
216
- probeRuntimeRootStorePrimitive: projectSkillRuntimeRootStartup.probeNativePublicationPrimitiveReadiness,
217
- initializeController: projectSkillControllerReadiness.ensure,
218
- recoverRuntimeRootsAndLeases: async () => projectSkillRuntimeRootStartup
219
- .recover(config.agentsRoot, executionJournal, protectedExecutionIds)
220
- .then(() => true, () => false),
221
- applyReadiness: projectSkillControllerReadiness.applyPreflight,
222
- });
206
+ const prepareCapabilities = async () => daemonCapabilityBindings(process.platform, [...storageCapabilities, ...await managedDaemonCapabilities(updateEligibility)]);
223
207
  const safeExecutionSend = (frame) => {
224
208
  try {
225
209
  if (ws?.readyState !== WebSocket.OPEN)
@@ -324,10 +308,10 @@ export function serve(config, opts = {}) {
324
308
  async function connect() {
325
309
  if (stopped)
326
310
  return;
327
- const capabilities = await prepareProjectSkillCapabilities();
311
+ const capabilities = await prepareCapabilities();
328
312
  if (stopped)
329
313
  return;
330
- const wsUrl = (opts.buildControlPlaneUrl ?? buildControlPlaneUrl)(config.serverUrl, config.machineToken, process.platform, undefined, projectSkillControllerReadiness.status() === "ready", capabilities.controlPlaneUrl);
314
+ const wsUrl = (opts.buildControlPlaneUrl ?? buildControlPlaneUrl)(config.serverUrl, config.machineToken, process.platform, undefined, capabilities.controlPlaneUrl);
331
315
  ws = createWebSocket(wsUrl);
332
316
  let connectionEpochToken;
333
317
  ws.on("open", () => {
@@ -827,9 +811,9 @@ export function serve(config, opts = {}) {
827
811
  // ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
828
812
  const r = msg;
829
813
  setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
830
- if (opts.storageMigration?.ready === true && typeof r.workspaceId === "string") {
814
+ if (opts.storageMigration?.ready === true && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
831
815
  try {
832
- opts.storageMigration.bindServerIdentity?.({ workspaceId: r.workspaceId });
816
+ opts.storageMigration.bindServerIdentity?.({ workspaceId: r.workspaceId, machineId: r.machineId });
833
817
  machineHelloPublisher.republish();
834
818
  }
835
819
  catch (error) {
@@ -843,8 +827,9 @@ export function serve(config, opts = {}) {
843
827
  ? r.serverCapabilities.filter((c) => typeof c === "string")
844
828
  : []);
845
829
  if (serverCapabilities.has(PROJECT_SKILLS_CAPABILITY)) {
846
- if (await projectSkillControllerReadiness.ensure())
847
- await projectSkillsController.publishCurrent();
830
+ void projectSkillsController.initialize()
831
+ .then(() => projectSkillsController.publishCurrent())
832
+ .catch(logRegistryFailure);
848
833
  }
849
834
  return;
850
835
  }
@@ -869,10 +854,6 @@ export function serve(config, opts = {}) {
869
854
  const result = serverCapabilities.has(PROJECT_SKILLS_CAPABILITY)
870
855
  ? await projectSkillsController.handle(msg, connectionEpochToken)
871
856
  : { ok: false, error: "capability_unavailable" };
872
- if (result.ok && (msg.type === "project:remove"
873
- || msg.type === "project:rescan")) {
874
- void projectSkillControllerReadiness.ensure();
875
- }
876
857
  try {
877
858
  ws?.send(JSON.stringify({ type: "fs:result", reqId: request.reqId, ...result }));
878
859
  }
@@ -1335,13 +1316,32 @@ export function serve(config, opts = {}) {
1335
1316
  });
1336
1317
  }
1337
1318
  const ready = (async () => {
1338
- executionJournal = await initializeProjectSkillProtectedExecutionJournal({
1339
- config,
1319
+ executionJournal = injectedExecutionJournal ?? createExecutionJournal(config.agentsRoot, {
1320
+ onOperationTiming: (event) => {
1321
+ if (event.operation !== "mark_runtime_ready"
1322
+ && event.queueWaitMs < 250
1323
+ && event.operationMs < 250)
1324
+ return;
1325
+ dslog("execution.journal_operation", "execution journal 操作完成", {
1326
+ level: event.outcome === "succeeded" ? "INFO" : "ERROR",
1327
+ execution_id: event.executionId,
1328
+ operation: event.operation,
1329
+ outcome: event.outcome,
1330
+ queue_wait_ms: event.queueWaitMs,
1331
+ operation_ms: event.operationMs,
1332
+ total_ms: event.queueWaitMs + event.operationMs,
1333
+ });
1334
+ },
1335
+ });
1336
+ await reconcileExecutionJournal(executionJournal, {
1337
+ agentsRoot: config.agentsRoot,
1338
+ serverUrl: config.serverUrl,
1340
1339
  ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
1341
- startup: projectSkillRuntimeRootStartup,
1342
- protectedExecutionIds,
1343
- ...(injectedExecutionJournal === undefined ? {} : { injectedJournal: injectedExecutionJournal }),
1340
+ log: dslog,
1341
+ flush: flushSlog,
1342
+ writeStderr: (line) => process.stderr.write(line),
1344
1343
  });
1344
+ await runProjectSkillStartupCleanup(config.agentsRoot, executionJournal, dslog);
1345
1345
  await connect();
1346
1346
  })();
1347
1347
  const stop = () => {
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 }),
@@ -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);