@nowcrew/daemon 0.6.88-beta.16 → 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.
@@ -2,6 +2,8 @@
2
2
  * 采集本机信息上报给控制面 (machine:hello):hostname / os / daemon 版本 / 已装 runtimes。
3
3
  * runtimes 探测靠 `which <bin>`(win32 用 `where`),只报真实可执行的 CLI(用于展示 Detected Runtimes)。
4
4
  */
5
+ import { randomUUID } from "node:crypto";
6
+ import { dslog } from "./slog.js";
5
7
  import { hostname, arch, platform as osPlatform } from "node:os";
6
8
  import { lookupCmd } from "./platform.js";
7
9
  import { augmentedPath } from "./runtime-path.js";
@@ -117,31 +119,50 @@ export async function detectRuntimes() {
117
119
  }));
118
120
  return checks.filter((x) => x !== null);
119
121
  }
120
- async function supportsKimiAcp(signal) {
121
- return probeKimiAcp({ bin: "kimi", ...(signal ? { signal } : {}) });
122
+ async function supportsKimiAcp(signal, probeId) {
123
+ return probeKimiAcp({ bin: "kimi", ...(probeId ? { probeId } : {}), ...(signal ? { signal } : {}) });
122
124
  }
123
- async function supportsHermesAcp(signal) {
124
- return probeHermesAcp({ bin: "hermes", ...(signal ? { signal } : {}) });
125
+ async function supportsHermesAcp(signal, probeId) {
126
+ return probeHermesAcp({ bin: "hermes", ...(probeId ? { probeId } : {}), ...(signal ? { signal } : {}) });
125
127
  }
126
128
  async function supportsOpenCodeRun(signal) {
127
129
  return probeOpenCodeRun(signal);
128
130
  }
129
- async function supportsDeepSeekHarnessAcp(signal) {
131
+ async function supportsDeepSeekHarnessAcp(signal, probeId) {
130
132
  try {
131
- return await probeDeepSeekHarnessAcp({ bin: deepSeekHarnessLauncher().bin, ...(signal ? { signal } : {}) });
133
+ return await probeDeepSeekHarnessAcp({ bin: deepSeekHarnessLauncher().bin, ...(probeId ? { probeId } : {}), ...(signal ? { signal } : {}) });
132
134
  }
133
135
  catch {
134
136
  return false;
135
137
  }
136
138
  }
137
139
  /** Runtime adapters that can satisfy the durable protocol-v1 process contract. */
138
- export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp, hermesAcpProbe = supportsHermesAcp, openCodeProbe = supportsOpenCodeRun, deepSeekHarnessProbe = supportsDeepSeekHarnessAcp, signal) {
140
+ export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp, hermesAcpProbe = supportsHermesAcp, openCodeProbe = supportsOpenCodeRun, deepSeekHarnessProbe = supportsDeepSeekHarnessAcp, signal, probeId = randomUUID()) {
139
141
  const present = await installed;
142
+ const probe = (runtime, detect) => {
143
+ const startedAt = Date.now();
144
+ dslog("runtime_probe.optional_start", "开始可选 runtime 探测", { probe_id: probeId, runtime, aborted: signal?.aborted ?? false });
145
+ const complete = (outcome, ready) => {
146
+ dslog("runtime_probe.optional_complete", "可选 runtime 探测结束", {
147
+ probe_id: probeId, runtime, outcome, duration_ms: Date.now() - startedAt,
148
+ aborted: signal?.aborted ?? false, ...(ready === undefined ? {} : { ready }),
149
+ });
150
+ };
151
+ let pending;
152
+ try {
153
+ pending = detect(signal, probeId);
154
+ }
155
+ catch (error) {
156
+ complete("threw");
157
+ throw error;
158
+ }
159
+ return pending.then((ready) => { complete("resolved", ready); return ready; }, (error) => { complete("threw"); throw error; });
160
+ };
140
161
  const [kimiReady, hermesReady, openCodeReady, deepSeekHarnessReady] = await Promise.all([
141
- present.includes("kimi") ? kimiAcpProbe(signal) : false,
142
- present.includes("hermes") ? hermesAcpProbe(signal) : false,
143
- present.includes("opencode") ? openCodeProbe(signal) : false,
144
- present.includes("deepseek-harness") ? deepSeekHarnessProbe(signal) : false,
162
+ present.includes("kimi") ? probe("kimi", kimiAcpProbe) : false,
163
+ present.includes("hermes") ? probe("hermes", hermesAcpProbe) : false,
164
+ present.includes("opencode") ? probe("opencode", openCodeProbe) : false,
165
+ present.includes("deepseek-harness") ? probe("deepseek-harness", deepSeekHarnessProbe) : false,
145
166
  ]);
146
167
  return executableRuntimes(present).filter((runtime) => (runtime === "kimi" ? kimiReady
147
168
  : runtime === "hermes" ? hermesReady
@@ -149,7 +170,7 @@ export async function detectExecutionRuntimes(installed = detectRuntimes(), kimi
149
170
  : runtime === "deepseek-harness" ? deepSeekHarnessReady
150
171
  : true));
151
172
  }
152
- export const detectExecutionRuntimesWithSignal = (installed, signal) => detectExecutionRuntimes(installed, supportsKimiAcp, supportsHermesAcp, supportsOpenCodeRun, supportsDeepSeekHarnessAcp, signal);
173
+ export const detectExecutionRuntimesWithSignal = (installed, signal, probeId) => detectExecutionRuntimes(installed, supportsKimiAcp, supportsHermesAcp, supportsOpenCodeRun, supportsDeepSeekHarnessAcp, signal, probeId);
153
174
  export function daemonVersion() {
154
175
  try {
155
176
  const here = dirname(fileURLToPath(import.meta.url));
@@ -1,3 +1,5 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { dslog } from "./slog.js";
1
3
  import { executableRuntimes } from "./runtime-capabilities.js";
2
4
  export function conservativeExecutionRuntimes(installed) {
3
5
  return executableRuntimes(installed)
@@ -7,16 +9,43 @@ export function createRuntimeProbeCoordinator() {
7
9
  const abort = new AbortController();
8
10
  const flights = new Map();
9
11
  return {
10
- detect(installed, detector) {
12
+ detect(installed, detector, connectionId) {
11
13
  const key = [...installed].sort().join("\0");
12
14
  const existing = flights.get(key);
13
- if (existing)
14
- return existing;
15
- const pending = detector(installed, abort.signal).catch((error) => {
15
+ const probeId = existing?.probeId ?? randomUUID();
16
+ const fields = { probe_id: probeId, connection_id: connectionId, installed_runtimes: JSON.stringify(installed) };
17
+ dslog("runtime_probe.request", "请求 runtime 执行能力探测", fields);
18
+ if (existing) {
19
+ dslog("runtime_probe.cache_hit", "复用 runtime 探测结果", { ...fields, cache_state: existing.state });
20
+ return existing.promise;
21
+ }
22
+ const startedAt = Date.now();
23
+ const complete = (outcome, executionRuntimes) => {
24
+ dslog("runtime_probe.complete", "runtime 探测聚合结束", {
25
+ ...fields, outcome, duration_ms: Date.now() - startedAt, aborted: abort.signal.aborted,
26
+ ...(executionRuntimes === undefined ? {} : { execution_runtimes: JSON.stringify(executionRuntimes) }),
27
+ });
28
+ };
29
+ let detection;
30
+ try {
31
+ detection = detector(installed, abort.signal, probeId);
32
+ }
33
+ catch (error) {
34
+ complete("threw");
35
+ throw error;
36
+ }
37
+ const pending = detection.then((result) => {
38
+ const flight = flights.get(key);
39
+ if (flight)
40
+ flights.set(key, { ...flight, state: "resolved" });
41
+ complete("resolved", result);
42
+ return result;
43
+ }, (error) => {
16
44
  flights.delete(key);
45
+ complete("threw");
17
46
  throw error;
18
47
  });
19
- flights.set(key, pending);
48
+ flights.set(key, { promise: pending, probeId, state: "pending" });
20
49
  return pending;
21
50
  },
22
51
  stop() {
@@ -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,6 +200,13 @@ 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";
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 });
201
210
  const child = spawnProcess(options.bin, options.launchArgs ?? providerArgs(provider, options.configPath), {
202
211
  cwd: options.launchCwd ?? process.cwd(),
203
212
  // probe 在 daemon 自身 PATH 下运行,补上用户级 CLI 目录,与 which 探测保持一致。
@@ -206,6 +215,7 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
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, () => ({
@@ -222,18 +232,27 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
222
232
  clientCapabilities: {},
223
233
  clientInfo: { name: "nowcrew-daemon", version: "1" },
224
234
  });
225
- return initialized.protocolVersion === PROTOCOL_VERSION;
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
@@ -291,10 +291,11 @@ export function serve(config, opts = {}) {
291
291
  connectionEpochToken = projectSkillsController.beginConnectionEpoch();
292
292
  storageIdentityAdmission.beginIdentityBinding();
293
293
  const openedSocket = ws;
294
+ const connectionId = randomUUID();
294
295
  backoff = 1000;
295
296
  connectedAt = Date.now();
296
297
  log(`🔌 已连接控制面 ${config.serverUrl}`);
297
- dslog("daemon.ws_open", "已连接控制面", { server_url: config.serverUrl });
298
+ dslog("daemon.ws_open", "已连接控制面", { server_url: config.serverUrl, connection_id: connectionId });
298
299
  // 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
299
300
  void drainSpool();
300
301
  // 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
@@ -312,21 +313,36 @@ export function serve(config, opts = {}) {
312
313
  .then(async (hello) => {
313
314
  if (hello.executionProtocol === undefined) {
314
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
+ });
315
322
  return [];
316
323
  }
317
- const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable);
324
+ const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable, connectionId);
318
325
  executionRuntimesKnown = true;
319
- if (ws === openedSocket && openedSocket.readyState === WebSocket.OPEN
320
- && 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) {
321
336
  detectedExecutionRuntimes = detected;
322
337
  machineHelloPublisher.publish({ ...hello, executionRuntimes: [...detected] }, openedSocket);
323
338
  }
324
339
  return detected;
325
340
  })
326
- .catch((error) => {
341
+ .catch(() => {
327
342
  executionRuntimesKnown = true;
328
343
  dslog("execution.runtime_detection_failed", "runtime 探测失败", {
329
- 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,
330
346
  });
331
347
  return [];
332
348
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.88-beta.16",
3
+ "version": "0.6.88-beta.17",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",