@nowcrew/daemon 0.6.88-beta.15 → 0.6.88-beta.17

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 (39) hide show
  1. package/README.md +15 -0
  2. package/dist/daemon-update-eligibility.js +1 -1
  3. package/dist/local-executor.js +4 -2
  4. package/dist/machine-info.js +48 -15
  5. package/dist/main.js +4 -69
  6. package/dist/project-skills/execution-adapter.js +3 -2
  7. package/dist/project-skills/runtime-launch.js +14 -0
  8. package/dist/provider-env.js +3 -4
  9. package/dist/runtime-probe.js +34 -5
  10. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  11. package/dist/runtimes/deepseek-harness-runner.js +224 -0
  12. package/dist/runtimes/deepseek-harness.js +61 -10
  13. package/dist/runtimes/kimi-acp-runner.js +37 -6
  14. package/dist/serve.js +36 -68
  15. package/dist/storage-identity.js +0 -3
  16. package/dist/storage-serve-adapter.js +6 -72
  17. package/dist/supervised-runtime.js +17 -2
  18. package/package.json +1 -1
  19. package/dist/daemon-migration-controller.js +0 -211
  20. package/dist/daemon-migration-wiring.js +0 -22
  21. package/dist/storage-control.js +0 -212
  22. package/dist/storage-copy.js +0 -628
  23. package/dist/storage-credential-guard.js +0 -21
  24. package/dist/storage-doctor.js +0 -228
  25. package/dist/storage-manifest.js +0 -1027
  26. package/dist/storage-migration-controller.js +0 -440
  27. package/dist/storage-migration-manifest.js +0 -105
  28. package/dist/storage-migration-policy.js +0 -100
  29. package/dist/storage-runtime-controller.js +0 -381
  30. package/dist/storage-runtime-readiness.js +0 -240
  31. package/dist/storage-safe-fs.js +0 -302
  32. package/dist/storage-status-delivery.js +0 -224
  33. package/dist/storage-status-replay.js +0 -140
  34. package/dist/storage-target-confirmation.js +0 -129
  35. package/dist/storage-worker-command.js +0 -37
  36. package/dist/storage-worker-executor.js +0 -449
  37. package/dist/storage-worker-handoff.js +0 -151
  38. package/dist/storage-worker-operation.js +0 -294
  39. package/dist/storage-worker-status.js +0 -259
@@ -0,0 +1,224 @@
1
+ /** 新版 Harness 的短命 ACP 桥:产品状态与重试仍属于 server。 */
2
+ import { once } from "node:events";
3
+ import { Readable, Writable } from "node:stream";
4
+ import { parseArgs } from "node:util";
5
+ import { pathToFileURL } from "node:url";
6
+ import { isAbsolute } from "node:path";
7
+ import spawn from "cross-spawn";
8
+ import { z } from "zod";
9
+ import { client, methods, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
10
+ import { prepareHarnessProfile } from "./deepseek-harness-profile.js";
11
+ import { mapAcpUpdate } from "./kimi-acp-runner.js";
12
+ import { prepareThreadDelivery } from "../thread-input-delivery.js";
13
+ const absolutePath = z.string().refine(isAbsolute, "must be an absolute path");
14
+ export const HarnessInputSchema = z
15
+ .object({
16
+ systemPrompt: z.string(),
17
+ wakePrompt: z.string().min(1),
18
+ stateRoot: absolutePath,
19
+ skillRoots: z.array(absolutePath).default([]),
20
+ effectivePermission: z.enum([
21
+ "sandboxed",
22
+ "workspace_write",
23
+ "full_access",
24
+ ]),
25
+ })
26
+ .strict();
27
+ const emit = async (event) => {
28
+ if (!process.stdout.write(`${JSON.stringify(event)}\n`))
29
+ await once(process.stdout, "drain");
30
+ };
31
+ async function settleWithin(promise, ms) {
32
+ let timer;
33
+ try {
34
+ return await Promise.race([
35
+ promise,
36
+ new Promise((resolve) => {
37
+ timer = setTimeout(() => resolve(undefined), ms);
38
+ }),
39
+ ]);
40
+ }
41
+ finally {
42
+ if (timer)
43
+ clearTimeout(timer);
44
+ }
45
+ }
46
+ export async function runHarness(input, bin, patchPath) {
47
+ const delivery = await prepareThreadDelivery({
48
+ env: process.env,
49
+ resumed: false,
50
+ sessionId: "cold-harness",
51
+ wakePrompt: input.wakePrompt,
52
+ });
53
+ const profile = await prepareHarnessProfile({
54
+ ...input,
55
+ cwd: process.cwd(),
56
+ env: process.env,
57
+ ...(patchPath ? { patchPath } : {}),
58
+ });
59
+ const child = spawn(bin, profile.args, {
60
+ cwd: profile.home,
61
+ env: profile.env,
62
+ stdio: ["pipe", "pipe", "pipe"],
63
+ });
64
+ // Boot away from the repository: the CLI must not load a project's .env into
65
+ // trusted process credentials. ACP session/new carries the real workspace.
66
+ let spawnError;
67
+ const closed = new Promise((resolve) => {
68
+ child.once("error", (error) => {
69
+ spawnError = error;
70
+ resolve();
71
+ });
72
+ child.once("close", () => resolve());
73
+ });
74
+ if (!child.stdin || !child.stdout || !child.stderr)
75
+ throw new Error("DeepSeek Harness stdio unavailable");
76
+ // Provider diagnostics may include secrets or prompts; do not stream raw stderr.
77
+ child.stderr.resume();
78
+ let context;
79
+ let sessionId;
80
+ let cancelled = false;
81
+ let stage = "initialize";
82
+ let exitCode = 1;
83
+ let stopping;
84
+ const stop = () => (stopping ??= (async () => {
85
+ child.stdin?.end();
86
+ if (await settleWithin(closed.then(() => true), 2000))
87
+ return;
88
+ child.kill("SIGTERM");
89
+ if (await settleWithin(closed.then(() => true), 1000))
90
+ return;
91
+ child.kill("SIGKILL");
92
+ if (!(await settleWithin(closed.then(() => true), 2000)))
93
+ throw new Error("DeepSeek Harness process stop unconfirmed");
94
+ })());
95
+ const cancel = () => {
96
+ cancelled = true;
97
+ void (async () => {
98
+ if (context && sessionId)
99
+ await settleWithin(context
100
+ .notify(methods.agent.session.cancel, { sessionId })
101
+ .catch(() => undefined), 1000);
102
+ await stop();
103
+ })().catch(() => {
104
+ /* finally reports stop failure; supervisor owns process tree */
105
+ });
106
+ };
107
+ process.once("SIGTERM", cancel);
108
+ process.once("SIGINT", cancel);
109
+ const app = client({ name: "nowwork-harness" })
110
+ .onRequest(methods.client.session.requestPermission, () => ({
111
+ outcome: { outcome: "cancelled" },
112
+ }))
113
+ .onNotification(methods.client.session.update, async ({ params }) => {
114
+ if (params.sessionId !== sessionId)
115
+ return;
116
+ for (const event of mapAcpUpdate("deepseek-harness", params.update))
117
+ await emit(event);
118
+ });
119
+ try {
120
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
121
+ const result = await app.connectWith(stream, async (ctx) => {
122
+ context = ctx;
123
+ const initialized = await ctx.request(methods.agent.initialize, {
124
+ protocolVersion: PROTOCOL_VERSION,
125
+ clientCapabilities: {},
126
+ clientInfo: { name: "nowwork", version: "1" },
127
+ });
128
+ if (initialized.protocolVersion !== PROTOCOL_VERSION)
129
+ throw new Error("Incompatible ACP protocol");
130
+ if (cancelled)
131
+ throw new Error("cancelled");
132
+ stage = "session_new";
133
+ const session = await ctx.request(methods.agent.session.new, {
134
+ cwd: process.cwd(),
135
+ mcpServers: [],
136
+ });
137
+ sessionId = session.sessionId;
138
+ if (!sessionId || cancelled)
139
+ throw new Error("Harness session unavailable");
140
+ await emit({ type: "thread.started", thread_id: sessionId });
141
+ stage = "prompt";
142
+ const response = await ctx.request(methods.agent.session.prompt, {
143
+ sessionId,
144
+ prompt: [{ type: "text", text: delivery.prompt }],
145
+ });
146
+ if (initialized.agentCapabilities?.sessionCapabilities?.close != null) {
147
+ stage = "session_close";
148
+ const close = await settleWithin(ctx
149
+ .request(methods.agent.session.close, { sessionId })
150
+ .then(() => true), 5000);
151
+ if (!close)
152
+ throw new Error("Harness session close timed out");
153
+ }
154
+ return response;
155
+ });
156
+ const usage = result.usage;
157
+ if (!cancelled)
158
+ await emit({
159
+ type: "turn.completed",
160
+ stop_reason: result.stopReason,
161
+ ...(usage
162
+ ? {
163
+ usage: {
164
+ input_tokens: usage.inputTokens,
165
+ output_tokens: usage.outputTokens,
166
+ cache_read_input_tokens: usage.cachedReadTokens ?? 0,
167
+ cache_creation_input_tokens: usage.cachedWriteTokens ?? 0,
168
+ },
169
+ }
170
+ : {}),
171
+ });
172
+ exitCode =
173
+ result.stopReason === "end_turn"
174
+ ? 0
175
+ : result.stopReason === "cancelled"
176
+ ? 130
177
+ : 1;
178
+ }
179
+ catch {
180
+ if (!cancelled)
181
+ process.stderr.write(spawnError
182
+ ? "DeepSeek Harness launch failed; verify the installed dsh executable.\n"
183
+ : `DeepSeek Harness ACP execution failed at ${stage}; verify the pinned version, machine credentials and profile patch.\n`);
184
+ }
185
+ finally {
186
+ try {
187
+ await stop();
188
+ }
189
+ finally {
190
+ process.off("SIGTERM", cancel);
191
+ process.off("SIGINT", cancel);
192
+ }
193
+ }
194
+ if (cancelled)
195
+ return 130;
196
+ return child.exitCode !== 0 ? 1 : exitCode;
197
+ }
198
+ async function main() {
199
+ const { values } = parseArgs({
200
+ options: { bin: { type: "string" }, patch: { type: "string" } },
201
+ });
202
+ if (!values.bin)
203
+ throw new Error("DeepSeek Harness --bin is required");
204
+ process.stdin.setEncoding("utf8");
205
+ let text = "";
206
+ for await (const chunk of process.stdin) {
207
+ text += String(chunk);
208
+ if (Buffer.byteLength(text) > 16 * 1024 * 1024)
209
+ throw new Error("Harness input exceeds 16 MiB");
210
+ }
211
+ const input = HarnessInputSchema.parse(JSON.parse(text));
212
+ return runHarness(input, values.bin, values.patch);
213
+ }
214
+ if (process.argv[1] &&
215
+ import.meta.url === pathToFileURL(process.argv[1]).href) {
216
+ main()
217
+ .then((code) => {
218
+ process.exitCode = code;
219
+ })
220
+ .catch(() => {
221
+ process.stderr.write("DeepSeek Harness configuration or cleanup failed; check execution paths and machine patch.\n");
222
+ process.exitCode = 1;
223
+ });
224
+ }
@@ -1,20 +1,71 @@
1
1
  import spawn from "cross-spawn";
2
2
  import { isAbsolute } from "node:path";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { prepareHarnessProfile } from "./deepseek-harness-profile.js";
3
7
  import { probeKimiAcp } from "./kimi-acp-runner.js";
4
8
  export const DEEPSEEK_HARNESS_CONFIG_ENV = "NOWCREW_DEEPSEEK_HARNESS_CONFIG";
9
+ export const DEEPSEEK_HARNESS_PATCH_ENV = "NOWCREW_DEEPSEEK_HARNESS_PATCH";
10
+ export function deepSeekHarnessLauncher(env = process.env) {
11
+ const configPath = env[DEEPSEEK_HARNESS_CONFIG_ENV]?.trim();
12
+ const patchPath = env[DEEPSEEK_HARNESS_PATCH_ENV]?.trim();
13
+ if (configPath && !isAbsolute(configPath))
14
+ throw new Error(`${DEEPSEEK_HARNESS_CONFIG_ENV} must be an absolute path`);
15
+ if (patchPath && !isAbsolute(patchPath))
16
+ throw new Error(`${DEEPSEEK_HARNESS_PATCH_ENV} must be an absolute path`);
17
+ if (configPath && patchPath)
18
+ throw new Error("DeepSeek Harness legacy config and profile patch are mutually exclusive");
19
+ return configPath
20
+ ? { kind: "legacy", bin: "dsh-acp-demo", configPath }
21
+ : { kind: "profile", bin: "dsh", ...(patchPath ? { patchPath } : {}) };
22
+ }
5
23
  export function deepSeekHarnessConfigPath(env = process.env) {
6
24
  const path = env[DEEPSEEK_HARNESS_CONFIG_ENV]?.trim();
7
25
  return path && isAbsolute(path) ? path : null;
8
26
  }
9
27
  /** The published DeepSeek Harness ACP executable speaks JSON-RPC directly on stdio. */
10
- export function probeDeepSeekHarnessAcp(options, spawnProcess = spawn) {
11
- const configPath = options.configPath ?? deepSeekHarnessConfigPath();
12
- if (!configPath || !isAbsolute(configPath))
13
- return Promise.resolve(false);
14
- return probeKimiAcp({
15
- ...options,
16
- provider: "deepseek-harness",
17
- configPath,
18
- effectivePermission: "sandboxed",
19
- }, spawnProcess);
28
+ export async function probeDeepSeekHarnessAcp(options, spawnProcess = spawn) {
29
+ if (options.signal?.aborted)
30
+ return false;
31
+ let root;
32
+ try {
33
+ const launcher = options.configPath === undefined
34
+ ? deepSeekHarnessLauncher()
35
+ : deepSeekHarnessLauncher({
36
+ [DEEPSEEK_HARNESS_CONFIG_ENV]: options.configPath,
37
+ });
38
+ if (launcher.kind === "legacy")
39
+ return await probeKimiAcp({
40
+ ...options,
41
+ provider: "deepseek-harness",
42
+ configPath: launcher.configPath,
43
+ effectivePermission: "sandboxed",
44
+ }, spawnProcess);
45
+ root = await mkdtemp(join(tmpdir(), "nowwork-dsh-probe-"));
46
+ const profile = await prepareHarnessProfile({
47
+ stateRoot: root,
48
+ cwd: root,
49
+ systemPrompt: "",
50
+ skillRoots: [],
51
+ effectivePermission: "sandboxed",
52
+ env: process.env,
53
+ ...(launcher.patchPath ? { patchPath: launcher.patchPath } : {}),
54
+ });
55
+ return await probeKimiAcp({
56
+ ...options,
57
+ provider: "deepseek-harness",
58
+ effectivePermission: "sandboxed",
59
+ launchArgs: profile.args,
60
+ launchEnv: profile.env,
61
+ launchCwd: root,
62
+ }, spawnProcess);
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ finally {
68
+ if (root)
69
+ await rm(root, { recursive: true, force: true }).catch(() => undefined);
70
+ }
20
71
  }
@@ -1,3 +1,5 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { dslog } from "../slog.js";
1
3
  import { once } from "node:events";
2
4
  import { isAbsolute } from "node:path";
3
5
  import { parseArgs } from "node:util";
@@ -198,14 +200,22 @@ async function stopChild(child) {
198
200
  export async function probeKimiAcp(options, spawnProcess = spawn) {
199
201
  const provider = options.provider ?? "kimi";
200
202
  const permission = options.effectivePermission ?? "sandboxed";
201
- const child = spawnProcess(options.bin, providerArgs(provider, options.configPath), {
202
- cwd: process.cwd(),
203
+ const probeId = options.probeId ?? randomUUID();
204
+ const started = Date.now();
205
+ let reason = "initializing", protocolVersion;
206
+ let stderrSeen = false;
207
+ const fields = () => ({ probe_id: probeId, runtime: provider, duration_ms: Date.now() - started,
208
+ reason, protocol_version: protocolVersion, stderr_seen: stderrSeen, aborted: options.signal?.aborted ?? false });
209
+ dslog("execution.acp_probe_started", "ACP 探测开始", { probe_id: probeId, runtime: provider, node_version: process.version });
210
+ const child = spawnProcess(options.bin, options.launchArgs ?? providerArgs(provider, options.configPath), {
211
+ cwd: options.launchCwd ?? process.cwd(),
203
212
  // probe 在 daemon 自身 PATH 下运行,补上用户级 CLI 目录,与 which 探测保持一致。
204
- env: { ...providerEnv(provider, permission), PATH: augmentedPath() },
213
+ env: { ...(options.launchEnv ?? providerEnv(provider, permission)), PATH: augmentedPath() },
205
214
  stdio: ["pipe", "pipe", "pipe"],
206
215
  });
207
216
  if (child.stdin === null || child.stdout === null || child.stderr === null)
208
217
  return false;
218
+ child.stderr.on("data", () => { stderrSeen = true; });
209
219
  child.stderr.resume();
210
220
  const app = client({ name: `nowcrew-daemon-${provider}-probe` })
211
221
  .onRequest(methods.client.session.requestPermission, () => ({
@@ -217,23 +227,32 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
217
227
  try {
218
228
  const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
219
229
  const connected = app.connectWith(stream, async (context) => {
220
- await context.request(methods.agent.initialize, {
230
+ const initialized = await context.request(methods.agent.initialize, {
221
231
  protocolVersion: PROTOCOL_VERSION,
222
232
  clientCapabilities: {},
223
233
  clientInfo: { name: "nowcrew-daemon", version: "1" },
224
234
  });
225
- return true;
235
+ protocolVersion = initialized.protocolVersion;
236
+ const ready = protocolVersion === PROTOCOL_VERSION;
237
+ if (reason === "initializing")
238
+ reason = ready ? "ready" : "protocol_mismatch";
239
+ dslog("execution.acp_probe_handshake", "ACP 握手结果", { ...fields(), ready });
240
+ return ready;
226
241
  });
227
242
  const result = await Promise.race([
228
243
  connected,
229
244
  new Promise((resolve) => {
230
245
  timeout = setTimeout(() => {
246
+ if (reason === "initializing")
247
+ reason = "timeout";
231
248
  void stopChild(child);
232
249
  resolve(false);
233
250
  }, PROBE_TIMEOUT_MS);
234
251
  }),
235
252
  new Promise((resolve) => {
236
253
  onAbort = () => {
254
+ if (reason === "initializing")
255
+ reason = "aborted";
237
256
  void stopChild(child);
238
257
  resolve(false);
239
258
  };
@@ -246,6 +265,8 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
246
265
  return result;
247
266
  }
248
267
  catch {
268
+ if (reason === "initializing")
269
+ reason = options.signal?.aborted ? "aborted" : "transport_error";
249
270
  return false;
250
271
  }
251
272
  finally {
@@ -253,7 +274,17 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
253
274
  clearTimeout(timeout);
254
275
  if (onAbort !== undefined)
255
276
  options.signal?.removeEventListener("abort", onAbort);
256
- await stopChild(child);
277
+ dslog("execution.acp_probe_cleanup_started", "ACP 探测清理开始", fields());
278
+ let cleanupFinished = false;
279
+ try {
280
+ await stopChild(child);
281
+ cleanupFinished = true;
282
+ }
283
+ finally {
284
+ dslog("execution.acp_probe_finished", "ACP 探测结束", { ...fields(),
285
+ cleanup_finished: cleanupFinished, exit_code: child.exitCode, exit_signal: child.signalCode,
286
+ level: reason === "ready" && cleanupFinished ? "INFO" : "WARN" });
287
+ }
257
288
  }
258
289
  }
259
290
  export async function runKimiAcp(requestedOptions) {
package/dist/serve.js CHANGED
@@ -51,10 +51,9 @@ import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
51
51
  import { probeRuntimeHealth } from "./runtime-health.js";
52
52
  import { handleRuntimeHealthProbeFrame } from "./runtime-health-handler.js";
53
53
  import { buildControlPlaneUrl } from "./control-plane-url.js";
54
- import { createServeMigrationController, handleMigrationControlMessage } from "./daemon-migration-wiring.js";
55
54
  import { createCodexHomeModeController } from "./codex-home-mode-controller.js";
56
55
  import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
57
- import { attachStorageMigrationRuntime, bindStorageMigrationServerIdentity, createServeStorageStatusDelivery, handleStorageMigrationControl, replayStorageMigrationStatus, storageMigrationCapabilities, } from "./storage-serve-adapter.js";
56
+ import { bindStorageServerIdentity, createStorageIdentityAdmission, } from "./storage-serve-adapter.js";
58
57
  export { buildControlPlaneUrl } from "./control-plane-url.js";
59
58
  // normalize.ts 的活动种类 → activity 枚举
60
59
  const ACTIVITY_MAP = {
@@ -127,17 +126,7 @@ export function serve(config, opts = {}) {
127
126
  catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
128
127
  },
129
128
  });
130
- let storageMigrationAdmissionClosed = () => false;
131
- const migrationController = createServeMigrationController({
132
- config,
133
- ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
134
- isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0,
135
- sendStatus: (frame) => { try {
136
- if (ws?.readyState === WebSocket.OPEN)
137
- ws.send(JSON.stringify(frame));
138
- }
139
- catch { /* reconnect */ } },
140
- });
129
+ const storageIdentityAdmission = createStorageIdentityAdmission(opts.storageRuntime);
141
130
  const codexHomeModeController = createCodexHomeModeController({ ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }), scheduleRestart: opts.update?.scheduleRestart ?? scheduleServiceRestart, getServiceSpec: async () => { const eligibility = await updateEligibility(); return eligibility.eligible ? eligibility.serviceSpec : null; }, isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0, sendStatus: (frame) => { try {
142
131
  if (ws?.readyState === WebSocket.OPEN)
143
132
  ws.send(JSON.stringify(frame));
@@ -148,7 +137,7 @@ export function serve(config, opts = {}) {
148
137
  const agentAbilityRuntime = createAgentAbilityRuntime(config.agentsRoot, {
149
138
  ...opts.agentAbility,
150
139
  coordinator: projectionCoordinator,
151
- isAdmissionClosed: () => storageMigrationAdmissionClosed(),
140
+ isAdmissionClosed: storageIdentityAdmission.isAdmissionClosed,
152
141
  });
153
142
  let projectSkillsController;
154
143
  const projectSkillsReconciler = opts.projectSkills?.reconciler ?? createProjectSkillsReconciler({
@@ -169,7 +158,7 @@ export function serve(config, opts = {}) {
169
158
  }
170
159
  catch { /* 下一次 ready 或项目操作会重新发送完整快照 */ }
171
160
  },
172
- isAdmissionClosed: () => storageMigrationAdmissionClosed(),
161
+ isAdmissionClosed: storageIdentityAdmission.isAdmissionClosed,
173
162
  reconcile: (handle, bindings) => projectSkillsReconciler.reconcile(handle, bindings),
174
163
  });
175
164
  const logRegistryFailure = (error) => dslog("project_skills.registry_failed", "本机项目注册表读取失败,本次不上报项目清单(能力不受影响)", {
@@ -177,7 +166,7 @@ export function serve(config, opts = {}) {
177
166
  });
178
167
  const machineHelloPublisher = createMachineHelloPublisher({
179
168
  effective: (hello) => {
180
- const configuredIdentity = opts.storageMigration?.identity;
169
+ const configuredIdentity = opts.storageRuntime?.identity;
181
170
  const currentIdentity = typeof configuredIdentity === "function"
182
171
  ? configuredIdentity()
183
172
  : configuredIdentity;
@@ -187,23 +176,7 @@ export function serve(config, opts = {}) {
187
176
  });
188
177
  const initializedProjectSkills = initializedProjectSkillsReconciler(() => projectSkillsController.ensureScanned(), projectSkillsReconciler);
189
178
  const configuredCapabilities = opts.machineInfo?.capabilities ?? daemonCapabilities(process.platform);
190
- const storageCapabilities = storageMigrationCapabilities(configuredCapabilities, opts.storageMigration);
191
- const storageStatusDelivery = createServeStorageStatusDelivery(opts.storageMigration, {
192
- onPersistenceError: (error) => dslog("daemon.storage_status_outbox_failed", "存储迁移状态 outbox 持久化失败", { level: "ERROR", error_message: error.message }),
193
- transmit: (frame) => { try {
194
- if (ws?.readyState === WebSocket.OPEN)
195
- ws.send(JSON.stringify(frame));
196
- }
197
- catch { /* retry */ } },
198
- });
199
- const storageMigrationRuntime = attachStorageMigrationRuntime(opts.storageMigration, {
200
- isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0,
201
- waitForExecutionFrames: () => executionFrameQueue,
202
- flushLocalState: flushSlog,
203
- sendStatus: storageStatusDelivery.send,
204
- });
205
- storageMigrationAdmissionClosed = storageMigrationRuntime.isAdmissionClosed;
206
- const prepareCapabilities = async () => daemonCapabilityBindings(process.platform, [...storageCapabilities, ...await managedDaemonCapabilities(updateEligibility)]);
179
+ const prepareCapabilities = async () => daemonCapabilityBindings(process.platform, [...configuredCapabilities, ...await managedDaemonCapabilities(updateEligibility)]);
207
180
  const safeExecutionSend = (frame) => {
208
181
  try {
209
182
  if (ws?.readyState !== WebSocket.OPEN)
@@ -316,21 +289,16 @@ export function serve(config, opts = {}) {
316
289
  let connectionEpochToken;
317
290
  ws.on("open", () => {
318
291
  connectionEpochToken = projectSkillsController.beginConnectionEpoch();
319
- storageMigrationRuntime.beginIdentityBinding();
292
+ storageIdentityAdmission.beginIdentityBinding();
320
293
  const openedSocket = ws;
294
+ const connectionId = randomUUID();
321
295
  backoff = 1000;
322
296
  connectedAt = Date.now();
323
297
  log(`🔌 已连接控制面 ${config.serverUrl}`);
324
- dslog("daemon.ws_open", "已连接控制面", { server_url: config.serverUrl });
298
+ dslog("daemon.ws_open", "已连接控制面", { server_url: config.serverUrl, connection_id: connectionId });
325
299
  // 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
326
300
  void drainSpool();
327
301
  // 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
328
- storageStatusDelivery.replayPending();
329
- const storageReplay = replayStorageMigrationStatus(opts.storageMigration, openedSocket, () => ws === openedSocket, (error) => {
330
- dslog("daemon.storage_status_replay_failed", "存储迁移状态重放失败", {
331
- level: "ERROR", error_message: error.message,
332
- });
333
- }, storageStatusDelivery.send);
334
302
  const { detectInstalled, detectExecutable = detectExecutionRuntimesWithSignal } = opts.machineInfo ?? {};
335
303
  const helloPromise = (opts.machineInfo?.collect ?? collectMachineHello)(config.agentsRoot, config.executionLimits, process.platform, {
336
304
  ...(detectInstalled ? { detectInstalled } : {}),
@@ -345,28 +313,41 @@ export function serve(config, opts = {}) {
345
313
  .then(async (hello) => {
346
314
  if (hello.executionProtocol === undefined) {
347
315
  executionRuntimesKnown = true;
316
+ dslog("runtime_probe.hello_refresh", "runtime hello 刷新决策", {
317
+ connection_id: connectionId, reason: "protocol_unavailable",
318
+ current_socket: ws === openedSocket, socket_open: openedSocket.readyState === WebSocket.OPEN,
319
+ socket_ready_state: openedSocket.readyState, installed_runtimes: JSON.stringify(hello.runtimes),
320
+ execution_runtimes: JSON.stringify(hello.executionRuntimes),
321
+ });
348
322
  return [];
349
323
  }
350
- const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable);
324
+ const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable, connectionId);
351
325
  executionRuntimesKnown = true;
352
- await storageReplay;
353
- if (ws === openedSocket && openedSocket.readyState === WebSocket.OPEN
354
- && detected.some((runtime) => !hello.executionRuntimes.includes(runtime))) {
326
+ const currentSocket = ws === openedSocket;
327
+ const socketOpen = openedSocket.readyState === WebSocket.OPEN;
328
+ const hasNewRuntime = detected.some((runtime) => !hello.executionRuntimes.includes(runtime));
329
+ dslog("runtime_probe.hello_refresh", "runtime hello 刷新决策", {
330
+ connection_id: connectionId, current_socket: currentSocket, socket_open: socketOpen,
331
+ socket_ready_state: openedSocket.readyState, installed_runtimes: JSON.stringify(hello.runtimes),
332
+ initial_execution_runtimes: JSON.stringify(hello.executionRuntimes), execution_runtimes: JSON.stringify(detected),
333
+ reason: !currentSocket ? "stale_connection" : !socketOpen ? "socket_not_open" : !hasNewRuntime ? "unchanged" : "publish",
334
+ });
335
+ if (currentSocket && socketOpen && hasNewRuntime) {
355
336
  detectedExecutionRuntimes = detected;
356
337
  machineHelloPublisher.publish({ ...hello, executionRuntimes: [...detected] }, openedSocket);
357
338
  }
358
339
  return detected;
359
340
  })
360
- .catch((error) => {
341
+ .catch(() => {
361
342
  executionRuntimesKnown = true;
362
343
  dslog("execution.runtime_detection_failed", "runtime 探测失败", {
363
- level: "ERROR", error_message: error.message,
344
+ level: "ERROR", reason: "detection_threw", connection_id: connectionId,
345
+ current_socket: ws === openedSocket, socket_open: openedSocket.readyState === WebSocket.OPEN,
364
346
  });
365
347
  return [];
366
348
  });
367
349
  void helloPromise
368
350
  .then(async (hello) => {
369
- await storageReplay;
370
351
  if (ws !== openedSocket || openedSocket.readyState !== WebSocket.OPEN)
371
352
  return;
372
353
  detectedExecutionRuntimes = hello.executionRuntimes;
@@ -414,8 +395,6 @@ export function serve(config, opts = {}) {
414
395
  if (typeof decoded !== "object" || decoded === null)
415
396
  return;
416
397
  const rawType = "type" in decoded && typeof decoded.type === "string" ? decoded.type : "";
417
- if (storageStatusDelivery.acknowledge(decoded))
418
- return;
419
398
  if (rawType === "daemon:update" || rawType === "daemon:restart") {
420
399
  void updateController.handle(decoded).catch((error) => {
421
400
  dslog("daemon.control_failed", "daemon 控制操作处理失败", {
@@ -424,19 +403,10 @@ export function serve(config, opts = {}) {
424
403
  });
425
404
  return;
426
405
  }
427
- if (rawType === "daemon:migrate") {
428
- void handleMigrationControlMessage(migrationController, decoded).catch((error) => dslog("daemon.migration_failed", "daemon 布局迁移失败", { level: "ERROR", error_message: error.message }));
429
- return;
430
- }
431
406
  if (rawType === "daemon:codex-home-mode") {
432
407
  void codexHomeModeController.handle(decoded).catch((error) => dslog("daemon.codex_home_mode_failed", "Codex Home 模式切换失败", { level: "ERROR", error_message: error.message }));
433
408
  return;
434
409
  }
435
- if (handleStorageMigrationControl(opts.storageMigration, rawType, decoded, storageStatusDelivery.send, (kind, error) => {
436
- dslog(kind === "migration" ? "daemon.storage_migration_failed" : "daemon.storage_migration_confirmation_failed", kind === "migration" ? "存储迁移控制操作失败" : "存储迁移 hello 确认处理失败", { level: "ERROR", error_message: error.message });
437
- })) {
438
- return;
439
- }
440
410
  if (rawType.startsWith("execution:")) {
441
411
  const parsedExecution = ServerToDaemonExecutionFrameSchema.safeParse(decoded);
442
412
  if (!parsedExecution.success) {
@@ -498,10 +468,10 @@ export function serve(config, opts = {}) {
498
468
  return;
499
469
  }
500
470
  const spec = frame;
501
- if (storageMigrationRuntime.isAdmissionClosed()) {
471
+ if (storageIdentityAdmission.isAdmissionClosed()) {
502
472
  safeExecutionSend(ExecutionRejectedSchema.parse({
503
473
  type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
504
- reason: "resource_limit", message: "Storage migration is quiescing this machine",
474
+ reason: "resource_limit", message: "Storage identity binding is not complete",
505
475
  at: new Date().toISOString(),
506
476
  }));
507
477
  return;
@@ -812,17 +782,17 @@ export function serve(config, opts = {}) {
812
782
  // ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
813
783
  const r = msg;
814
784
  setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
815
- if (opts.storageMigration !== undefined && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
785
+ if (opts.storageRuntime !== undefined && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
816
786
  const bindingSocket = ws;
817
- bindStorageMigrationServerIdentity(opts.storageMigration, { workspaceId: r.workspaceId, machineId: r.machineId }, {
787
+ bindStorageServerIdentity(opts.storageRuntime, { workspaceId: r.workspaceId, machineId: r.machineId }, {
818
788
  onBound: () => {
819
789
  if (bindingSocket !== ws || bindingSocket?.readyState !== WebSocket.OPEN)
820
790
  return;
821
- storageMigrationRuntime.completeIdentityBinding();
791
+ storageIdentityAdmission.completeIdentityBinding();
822
792
  machineHelloPublisher.republish();
823
793
  },
824
794
  onError: (error) => {
825
- dslog("daemon.storage_identity_bind_failed", "存储迁移 Workspace 身份绑定失败", { level: "ERROR", error_message: error.message });
795
+ dslog("daemon.storage_identity_bind_failed", "存储 Workspace 身份绑定失败", { level: "ERROR", error_message: error.message });
826
796
  if (bindingSocket === ws && bindingSocket?.readyState === WebSocket.OPEN)
827
797
  bindingSocket.close(1011, "storage identity binding failed");
828
798
  },
@@ -925,7 +895,7 @@ export function serve(config, opts = {}) {
925
895
  }
926
896
  if (msg.type !== "agent:start")
927
897
  return; // ready/error 等忽略
928
- if (storageMigrationRuntime.isAdmissionClosed())
898
+ if (storageIdentityAdmission.isAdmissionClosed())
929
899
  return;
930
900
  // 任务键:scheduled run 用 runId(每次运行独立 cwd/work-log;overlap 由 server 端 skip_if_running 管,
931
901
  // daemon 的 running 去重只兜底"同一 run 重复投递");普通唤醒仍是 线程锚点 ?? 频道。
@@ -1357,7 +1327,6 @@ export function serve(config, opts = {}) {
1357
1327
  concurrencyTelemetry.stop();
1358
1328
  runtimeProbe.stop();
1359
1329
  completionRetransmitter.stop();
1360
- storageStatusDelivery.stop();
1361
1330
  const deadline = createShutdownDeadline(shutdownTimeoutMs);
1362
1331
  if (reconnectTimer !== null) {
1363
1332
  clearTimeout(reconnectTimer);
@@ -1367,7 +1336,6 @@ export function serve(config, opts = {}) {
1367
1336
  const pending = [
1368
1337
  webSocketClosed,
1369
1338
  updateController.drain(),
1370
- ...(migrationController === null ? [] : [migrationController.drain()]),
1371
1339
  codexHomeModeController.drain(),
1372
1340
  executionFrameQueue,
1373
1341
  ...executionRuns.values(),
@@ -39,8 +39,5 @@ export function storageHelloIdentity(input) {
39
39
  daemonRootFingerprint: fingerprint("daemon-root", layout.daemonRoot),
40
40
  agentRootFingerprint: fingerprint("agent-root", layout.agentItemsRoot),
41
41
  ...(ownership === undefined ? {} : { storageOwnership: ownership }),
42
- ...(installation.storageMigrationId === undefined
43
- ? {}
44
- : { storageMigrationId: installation.storageMigrationId }),
45
42
  });
46
43
  }