@nowcrew/daemon 0.6.91-beta.1 → 0.6.91-beta.11

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.
@@ -81,7 +81,7 @@ export const DaemonAbilityManifestSchema = z.object({
81
81
  cases: z.array(SafeAbilityPathSchema).max(1_000).default([]),
82
82
  }).strict().default({ cases: [] }),
83
83
  compatibility: z.object({
84
- providers: z.array(z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness"])).min(1),
84
+ providers: z.array(z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "pi"])).min(1),
85
85
  minNowCrewVersion: z.string().regex(/^\d+\.\d+\.\d+$/u),
86
86
  }).strict().optional(),
87
87
  permissions: z.object({
package/dist/console.js CHANGED
@@ -281,17 +281,18 @@ export function toConsoleLines(event, runtime) {
281
281
  return text ? [{ stream: "tool_result", text: clip(text, TOOL_RESULT_CAP) }] : [];
282
282
  }
283
283
  if (e.type === "system" && e.subtype === "init") {
284
- return [{ stream: "system", text: `● ${td("Claude session started")}` }];
284
+ return [{ stream: "system", text: `● ${td("Claude Code session started")}` }];
285
285
  }
286
286
  if (e.type === "thread.started") {
287
287
  // thread.started is shared by Codex and ACP adapters; its shape does not
288
288
  // identify the runtime. Use the executor-owned selection, not event fields.
289
289
  const messages = {
290
- claude: "Claude session started",
291
- codex: "Codex session started",
292
- kimi: "Kimi session started",
290
+ claude: "Claude Code session started",
291
+ codex: "Codex CLI session started",
292
+ kimi: "Kimi Code session started",
293
293
  hermes: "Hermes session started",
294
294
  opencode: "OpenCode session started",
295
+ pi: "Pi session started",
295
296
  "deepseek-harness": "DeepSeek Harness session started",
296
297
  };
297
298
  const message = runtime && Object.hasOwn(messages, runtime)
@@ -1,3 +1,4 @@
1
+ import { dslog } from "./slog.js";
1
2
  import { z } from "zod";
2
3
  import { isDaemonReleaseVersion } from "./daemon-release-version.js";
3
4
  const DaemonUpdateMessageSchema = z.object({
@@ -42,15 +43,16 @@ export function createDaemonUpdateController(deps) {
42
43
  failed(message.updateId, preparation.errorCode);
43
44
  return;
44
45
  }
45
- let released = false;
46
- const releaseOnce = async () => {
47
- if (released)
48
- return;
49
- released = true;
50
- await preparation.release();
46
+ let releaseTask;
47
+ const releaseOnce = () => {
48
+ releaseTask ??= preparation.release().catch(error => {
49
+ releaseTask = undefined;
50
+ throw error;
51
+ });
52
+ return releaseTask;
51
53
  };
52
- deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "restarting" });
53
54
  try {
55
+ deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "restarting" });
54
56
  const handoff = deps.scheduleRestart(eligibility.serviceSpec);
55
57
  const state = {
56
58
  release: releaseOnce,
@@ -92,7 +94,13 @@ export function createDaemonUpdateController(deps) {
92
94
  failed(message.updateId, "update_in_progress");
93
95
  return true;
94
96
  }
95
- const done = execute(message).finally(() => {
97
+ const done = execute(message).catch(error => {
98
+ dslog("daemon.update_failed", "Daemon 更新或重启准备失败", {
99
+ level: "ERROR", update_id: message.updateId, operation: message.type,
100
+ error_message: error instanceof Error ? error.message : String(error),
101
+ });
102
+ failed(message.updateId, message.type === "daemon:restart" ? "restart_failed" : "install_failed");
103
+ }).finally(() => {
96
104
  handled.add(message.updateId);
97
105
  if (running?.id === message.updateId)
98
106
  running = null;
@@ -1,3 +1,4 @@
1
+ import { dslog } from "./slog.js";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { resolve } from "node:path";
3
4
  import { systemCommandRunner } from "./computer-service.js";
@@ -17,6 +18,8 @@ export async function prepareDaemonRestart(input) {
17
18
  const local = input.localSlots.tryAcquireExclusive();
18
19
  if (local === null)
19
20
  return { ok: false, errorCode: "runtime_busy" };
21
+ const acquiredAt = Date.now();
22
+ dslog("daemon.maintenance_barrier_acquired", "Daemon 已暂停接收执行,准备维护");
20
23
  let released = false;
21
24
  return {
22
25
  ok: true,
@@ -25,6 +28,7 @@ export async function prepareDaemonRestart(input) {
25
28
  return;
26
29
  released = true;
27
30
  local.release();
31
+ dslog("daemon.maintenance_barrier_released", "维护屏障已释放", { held_ms: Date.now() - acquiredAt });
28
32
  },
29
33
  };
30
34
  }
@@ -67,16 +71,10 @@ export async function installExactDaemonUpdate(input) {
67
71
  || installation.npmPrefix !== input.npmPrefix) {
68
72
  return { ok: false, errorCode: "ineligible" };
69
73
  }
70
- const local = input.localSlots.tryAcquireExclusive();
71
- if (local === null)
72
- return { ok: false, errorCode: "runtime_busy" };
73
- let released = false;
74
- const release = async () => {
75
- if (released)
76
- return;
77
- released = true;
78
- local.release();
79
- };
74
+ const barrier = await prepareDaemonRestart(input);
75
+ if (!barrier.ok)
76
+ return barrier;
77
+ const release = barrier.release;
80
78
  const lockedInstallation = daemonGlobalInstallation(input.currentEntryPath ?? process.argv[1] ?? "", input.platform ?? process.platform);
81
79
  if (lockedInstallation === null
82
80
  || lockedInstallation.packageRoot !== input.packageRoot
@@ -6,6 +6,7 @@ const RUNTIMES = [
6
6
  "hermes",
7
7
  "opencode",
8
8
  "deepseek-harness",
9
+ "pi",
9
10
  ];
10
11
  function snapshotFields(snapshot, tracked) {
11
12
  const runtimeFields = {};
@@ -8,7 +8,7 @@ import { provePosixProcessGroupStopped } from "./execution-posix-stop-proof.js";
8
8
  export { JournalLockedError, JournalLockCorruptionError } from "./execution-journal-lock.js";
9
9
  const ExecutionIdSchema = z.string().uuid();
10
10
  const TimestampSchema = z.string().datetime({ offset: true });
11
- const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness"]);
11
+ const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "pi"]);
12
12
  const RUNTIME_READY_SUFFIX = ".runtime-ready";
13
13
  const RawJournalEntrySchema = z.object({
14
14
  executionId: ExecutionIdSchema,
@@ -13,7 +13,7 @@ const ExitCodeSchema = z.number().int().min(MIN_SIGNED_32_INTEGER).max(MAX_PG_IN
13
13
  const ProviderErrorCategorySchema = z.enum(["auth", "quota", "rate_limit", "timeout", "network", "upstream", "cli", "unknown"]);
14
14
  const ProviderHttpStatusSchema = z.number().int().min(100).max(599);
15
15
  const ProviderErrorCodeSchema = z.string().min(1).max(128).regex(/^[A-Za-z0-9_.:-]+$/u);
16
- const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness"]);
16
+ const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "pi"]);
17
17
  const UnsafePathCharacterSchema = /[\p{Cc}<>:"/\\|?*]/u;
18
18
  const WindowsReservedNameSchema = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
19
19
  export const AgentHandleSchema = z.string().min(1).max(64).refine((handle) => handle !== "."
@@ -285,11 +285,12 @@ function admission(spec, config, dependencies, at) {
285
285
  return { rejected: rejection(spec.executionId, "resource_limit", "Local execution capacity is exhausted", at) };
286
286
  }
287
287
  const permission = effectivePermission(spec, config);
288
- if ((spec.runtime.name === "kimi" || spec.runtime.name === "hermes" || spec.runtime.name === "opencode")
288
+ if ((spec.runtime.name === "kimi" || spec.runtime.name === "hermes" || spec.runtime.name === "opencode" || spec.runtime.name === "pi")
289
289
  && permission !== "full_access") {
290
290
  const displayName = spec.runtime.name === "kimi"
291
291
  ? "Kimi"
292
- : spec.runtime.name === "hermes" ? "Hermes" : "OpenCode";
292
+ : spec.runtime.name === "hermes" ? "Hermes"
293
+ : spec.runtime.name === "pi" ? "Pi" : "OpenCode";
293
294
  return {
294
295
  rejected: rejection(spec.executionId, "local_policy_denied", `${displayName} cannot enforce ${permission} permission`, at),
295
296
  };
@@ -212,7 +212,8 @@ export function decodeExternalOutputEvent(runtime, event, decoder) {
212
212
  }
213
213
  if ((runtime === "hermes" && candidate.type === "hermes.acp.text_delta")
214
214
  || (runtime === "deepseek-harness" && candidate.type === "deepseek-harness.acp.text_delta")
215
- || (runtime === "opencode" && candidate.type === "opencode.text_delta")) {
215
+ || (runtime === "opencode" && candidate.type === "opencode.text_delta")
216
+ || (runtime === "pi" && candidate.type === "pi.text_delta")) {
216
217
  return typeof candidate.text === "string" ? decoder.push(candidate.text) : [];
217
218
  }
218
219
  return candidate.type === undefined
@@ -286,13 +286,19 @@ export function createHostExecutionCoordinator(options = {}) {
286
286
  ...Array.from({ length: executionSlots }, (_, slot) => join(root, "slots", String(slot))),
287
287
  join(root, "memory-prune-slots", "0"),
288
288
  ];
289
- for (const target of targets) {
290
- const lease = await acquireLease(target);
291
- if (lease === null) {
292
- await retryRelease("exclusive_execution_partial", () => Promise.all(leases.map((owned) => owned.close())).then(() => undefined));
293
- return null;
289
+ try {
290
+ for (const target of targets) {
291
+ const lease = await acquireLease(target);
292
+ if (lease === null) {
293
+ await retryRelease("exclusive_execution_partial", () => Promise.all(leases.map((owned) => owned.close())).then(() => undefined));
294
+ return null;
295
+ }
296
+ leases.push(lease);
294
297
  }
295
- leases.push(lease);
298
+ }
299
+ catch (error) {
300
+ await retryRelease("exclusive_execution_partial", () => Promise.all(leases.map(owned => owned.close())).then(() => undefined));
301
+ throw error;
296
302
  }
297
303
  let releasePromise = null;
298
304
  return {
package/dist/i18n.js CHANGED
@@ -16,12 +16,13 @@ export function detectDaemonLang(env = process.env) {
16
16
  ?? "en";
17
17
  }
18
18
  const zh = {
19
- "Claude session started": "Claude 会话启动",
20
- "Codex session started": "Codex 会话启动",
21
- "Kimi session started": "Kimi 会话启动",
19
+ "Claude Code session started": "Claude Code 会话启动",
20
+ "Codex CLI session started": "Codex CLI 会话启动",
21
+ "Kimi Code session started": "Kimi Code 会话启动",
22
22
  "Hermes session started": "Hermes 会话启动",
23
23
  "OpenCode session started": "OpenCode 会话启动",
24
24
  "DeepSeek Harness session started": "DeepSeek Harness 会话启动",
25
+ "Pi session started": "Pi 会话启动",
25
26
  "Session started": "会话启动",
26
27
  "Plan updated": "计划更新",
27
28
  "Thinking (~{{tokens}} tokens, content not streamed by Claude Code)": "思考(约 {{tokens}} tokens,Claude Code 未输出思考正文)",
@@ -89,7 +89,7 @@ export function awaitExit(child) {
89
89
  }
90
90
  export function exitActivity(runtime, exitCode, stderrTail) {
91
91
  if ((runtime === "codex" || runtime === "kimi" || runtime === "hermes"
92
- || runtime === "opencode" || runtime === "deepseek-harness" || exitCode === -1) && exitCode !== 0) {
92
+ || runtime === "opencode" || runtime === "deepseek-harness" || runtime === "pi" || exitCode === -1) && exitCode !== 0) {
93
93
  return {
94
94
  kind: "error",
95
95
  label: "运行出错",
@@ -97,7 +97,7 @@ export function exitActivity(runtime, exitCode, stderrTail) {
97
97
  };
98
98
  }
99
99
  if ((runtime === "kimi" || runtime === "hermes" || runtime === "opencode"
100
- || runtime === "deepseek-harness") && exitCode === 0) {
100
+ || runtime === "deepseek-harness" || runtime === "pi") && exitCode === 0) {
101
101
  return { kind: "done", label: "本轮结束" };
102
102
  }
103
103
  return null;
@@ -31,6 +31,7 @@ export function createMachineEnvironmentProvider(options = {}) {
31
31
  let lastFailureTime = -Infinity;
32
32
  let stopped = false;
33
33
  let unsafeCleanup = false;
34
+ let isCleanupComplete;
34
35
  let flight;
35
36
  let controller;
36
37
  let captureEpoch = 0;
@@ -50,8 +51,19 @@ export function createMachineEnvironmentProvider(options = {}) {
50
51
  return module.captureWindowsMachineEnvironment(request);
51
52
  });
52
53
  const refresh = async (force, signal) => {
53
- if (!selected.source || stopped || unsafeCleanup)
54
+ if (!selected.source || stopped)
54
55
  return snapshot;
56
+ if (unsafeCleanup) {
57
+ try {
58
+ if (!isCleanupComplete?.())
59
+ return snapshot;
60
+ }
61
+ catch {
62
+ return snapshot;
63
+ }
64
+ unsafeCleanup = false;
65
+ isCleanupComplete = undefined;
66
+ }
55
67
  if (!force && now() - lastFailureTime < (options.backoffMs ?? 30_000))
56
68
  return snapshot;
57
69
  const attemptedAt = new Date(now()).toISOString();
@@ -64,7 +76,7 @@ export function createMachineEnvironmentProvider(options = {}) {
64
76
  return snapshot;
65
77
  publish(snapshot.env, { state: "loading", attemptedAt, errorCode: null });
66
78
  capturing = true;
67
- const deadline = Date.now() + (options.captureTimeoutMs ?? (selected.source === "windows-registry" ? 8_000 : 5_000));
79
+ const deadline = Date.now() + (options.captureTimeoutMs ?? (selected.source === "windows-registry" ? 8_000 : 10_000));
68
80
  const capture = selected.source === "windows-registry" ? captureWindows : options.captureShell ?? captureShellMachineEnvironment;
69
81
  for (let attempt = 0; attempt < 2; attempt++) {
70
82
  try {
@@ -111,8 +123,10 @@ export function createMachineEnvironmentProvider(options = {}) {
111
123
  }
112
124
  catch (error) {
113
125
  const errorCode = error instanceof MachineEnvironmentCaptureError ? error.code : selected.source === "windows-registry" ? "windows_environment_unavailable" : "shell_capture_failed";
114
- if (errorCode === "shell_cleanup_failed")
126
+ if (errorCode === "shell_cleanup_failed") {
115
127
  unsafeCleanup = true;
128
+ isCleanupComplete = error instanceof MachineEnvironmentCaptureError ? error.isCleanupComplete : undefined;
129
+ }
116
130
  lastFailureTime = now();
117
131
  return publish(lastGood?.env ?? baseline, { state: lastGood ? "stale" : "fallback", errorCode, attemptedAt });
118
132
  }
@@ -129,7 +143,7 @@ export function createMachineEnvironmentProvider(options = {}) {
129
143
  }
130
144
  return flight;
131
145
  }
132
- if (stopped || unsafeCleanup || selected.source === null)
146
+ if (stopped || selected.source === null)
133
147
  return Promise.resolve(snapshot);
134
148
  controller = new AbortController();
135
149
  flight = refresh(acquireOptions?.force === true, controller.signal).finally(() => { flight = undefined; controller = undefined; });
@@ -1,8 +1,10 @@
1
1
  export class MachineEnvironmentCaptureError extends Error {
2
2
  code;
3
- constructor(code) {
3
+ isCleanupComplete;
4
+ constructor(code, isCleanupComplete) {
4
5
  super(code);
5
6
  this.code = code;
7
+ this.isCleanupComplete = isCleanupComplete;
6
8
  this.name = "MachineEnvironmentCaptureError";
7
9
  }
8
10
  }
@@ -44,7 +44,11 @@ export async function runMachineEnvironmentProcess(options) {
44
44
  stdio[collectFd] = "pipe";
45
45
  if (options.metadataFd !== undefined)
46
46
  stdio[options.metadataFd] = "pipe";
47
- const child = spawn(options.file, [...options.args], { cwd: options.cwd, env: options.env, detached: true, stdio });
47
+ // launchd Background 的 I/O 策略会让 shell 初始化停滞;只调整短命采集子进程。
48
+ // taskpolicy 原地 exec,保持同一 PID/进程组、环境和私有管道。
49
+ const file = process.platform === "darwin" ? "/usr/sbin/taskpolicy" : options.file;
50
+ const args = process.platform === "darwin" ? ["-a", "--", options.file, ...options.args] : [...options.args];
51
+ const child = spawn(file, args, { cwd: options.cwd, env: options.env, detached: true, stdio });
48
52
  const chunks = [];
49
53
  let size = 0;
50
54
  const metadata = [];
@@ -102,6 +106,22 @@ export async function runMachineEnvironmentProcess(options) {
102
106
  try {
103
107
  await cleanupOnce();
104
108
  }
109
+ catch (error) {
110
+ const pid = child.pid;
111
+ if (error instanceof MachineEnvironmentCaptureError && error.code === "shell_cleanup_failed" && pid !== undefined) {
112
+ // 后续只验证旧进程组消失,不对可能复用的 PID 再发送终止信号。
113
+ throw new MachineEnvironmentCaptureError("shell_cleanup_failed", () => {
114
+ try {
115
+ process.kill(-pid, 0);
116
+ return false;
117
+ }
118
+ catch (cause) {
119
+ return cause.code === "ESRCH";
120
+ }
121
+ });
122
+ }
123
+ throw error;
124
+ }
105
125
  finally {
106
126
  for (const stream of child.stdio)
107
127
  stream?.destroy();
package/dist/normalize.js CHANGED
@@ -37,7 +37,8 @@ export function extractFinalText(event) {
37
37
  const e = (event ?? {});
38
38
  if ((e.type === "kimi.acp.text_delta" || e.type === "hermes.acp.text_delta"
39
39
  || e.type === "deepseek-harness.acp.text_delta"
40
- || e.type === "opencode.text_delta") && e.text)
40
+ || e.type === "opencode.text_delta"
41
+ || e.type === "pi.text_delta") && e.text)
41
42
  return e.text;
42
43
  if (e.type === "result" && !e.is_error && e.result?.trim())
43
44
  return e.result.trim();
@@ -70,15 +71,18 @@ export function normalizeEvent(event) {
70
71
  }
71
72
  if ((e.type === "kimi.acp.text_delta" || e.type === "hermes.acp.text_delta"
72
73
  || e.type === "deepseek-harness.acp.text_delta"
73
- || e.type === "opencode.text_delta") && e.text?.trim()) {
74
+ || e.type === "opencode.text_delta"
75
+ || e.type === "pi.text_delta") && e.text?.trim()) {
74
76
  return [{ kind: "text", label: "思考/说明", detail: e.text }];
75
77
  }
76
78
  if (e.type === "kimi.acp.tool_call" || e.type === "hermes.acp.tool_call"
77
- || e.type === "deepseek-harness.acp.tool_call" || e.type === "opencode.tool_call") {
79
+ || e.type === "deepseek-harness.acp.tool_call" || e.type === "opencode.tool_call"
80
+ || e.type === "pi.tool_call") {
78
81
  return [{ kind: "tool", label: e.title ? `工具:${e.title}` : "工具调用" }];
79
82
  }
80
83
  if (e.type === "kimi.acp.tool_result" || e.type === "hermes.acp.tool_result"
81
- || e.type === "deepseek-harness.acp.tool_result" || e.type === "opencode.tool_result") {
84
+ || e.type === "deepseek-harness.acp.tool_result" || e.type === "opencode.tool_result"
85
+ || e.type === "pi.tool_result") {
82
86
  return [{ kind: "tool_result", label: "工具返回" }];
83
87
  }
84
88
  if (e.type === "system" && e.subtype === "init") {
@@ -97,7 +101,7 @@ export function normalizeEvent(event) {
97
101
  if (e.type === "turn.completed") {
98
102
  return [{ kind: "done", label: "本轮结束" }];
99
103
  }
100
- if (e.type === "opencode.error") {
104
+ if (e.type === "opencode.error" || e.type === "pi.error") {
101
105
  const detail = typeof event.message === "string"
102
106
  ? event.message
103
107
  : undefined;
@@ -2,7 +2,7 @@ import { ExecutionCompletedSchema, ExecutionRejectedSchema } from "./execution-p
2
2
  import { hashExecutionSpec } from "./execution-runner.js";
3
3
  import { dslog } from "./slog.js";
4
4
  /** Queue rejection is admission work too: own the ID before any asynchronous journal write. */
5
- export async function rejectQueuedAdmission(context, spec, executionClass, facts) {
5
+ export async function rejectQueuedAdmission(context, spec, executionClass, facts, rejectionReason = "machine_queue_full") {
6
6
  const id = spec.executionId;
7
7
  const hash = hashExecutionSpec(spec);
8
8
  if (context.knownExecutionHashes.has(id) || context.executionRuns.has(id)) {
@@ -16,10 +16,11 @@ export async function rejectQueuedAdmission(context, spec, executionClass, facts
16
16
  context.knownExecutionHashes.set(id, hash);
17
17
  const rejected = ExecutionRejectedSchema.parse({
18
18
  type: "execution:rejected", protocolVersion: 1, executionId: id,
19
- reason: "resource_limit", message: "Local machine execution queue is full", at: new Date().toISOString(),
19
+ reason: "resource_limit", message: rejectionReason === "maintenance" ? "Local machine is temporarily unavailable for maintenance"
20
+ : rejectionReason === "agent_queue_full" ? "Local agent execution queue is full" : "Local machine execution queue is full", at: new Date().toISOString(),
20
21
  });
21
- dslog("execution.machine_queue_rejected", "机器执行队列已满", {
22
- level: "WARN", execution_id: id, agent_handle: spec.agent.handle, execution_class: executionClass, ...facts,
22
+ dslog("execution.machine_queue_rejected", rejectionReason === "maintenance" ? "机器维护期间暂停接收执行" : "执行等待队列已满", {
23
+ level: "WARN", execution_id: id, agent_handle: spec.agent.handle, execution_class: executionClass, admission_reason: rejectionReason, ...facts,
23
24
  });
24
25
  const rejecting = persistPreAdmissionRejection(context.journal, spec, rejected)
25
26
  .then(async (entry) => {
@@ -1,6 +1,6 @@
1
1
  import { ATTACHMENT_NATIVE_IMAGE_MAX_BYTES } from "./attachment-limits.js";
2
2
  export const LOCAL_EXECUTION_RUNTIMES = [
3
- "claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness",
3
+ "claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "pi",
4
4
  ];
5
5
  export const LOCAL_RUNTIME_CAPABILITIES = Object.freeze({
6
6
  claude: Object.freeze({
@@ -33,6 +33,7 @@ export const LOCAL_RUNTIME_CAPABILITIES = Object.freeze({
33
33
  nativeResume: false,
34
34
  systemPromptTransport: "protocol",
35
35
  }),
36
+ pi: Object.freeze({ transport: "pi-json", nativeResume: false, systemPromptTransport: "protocol" }),
36
37
  });
37
38
  export function runtimeCapability(runtime) {
38
39
  return LOCAL_RUNTIME_CAPABILITIES[runtime];
@@ -95,6 +95,7 @@ export function createRuntimeStartupGate(limits, now = Date.now) {
95
95
  kimi: activeByRuntimeAndLane.get(activeKey("kimi", "foreground")) ?? 0,
96
96
  hermes: activeByRuntimeAndLane.get(activeKey("hermes", "foreground")) ?? 0,
97
97
  opencode: activeByRuntimeAndLane.get(activeKey("opencode", "foreground")) ?? 0,
98
+ pi: activeByRuntimeAndLane.get(activeKey("pi", "foreground")) ?? 0,
98
99
  "deepseek-harness": activeByRuntimeAndLane.get(activeKey("deepseek-harness", "foreground")) ?? 0,
99
100
  },
100
101
  }),
@@ -0,0 +1,28 @@
1
+ import { once } from "node:events";
2
+ import { createInterface } from "node:readline";
3
+ import { parseArgs } from "node:util";
4
+ import { pathToFileURL } from "node:url";
5
+ import spawn from "cross-spawn";
6
+ import { augmentedPath } from "../runtime-path.js";
7
+ import { buildPiArgs, createPiEventDecoder } from "./pi.js";
8
+ async function readInput() { process.stdin.setEncoding("utf8"); let text = ""; for await (const chunk of process.stdin)
9
+ text += String(chunk); return JSON.parse(text); }
10
+ async function emit(event) { if (process.stdout.write(`${JSON.stringify(event)}\n`))
11
+ return; await once(process.stdout, "drain"); }
12
+ export async function runPi(bin) { const input = await readInput(); const child = spawn(bin, buildPiArgs(input), { cwd: process.cwd(), env: { ...process.env, PATH: augmentedPath(), PWD: process.cwd() }, stdio: ["ignore", "pipe", "pipe"] }); if (!child.stdout || !child.stderr)
13
+ throw new Error("Pi process did not expose stdio"); child.stderr.pipe(process.stderr, { end: false }); const exit = once(child, "close"); const decoder = createPiEventDecoder(); for await (const line of createInterface({ input: child.stdout })) {
14
+ try {
15
+ for (const event of decoder.push(JSON.parse(line)))
16
+ await emit(event);
17
+ }
18
+ catch { }
19
+ } const [code, signal] = await exit; const result = decoder.finish(); if (!result.ok) {
20
+ process.stderr.write(`${result.error}\n`);
21
+ return 1;
22
+ } return code === 0 ? 0 : (code ?? (signal ? 128 : 1)); }
23
+ const { values } = parseArgs({ args: process.argv.slice(2), options: { bin: { type: "string" } } });
24
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
25
+ if (!values.bin)
26
+ throw new Error("--bin is required");
27
+ runPi(values.bin).then(code => { process.exitCode = code; }).catch(error => { process.stderr.write(`Pi runner failed: ${String(error)}\n`); process.exitCode = 1; });
28
+ }
@@ -0,0 +1,17 @@
1
+ export function buildPiArgs(options) { return ["--mode", "json", "--print", "--no-session", "--system-prompt", options.systemPrompt, ...(options.model ? ["--model", options.model] : []), ...(options.reasoning && options.reasoning !== "default" ? ["--thinking", options.reasoning] : []), options.wakePrompt]; }
2
+ export function createPiEventDecoder() { let sessionId = null; let finalText = ""; let error = null; let sawTurnEnd = false; let usage; return { push(raw) { const e = (raw ?? {}); const out = []; if (e.type === "session" && typeof e.id === "string") {
3
+ sessionId = e.id;
4
+ out.push({ type: "thread.started", thread_id: e.id });
5
+ } const ae = e.assistantMessageEvent; if (e.type === "message_update" && ae?.type === "text_delta" && typeof ae.delta === "string") {
6
+ finalText += ae.delta;
7
+ out.push({ type: "pi.text_delta", text: ae.delta });
8
+ } if (e.type === "message_update" && typeof ae?.type === "string" && /tool(call|_call)/iu.test(ae.type))
9
+ out.push({ type: "pi.tool_call", ...(ae.name ? { title: ae.name } : {}) }); if (e.type === "message_end" && e.message?.role === "assistant")
10
+ usage = e.message.usage; if (e.type === "turn_end") {
11
+ sawTurnEnd = true;
12
+ const u = usage;
13
+ out.push({ type: "turn.completed", ...(u ? { usage: { input_tokens: u.input ?? 0, output_tokens: u.output ?? 0, cache_read_input_tokens: u.cacheRead ?? 0, cache_creation_input_tokens: u.cacheWrite ?? 0 } } : {}) });
14
+ } if (e.type === "error" || e.type === "agent_error") {
15
+ error = typeof e.error === "string" ? e.error : (e.error?.message ?? "Pi error");
16
+ out.push({ type: "pi.error", message: error });
17
+ } return out; }, finish() { return { ok: error === null && sawTurnEnd, ...(error === null && sawTurnEnd ? {} : { error: error ?? "Pi stream ended without turn completion" }), finalText, sessionId, usage }; } }; }
package/dist/serve.js CHANGED
@@ -488,7 +488,7 @@ export function serve(config, opts = {}) {
488
488
  let reservation = sharedSlots.reserve(spec.agent.handle, "execution", executionClass);
489
489
  if (!reservation.accepted) {
490
490
  await rejectQueuedAdmission({ journal: executionJournal, knownExecutionHashes, executionRuns,
491
- report: reportExecutionFrame, replay: sendJournalStatus }, spec, executionClass, reservation.facts);
491
+ report: reportExecutionFrame, replay: sendJournalStatus }, spec, executionClass, reservation.facts, reservation.rejectionReason);
492
492
  return;
493
493
  }
494
494
  knownExecutionHashes.set(spec.executionId, hash);
@@ -59,6 +59,7 @@ export function createSharedSlotManager(limits) {
59
59
  if (exclusive) {
60
60
  return {
61
61
  accepted: false,
62
+ rejectionReason: "maintenance",
62
63
  facts,
63
64
  ready: Promise.resolve(),
64
65
  isQueued: () => false,
@@ -77,6 +78,7 @@ export function createSharedSlotManager(limits) {
77
78
  || laneQueuedTotal >= limits.maxQueuedTotal)) {
78
79
  return {
79
80
  accepted: false,
81
+ rejectionReason: queuedForAgent >= limits.maxQueuedPerAgent ? "agent_queue_full" : "machine_queue_full",
80
82
  facts,
81
83
  ready: Promise.resolve(),
82
84
  isQueued: () => false,
@@ -61,6 +61,26 @@ export function supervisorLaunch(request) {
61
61
  }),
62
62
  };
63
63
  }
64
+ if (request.runtime === "pi") {
65
+ if (request.effectivePermission !== "full_access") {
66
+ throw new Error(`Pi cannot enforce ${request.effectivePermission} permission`);
67
+ }
68
+ return {
69
+ command: process.execPath,
70
+ args: [
71
+ fileURLToPath(new URL("./runtimes/pi-runner.js", import.meta.url)),
72
+ "--bin", request.bin,
73
+ ],
74
+ cwd: request.cwd,
75
+ env: request.env,
76
+ stdinText: JSON.stringify({
77
+ systemPrompt: request.systemPrompt,
78
+ wakePrompt: request.wakePrompt,
79
+ ...(request.model === undefined ? {} : { model: request.model }),
80
+ ...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
81
+ }),
82
+ };
83
+ }
64
84
  if (request.runtime === "opencode") {
65
85
  if (request.effectivePermission !== "full_access") {
66
86
  throw new Error(`OpenCode cannot enforce ${request.effectivePermission} permission`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.91-beta.1",
3
+ "version": "0.6.91-beta.11",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",