@nowcrew/daemon 0.6.88-beta.19 → 0.6.88-beta.20

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.
@@ -0,0 +1,69 @@
1
+ import { Readable as NodeReadable } from "node:stream";
2
+ import { ReadableStream, WritableStream } from "node:stream/web";
3
+ /** Probe-only counters. Never retain/log chunks, JSON-RPC params, stderr or env values. */
4
+ export class AcpProbeTransport {
5
+ elapsed;
6
+ initializeRequestedMs;
7
+ initializeResponseMs;
8
+ firstStdinWriteMs;
9
+ firstStdoutMs;
10
+ stdinBytesWritten = 0;
11
+ stdoutBytes = 0;
12
+ stdoutEnded = false;
13
+ constructor(elapsed) {
14
+ this.elapsed = elapsed;
15
+ }
16
+ initializeRequested() { this.initializeRequestedMs = this.elapsed(); }
17
+ initializeResponse() { this.initializeResponseMs = this.elapsed(); }
18
+ fields() {
19
+ return {
20
+ initialize_requested_ms: this.initializeRequestedMs,
21
+ initialize_response_ms: this.initializeResponseMs,
22
+ first_stdin_write_ms: this.firstStdinWriteMs,
23
+ first_stdout_ms: this.firstStdoutMs,
24
+ stdin_bytes_written: this.stdinBytesWritten,
25
+ stdout_bytes: this.stdoutBytes,
26
+ stdout_ended: this.stdoutEnded,
27
+ };
28
+ }
29
+ /** A write callback confirms acceptance by the pipe, NOT receipt by the agent. */
30
+ writable(stdin) {
31
+ return new WritableStream({
32
+ start: (controller) => { stdin.once("error", (error) => controller.error(error)); },
33
+ write: (chunk) => new Promise((resolve, reject) => {
34
+ stdin.write(chunk, (error) => {
35
+ if (error) {
36
+ reject(error);
37
+ return;
38
+ }
39
+ this.firstStdinWriteMs ??= this.elapsed();
40
+ this.stdinBytesWritten += chunk.byteLength;
41
+ resolve();
42
+ });
43
+ }),
44
+ close: () => new Promise((resolve, reject) => {
45
+ stdin.end((error) => error ? reject(error) : resolve());
46
+ }),
47
+ abort: () => { stdin.destroy(); },
48
+ });
49
+ }
50
+ /** Observe before NDJSON decoding, without adding a competing 'data' listener. */
51
+ readable(stdout) {
52
+ const reader = NodeReadable.toWeb(stdout).getReader();
53
+ return new ReadableStream({
54
+ pull: async (controller) => {
55
+ const item = await reader.read();
56
+ if (item.done) {
57
+ this.stdoutEnded = true;
58
+ controller.close();
59
+ return;
60
+ }
61
+ const chunk = item.value;
62
+ this.firstStdoutMs ??= this.elapsed();
63
+ this.stdoutBytes += chunk.byteLength;
64
+ controller.enqueue(chunk);
65
+ },
66
+ cancel: (reason) => reader.cancel(reason),
67
+ });
68
+ }
69
+ }
@@ -1,3 +1,4 @@
1
+ import { AcpProbeTransport } from "./acp-probe-transport.js";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { dslog } from "../slog.js";
3
4
  import { once } from "node:events";
@@ -204,18 +205,27 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
204
205
  const provider = options.provider ?? "kimi";
205
206
  const permission = options.effectivePermission ?? "sandboxed";
206
207
  const probeId = options.probeId ?? randomUUID();
207
- const started = Date.now();
208
+ const started = performance.now();
209
+ const elapsed = () => Math.round(performance.now() - started);
210
+ const transport = new AcpProbeTransport(elapsed);
211
+ let spawnConfirmedMs;
208
212
  let reason = "initializing", protocolVersion;
209
213
  let stderrSeen = false;
210
- const fields = () => ({ probe_id: probeId, runtime: provider, duration_ms: Date.now() - started,
214
+ const fields = () => ({ probe_id: probeId, runtime: provider, duration_ms: elapsed(),
215
+ ...transport.fields(), spawn_confirmed_ms: spawnConfirmedMs, child_pid: child.pid,
211
216
  reason, protocol_version: protocolVersion, stderr_seen: stderrSeen, aborted: options.signal?.aborted ?? false });
212
217
  dslog("execution.acp_probe_started", "ACP 探测开始", { probe_id: probeId, runtime: provider, node_version: process.version });
218
+ const launchEnv = options.launchEnv ?? providerEnv(provider, permission);
213
219
  const child = spawnProcess(options.bin, options.launchArgs ?? providerArgs(provider, options.configPath), {
214
220
  cwd: options.launchCwd ?? process.cwd(),
215
- // probe 在 daemon 自身 PATH 下运行,补上用户级 CLI 目录,与 which 探测保持一致。
216
- env: { ...(options.launchEnv ?? providerEnv(provider, permission)), PATH: augmentedPath() },
221
+ // 显式 launchEnv 也决定 PATH/HOME;不能把它与父进程 PATH 混用。
222
+ env: { ...launchEnv, PATH: augmentedPath(launchEnv) },
217
223
  stdio: ["pipe", "pipe", "pipe"],
218
224
  });
225
+ child.once("spawn", () => {
226
+ spawnConfirmedMs = elapsed();
227
+ dslog("execution.acp_probe_spawned", "ACP 探测子进程已启动", fields());
228
+ });
219
229
  if (child.stdin === null || child.stdout === null || child.stderr === null)
220
230
  return false;
221
231
  child.stderr.on("data", () => { stderrSeen = true; });
@@ -228,13 +238,15 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
228
238
  let timeout;
229
239
  let onAbort;
230
240
  try {
231
- const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
241
+ const stream = ndJsonStream(transport.writable(child.stdin), transport.readable(child.stdout));
232
242
  const connected = app.connectWith(stream, async (context) => {
243
+ transport.initializeRequested();
233
244
  const initialized = await context.request(methods.agent.initialize, {
234
245
  protocolVersion: PROTOCOL_VERSION,
235
246
  clientCapabilities: {},
236
247
  clientInfo: { name: "nowcrew-daemon", version: "1" },
237
248
  });
249
+ transport.initializeResponse();
238
250
  protocolVersion = initialized.protocolVersion;
239
251
  const ready = protocolVersion === PROTOCOL_VERSION;
240
252
  if (reason === "initializing")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.88-beta.19",
3
+ "version": "0.6.88-beta.20",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",