@nowcrew/daemon 0.6.98-beta.3 → 0.6.98

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.
@@ -962,17 +962,22 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
962
962
  await writeFile(legacyCwdMarker, "1", "utf8");
963
963
  }
964
964
  }
965
+ const externalAnswer = runtime.name === "pi"
966
+ ? externalOutput.lastClosedBlock
967
+ : externalOutput.lastClosedBlock
968
+ ?? (finalText === null ? null : extractExternalAnswer(finalText));
969
+ const reportFinalText = runtime.name === "pi" && externalAnswer === null
970
+ ? null
971
+ : finalText;
965
972
  const cloudReportResult = exitCode === 0
966
973
  ? await deliverCloudReports({
967
- facts: cloudReportUpload, ...reportRoots, before: reportSnapshot, finalText,
974
+ facts: cloudReportUpload, ...reportRoots, before: reportSnapshot, finalText: reportFinalText,
968
975
  receiptFile: reportContext.receiptFile,
969
- externalAnswer: externalOutput.lastClosedBlock
970
- ?? (finalText === null ? null : extractExternalAnswer(finalText)),
976
+ externalAnswer,
971
977
  captureFinal: input.captureFinal, executionId: input.executionId, agentHandle: input.handle,
972
978
  ...(dependencies.uploadCloudReports === undefined ? {} : { upload: dependencies.uploadCloudReports }),
973
979
  })
974
- : { finalText, externalAnswer: externalOutput.lastClosedBlock
975
- ?? (finalText === null ? null : extractExternalAnswer(finalText)) };
980
+ : { finalText: reportFinalText, externalAnswer };
976
981
  memoryPruneExecutorCompleted = true;
977
982
  localMemoryTelemetry?.markExecutorCompleted();
978
983
  return {
@@ -988,7 +993,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
988
993
  errorMessage: errorTail || null,
989
994
  ...(providerError === undefined ? {} : { providerError }),
990
995
  finalText: input.captureFinal && cloudReportResult.finalText !== null
991
- ? stripExternalAnswerMarkers(cloudReportResult.finalText)
996
+ ? runtime.name === "pi" && cloudReportResult.externalAnswer === null
997
+ ? null
998
+ : stripExternalAnswerMarkers(cloudReportResult.finalText)
992
999
  : null,
993
1000
  externalAnswer: input.captureFinal ? cloudReportResult.externalAnswer : null,
994
1001
  sentViaCrew,
@@ -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
  /**
@@ -33,13 +34,13 @@ export const SELF_TRAINING_EXECUTION_CAPABILITY = "self_training_execution_v1";
33
34
  export const EXECUTION_LARGE_ATTACHMENTS_CAPABILITY = "execution_attachments_300m_v1";
34
35
  export const PROJECT_SKILL_PROJECTION_CAPABILITY = "project_skill_projection_v2";
35
36
  export const MACHINE_ENVIRONMENT_CAPABILITY = "machine_environment_v1";
36
- export const MACHINE_HELLO_ACK_CAPABILITY = "machine_hello_ack_v1";
37
37
  export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
38
38
  resolverReady: true,
39
39
  nativeRuntimeAdaptersReady: true,
40
40
  });
41
41
  export const DAEMON_CAPABILITIES = Object.freeze([
42
42
  MARKETPLACE_CAPABILITY,
43
+ MAINTENANCE_CAPABILITY,
43
44
  "scheduled_job_v1",
44
45
  "reply_origin_v1",
45
46
  "origin_decision_v1",
@@ -63,7 +64,6 @@ export const DAEMON_CAPABILITIES = Object.freeze([
63
64
  "agent_ability_runtime_assets_v1",
64
65
  "agent_environments_v1",
65
66
  MACHINE_ENVIRONMENT_CAPABILITY,
66
- MACHINE_HELLO_ACK_CAPABILITY,
67
67
  ]);
68
68
  /** 能力只按平台过滤,进程存活期间静态(CLAUDE.md 红线)。Windows 在 junction/copy 真机 smoke 通过前不宣告 Project Skills。 */
69
69
  export const daemonCapabilities = (runtimePlatform = process.platform, readiness = NATIVE_PROJECT_WORKSPACE_READINESS) => {
@@ -72,7 +72,7 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
72
72
  const nativeWorkspaceReady = nativeWorkspacePlatform
73
73
  && readiness.resolverReady
74
74
  && readiness.nativeRuntimeAdaptersReady;
75
- return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== MARKETPLACE_CAPABILITY || nativeWorkspacePlatform)
75
+ return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => (((capability !== MARKETPLACE_CAPABILITY && capability !== MAINTENANCE_CAPABILITY) || nativeWorkspacePlatform)
76
76
  && (capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
77
77
  && (capability !== "message_file_preview_v1" || nativeWorkspacePlatform)
78
78
  && (nativeWorkspacePlatform
package/dist/main.js CHANGED
File without changes
@@ -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")
@@ -1,9 +1,9 @@
1
1
  import { posix, win32 } from "node:path";
2
- /** Execute npm through its owning Node binary so PATH and cmd.exe quoting are irrelevant. */
2
+ /** Execute POSIX npm through its owning Node binary so service PATH is irrelevant. */
3
3
  export function npmInvocation(platform, nodePath = process.execPath) {
4
4
  const paths = platform === "win32" ? win32 : posix;
5
- const npmPath = platform === "win32"
6
- ? paths.join(paths.dirname(nodePath), "node_modules", "npm", "bin", "npm-cli.js")
7
- : paths.join(paths.dirname(nodePath), "npm");
8
- return { command: nodePath, argsPrefix: [npmPath] };
5
+ const npmPath = paths.join(paths.dirname(nodePath), platform === "win32" ? "npm.cmd" : "npm");
6
+ return platform === "win32"
7
+ ? { command: npmPath, argsPrefix: [] }
8
+ : { command: nodePath, argsPrefix: [npmPath] };
9
9
  }
@@ -1,109 +1,20 @@
1
1
  import { WebSocket } from "ws";
2
- const HELLO_RETRY_DELAYS_MS = [1_000, 2_000, 4_000];
3
2
  /** 记住最近一次 hello 与 socket,便于存储身份变化后重发;Project Skills 状态恒为 ready。 */
4
3
  export function createMachineHelloPublisher(input) {
5
4
  let latest = null;
6
5
  let latestSocket = null;
7
- let acknowledged = false;
8
- let attempts = 0;
9
- // 发送代次:每开一轮新发送(attach/publish/republish)自增,随帧发出并要求 ack 回显。
10
- let generation = 0;
11
- let retryTimer;
12
- const clearRetry = () => {
13
- if (retryTimer !== undefined) {
14
- clearTimeout(retryTimer);
15
- retryTimer = undefined;
16
- }
17
- };
18
- const scheduleRetry = () => {
19
- clearRetry();
20
- if (acknowledged || latest === null || latestSocket?.readyState !== WebSocket.OPEN)
21
- return;
22
- // 当前 server 不支持 ack → 不排重试(否则永远等不到确认,白跑满退避)。
23
- if (input.ackSupported && !input.ackSupported())
24
- return;
25
- const delay = HELLO_RETRY_DELAYS_MS[attempts - 1];
26
- if (delay === undefined) {
27
- // 退避跑满仍未 ack:期待 ack(ackSupported 已声明支持)才升级为断连重连,避免旧行为下反复重连。
28
- if (input.ackSupported !== undefined && latestSocket !== null)
29
- input.onExhausted?.(latestSocket);
30
- return;
31
- }
32
- retryTimer = setTimeout(() => {
33
- retryTimer = undefined;
34
- sendCurrent();
35
- }, delay);
36
- };
37
- function sendCurrent() {
38
- if (latest === null || latestSocket?.readyState !== WebSocket.OPEN || acknowledged)
6
+ const republish = () => {
7
+ if (latest === null || latestSocket?.readyState !== WebSocket.OPEN)
39
8
  return;
40
- // helloSeq 随帧发出;同一代次的多次重试复用同一 seq,ack 回显该 seq 即视为确认。
41
- const hello = input.effective({ ...latest, projectSkillsStatus: "ready", helloSeq: generation });
42
- const attempt = attempts + 1;
43
- attempts = attempt;
9
+ const hello = input.effective({ ...latest, projectSkillsStatus: "ready" });
44
10
  try {
45
11
  latestSocket.send(JSON.stringify(hello));
46
- input.onSent(hello, attempt);
47
- }
48
- catch (error) {
49
- input.onSendFailed?.(hello, attempt, error);
12
+ input.onSent(hello);
50
13
  }
51
- scheduleRetry();
52
- }
53
- const attach = (socket) => {
54
- clearRetry();
55
- latestSocket = socket;
56
- acknowledged = false;
57
- attempts = 0;
58
- generation += 1;
59
- sendCurrent();
60
- };
61
- const detach = (socket) => {
62
- if (latestSocket !== socket)
63
- return;
64
- clearRetry();
65
- latestSocket = null;
66
- acknowledged = false;
67
- attempts = 0;
68
- };
69
- const republish = () => {
70
- if (latest === null || latestSocket === null)
71
- return;
72
- clearRetry();
73
- acknowledged = false;
74
- attempts = 0;
75
- generation += 1;
76
- sendCurrent();
77
- };
78
- const acknowledge = (socket, seq) => {
79
- // 非当前 socket / 已确认 / 该 socket 已在关闭(close 后迟到的 ack)一律忽略:
80
- // 后者会错误触发 onAck→清零强制重连计数,削弱重连上限。
81
- if (latestSocket !== socket || latest === null || acknowledged || socket.readyState !== WebSocket.OPEN)
82
- return;
83
- // 代次校验:只接受回显当前代次的 ack,忽略上一代次迟到的 ack(否则会误停新代次的重试)。
84
- // seq 缺省(老 server 不回显)时退回仅按 socket 匹配,保持兼容。
85
- if (seq !== undefined && seq !== generation)
86
- return;
87
- acknowledged = true;
88
- clearRetry();
89
- input.onAck?.(input.effective({ ...latest, projectSkillsStatus: "ready" }));
14
+ catch { /* 非 OPEN,忽略 */ }
90
15
  };
91
16
  return Object.freeze({
92
- publish: (hello, socket) => {
93
- latest = hello;
94
- if (latestSocket !== socket) {
95
- attach(socket);
96
- return;
97
- }
98
- acknowledged = false;
99
- attempts = 0;
100
- generation += 1;
101
- clearRetry();
102
- sendCurrent();
103
- },
104
- attach,
105
- detach,
17
+ publish: (hello, socket) => { latest = hello; latestSocket = socket; republish(); },
106
18
  republish,
107
- acknowledge,
108
19
  });
109
20
  }
@@ -1,17 +1,85 @@
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] : [])]; }
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 }; } }; }
1
+ export function buildPiArgs(options) {
2
+ return ["--mode", "json", "--print", "--no-session", "--system-prompt", options.systemPrompt,
3
+ ...(options.model ? ["--model", options.model] : []),
4
+ ...(options.reasoning && options.reasoning !== "default" ? ["--thinking", options.reasoning] : [])];
5
+ }
6
+ // 保留稳定的原因标识供 server 本地化;不要用 token 数量猜测结束原因。
7
+ function failureReason(message) {
8
+ const reason = message?.stopReason;
9
+ return reason === "stop" || reason === "toolUse" ? null : `pi_runtime_failed: ${reason ?? "incomplete"}`;
10
+ }
11
+ export function createPiEventDecoder() {
12
+ let sessionId = null;
13
+ let finalText = "";
14
+ let error = null;
15
+ let lastAssistant = null;
16
+ let assistantOpen = false;
17
+ let sawTurnEnd = false;
18
+ let usage;
19
+ return {
20
+ push(raw) {
21
+ const e = (raw ?? {});
22
+ const out = [];
23
+ if (e.type === "session" && typeof e.id === "string") {
24
+ sessionId = e.id;
25
+ out.push({ type: "thread.started", thread_id: e.id });
26
+ }
27
+ if (e.type === "turn_start")
28
+ sawTurnEnd = false;
29
+ if ((e.type === "message_start" || e.type === "message_update") && e.message?.role === "assistant") {
30
+ assistantOpen = true;
31
+ sawTurnEnd = false;
32
+ }
33
+ const ae = e.assistantMessageEvent;
34
+ if (e.type === "message_update" && ae?.type === "text_delta" && typeof ae.delta === "string") {
35
+ finalText += ae.delta;
36
+ out.push({ type: "pi.text_delta", text: ae.delta });
37
+ }
38
+ if (e.type === "message_update" && ae?.type === "thinking_delta" && typeof ae.delta === "string") {
39
+ out.push({ type: "pi.thinking_delta", text: ae.delta });
40
+ }
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 } : {}) });
43
+ }
44
+ if (e.type === "message_end" && e.message?.role === "assistant") {
45
+ lastAssistant = e.message;
46
+ usage = e.message.usage;
47
+ assistantOpen = false;
48
+ sawTurnEnd = false;
49
+ }
50
+ if (e.type === "turn_end") {
51
+ // 原生 turn_end 自带本轮消息,不能把工具调用轮的结束当成整次执行成功。
52
+ if (e.message?.role === "assistant") {
53
+ lastAssistant = e.message;
54
+ usage = e.message.usage;
55
+ assistantOpen = false;
56
+ }
57
+ else if (e.message) {
58
+ lastAssistant = null;
59
+ }
60
+ 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 });
69
+ }
70
+ if (e.type === "error" || e.type === "agent_error") {
71
+ error = typeof e.error === "string" ? e.error : (e.error?.message ?? "Pi error");
72
+ out.push({ type: "pi.error", message: error });
73
+ }
74
+ return out;
75
+ },
76
+ 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 };
83
+ },
84
+ };
85
+ }