@nowcrew/daemon 0.6.98-beta.6 → 0.6.98-beta.7
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/README.md +9 -4
- package/dist/computer-service.js +30 -5
- package/dist/console.js +17 -2
- package/dist/machine-info.js +3 -1
- package/dist/marketplace-controller.js +21 -8
- package/dist/marketplace-maintenance-bridge.js +79 -0
- package/dist/marketplace-maintenance-protocol.js +40 -0
- package/dist/marketplace-serve.js +4 -2
- package/dist/runtimes/pi-runner.js +1 -0
- package/dist/runtimes/pi.js +54 -20
- package/dist/serve.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -68,10 +68,15 @@ on stderr and the final result on stdout. On Windows shells that block npm.ps1/n
|
|
|
68
68
|
the package entry; the setup arguments and workflow are unchanged.
|
|
69
69
|
|
|
70
70
|
Deployment requires `CREW_PUBLIC_URL` (or `CREW_SERVER_URL`) and
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
a compatible published installer (beta >= 0.6.98-beta.5; stable >= 0.6.99).
|
|
72
|
+
`CREW_COMPUTER_TOKEN_SETUP_VERSION` is an optional exact-version pin, not a feature switch.
|
|
73
|
+
When unset or blank, the server selects the latest beta for dev or stable for production from its refreshed release catalog.
|
|
74
|
+
Both the two-step installation command and the one-step npx command pin that exact version.
|
|
75
|
+
An explicit pin must be published, compatible and in the same channel; invalid pins do not fall back.
|
|
76
|
+
The command keeps `--machine` for target validation and `--setup-token` for the short-lived, one-time preapproval.
|
|
77
|
+
The server checks its published release catalog and channel before issuing a Token command. If no
|
|
78
|
+
compatible published version is available, it returns `computer_token_setup_unavailable` instead of
|
|
79
|
+
generating a command for an unsupported package. Existing clients retain browser setup.
|
|
75
80
|
|
|
76
81
|
## Browser-Approved Computer Setup (Compatibility)
|
|
77
82
|
|
package/dist/computer-service.js
CHANGED
|
@@ -12,6 +12,8 @@ import { powershellCommandArgs } from "./powershell-command.js";
|
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
const WINDOWS_STOP_TIMEOUT_MS = 5_000;
|
|
14
14
|
const WINDOWS_STOP_POLL_MS = 100;
|
|
15
|
+
const DARWIN_STOP_TIMEOUT_MS = 30_000;
|
|
16
|
+
const DARWIN_STOP_POLL_MS = 100;
|
|
15
17
|
/** Windows cannot spawn .cmd/.bat shims directly with shell:false; route them through cmd.exe explicitly. */
|
|
16
18
|
export function systemCommandInvocation(command, args, platform = process.platform, comSpec = process.env.ComSpec ?? process.env.COMSPEC ?? "cmd.exe") {
|
|
17
19
|
if (platform === "win32" && /\.(?:cmd|bat)$/iu.test(command)) {
|
|
@@ -186,6 +188,32 @@ async function checked(runner, command, args, action) {
|
|
|
186
188
|
}
|
|
187
189
|
return result;
|
|
188
190
|
}
|
|
191
|
+
async function stopLaunchdService(spec, initial, runner) {
|
|
192
|
+
const assertQueryable = (result) => {
|
|
193
|
+
// launchctl uses 113 for an absent service. Other failures cannot prove it stopped.
|
|
194
|
+
if (result.exitCode !== 0 && result.exitCode !== 113) {
|
|
195
|
+
throw new Error(`launchd status failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`}`);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
assertQueryable(initial);
|
|
199
|
+
if (initial.exitCode === 113)
|
|
200
|
+
return;
|
|
201
|
+
const target = `${spec.managerDomain}/${spec.id}`;
|
|
202
|
+
await checked(runner, "launchctl", ["bootout", target], "launchd stop");
|
|
203
|
+
const deadline = Date.now() + DARWIN_STOP_TIMEOUT_MS;
|
|
204
|
+
while (true) {
|
|
205
|
+
const current = await runner("launchctl", ["print", target]);
|
|
206
|
+
assertQueryable(current);
|
|
207
|
+
if (current.exitCode === 113)
|
|
208
|
+
return;
|
|
209
|
+
// bootout returns while the job is SIGTERMed and still owns its installation lease.
|
|
210
|
+
// Wait for removal, not merely a non-running state, before installing or bootstrapping.
|
|
211
|
+
if (Date.now() >= deadline) {
|
|
212
|
+
throw new Error(`launchd service '${spec.id}' did not unload within ${DARWIN_STOP_TIMEOUT_MS}ms`);
|
|
213
|
+
}
|
|
214
|
+
await new Promise((resolve) => setTimeout(resolve, DARWIN_STOP_POLL_MS));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
189
217
|
function sameWindowsPath(left, right) {
|
|
190
218
|
return left !== undefined
|
|
191
219
|
&& left.replaceAll("/", "\\").toLowerCase() === right.replaceAll("/", "\\").toLowerCase();
|
|
@@ -356,9 +384,7 @@ export async function uninstallService(spec, runner = systemCommandRunner) {
|
|
|
356
384
|
}
|
|
357
385
|
if (spec.platform === "darwin") {
|
|
358
386
|
const status = await serviceStatus(spec, runner);
|
|
359
|
-
|
|
360
|
-
await checked(runner, "launchctl", ["bootout", `${spec.managerDomain}/${spec.id}`], "launchd bootout");
|
|
361
|
-
}
|
|
387
|
+
await stopLaunchdService(spec, status, runner);
|
|
362
388
|
}
|
|
363
389
|
else {
|
|
364
390
|
const disable = await runner("systemctl", ["--user", "disable", "--now", spec.id]);
|
|
@@ -385,8 +411,7 @@ export async function serviceAction(spec, action, runner = systemCommandRunner)
|
|
|
385
411
|
await checked(runner, "launchctl", ["bootstrap", spec.managerDomain, spec.descriptorPath], "launchd start");
|
|
386
412
|
}
|
|
387
413
|
else if (action === "stop") {
|
|
388
|
-
|
|
389
|
-
await checked(runner, "launchctl", ["bootout", target], "launchd stop");
|
|
414
|
+
await stopLaunchdService(spec, status, runner);
|
|
390
415
|
}
|
|
391
416
|
else {
|
|
392
417
|
if (status.loaded)
|
package/dist/console.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* 会附带结构化 payload(diff/命令/todo 清单/计划…),供前端做富渲染(红绿 diff、清单勾选);
|
|
10
10
|
* 前端不识别 payload.kind 时降级为纯文本行,协议向后兼容。
|
|
11
11
|
*
|
|
12
|
-
*
|
|
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
|
}
|
package/dist/machine-info.js
CHANGED
|
@@ -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, {
|
|
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, {
|
|
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,
|
|
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;
|
package/dist/runtimes/pi.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
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
|
-
//
|
|
91
|
+
// turn_end 只是 Pi 内部一轮:工具调用后会继续,provider 出错还会自动重试(agent_end.willRetry)。
|
|
52
92
|
if (e.message?.role === "assistant") {
|
|
53
93
|
lastAssistant = e.message;
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
78
|
-
const failure =
|
|
79
|
-
|
|
80
|
-
:
|
|
81
|
-
|
|
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.98-beta.
|
|
3
|
+
"version": "0.6.98-beta.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"build": "tsc -p tsconfig.json",
|
|
24
24
|
"prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
|
|
25
25
|
"test": "vitest run",
|
|
26
|
+
"test:release-critical": "vitest run test/automatic-daemon-installer.test.ts test/automatic-install-target.test.ts test/automatic-service-migration.test.ts test/computer-cli.test.ts test/computer-service.test.ts test/computer-setup-client.test.ts test/computer-setup-orchestration.test.ts test/computer-setup.test.ts test/daemon-release-version.test.ts test/daemon-update-controller.test.ts test/daemon-update-eligibility.test.ts test/daemon-updater.test.ts test/execution-protocol.test.ts test/machine-info.test.ts --maxWorkers=2 --minWorkers=1",
|
|
26
27
|
"typecheck": "tsc --noEmit"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|