@nowcrew/daemon 0.6.87-beta.2 → 0.6.87-beta.4

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 (36) hide show
  1. package/dist/cloud-report-uploader.js +27 -25
  2. package/dist/execution-runner.js +4 -2
  3. package/dist/external-output.js +154 -68
  4. package/dist/local-executor.js +37 -5
  5. package/dist/local-memory/pending.js +136 -2
  6. package/dist/local-memory/runtime.js +8 -1
  7. package/dist/local-memory/write.js +126 -4
  8. package/dist/main.js +6 -2
  9. package/dist/project-skills/capability.js +109 -0
  10. package/dist/project-skills/controller-convergence.js +57 -0
  11. package/dist/project-skills/projection-set-switch.js +419 -0
  12. package/dist/project-skills/projection-state-domain.js +170 -0
  13. package/dist/project-skills/projection-state-store.js +842 -0
  14. package/dist/project-skills/projection-state-transaction.js +322 -0
  15. package/dist/project-skills/projection-state.js +3 -0
  16. package/dist/project-skills/runtime-root-bootstrap.js +47 -0
  17. package/dist/project-skills/runtime-root-domain.js +268 -0
  18. package/dist/project-skills/runtime-root-gc.js +293 -0
  19. package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
  20. package/dist/project-skills/runtime-root-leases.js +487 -0
  21. package/dist/project-skills/runtime-root-source-identity.js +60 -0
  22. package/dist/project-skills/runtime-root-startup.js +49 -0
  23. package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
  24. package/dist/project-skills/runtime-root-state-index.js +356 -0
  25. package/dist/project-skills/runtime-root-store.js +722 -0
  26. package/dist/project-skills/serve-capability.js +28 -0
  27. package/dist/project-skills/serve-startup.js +37 -0
  28. package/dist/prompt.js +4 -1
  29. package/dist/runner.js +3 -1
  30. package/dist/serve.js +17 -11
  31. package/dist/storage-identity.js +4 -1
  32. package/dist/storage-ownership.js +93 -5
  33. package/dist/storage-runtime-controller.js +40 -20
  34. package/dist/storage-runtime.js +38 -2
  35. package/dist/storage-serve-adapter.js +22 -5
  36. package/package.json +1 -1
@@ -0,0 +1,28 @@
1
+ import { WebSocket } from "ws";
2
+ import { daemonCapabilityBindings, } from "../machine-info.js";
3
+ import { prepareProjectSkillConnectionCapabilities, } from "./capability.js";
4
+ export function createProjectSkillConnectionCapabilityProvider(input) {
5
+ return async () => daemonCapabilityBindings(input.platform, await prepareProjectSkillConnectionCapabilities(input));
6
+ }
7
+ export function createProjectSkillMachineHelloPublisher(input) {
8
+ let latest = null;
9
+ let latestSocket = null;
10
+ const republish = () => {
11
+ if (latest === null || latestSocket?.readyState !== WebSocket.OPEN)
12
+ return;
13
+ const hello = input.effective(latest, input.status());
14
+ try {
15
+ latestSocket.send(JSON.stringify(hello));
16
+ input.onSent(hello);
17
+ }
18
+ catch { /* 非 OPEN,忽略 */ }
19
+ };
20
+ return Object.freeze({
21
+ publish: (hello, socket) => {
22
+ latest = hello;
23
+ latestSocket = socket;
24
+ republish();
25
+ },
26
+ republish,
27
+ });
28
+ }
@@ -0,0 +1,37 @@
1
+ import { createExecutionJournal } from "../execution-journal.js";
2
+ import { reconcileExecutionJournal } from "../execution-recovery.js";
3
+ import { dslog, flushSlog } from "../slog.js";
4
+ export async function initializeProjectSkillProtectedExecutionJournal(input) {
5
+ return input.startup.initializeAfterLeaseProtection(input.config.agentsRoot, async (snapshot) => {
6
+ input.protectedExecutionIds.clear();
7
+ for (const executionId of snapshot)
8
+ input.protectedExecutionIds.add(executionId);
9
+ const journal = input.injectedJournal ?? createExecutionJournal(input.config.agentsRoot, {
10
+ protectedExecutionIds: () => input.protectedExecutionIds,
11
+ onOperationTiming: (event) => {
12
+ if (event.operation !== "mark_runtime_ready"
13
+ && event.queueWaitMs < 250
14
+ && event.operationMs < 250)
15
+ return;
16
+ dslog("execution.journal_operation", "execution journal 操作完成", {
17
+ level: event.outcome === "succeeded" ? "INFO" : "ERROR",
18
+ execution_id: event.executionId,
19
+ operation: event.operation,
20
+ outcome: event.outcome,
21
+ queue_wait_ms: event.queueWaitMs,
22
+ operation_ms: event.operationMs,
23
+ total_ms: event.queueWaitMs + event.operationMs,
24
+ });
25
+ },
26
+ });
27
+ await reconcileExecutionJournal(journal, {
28
+ agentsRoot: input.config.agentsRoot,
29
+ serverUrl: input.config.serverUrl,
30
+ ...(input.profileName === undefined ? {} : { profileName: input.profileName }),
31
+ log: dslog,
32
+ flush: flushSlog,
33
+ writeStderr: (line) => process.stderr.write(line),
34
+ });
35
+ return journal;
36
+ });
37
+ }
package/dist/prompt.js CHANGED
@@ -279,6 +279,9 @@ ${newThreadRule}
279
279
 
280
280
  ## Skill 指定
281
281
  - 如果用户输入 \`/xxxx\`、或消息开头/正文中有形如 \`/skill-name\` 的片段,优先判断用户是否在精准指定某个 skill。若该 skill 可用且适合当前任务,按该 skill 的工作流处理;若名称不明确或不可用,先说明不确定并按最接近的可用能力处理。`;
282
+ const memoryWritePolicy = ctx.localMemoryV2Enabled
283
+ ? "- **V2 结构化本地记忆**:普通任务不得直接编辑共享 `$CREW_HOME/MEMORY.md` 或 notes/;即使用户明确要求长期记忆,也只记录到 `$CREW_TASK_LOG`,由任务结束时的 memory-prune 结构化提交。只有 memory-prune 收尾提示明确要求写入 daemon result 文件时才提交候选。"
284
+ : "- **当前请求明确要求更新或记住长期记忆,明确表达需要跨后续任务持续生效的具体规则、偏好或约束,或当前消息是对本线程紧邻记忆方案的无歧义确认时**,可以按最小范围直接写共享记忆。例如“以后所有测试报告统一按五部分写”属于明确长期意图,即使没有使用“记忆”一词。先读取相关索引和 note,语义已存在时不重复追加,不要把 Agent 自己判断值得记住视为用户授权。";
282
285
  return `你是 "${ctx.handle}",${product}(一个让人类与 AI agent 协作的共享工作区)中的 AI 成员。${product} 为可能运行在不同机器上的人与 agent 提供共享的消息服务。
283
286
 
284
287
  ## 你是谁
@@ -293,7 +296,7 @@ ${newThreadRule}
293
296
  ## 并行与记忆(CRITICAL — 防冲突 + 防上下文膨胀)
294
297
  你**可能同时在处理多个任务**(你自己的多个并行进程)。为避免互相覆盖、并保持上下文精简,严格遵守:
295
298
  - **普通任务默认只写两处**:① 本任务的隔离 cwd(代码/草稿);② 本任务的工作日志 **\`$CREW_TASK_LOG\`**(每任务一份,别人不会碰)。默认不要修改共享 \`$CREW_HOME/MEMORY.md\` 或 notes/;只有当前用户明确要求更新或记住长期记忆,或明确表达具体且跨后续任务持续生效的规则、偏好或约束时,才按下方条件写入相关共享文件。在工作日志里记:当前阶段 / 下一步 / 卡在谁 / 关键结论(每条尽量 ≤3 行)。
296
- - **当前请求明确要求更新或记住长期记忆,明确表达需要跨后续任务持续生效的具体规则、偏好或约束,或当前消息是对本线程紧邻记忆方案的无歧义确认时**,可以按最小范围直接写共享记忆。例如“以后所有测试报告统一按五部分写”属于明确长期意图,即使没有使用“记忆”一词。先读取相关索引和 note,语义已存在时不重复追加,不要把 Agent 自己判断值得记住视为用户授权。
299
+ ${memoryWritePolicy}
297
300
  - 本节的直接更新共享记忆和相关 task 流转仅适用于用户消息触发的普通交互执行。memory_prune 和 scheduled 执行只遵守各自唤醒指令,不得依据本节认领、创建、重开或推进 task。
298
301
  ${memoryTaskReadRule}
299
302
  - 普通交互执行将写共享记忆时,使用当前线程唯一的普通 task:已有非终态 task 时先按归属规则确保当前 Agent 成功认领;当前线程没有 task 时先创建并认领一个普通 task;已有 task 为 done/closed 时不得由 Agent 重开,done 报告当前轻量方案无法安全复用终态 task,closed 保持人工终态。遇到终态或归属问题时报告冲突且不得创建第二个 task 或新的任务类型。完成写入后,只有任务本身的全部工作已完成时才推进终态以触发 prune。
package/dist/runner.js CHANGED
@@ -30,6 +30,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
30
30
  }), dependencies.cancellation);
31
31
  const providerConfig = credential.config ?? {};
32
32
  const runtime = runtimeName(providerConfig.runtime ?? config.runtimeBin);
33
+ const localMemoryV2Enabled = config.localMemoryV2Handles?.has(input.handle) === true;
33
34
  const reasoning = ReasoningSchema.safeParse(providerConfig.reasoning);
34
35
  const baseWake = input.wake ?? buildWakePrompt(input.channelId);
35
36
  const executionId = input.runId ?? `legacy-${randomUUID()}`;
@@ -61,6 +62,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
61
62
  memory: capMemoryForInject(workspace.memory),
62
63
  ...(resuming ? {} : { workLog: capWorkLogForInject(workspace.workLog) }),
63
64
  collaborationToolsEnabled: config.codexCollaborationTools && runtime === "codex",
65
+ localMemoryV2Enabled,
64
66
  }),
65
67
  wakePrompt: ({ resuming, rotatedForBudget, nearBudget }) => {
66
68
  const distillHint = nearBudget
@@ -111,7 +113,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
111
113
  softTokens: config.sessionSoftTokens,
112
114
  maxTurns: config.sessionMaxTurns,
113
115
  },
114
- localMemoryV2Enabled: config.localMemoryV2Handles?.has(input.handle) === true,
116
+ localMemoryV2Enabled,
115
117
  }, {
116
118
  onActivity,
117
119
  onConsole,
package/dist/serve.js CHANGED
@@ -54,7 +54,7 @@ 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
56
  import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
57
- import { attachStorageMigrationRuntime, createServeStorageStatusDelivery, handleStorageMigrationControl, replayStorageMigrationStatus, storageMigrationCapabilities, } from "./storage-serve-adapter.js";
57
+ import { attachStorageMigrationRuntime, bindStorageMigrationServerIdentity, createServeStorageStatusDelivery, handleStorageMigrationControl, replayStorageMigrationStatus, storageMigrationCapabilities, } from "./storage-serve-adapter.js";
58
58
  export { buildControlPlaneUrl } from "./control-plane-url.js";
59
59
  // normalize.ts 的活动种类 → activity 枚举
60
60
  const ACTIVITY_MAP = {
@@ -316,6 +316,7 @@ export function serve(config, opts = {}) {
316
316
  let connectionEpochToken;
317
317
  ws.on("open", () => {
318
318
  connectionEpochToken = projectSkillsController.beginConnectionEpoch();
319
+ storageMigrationRuntime.beginIdentityBinding();
319
320
  const openedSocket = ws;
320
321
  backoff = 1000;
321
322
  connectedAt = Date.now();
@@ -811,16 +812,21 @@ export function serve(config, opts = {}) {
811
812
  // ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
812
813
  const r = msg;
813
814
  setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
814
- if (opts.storageMigration?.ready === true && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
815
- try {
816
- opts.storageMigration.bindServerIdentity?.({ workspaceId: r.workspaceId, machineId: r.machineId });
817
- machineHelloPublisher.republish();
818
- }
819
- catch (error) {
820
- dslog("daemon.storage_identity_bind_failed", "存储迁移 Workspace 身份绑定失败", {
821
- level: "ERROR", error_message: error.message,
822
- });
823
- }
815
+ if (opts.storageMigration !== undefined && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
816
+ const bindingSocket = ws;
817
+ bindStorageMigrationServerIdentity(opts.storageMigration, { workspaceId: r.workspaceId, machineId: r.machineId }, {
818
+ onBound: () => {
819
+ if (bindingSocket !== ws || bindingSocket?.readyState !== WebSocket.OPEN)
820
+ return;
821
+ storageMigrationRuntime.completeIdentityBinding();
822
+ machineHelloPublisher.republish();
823
+ },
824
+ onError: (error) => {
825
+ dslog("daemon.storage_identity_bind_failed", "存储迁移 Workspace 身份绑定失败", { level: "ERROR", error_message: error.message });
826
+ if (bindingSocket === ws && bindingSocket?.readyState === WebSocket.OPEN)
827
+ bindingSocket.close(1011, "storage identity binding failed");
828
+ },
829
+ });
824
830
  }
825
831
  // 旧 server 无此字段 → 空集合(daemon 保持全部旧行为)。
826
832
  serverCapabilities = new Set(Array.isArray(r.serverCapabilities)
@@ -4,6 +4,9 @@ import { createStorageOwnershipClaim } from "./storage-ownership.js";
4
4
  function fingerprint(namespace, value) {
5
5
  return `sha256:${createHash("sha256").update(`${namespace}\0${value}`).digest("hex")}`;
6
6
  }
7
+ export function storageWorkspaceRootFingerprint(workspaceRoot) {
8
+ return fingerprint("workspace-root", workspaceRoot);
9
+ }
7
10
  export function storageIdFingerprint(storageId) {
8
11
  return fingerprint("storage", storageId);
9
12
  }
@@ -15,7 +18,7 @@ export function storageHelloIdentity(input) {
15
18
  || installation.runtimeRoot !== layout.runtimeRoot) {
16
19
  throw new Error("installation storage identity does not match resolved layout");
17
20
  }
18
- const workspaceRootFingerprint = fingerprint("workspace-root", layout.workspaceRoot);
21
+ const workspaceRootFingerprint = storageWorkspaceRootFingerprint(layout.workspaceRoot);
19
22
  let ownership;
20
23
  if (input.ownership !== undefined) {
21
24
  const { claimFingerprint: reportedFingerprint, ...claimIdentity } = input.ownership;
@@ -1,8 +1,11 @@
1
- import { createHash } from "node:crypto";
2
- import { lstat, readFile } from "node:fs/promises";
3
- import { join } from "node:path";
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { chmod, lstat, mkdir, open, readFile, rm } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
4
  import { z } from "zod";
5
+ import { atomicRenameNoReplace } from "./atomic-no-replace-rename.js";
6
+ import { durableDirectorySync } from "./atomic-private-write.js";
5
7
  const FingerprintSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/u);
8
+ const UuidSchema = z.string().uuid();
6
9
  const StorageOwnershipClaimSchema = z.object({
7
10
  version: z.literal(1),
8
11
  machineId: z.string().uuid(),
@@ -38,8 +41,10 @@ export function createStorageOwnershipClaim(input) {
38
41
  export function storageOwnershipClaimPath(nowworkHome) {
39
42
  return join(nowworkHome, "storage-layout.json");
40
43
  }
41
- export async function readStorageOwnershipClaim(nowworkHome) {
42
- const path = storageOwnershipClaimPath(nowworkHome);
44
+ export function workspaceStorageOwnershipClaimPath(nowworkHome, workspaceId) {
45
+ return join(nowworkHome, ".storage-ownership", `${UuidSchema.parse(workspaceId).toLowerCase()}.json`);
46
+ }
47
+ async function readClaim(path) {
43
48
  const metadata = await lstat(path);
44
49
  if (!metadata.isFile())
45
50
  throw new Error("storage ownership claim must be a file");
@@ -53,3 +58,86 @@ export async function readStorageOwnershipClaim(nowworkHome) {
53
58
  }
54
59
  return expected;
55
60
  }
61
+ function missingClaim(message) {
62
+ return Object.assign(new Error(message), { code: "ENOENT" });
63
+ }
64
+ export async function readStorageOwnershipClaim(nowworkHome, workspaceId) {
65
+ if (workspaceId !== undefined) {
66
+ const normalizedWorkspaceId = UuidSchema.parse(workspaceId).toLowerCase();
67
+ try {
68
+ return await readClaim(workspaceStorageOwnershipClaimPath(nowworkHome, normalizedWorkspaceId));
69
+ }
70
+ catch (error) {
71
+ if (error.code !== "ENOENT")
72
+ throw error;
73
+ }
74
+ const legacy = await readClaim(storageOwnershipClaimPath(nowworkHome));
75
+ if (legacy.workspaceId !== normalizedWorkspaceId) {
76
+ throw missingClaim(`storage ownership claim is not provisioned for workspace '${normalizedWorkspaceId}'`);
77
+ }
78
+ return legacy;
79
+ }
80
+ return readClaim(storageOwnershipClaimPath(nowworkHome));
81
+ }
82
+ export async function resolveWorkspaceStorageOwnershipClaim(input) {
83
+ const machineId = UuidSchema.parse(input.machineId).toLowerCase();
84
+ const workspaceId = UuidSchema.parse(input.workspaceId).toLowerCase();
85
+ try {
86
+ const existing = await readStorageOwnershipClaim(input.nowworkHome, workspaceId);
87
+ const expected = createStorageOwnershipClaim({
88
+ machineId,
89
+ workspaceId,
90
+ environment: input.environment,
91
+ volumeFingerprint: existing.volumeFingerprint,
92
+ workspaceRootFingerprint: input.workspaceRootFingerprint,
93
+ ownerEpoch: existing.ownerEpoch,
94
+ });
95
+ if (existing.claimFingerprint !== expected.claimFingerprint) {
96
+ throw new Error("storage ownership claim conflicts with the authenticated workspace identity");
97
+ }
98
+ return existing;
99
+ }
100
+ catch (error) {
101
+ if (error.code !== "ENOENT")
102
+ throw error;
103
+ }
104
+ const anchor = await readClaim(storageOwnershipClaimPath(input.nowworkHome));
105
+ if (anchor.environment !== input.environment.toLowerCase()) {
106
+ throw new Error("storage ownership volume anchor environment does not match");
107
+ }
108
+ const claim = createStorageOwnershipClaim({
109
+ machineId,
110
+ workspaceId,
111
+ environment: input.environment,
112
+ volumeFingerprint: anchor.volumeFingerprint,
113
+ workspaceRootFingerprint: input.workspaceRootFingerprint,
114
+ ownerEpoch: 1,
115
+ });
116
+ const path = workspaceStorageOwnershipClaimPath(input.nowworkHome, workspaceId);
117
+ const directory = dirname(path);
118
+ await mkdir(directory, { recursive: true, mode: 0o700 });
119
+ await chmod(directory, 0o700);
120
+ const temporary = `${path}.${randomUUID()}.tmp`;
121
+ let handle = null;
122
+ try {
123
+ handle = await open(temporary, "wx", 0o600);
124
+ await handle.writeFile(`${JSON.stringify({ version: 1, ...claim })}\n`, "utf8");
125
+ await handle.sync();
126
+ await handle.close();
127
+ handle = null;
128
+ const published = await atomicRenameNoReplace(temporary, path);
129
+ if (published === "published") {
130
+ await durableDirectorySync(directory);
131
+ return claim;
132
+ }
133
+ const raced = await readClaim(path);
134
+ if (raced.claimFingerprint !== claim.claimFingerprint) {
135
+ throw new Error("storage ownership claim was concurrently provisioned for another identity");
136
+ }
137
+ return raced;
138
+ }
139
+ finally {
140
+ await handle?.close().catch(() => undefined);
141
+ await rm(temporary, { force: true }).catch(() => undefined);
142
+ }
143
+ }
@@ -5,12 +5,13 @@ import { buildServiceSpec } from "./computer-service.js";
5
5
  import { buildManagedServiceRecord } from "./managed-service-lifecycle.js";
6
6
  import { createStorageControlController } from "./storage-control.js";
7
7
  import { runStorageDoctor } from "./storage-doctor.js";
8
- import { storageHelloIdentity } from "./storage-identity.js";
8
+ import { storageHelloIdentity, storageWorkspaceRootFingerprint, } from "./storage-identity.js";
9
9
  import { storageLayout, validateStorageEnvironment } from "./storage-layout.js";
10
10
  import { StorageMigrationFailure } from "./storage-migration-controller.js";
11
11
  import { launchStorageMigrationWorker, writeStorageWorkerHandoff } from "./storage-worker-handoff.js";
12
12
  import { writeStorageWorkerOperation } from "./storage-worker-operation.js";
13
13
  import { latestSourceStorageMigrationReceipt, monitorStorageMigrationStatus, storageMigrationStatusReplay, } from "./storage-status-replay.js";
14
+ import { resolveWorkspaceStorageOwnershipClaim, } from "./storage-ownership.js";
14
15
  const UUID = z.string().uuid();
15
16
  function fingerprint(value) {
16
17
  return `sha256:${createHash("sha256").update(value).digest("hex")}`;
@@ -22,13 +23,13 @@ function deterministicUuid(value) {
22
23
  const hex = chars.join("");
23
24
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
24
25
  }
25
- function storageId(input, workspaceId) {
26
- if (input.ownership.workspaceId !== workspaceId || input.ownership.environment !== input.environment) {
26
+ function storageId(input, workspaceId, ownership) {
27
+ if (ownership.workspaceId !== workspaceId || ownership.environment !== input.environment) {
27
28
  throw new Error("Storage ownership claim does not match the migration identity");
28
29
  }
29
- return `storage_${input.ownership.claimFingerprint.slice("sha256:".length)}`;
30
+ return `storage_${ownership.claimFingerprint.slice("sha256:".length)}`;
30
31
  }
31
- function installationBase(input, workspaceId, migrationId, createdAt) {
32
+ function installationBase(input, workspaceId, migrationId, createdAt, ownership) {
32
33
  const layout = storageLayout({
33
34
  nowworkHome: input.nowworkHome,
34
35
  environment: input.environment,
@@ -43,7 +44,7 @@ function installationBase(input, workspaceId, migrationId, createdAt) {
43
44
  serverUrl: input.serverUrl,
44
45
  environment: input.environment,
45
46
  workspaceId,
46
- storageId: storageId(input, workspaceId),
47
+ storageId: storageId(input, workspaceId, ownership),
47
48
  nowworkHome: input.nowworkHome,
48
49
  runtimeRoot: layout.runtimeRoot,
49
50
  credentialRef: `profile:${input.profile}`,
@@ -99,6 +100,7 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
99
100
  writeHandoff: writeStorageWorkerHandoff,
100
101
  launchWorker: launchStorageMigrationWorker,
101
102
  monitorWorker: monitorStorageMigrationStatus,
103
+ resolveOwnership: resolveWorkspaceStorageOwnershipClaim,
102
104
  now: () => new Date(),
103
105
  ...injected,
104
106
  };
@@ -108,6 +110,8 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
108
110
  let gate = null;
109
111
  let sendStatus = null;
110
112
  let control = null;
113
+ let ownership = input.ownership;
114
+ let identityBindTail = Promise.resolve();
111
115
  const pendingControlMessages = [];
112
116
  let drainingPendingControlMessages = false;
113
117
  const drainPendingControlMessages = () => {
@@ -197,8 +201,8 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
197
201
  backupsRoot: input.local.backupsRoot,
198
202
  backupArtifacts: input.backupArtifacts.map((artifact) => ({ ...artifact })),
199
203
  ownership: {
200
- ownerEpoch: input.ownership.ownerEpoch,
201
- ownershipClaimFingerprint: input.ownership.claimFingerprint,
204
+ ownerEpoch: ownership.ownerEpoch,
205
+ ownershipClaimFingerprint: ownership.claimFingerprint,
202
206
  },
203
207
  acceptedWarnings: [...input.policy.acceptedWarnings],
204
208
  },
@@ -214,7 +218,7 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
214
218
  installation: {
215
219
  configRoot: input.local.configRoot,
216
220
  target: {
217
- ...installationBase(input, workspaceId, doctor.manifest.migrationId, createdAt),
221
+ ...installationBase(input, workspaceId, doctor.manifest.migrationId, createdAt, ownership),
218
222
  storageMigrationId: doctor.manifest.migrationId,
219
223
  },
220
224
  },
@@ -236,8 +240,8 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
236
240
  sourceStorageFingerprint,
237
241
  targetStorageFingerprint: identity.storageIdFingerprint,
238
242
  ownership: {
239
- ownerEpoch: input.ownership.ownerEpoch,
240
- ownershipClaimFingerprint: input.ownership.claimFingerprint,
243
+ ownerEpoch: ownership.ownerEpoch,
244
+ ownershipClaimFingerprint: ownership.claimFingerprint,
241
245
  },
242
246
  runDoctor: async (migrationId) => dependencies.runDoctor(await doctorInput(migrationId)),
243
247
  runMigration: async (doctor) => {
@@ -294,7 +298,7 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
294
298
  sendStatus,
295
299
  });
296
300
  };
297
- const bindServerIdentity = (server) => {
301
+ const bindServerIdentityOnce = async (server) => {
298
302
  const nextWorkspaceId = UUID.parse(server.workspaceId).toLowerCase();
299
303
  const nextMachineId = UUID.parse(server.machineId).toLowerCase();
300
304
  if (workspaceId !== null && workspaceId !== nextWorkspaceId) {
@@ -303,13 +307,6 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
303
307
  if (machineId !== null && machineId !== nextMachineId) {
304
308
  throw new Error("Storage migration server machine identity changed");
305
309
  }
306
- if (input.ownership.workspaceId !== nextWorkspaceId
307
- || input.ownership.machineId !== nextMachineId
308
- || input.ownership.environment !== input.environment) {
309
- throw new Error("Storage ownership claim does not match authenticated server identity");
310
- }
311
- const createdAt = dependencies.now().toISOString();
312
- const installation = installationBase(input, nextWorkspaceId, undefined, createdAt);
313
310
  const layout = storageLayout({
314
311
  nowworkHome: input.nowworkHome,
315
312
  environment: input.environment,
@@ -317,13 +314,36 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
317
314
  runtimeBase: input.local.runtimeBase,
318
315
  platform: input.platform,
319
316
  });
320
- const nextIdentity = storageHelloIdentity({ installation, layout, ownership: input.ownership });
317
+ if (ownership.workspaceId !== nextWorkspaceId
318
+ || ownership.machineId !== nextMachineId
319
+ || ownership.environment !== input.environment) {
320
+ ownership = await dependencies.resolveOwnership({
321
+ nowworkHome: input.nowworkHome,
322
+ machineId: nextMachineId,
323
+ workspaceId: nextWorkspaceId,
324
+ environment: input.environment,
325
+ workspaceRootFingerprint: storageWorkspaceRootFingerprint(layout.workspaceRoot),
326
+ });
327
+ }
328
+ if (ownership.workspaceId !== nextWorkspaceId
329
+ || ownership.machineId !== nextMachineId
330
+ || ownership.environment !== input.environment) {
331
+ throw new Error("Storage ownership claim does not match authenticated server identity");
332
+ }
333
+ const createdAt = dependencies.now().toISOString();
334
+ const installation = installationBase(input, nextWorkspaceId, undefined, createdAt, ownership);
335
+ const nextIdentity = storageHelloIdentity({ installation, layout, ownership });
321
336
  workspaceId = nextWorkspaceId;
322
337
  machineId = nextMachineId;
323
338
  identity = nextIdentity;
324
339
  ensureControl();
325
340
  drainPendingControlMessages();
326
341
  };
342
+ const bindServerIdentity = (server) => {
343
+ const current = identityBindTail.then(() => bindServerIdentityOnce(server));
344
+ identityBindTail = current.catch(() => undefined);
345
+ return current;
346
+ };
327
347
  const attach = (runtime) => {
328
348
  gate = runtime.gate;
329
349
  sendStatus = runtime.sendStatus;
@@ -1,7 +1,7 @@
1
1
  import { findInstallationForProfile, readInstallations, resolveLocalStorageRoots, } from "./storage-config.js";
2
2
  import { storageHelloIdentity } from "./storage-identity.js";
3
3
  import { storageLayout } from "./storage-layout.js";
4
- import { readStorageOwnershipClaim } from "./storage-ownership.js";
4
+ import { readStorageOwnershipClaim, resolveWorkspaceStorageOwnershipClaim, } from "./storage-ownership.js";
5
5
  const canonicalServerUrl = (value) => value.replace(/\/+$/u, "");
6
6
  export function resolveStorageRuntime(input) {
7
7
  const installation = findInstallationForProfile(input.records, input.profile);
@@ -45,7 +45,7 @@ export async function loadStorageRuntime(input) {
45
45
  return null;
46
46
  let ownership;
47
47
  try {
48
- ownership = await readStorageOwnershipClaim(baseline.layout.persistentRoot);
48
+ ownership = await readStorageOwnershipClaim(baseline.layout.persistentRoot, baseline.layout.workspaceId);
49
49
  }
50
50
  catch (error) {
51
51
  if (error.code !== "ENOENT")
@@ -53,3 +53,39 @@ export async function loadStorageRuntime(input) {
53
53
  }
54
54
  return ownership === undefined ? baseline : resolveStorageRuntime({ ...input, records, ownership });
55
55
  }
56
+ export async function bindStorageRuntimeOwnership(runtime, server) {
57
+ const workspaceId = server.workspaceId.toLowerCase();
58
+ const machineId = server.machineId.toLowerCase();
59
+ if (runtime.installation.workspaceId !== workspaceId) {
60
+ throw new Error("Storage runtime server workspace identity changed");
61
+ }
62
+ const existing = runtime.identity.storageOwnership;
63
+ if (existing !== undefined) {
64
+ if (existing.machineId !== machineId)
65
+ throw new Error("Storage runtime server machine identity changed");
66
+ return runtime;
67
+ }
68
+ const ownership = await resolveWorkspaceStorageOwnershipClaim({
69
+ nowworkHome: runtime.layout.persistentRoot,
70
+ machineId,
71
+ workspaceId,
72
+ environment: runtime.installation.environment,
73
+ workspaceRootFingerprint: runtime.identity.workspaceRootFingerprint,
74
+ });
75
+ return Object.freeze({
76
+ ...runtime,
77
+ identity: storageHelloIdentity({ installation: runtime.installation, layout: runtime.layout, ownership }),
78
+ });
79
+ }
80
+ export function createStorageRuntimeOwnershipBinding(initial) {
81
+ let runtime = initial;
82
+ let bindTail = Promise.resolve();
83
+ const bind = (server) => {
84
+ const current = bindTail.then(async () => {
85
+ runtime = await bindStorageRuntimeOwnership(runtime, server);
86
+ });
87
+ bindTail = current.catch(() => undefined);
88
+ return current;
89
+ };
90
+ return Object.freeze({ identity: () => runtime.identity, bind });
91
+ }
@@ -9,6 +9,15 @@ export function storageMigrationCapabilities(configured, options) {
9
9
  ...(options.ready ? [STORAGE_MIGRATION_CAPABILITY] : []),
10
10
  ])]);
11
11
  }
12
+ export function bindStorageMigrationServerIdentity(options, identity, callbacks) {
13
+ const bind = options?.bindServerIdentity;
14
+ if (bind === undefined) {
15
+ if (options?.ready === true)
16
+ callbacks.onBound();
17
+ return;
18
+ }
19
+ void Promise.resolve().then(() => bind(identity)).then(callbacks.onBound).catch(callbacks.onError);
20
+ }
12
21
  export function createServeStorageStatusDelivery(options, dependencies) {
13
22
  return createStorageStatusDelivery({
14
23
  ...(options?.statusOutboxPath === undefined ? {} : { outboxPath: options.statusOutboxPath }),
@@ -17,28 +26,36 @@ export function createServeStorageStatusDelivery(options, dependencies) {
17
26
  });
18
27
  }
19
28
  export function attachStorageMigrationRuntime(options, dependencies) {
20
- let admissionClosed = false;
29
+ let migrationAdmissionClosed = false;
30
+ let identityAdmissionClosed = options?.bindServerIdentity !== undefined;
21
31
  if (options?.ready === true) {
22
32
  options.attach?.({
23
33
  gate: {
24
34
  isBusy: dependencies.isBusy,
25
35
  closeAndFlush: async () => {
26
- admissionClosed = true;
36
+ migrationAdmissionClosed = true;
27
37
  await dependencies.waitForExecutionFrames();
28
38
  if (dependencies.isBusy()) {
29
- admissionClosed = false;
39
+ migrationAdmissionClosed = false;
30
40
  throw Object.assign(new Error("active execution entered before storage migration quiesce"), {
31
41
  code: "runtime_busy",
32
42
  });
33
43
  }
34
44
  await dependencies.flushLocalState();
35
45
  },
36
- reopen: () => { admissionClosed = false; },
46
+ reopen: () => { migrationAdmissionClosed = false; },
37
47
  },
38
48
  sendStatus: dependencies.sendStatus,
39
49
  });
40
50
  }
41
- return Object.freeze({ isAdmissionClosed: () => admissionClosed });
51
+ return Object.freeze({
52
+ isAdmissionClosed: () => migrationAdmissionClosed || identityAdmissionClosed,
53
+ beginIdentityBinding: () => {
54
+ if (options?.bindServerIdentity !== undefined)
55
+ identityAdmissionClosed = true;
56
+ },
57
+ completeIdentityBinding: () => { identityAdmissionClosed = false; },
58
+ });
42
59
  }
43
60
  export function replayStorageMigrationStatus(options, socket, isCurrent, onError, deliver) {
44
61
  return options?.replayStatus?.((frame) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.87-beta.2",
3
+ "version": "0.6.87-beta.4",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",