@nowcrew/daemon 0.6.99 → 0.6.100

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.
package/dist/console.js CHANGED
@@ -9,8 +9,8 @@
9
9
  * 会附带结构化 payload(diff/命令/todo 清单/计划…),供前端做富渲染(红绿 diff、清单勾选);
10
10
  * 前端不识别 payload.kind 时降级为纯文本行,协议向后兼容。
11
11
  *
12
- * 覆盖三种 runtime 的流:claude(stream-json)、codex(exec --json 的 item.* 事件)、
13
- * kimi(OpenAI 消息风格行)。
12
+ * 覆盖 runtime 的流:claude(stream-json)、codex(exec --json 的 item.* 事件)、
13
+ * kimi(OpenAI 消息风格行)、ACP 适配器与 Pi 适配器(pi.* 块级事件)。
14
14
  */
15
15
  import { detectDaemonLang, translateDaemon } from "./i18n.js";
16
16
  import { buildFilePreview, buildSnippetDiff } from "./console-payload.js";
@@ -280,6 +280,21 @@ export function toConsoleLines(event, runtime) {
280
280
  const text = typeof e.content === "string" ? e.content.trim() : "";
281
281
  return text ? [{ stream: "tool_result", text: clip(text, TOOL_RESULT_CAP) }] : [];
282
282
  }
283
+ if (e.type === "pi.text" && e.text)
284
+ return [{ stream: "text", text: e.text }];
285
+ if (e.type === "pi.thinking" && e.text)
286
+ return [{ stream: "thinking", text: e.text }];
287
+ if (e.type === "pi.tool_call") {
288
+ return [{ stream: "tool", text: e.title ? `⏺ ${clip(e.title, TOOL_INPUT_CAP)}` : "⏺ Pi tool" }];
289
+ }
290
+ if (e.type === "pi.tool_result") {
291
+ const text = typeof e.content === "string" ? e.content.trim() : "";
292
+ return text ? [{ stream: e.is_error ? "error" : "tool_result", text: clip(text, TOOL_RESULT_CAP) }] : [];
293
+ }
294
+ if (e.type === "pi.error") {
295
+ const message = event.message;
296
+ return typeof message === "string" && message.trim() ? [{ stream: "error", text: message.trim() }] : [];
297
+ }
283
298
  if (e.type === "system" && e.subtype === "init") {
284
299
  return [{ stream: "system", text: `● ${td("Claude Code session started")}` }];
285
300
  }
@@ -1,3 +1,4 @@
1
+ import { MAINTENANCE_CAPABILITY } from "./marketplace-maintenance-protocol.js";
1
2
  import { retryHarnessProbe } from "./runtimes/harness-probe-retry.js";
2
3
  import { MARKETPLACE_CAPABILITY } from "./marketplace-protocol.js";
3
4
  /**
@@ -40,6 +41,7 @@ export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
40
41
  });
41
42
  export const DAEMON_CAPABILITIES = Object.freeze([
42
43
  MARKETPLACE_CAPABILITY,
44
+ MAINTENANCE_CAPABILITY,
43
45
  "scheduled_job_v1",
44
46
  "reply_origin_v1",
45
47
  "origin_decision_v1",
@@ -72,7 +74,7 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
72
74
  const nativeWorkspaceReady = nativeWorkspacePlatform
73
75
  && readiness.resolverReady
74
76
  && readiness.nativeRuntimeAdaptersReady;
75
- return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== MARKETPLACE_CAPABILITY || nativeWorkspacePlatform)
77
+ return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => (((capability !== MARKETPLACE_CAPABILITY && capability !== MAINTENANCE_CAPABILITY) || nativeWorkspacePlatform)
76
78
  && (capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
77
79
  && (capability !== "message_file_preview_v1" || nativeWorkspacePlatform)
78
80
  && (nativeWorkspacePlatform
@@ -1,3 +1,4 @@
1
+ import { MaintenanceRequestSchema, MaintenanceReportSchema } from "./marketplace-maintenance-protocol.js";
1
2
  import { MarketplaceRequestSchema, marketplaceActive } from "./marketplace-protocol.js";
2
3
  import { resolveMarketplaceScopes, runMarketplaceScope } from "./marketplace-runner.js";
3
4
  export function createMarketplaceController(deps) {
@@ -11,22 +12,31 @@ export function createMarketplaceController(deps) {
11
12
  const deadline = now() + 10 * 60_000;
12
13
  let lease = null;
13
14
  try {
14
- const scopes = await (deps.resolve ?? resolveMarketplaceScopes)(request.targets, deps.agentsRoot);
15
+ const scopes = request.type === "marketplace:operation" ? await (deps.resolve ?? resolveMarketplaceScopes)(request.targets, deps.agentsRoot) : [];
15
16
  // 查询不使用任务执行槽;只有写入运行时共用市场/插件目录时才等待独占保护。
16
- if (request.mode === "update") {
17
+ if (request.type === "marketplace:maintenance" || request.mode === "update") {
17
18
  while (!stopped && now() < deadline && lease === null) {
18
19
  lease = await deps.acquire();
19
20
  if (!lease)
20
21
  await wait();
21
22
  }
22
23
  if (!lease || stopped) {
23
- operations.set(id, { operationId: id, status: "failed", results: [], errorCode: "runtime_busy" });
24
+ operations.set(id, { ...operations.get(id), status: "failed", errorCode: "runtime_busy" });
24
25
  return;
25
26
  }
26
27
  }
27
28
  if (stopped || now() >= deadline)
28
29
  throw new Error("operation_deadline");
29
- operations.set(id, { operationId: id, status: "running", results: [], errorCode: null });
30
+ operations.set(id, { ...operations.get(id), status: "running" });
31
+ if (request.type === "marketplace:maintenance") {
32
+ if (!deps.maintain)
33
+ throw new Error("maintenance_unavailable");
34
+ const result = MaintenanceReportSchema.parse(await deps.maintain(request));
35
+ if (result.operationId !== id)
36
+ throw new Error("operation_mismatch");
37
+ operations.set(id, result);
38
+ return;
39
+ }
30
40
  const results = [];
31
41
  for (const scope of scopes) {
32
42
  if (stopped || now() >= deadline)
@@ -41,7 +51,9 @@ export function createMarketplaceController(deps) {
41
51
  }
42
52
  catch {
43
53
  const previous = operations.get(id);
44
- operations.set(id, { ...previous, status: previous.results.some(x => !x.errorCode) ? "partial" : "failed", errorCode: "execution_failed" });
54
+ operations.set(id, request.type === "marketplace:maintenance"
55
+ ? { ...previous, status: "unknown", errorCode: "operation_unknown" }
56
+ : { ...previous, status: previous.results.some(x => !x.errorCode) ? "partial" : "failed", errorCode: "execution_failed" });
45
57
  }
46
58
  finally {
47
59
  try {
@@ -55,22 +67,23 @@ export function createMarketplaceController(deps) {
55
67
  };
56
68
  return {
57
69
  handle(raw) {
58
- const parsed = MarketplaceRequestSchema.safeParse(raw);
70
+ const parsed = MarketplaceRequestSchema.or(MaintenanceRequestSchema).safeParse(raw);
59
71
  if (!parsed.success || stopped)
60
72
  return { ok: false, error: "invalid_request" };
61
73
  const request = parsed.data;
74
+ const version = request.type === "marketplace:maintenance" ? { version: 2 } : {};
62
75
  const existing = operations.get(request.operationId);
63
76
  if (existing)
64
77
  return { ok: true, data: existing };
65
78
  if (request.action === "status")
66
- return { ok: true, data: { operationId: request.operationId, status: "unknown", results: [], errorCode: "operation_unknown" } };
79
+ return { ok: true, data: { ...version, operationId: request.operationId, status: "unknown", results: [], errorCode: "operation_unknown" } };
67
80
  if (process.platform !== "darwin" && process.platform !== "linux")
68
81
  return { ok: false, error: "unsupported_platform" };
69
82
  if (current !== null || [...operations.values()].some(x => marketplaceActive(x.status)))
70
83
  return { ok: false, error: "runtime_busy" };
71
84
  if (operations.size >= 100)
72
85
  operations.delete(operations.keys().next().value);
73
- const accepted = { operationId: request.operationId, status: "queued", results: [], errorCode: null };
86
+ const accepted = { ...version, operationId: request.operationId, status: "queued", results: [], errorCode: null };
74
87
  operations.set(request.operationId, accepted);
75
88
  current = execute(request).finally(() => { current = null; });
76
89
  return { ok: true, data: accepted };
@@ -0,0 +1,79 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ import { augmentedPath } from "./runtime-path.js";
7
+ import { MaintenanceReportSchema } from "./marketplace-maintenance-protocol.js";
8
+ export function maintenanceEnvironment(env) {
9
+ // Keep the machine user's login/config, never forward control-plane credentials.
10
+ return Object.fromEntries(Object.entries(env).filter(([key]) => !/^(CREW_|NOWCREW_|NOWWORK_)/i.test(key)));
11
+ }
12
+ async function supervise(file, id, cwd, env) {
13
+ await new Promise((resolve, reject) => {
14
+ const child = spawn(process.execPath, [file, "--run", id], { cwd, env, detached: true, stdio: "ignore" });
15
+ let expired = false;
16
+ const kill = () => { if (child.pid) {
17
+ try {
18
+ process.kill(-child.pid, "SIGKILL");
19
+ }
20
+ catch { /* already gone */ }
21
+ } };
22
+ const timer = setTimeout(() => { expired = true; kill(); }, 18 * 60_000);
23
+ child.once("error", () => { clearTimeout(timer); kill(); reject(new Error("worker_failed")); });
24
+ child.once("close", code => {
25
+ clearTimeout(timer);
26
+ // Never release the machine lock while descendants can still write shared configuration.
27
+ kill();
28
+ if (expired || code !== 0)
29
+ reject(new Error("worker_failed"));
30
+ else
31
+ resolve();
32
+ });
33
+ });
34
+ }
35
+ export async function runMaintenanceBridge(config, request, acquireEnv) {
36
+ const fail = (errorCode) => ({ version: 2, operationId: request.operationId, status: "failed", results: [], errorCode });
37
+ let dir;
38
+ let started = false;
39
+ try {
40
+ const response = await fetch(`${config.serverUrl.replace(/\/+$/, "")}/machine/marketplace-maintenance/${request.operationId}/runner/${request.plan.runner.releaseId}`, {
41
+ headers: { authorization: `Bearer ${config.machineToken}` }, redirect: "error", signal: AbortSignal.timeout(15_000),
42
+ });
43
+ if (!response.ok)
44
+ return fail("runner_unavailable");
45
+ // Bound even a malformed server response before buffering or executing it.
46
+ if (!response.body)
47
+ return fail("runner_unavailable");
48
+ const chunks = [];
49
+ let bytes = 0;
50
+ for await (const chunk of response.body) {
51
+ bytes += chunk.length;
52
+ if (bytes > 256_000)
53
+ return fail("runner_unavailable");
54
+ chunks.push(chunk);
55
+ }
56
+ const content = Buffer.concat(chunks);
57
+ if (createHash("sha256").update(content).digest("hex") !== request.plan.runner.sha256)
58
+ return fail("runner_unavailable");
59
+ const env = maintenanceEnvironment(await acquireEnv());
60
+ env.PATH = augmentedPath(env, {}, process.platform, "append");
61
+ dir = await mkdtemp(join(tmpdir(), "nowwork-maintenance-"));
62
+ const file = join(dir, "runner.mjs");
63
+ await writeFile(file, content, { mode: 0o600 });
64
+ started = true;
65
+ await supervise(file, request.operationId, dir, env);
66
+ const report = MaintenanceReportSchema.parse(JSON.parse(await readFile(join(dir, "report.json"), "utf8")));
67
+ if (report.operationId !== request.operationId)
68
+ throw new Error("operation_mismatch");
69
+ return report;
70
+ }
71
+ catch {
72
+ // A killed worker may have changed files: require readback/manual confirmation before another write.
73
+ return started ? { ...fail("operation_unknown"), status: "unknown" } : fail("runner_unavailable");
74
+ }
75
+ finally {
76
+ if (dir)
77
+ await rm(dir, { recursive: true, force: true });
78
+ }
79
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ // 与 daemon 同名文件逐字一致;这是独立于旧单插件协议的版本化契约。
3
+ export const MAINTENANCE_CAPABILITY = "marketplace_codex_maintenance_v1";
4
+ export const MaintenancePlanSchema = z.object({
5
+ marketplace: z.literal("nowwork-pm"),
6
+ actions: z.tuple([z.literal("install-missing"), z.literal("update-installed")]),
7
+ hosts: z.tuple([z.literal("claude"), z.literal("codex"), z.literal("pi")]),
8
+ runner: z.object({ releaseId: z.string().regex(/^[a-f0-9]{64}$/), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict(),
9
+ }).strict();
10
+ export const MaintenanceRequestSchema = z.discriminatedUnion("action", [
11
+ z.object({ type: z.literal("marketplace:maintenance"), action: z.literal("start"), operationId: z.string().uuid(), plan: MaintenancePlanSchema }).strict(),
12
+ z.object({ type: z.literal("marketplace:maintenance"), action: z.literal("status"), operationId: z.string().uuid() }).strict(),
13
+ ]);
14
+ export const MaintenanceErrorSchema = z.enum(["runtime_busy", "execution_failed", "operation_unknown", "runner_unavailable",
15
+ "manager_missing", "cli_missing", "command_failed", "verification_failed", "source_conflict", "unsupported_platform"]);
16
+ const Version = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/).nullable();
17
+ export const MaintenanceResultSchema = z.object({
18
+ runtime: z.enum(["claude", "codex", "pi"]), plugin: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,127}$/).nullable(),
19
+ status: z.enum(["installed", "updated", "unchanged", "empty", "missing_cli", "failed"]),
20
+ beforeVersion: Version, afterVersion: Version, targetVersion: Version,
21
+ activation: z.enum(["reload_required", "not_changed", "unverified"]), errorCode: MaintenanceErrorSchema.nullable(),
22
+ }).strict();
23
+ export const MaintenanceReportSchema = z.object({
24
+ version: z.literal(2), operationId: z.string().uuid(),
25
+ status: z.enum(["queued", "running", "completed", "partial", "failed", "unknown"]),
26
+ results: z.array(MaintenanceResultSchema).max(1500), errorCode: MaintenanceErrorSchema.nullable(),
27
+ }).strict().superRefine((r, ctx) => {
28
+ const keys = r.results.map(x => `${x.runtime}:${x.plugin}`);
29
+ if (new Set(keys).size !== keys.length)
30
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Duplicate plugin result" });
31
+ for (const x of r.results) {
32
+ if (["installed", "updated", "unchanged"].includes(x.status) && (!x.plugin || !x.targetVersion || x.targetVersion !== x.afterVersion || x.errorCode)) {
33
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Successful plugin result requires a verified target version" });
34
+ }
35
+ }
36
+ if (r.status === "completed" && (r.errorCode || ["claude", "codex", "pi"].some(host => !r.results.some(x => x.runtime === host))
37
+ || r.results.some(x => x.errorCode || x.status === "failed" || x.status === "missing_cli"))) {
38
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Completion requires verified coverage of every host" });
39
+ }
40
+ });
@@ -1,7 +1,9 @@
1
+ import { runMaintenanceBridge } from "./marketplace-maintenance-bridge.js";
1
2
  import { createMarketplaceController } from "./marketplace-controller.js";
2
3
  import { prepareDaemonRestart } from "./daemon-updater.js";
3
- export function createServeMarketplace(config, localSlots, hostCoordinator) {
4
+ export function createServeMarketplace(config, localSlots, hostCoordinator, acquireEnv = async () => ({ ...process.env })) {
4
5
  const controller = createMarketplaceController({ agentsRoot: config.agentsRoot,
6
+ maintain: request => runMaintenanceBridge(config, request, acquireEnv),
5
7
  acquire: async () => {
6
8
  // Marketplace updates can mutate shared Codex/Claude homes across profiles.
7
9
  const local = await prepareDaemonRestart({ localSlots });
@@ -29,7 +31,7 @@ export function createServeMarketplace(config, localSlots, hostCoordinator) {
29
31
  } });
30
32
  return {
31
33
  handle(raw, send) {
32
- if (!raw || typeof raw !== "object" || !("type" in raw) || raw.type !== "marketplace:operation")
34
+ if (!raw || typeof raw !== "object" || !("type" in raw) || (raw.type !== "marketplace:operation" && raw.type !== "marketplace:maintenance"))
33
35
  return false;
34
36
  const { reqId, ...request } = raw;
35
37
  if (typeof reqId === "string")
@@ -40,6 +40,7 @@ export async function runPi(bin) {
40
40
  if (inputError)
41
41
  throw new Error(`Pi prompt delivery failed: ${inputError.message}`);
42
42
  const result = decoder.finish();
43
+ await emit(result.event);
43
44
  if (!result.ok) {
44
45
  process.stderr.write(`${result.error}\n`);
45
46
  return 1;
@@ -3,6 +3,23 @@ export function buildPiArgs(options) {
3
3
  ...(options.model ? ["--model", options.model] : []),
4
4
  ...(options.reasoning && options.reasoning !== "default" ? ["--thinking", options.reasoning] : [])];
5
5
  }
6
+ const TOOL_ARG_KEYS = ["command", "path", "file_path", "pattern", "url"];
7
+ function toolTitle(name, args) {
8
+ const arg = TOOL_ARG_KEYS.map(key => args?.[key]).find((value) => typeof value === "string" && !!value.trim());
9
+ return arg ? `${name}(${arg.trim()})` : name;
10
+ }
11
+ function toolResultText(result) {
12
+ return (result?.content ?? []).map(item => item.type === "text" && typeof item.text === "string" ? item.text : "")
13
+ .filter(Boolean).join("\n");
14
+ }
15
+ function addUsage(total, next) {
16
+ if (!next)
17
+ return total;
18
+ return {
19
+ input: (total?.input ?? 0) + (next.input ?? 0), output: (total?.output ?? 0) + (next.output ?? 0),
20
+ cacheRead: (total?.cacheRead ?? 0) + (next.cacheRead ?? 0), cacheWrite: (total?.cacheWrite ?? 0) + (next.cacheWrite ?? 0),
21
+ };
22
+ }
6
23
  // 保留稳定的原因标识供 server 本地化;不要用 token 数量猜测结束原因。
7
24
  function failureReason(message) {
8
25
  const reason = message?.stopReason;
@@ -15,7 +32,17 @@ export function createPiEventDecoder() {
15
32
  let lastAssistant = null;
16
33
  let assistantOpen = false;
17
34
  let sawTurnEnd = false;
35
+ let turnUsageCounted = false;
18
36
  let usage;
37
+ // 正常 stop 可为空(任务可能要求静默);length/error/aborted 或停在工具轮都必须明确失败。
38
+ const runFailure = () => error ?? (assistantOpen
39
+ ? "pi_runtime_failed: incomplete"
40
+ : failureReason(lastAssistant)
41
+ ?? (!sawTurnEnd || lastAssistant?.stopReason === "toolUse" ? "pi_runtime_failed: incomplete" : null));
42
+ const normalizedUsage = () => usage ? { usage: {
43
+ input_tokens: usage.input ?? 0, output_tokens: usage.output ?? 0,
44
+ cache_read_input_tokens: usage.cacheRead ?? 0, cache_creation_input_tokens: usage.cacheWrite ?? 0,
45
+ } } : {};
19
46
  return {
20
47
  push(raw) {
21
48
  const e = (raw ?? {});
@@ -24,8 +51,10 @@ export function createPiEventDecoder() {
24
51
  sessionId = e.id;
25
52
  out.push({ type: "thread.started", thread_id: e.id });
26
53
  }
27
- if (e.type === "turn_start")
54
+ if (e.type === "turn_start") {
28
55
  sawTurnEnd = false;
56
+ turnUsageCounted = false;
57
+ }
29
58
  if ((e.type === "message_start" || e.type === "message_update") && e.message?.role === "assistant") {
30
59
  assistantOpen = true;
31
60
  sawTurnEnd = false;
@@ -38,34 +67,39 @@ export function createPiEventDecoder() {
38
67
  if (e.type === "message_update" && ae?.type === "thinking_delta" && typeof ae.delta === "string") {
39
68
  out.push({ type: "pi.thinking_delta", text: ae.delta });
40
69
  }
41
- if (e.type === "message_update" && typeof ae?.type === "string" && /tool(call|_call)/iu.test(ae.type)) {
42
- out.push({ type: "pi.tool_call", ...(ae.name ? { title: ae.name } : {}) });
70
+ // delta 逐 token 到达,console 只消费块结束事件,避免一个 token 一行。
71
+ if (e.type === "message_update" && ae?.type === "text_end" && ae.content?.trim()) {
72
+ out.push({ type: "pi.text", text: ae.content.trim() });
73
+ }
74
+ if (e.type === "message_update" && ae?.type === "thinking_end" && ae.content?.trim()) {
75
+ out.push({ type: "pi.thinking", text: ae.content.trim() });
76
+ }
77
+ if (e.type === "tool_execution_start" && typeof e.toolName === "string") {
78
+ out.push({ type: "pi.tool_call", title: toolTitle(e.toolName, e.args) });
79
+ }
80
+ if (e.type === "tool_execution_end") {
81
+ out.push({ type: "pi.tool_result", content: toolResultText(e.result), is_error: e.isError === true });
43
82
  }
44
83
  if (e.type === "message_end" && e.message?.role === "assistant") {
45
84
  lastAssistant = e.message;
46
- usage = e.message.usage;
85
+ usage = addUsage(usage, e.message.usage);
86
+ turnUsageCounted = true;
47
87
  assistantOpen = false;
48
88
  sawTurnEnd = false;
49
89
  }
50
90
  if (e.type === "turn_end") {
51
- // 原生 turn_end 自带本轮消息,不能把工具调用轮的结束当成整次执行成功。
91
+ // turn_end 只是 Pi 内部一轮:工具调用后会继续,provider 出错还会自动重试(agent_end.willRetry)。
52
92
  if (e.message?.role === "assistant") {
53
93
  lastAssistant = e.message;
54
- usage = e.message.usage;
94
+ if (!turnUsageCounted)
95
+ usage = addUsage(usage, e.message.usage);
55
96
  assistantOpen = false;
56
97
  }
57
98
  else if (e.message) {
58
99
  lastAssistant = null;
59
100
  }
60
101
  sawTurnEnd = true;
61
- const normalizedUsage = usage ? { usage: {
62
- input_tokens: usage.input ?? 0, output_tokens: usage.output ?? 0,
63
- cache_read_input_tokens: usage.cacheRead ?? 0, cache_creation_input_tokens: usage.cacheWrite ?? 0,
64
- } } : {};
65
- const failure = failureReason(lastAssistant);
66
- out.push(failure
67
- ? { type: "result", is_error: true, result: failure, ...normalizedUsage }
68
- : { type: "turn.completed", ...normalizedUsage });
102
+ turnUsageCounted = false;
69
103
  }
70
104
  if (e.type === "error" || e.type === "agent_error") {
71
105
  error = typeof e.error === "string" ? e.error : (e.error?.message ?? "Pi error");
@@ -74,12 +108,12 @@ export function createPiEventDecoder() {
74
108
  return out;
75
109
  },
76
110
  finish() {
77
- // 正常 stop 可为空(任务可能要求静默);length/error/aborted 必须明确失败。
78
- const failure = error ?? (assistantOpen
79
- ? "pi_runtime_failed: incomplete"
80
- : failureReason(lastAssistant)
81
- ?? (!sawTurnEnd || lastAssistant?.stopReason === "toolUse" ? "pi_runtime_failed: incomplete" : null));
82
- return { ok: failure === null, ...(failure ? { error: failure } : {}), finalText, sessionId, usage };
111
+ // 终态只在 stdout 结束后产出一次:重试/中断都以最终状态为准,usage 覆盖所有已完成轮次。
112
+ const failure = runFailure();
113
+ const event = failure
114
+ ? { type: "result", is_error: true, result: failure, ...normalizedUsage() }
115
+ : { type: "turn.completed", ...normalizedUsage() };
116
+ return { ok: failure === null, ...(failure ? { error: failure } : {}), finalText, sessionId, usage, event };
83
117
  },
84
118
  };
85
119
  }
package/dist/serve.js CHANGED
@@ -122,7 +122,7 @@ export function serve(config, opts = {}) {
122
122
  catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
123
123
  },
124
124
  });
125
- const marketplace = createServeMarketplace(config, localSlots, hostCoordinator);
125
+ const marketplace = createServeMarketplace(config, localSlots, hostCoordinator, async () => ({ ...(await machineRuntime.provider.acquire()).env }));
126
126
  const storageIdentityAdmission = createStorageIdentityAdmission(opts.storageRuntime);
127
127
  const projectionCoordinator = createAgentProjectionCoordinator();
128
128
  const { projectRegistry, projectWorkspaceRegistry, projectDependencyProvisioner } = createProjectRegistryWiring(config.agentsRoot, opts.execution?.dependencies);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.99",
3
+ "version": "0.6.100",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",