@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.
- package/dist/agent-ability/types.js +1 -1
- package/dist/console.js +5 -4
- package/dist/daemon-update-controller.js +16 -8
- package/dist/daemon-updater.js +8 -10
- package/dist/execution-concurrency-telemetry.js +1 -0
- package/dist/execution-journal.js +1 -1
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +3 -2
- package/dist/external-output.js +2 -1
- package/dist/host-execution-coordinator.js +12 -6
- package/dist/i18n.js +4 -3
- package/dist/local-executor.js +2 -2
- package/dist/machine-environment-cache.js +18 -4
- package/dist/machine-environment-contract.js +3 -1
- package/dist/machine-environment-process.js +21 -1
- package/dist/normalize.js +9 -5
- package/dist/pre-admission-rejection.js +5 -4
- package/dist/runtime-capabilities.js +2 -1
- package/dist/runtime-startup-gate.js +1 -0
- package/dist/runtimes/pi-runner.js +28 -0
- package/dist/runtimes/pi.js +17 -0
- package/dist/serve.js +1 -1
- package/dist/shared-execution-slots.js +2 -0
- package/dist/supervised-runtime.js +20 -0
- package/package.json +1 -1
|
@@ -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
|
|
46
|
-
const releaseOnce =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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).
|
|
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;
|
package/dist/daemon-updater.js
CHANGED
|
@@ -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
|
|
71
|
-
if (
|
|
72
|
-
return
|
|
73
|
-
|
|
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
|
|
@@ -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 !== "."
|
package/dist/execution-runner.js
CHANGED
|
@@ -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"
|
|
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
|
};
|
package/dist/external-output.js
CHANGED
|
@@ -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
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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 未输出思考正文)",
|
package/dist/local-executor.js
CHANGED
|
@@ -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
|
|
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 :
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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
|
|
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`);
|