@co0ontty/wand 4.85.1 → 4.87.0-beta.gd2f0f8f

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.
Files changed (108) hide show
  1. package/README.md +4 -0
  2. package/dist/agent-dispatch.js +5 -3
  3. package/dist/ai-team-runner.js +1 -0
  4. package/dist/ai-team-types.d.ts +1 -0
  5. package/dist/ai-team-types.js +6 -2
  6. package/dist/build-info.json +4 -4
  7. package/dist/cli.js +12 -6
  8. package/dist/config.d.ts +1 -1
  9. package/dist/config.js +33 -0
  10. package/dist/core-runner.js +1 -1
  11. package/dist/daemon-maintenance-state.d.ts +6 -0
  12. package/dist/daemon-maintenance-state.js +9 -0
  13. package/dist/daemon-maintenance-targets.d.ts +20 -0
  14. package/dist/daemon-maintenance-targets.js +183 -0
  15. package/dist/daemon-maintenance.d.ts +53 -0
  16. package/dist/daemon-maintenance.js +164 -0
  17. package/dist/decision-expert-employee.d.ts +6 -0
  18. package/dist/decision-expert-employee.js +22 -0
  19. package/dist/decision-expert-identity.d.ts +7 -0
  20. package/dist/decision-expert-identity.js +7 -0
  21. package/dist/decision-hardware.d.ts +19 -0
  22. package/dist/decision-hardware.js +19 -0
  23. package/dist/decision-service.d.ts +6 -1
  24. package/dist/decision-service.js +60 -1
  25. package/dist/decision-types.d.ts +1 -0
  26. package/dist/decision-types.js +12 -0
  27. package/dist/git-quick-commit.js +1 -1
  28. package/dist/laya-model-manifest.d.ts +11 -0
  29. package/dist/laya-model-manifest.js +18 -0
  30. package/dist/local-model-setup.d.ts +46 -0
  31. package/dist/local-model-setup.js +248 -0
  32. package/dist/local-model-types.d.ts +34 -0
  33. package/dist/local-model-types.js +1 -0
  34. package/dist/message-truncator.d.ts +4 -2
  35. package/dist/message-truncator.js +20 -6
  36. package/dist/missions.js +6 -3
  37. package/dist/model-file-store.d.ts +13 -0
  38. package/dist/model-file-store.js +108 -0
  39. package/dist/model-groups.js +2 -2
  40. package/dist/model-setup-process.d.ts +2 -0
  41. package/dist/model-setup-process.js +80 -0
  42. package/dist/models.js +3 -3
  43. package/dist/process-manager.js +21 -8
  44. package/dist/provider-catalog.d.ts +13 -1
  45. package/dist/provider-catalog.js +14 -2
  46. package/dist/render-binary.d.ts +1 -1
  47. package/dist/render-binary.js +4 -4
  48. package/dist/render-daemon-client.d.ts +12 -1
  49. package/dist/render-daemon-client.js +72 -4
  50. package/dist/render-host.d.ts +1 -0
  51. package/dist/render-host.js +9 -56
  52. package/dist/render-structured-host.d.ts +1 -0
  53. package/dist/render-structured-host.js +2 -2
  54. package/dist/server-employee-routes.js +3 -3
  55. package/dist/server-local-model-routes.d.ts +8 -0
  56. package/dist/server-local-model-routes.js +62 -0
  57. package/dist/server-resume-routes.js +1 -1
  58. package/dist/server-session-routes.js +34 -8
  59. package/dist/server-settings-routes.js +2 -0
  60. package/dist/server-speech-routes.d.ts +8 -0
  61. package/dist/server-speech-routes.js +87 -0
  62. package/dist/server-task-routes.js +3 -1
  63. package/dist/server-workspace-routes.js +2 -0
  64. package/dist/server.js +63 -18
  65. package/dist/session-completion-state.d.ts +1 -0
  66. package/dist/session-completion-state.js +1 -0
  67. package/dist/session-transport.js +1 -0
  68. package/dist/silicon-employee-dispatch.js +5 -2
  69. package/dist/silicon-employee-draft.js +1 -1
  70. package/dist/speech-models.d.ts +24 -0
  71. package/dist/speech-models.js +16 -0
  72. package/dist/speech-service.d.ts +57 -0
  73. package/dist/speech-service.js +399 -0
  74. package/dist/speech-types.d.ts +47 -0
  75. package/dist/speech-types.js +3 -0
  76. package/dist/storage.d.ts +2 -0
  77. package/dist/storage.js +13 -0
  78. package/dist/structured-failure.js +1 -1
  79. package/dist/structured-session-manager.d.ts +15 -2
  80. package/dist/structured-session-manager.js +58 -19
  81. package/dist/terminal-daemon-build.d.ts +4 -0
  82. package/dist/terminal-daemon-build.js +54 -0
  83. package/dist/terminal-daemon-protocol.d.ts +1 -1
  84. package/dist/terminal-daemon-server.js +20 -1
  85. package/dist/terminal-host.d.ts +4 -0
  86. package/dist/terminal-host.js +7 -4
  87. package/dist/tui/commands.js +1 -1
  88. package/dist/types.d.ts +14 -2
  89. package/dist/web-ui/chat-history-window.d.ts +23 -0
  90. package/dist/web-ui/chat-history-window.js +29 -0
  91. package/dist/web-ui/content/ai-teams.js +12 -12
  92. package/dist/web-ui/content/scripts.js +316 -233
  93. package/dist/web-ui/content/styles.css +1 -1
  94. package/dist/web-ui/content/tailwind.css +1 -1
  95. package/dist/web-ui/embedded-assets.js +3 -3
  96. package/dist/web-ui/index.js +3 -3
  97. package/dist/web-ui/provider-identity.d.ts +28 -1
  98. package/dist/web-ui/provider-identity.js +31 -2
  99. package/dist/web-ui/session-activity.d.ts +1 -0
  100. package/dist/web-ui/session-activity.js +5 -2
  101. package/dist/web-ui/speech-audio.d.ts +2 -0
  102. package/dist/web-ui/speech-audio.js +41 -0
  103. package/dist/web-ui/vendor-loader.d.ts +2 -0
  104. package/dist/web-ui/vendor-loader.js +50 -0
  105. package/dist/ws-broadcast.js +10 -4
  106. package/package.json +7 -3
  107. package/scripts/install-laya-runtime.js +67 -0
  108. package/scripts/install-speech-runtime.js +82 -0
@@ -1362,9 +1362,12 @@ export class StructuredSessionManager {
1362
1362
  const selectedModel = options.model?.trim() || null;
1363
1363
  const initialThinkingEffort = normalizeThinkingEffort(options.thinkingEffort);
1364
1364
  const autoCompaction = this.config.harness?.compaction?.enabled ?? true;
1365
+ // 显式引擎优先:用户新建会话时选了 Wand Agent(core)就不要再被员工候选或默认推断改口。
1366
+ const explicitEngine = provider === "pi" ? options.engine : undefined;
1365
1367
  // 员工 SDK 会话不静默继承用户的全局扩展,也不继承 CLI 专属的资源/CodeMode 默认。
1366
1368
  const sdkEmployeeSession = provider === "pi"
1367
- && options.employeeCandidates?.[options.employeeCandidateIndex ?? 0]?.engine === "sdk";
1369
+ && (explicitEngine ? explicitEngine === "core"
1370
+ : options.employeeCandidates?.[options.employeeCandidateIndex ?? 0]?.engine === "sdk");
1368
1371
  // CLI Pi 会话从「上次设置」起步:用户在任一 Pi 会话改过的自动选择 / CodeMode / 资源选择
1369
1372
  // 就是新会话的默认,不需要每开一个会话重配一次。autoCompaction 始终取当前配置。
1370
1373
  const rememberedPiSettings = provider === "pi" && !sdkEmployeeSession ? this.storage.getPiSessionDefaults() : null;
@@ -1420,6 +1423,7 @@ export class StructuredSessionManager {
1420
1423
  structuredState: {
1421
1424
  provider,
1422
1425
  runner,
1426
+ ...(explicitEngine ? { engine: explicitEngine } : {}),
1423
1427
  model: selectedModel ?? undefined,
1424
1428
  inFlight: false,
1425
1429
  activeRequestId: null,
@@ -1502,6 +1506,7 @@ export class StructuredSessionManager {
1502
1506
  const selected = candidates[index];
1503
1507
  // Explicit tool/model changes, including changes during the pending request, remain authoritative.
1504
1508
  if (!selected || original.provider !== selected.provider
1509
+ || (original.structuredState?.engine && (original.structuredState.engine === "core" ? "sdk" : "cli") !== (selected.engine ?? "cli"))
1505
1510
  || (original.selectedModel && original.selectedModel !== this.employeeCandidateModel(selected)))
1506
1511
  return null;
1507
1512
  const current = this.sessions.get(id);
@@ -1812,8 +1817,10 @@ export class StructuredSessionManager {
1812
1817
  }
1813
1818
  else if (provider === "pi") {
1814
1819
  const employeeAgent = updated.employeeCandidates?.[updated.employeeCandidateIndex ?? 0];
1815
- const sdkRequested = employeeAgent?.engine === "sdk";
1816
- // 对话框默认固定走 CLI;只有员工候选明确配置 SDK 时,员工会话才启用 core。
1820
+ // 会话已裁决过引擎就沿用(新建会话选了 Wand Agent 或 Pi CLI);未裁决的旧会话/员工会话回落到候选配置。
1821
+ const decidedEngine = updated.structuredState?.engine;
1822
+ const sdkRequested = decidedEngine ? decidedEngine === "core" : employeeAgent?.engine === "sdk";
1823
+ // 没显式选择时对话框固定走 CLI;只有选定 Wand Agent 或员工候选配置 SDK 才是 core。
1817
1824
  const resolution = sdkRequested
1818
1825
  ? resolveHarnessEngineSync({ ...this.config.harness, engine: "core" }, "pi", {
1819
1826
  coreRunnerSupplied: this.coreRunnerSupplied,
@@ -1832,7 +1839,7 @@ export class StructuredSessionManager {
1832
1839
  await this.runClaudeStreaming(id, enginePatched, prompt, requestId, {
1833
1840
  runner: this.coreRunner,
1834
1841
  provider: "pi",
1835
- commandLabel: "pi core(员工 SDK)",
1842
+ commandLabel: "Wand Agent(SDK)",
1836
1843
  logKind: "pi-core",
1837
1844
  });
1838
1845
  }
@@ -2063,22 +2070,39 @@ export class StructuredSessionManager {
2063
2070
  });
2064
2071
  return updated;
2065
2072
  }
2066
- /** 新建空白对话可原位换 CLI;接受过输入、恢复会话和自动化不跨 provider 搬历史。 */
2067
- setSessionProvider(sessionId, provider) {
2073
+ /** 空白对话原位换执行工具;引擎也是工具身份,不跨工具搬历史或能力设置。 */
2074
+ setSessionProvider(sessionId, provider, engine) {
2068
2075
  const session = this.requireSession(sessionId);
2069
2076
  if (!isSessionProvider(provider))
2070
- throw new Error("请选择有效的 CLI 工具。");
2077
+ throw new Error("请选择有效的执行工具。");
2078
+ if (engine !== undefined && engine !== "cli" && engine !== "sdk")
2079
+ throw new Error("执行引擎必须是 cli 或 sdk。");
2080
+ if (engine === "sdk" && provider !== "pi")
2081
+ throw new Error("Wand Agent 只支持 Pi 结构化会话。");
2071
2082
  if (session.status !== "idle" || session.archived || session.structuredState?.inFlight ||
2072
2083
  this.pendingRunnerExecutions.has(sessionId) || session.claudeSessionId ||
2084
+ session.resumedFromSessionId || session.autoRecovered ||
2073
2085
  (session.messages?.length ?? 0) > 0 || (session.queuedMessages?.length ?? 0) > 0 ||
2074
2086
  session.automationId || (session.sessionSource && session.sessionSource !== "interactive") ||
2075
2087
  this.relayHandlerFor(session)) {
2076
2088
  throw new Error("只有尚未发送消息的新建空白对话可以更换工具。");
2077
2089
  }
2078
- if (session.provider === provider)
2079
- return session;
2080
2090
  const candidates = session.employeeCandidates ?? [];
2081
- const candidateIndex = candidates.findIndex((candidate) => candidate.provider === provider);
2091
+ const currentCandidate = candidates[session.employeeCandidateIndex ?? 0];
2092
+ const currentEngine = session.provider === "pi" && (session.structuredState?.engine === "core"
2093
+ || (!session.structuredState?.engine && currentCandidate?.provider === "pi" && currentCandidate.engine === "sdk")) ? "sdk" : "cli";
2094
+ // 旧客户端只发 provider 时,同 provider 保持原引擎;显式 Pi 选项始终传 cli。
2095
+ const targetEngine = provider === "pi" ? engine ?? (session.provider === provider ? currentEngine : "cli") : "cli";
2096
+ if (session.provider === provider && currentEngine === targetEngine)
2097
+ return session;
2098
+ if (targetEngine === "sdk" && session.provider === "pi" && (session.piSettings?.resources?.skills.length
2099
+ || session.piSettings?.resources?.mcpServers.length || session.piSettings?.lockedSkills?.length
2100
+ || session.piSettings?.autoResources || session.piSettings?.codemodeOverride !== undefined)) {
2101
+ throw new Error("Wand Agent 不支持会话级 Skills / MCP、自动选择或 CodeMode 覆盖;请先在 Pi 设置中关闭这些选项再切换。");
2102
+ }
2103
+ const resolution = provider === "pi" ? this.resolveNewSessionPiEngine(targetEngine) : undefined;
2104
+ const candidateIndex = candidates.findIndex((candidate) => candidate.provider === provider
2105
+ && (candidate.engine ?? "cli") === targetEngine);
2082
2106
  const candidate = candidateIndex >= 0 ? candidates[candidateIndex] : undefined;
2083
2107
  const runner = defaultStructuredRunner(provider);
2084
2108
  const model = candidate && candidate.model !== "default" ? candidate.model
@@ -2094,18 +2118,19 @@ export class StructuredSessionManager {
2094
2118
  ...session,
2095
2119
  provider,
2096
2120
  runner,
2097
- // 从别的 provider 切到 Pi 的空白会话与新建 Pi 会话同等:以「上次设置」起步。
2098
- ...(provider === "pi" && !session.piSettings ? { piSettings: candidate?.engine === "sdk"
2099
- ? defaultPiSessionSettings(this.config.harness?.compaction?.enabled ?? true)
2100
- : { ...(this.storage.getPiSessionDefaults() ?? defaultPiCliSessionSettings(this.config.harness?.compaction?.enabled ?? true)),
2101
- autoCompaction: this.config.harness?.compaction?.enabled ?? true } } : {}),
2102
- command: recoveredCommandLabel(runner),
2121
+ // 换引擎也是换工具:使用目标自己的默认设置,不复用另一路的能力配置。
2122
+ piSettings: provider !== "pi" ? undefined : targetEngine === "sdk"
2123
+ ? defaultPiSessionSettings(this.config.harness?.compaction?.enabled ?? true)
2124
+ : { ...(this.storage.getPiSessionDefaults() ?? defaultPiCliSessionSettings(this.config.harness?.compaction?.enabled ?? true)),
2125
+ autoCompaction: this.config.harness?.compaction?.enabled ?? true },
2126
+ command: targetEngine === "sdk" ? "Wand Agent(SDK)" : recoveredCommandLabel(runner),
2103
2127
  mode,
2104
2128
  autoApprovePermissions: shouldAutoApproveForMode(mode),
2105
2129
  selectedModel: model,
2106
2130
  thinkingEffort,
2107
2131
  employeeCandidateIndex: candidateIndex >= 0 ? candidateIndex : undefined,
2108
- structuredState: { ...defaultStructuredState(provider, runner), model: model ?? undefined },
2132
+ structuredState: { ...defaultStructuredState(provider, runner), model: model ?? undefined,
2133
+ ...(resolution ? { engine: resolution.engine, engineReason: resolution.reason } : {}) },
2109
2134
  };
2110
2135
  // 身份、知识归属、任务/目录和候选快照不变;只更新本会话的执行参数。
2111
2136
  this.storage.saveSession(updated);
@@ -2118,12 +2143,26 @@ export class StructuredSessionManager {
2118
2143
  });
2119
2144
  return updated;
2120
2145
  }
2146
+ /**
2147
+ * 新建 pi 结构化会话前的引擎裁决。
2148
+ *
2149
+ * 用户在新建会话里选的是两个不同的东西:Pi CLI(`cli`)与 Wand Agent(`sdk`,进程内 harness)。
2150
+ * 显式选了 Wand Agent 就必须真的可用:不可用时抛错由路由告诉用户,不静默退回 CLI 冒充。
2151
+ */
2152
+ resolveNewSessionPiEngine(engine) {
2153
+ if (engine !== "sdk")
2154
+ return { engine: "cli", reason: "Pi 主功能固定使用 CLI JSON" };
2155
+ return resolveHarnessEngineSync({ ...this.config.harness, engine: "core" }, "pi", {
2156
+ coreRunnerSupplied: this.coreRunnerSupplied,
2157
+ });
2158
+ }
2121
2159
  getPiSettings(sessionId) {
2122
2160
  const session = this.requireSession(sessionId);
2123
2161
  if (session.provider !== "pi")
2124
2162
  throw new Error("这些设置仅适用于 Pi 结构化会话。");
2125
2163
  const employeeAgent = session.employeeCandidates?.[session.employeeCandidateIndex ?? 0];
2126
- const sdkRequested = employeeAgent?.engine === "sdk";
2164
+ const decidedEngine = session.structuredState?.engine;
2165
+ const sdkRequested = decidedEngine ? decidedEngine === "core" : employeeAgent?.engine === "sdk";
2127
2166
  let resolution;
2128
2167
  if (sdkRequested) {
2129
2168
  try {
@@ -2157,7 +2196,7 @@ export class StructuredSessionManager {
2157
2196
  || patch.codemodeOverride !== undefined
2158
2197
  || patch.autoResources !== undefined
2159
2198
  || patch.lockedSkills !== undefined)) {
2160
- throw new Error("当前 SDK 候选尚不支持此会话级设置;没有改用全局资源或 CodeMode 配置。");
2199
+ throw new Error("Wand Agent 尚不支持此会话级设置;没有改用全局资源或 CodeMode 配置。");
2161
2200
  }
2162
2201
  if (resolution.engine !== "core") {
2163
2202
  // 写入裁决与启动参数用同一套能力边界:表达不出的字段/取值不落库。
@@ -0,0 +1,4 @@
1
+ /** Captured at process start: replacing files cannot make an old owner current. */
2
+ export declare const TERMINAL_DAEMON_BUILD_ID: string;
3
+ /** Pause replacement while npm is changing this installation or awaiting Server relaunch. */
4
+ export declare function terminalDaemonBuildIsCurrent(): boolean;
@@ -0,0 +1,54 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ const require = createRequire(import.meta.url);
5
+ /** Component content, not the Wand release version or timestamp. The runtime
6
+ * import graph is covered by tests/terminal-daemon-build.test.ts. */
7
+ function daemonBuildId() {
8
+ const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
9
+ const hash = createHash("sha256");
10
+ hash.update(JSON.stringify([process.versions.node, process.platform, process.arch]));
11
+ for (const name of ["terminal-daemon-server", "terminal-daemon-protocol", "terminal-daemon-build", "terminal-host",
12
+ "structured-exec-host", "pty-terminal-state", "pty-shell-launch", "unix-socket-keepalive",
13
+ "ensure-node-pty-helper", "signal-utils", "pty-text-utils", "shell-quote", "error-utils", "render-protocol"]) {
14
+ hash.update(name).update("\0").update(readFileSync(new URL(`./${name}.${extension}`, import.meta.url)));
15
+ }
16
+ // Read installed versions afresh, not require()'s cached JSON. Unrelated
17
+ // Server/UI dependencies and package metadata must not replace terminald.
18
+ for (const name of ["node-pty", "@xterm/headless", "@xterm/addon-serialize", "@xterm/addon-unicode11"]) {
19
+ const { version } = JSON.parse(readFileSync(require.resolve(`${name}/package.json`), "utf8"));
20
+ if (typeof version !== "string" || !version)
21
+ throw new Error(`Missing daemon dependency version: ${name}`);
22
+ hash.update(name).update("\0").update(version);
23
+ }
24
+ return hash.digest("hex");
25
+ }
26
+ /** Captured at process start: replacing files cannot make an old owner current. */
27
+ export const TERMINAL_DAEMON_BUILD_ID = daemonBuildId();
28
+ // The installation fence has a different purpose from component equality:
29
+ // even a Server-only install must pause the old worker until Server relaunch.
30
+ function installationId(buildId) {
31
+ const hash = createHash("sha256").update(buildId);
32
+ for (const file of ["./build-info.json", "../package.json"]) {
33
+ hash.update(file).update("\0");
34
+ try {
35
+ hash.update(readFileSync(new URL(file, import.meta.url)));
36
+ }
37
+ catch (error) {
38
+ if (error.code !== "ENOENT")
39
+ throw error;
40
+ hash.update("missing"); // source checkouts have no build stamp
41
+ }
42
+ }
43
+ return hash.digest("hex");
44
+ }
45
+ const INSTALLATION_ID = installationId(TERMINAL_DAEMON_BUILD_ID);
46
+ /** Pause replacement while npm is changing this installation or awaiting Server relaunch. */
47
+ export function terminalDaemonBuildIsCurrent() {
48
+ try {
49
+ return installationId(daemonBuildId()) === INSTALLATION_ID;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
@@ -10,7 +10,7 @@ export type TerminalDaemonRequest = {
10
10
  id: number;
11
11
  token: string;
12
12
  protocolVersion: number;
13
- method: "hello" | "list" | "createOrAttach" | "write" | "resize" | "kill" | "forget" | "structuredSpawn" | "structuredAttach" | "structuredList" | "structuredKill" | "structuredForget";
13
+ method: "hello" | "list" | "createOrAttach" | "write" | "resize" | "kill" | "forget" | "structuredSpawn" | "structuredAttach" | "structuredList" | "structuredKill" | "structuredForget" | "shutdownIfIdle";
14
14
  params?: Record<string, unknown>;
15
15
  };
16
16
  export type TerminalDaemonResponse = {
@@ -6,6 +6,7 @@ import path from "node:path";
6
6
  import { StringDecoder } from "node:string_decoder";
7
7
  import pty from "node-pty";
8
8
  import { keepUnixSocketAlive } from "./unix-socket-keepalive.js";
9
+ import { TERMINAL_DAEMON_BUILD_ID } from "./terminal-daemon-build.js";
9
10
  import { ensureNodePtyHelperExecutable } from "./ensure-node-pty-helper.js";
10
11
  import { signalNumberFromName } from "./signal-utils.js";
11
12
  import { STRUCTURED_RUN_LOG_MAX_CHARS, createUtf8TextDecoder, } from "./structured-exec-host.js";
@@ -81,6 +82,7 @@ export async function runTerminalDaemon(configPath) {
81
82
  const sessions = new Map();
82
83
  const clients = new Set();
83
84
  const structuredRuns = new Map();
85
+ let retiring = false;
84
86
  const send = (socket, message) => {
85
87
  if (!socket.destroyed && socket.writable)
86
88
  socket.write(`${JSON.stringify(message)}\n`);
@@ -356,12 +358,25 @@ export async function runTerminalDaemon(configPath) {
356
358
  const params = request.params ?? {};
357
359
  switch (request.method) {
358
360
  case "hello":
359
- result = { protocolVersion: TERMINAL_DAEMON_PROTOCOL_VERSION, pid: process.pid };
361
+ result = { protocolVersion: TERMINAL_DAEMON_PROTOCOL_VERSION, pid: process.pid,
362
+ buildId: TERMINAL_DAEMON_BUILD_ID, shutdownIfIdle: true };
360
363
  break;
364
+ case "shutdownIfIdle": {
365
+ const busy = [...sessions.values(), ...structuredRuns.values()].some((entry) => entry.status === "running");
366
+ result = { accepted: !busy };
367
+ if (!busy && !retiring) {
368
+ // The idle check and admission closure happen in the same event-loop turn.
369
+ retiring = true;
370
+ setTimeout(shutdown, 25).unref();
371
+ }
372
+ break;
373
+ }
361
374
  case "list":
362
375
  result = Array.from(sessions.values(), serialize);
363
376
  break;
364
377
  case "createOrAttach":
378
+ if (retiring)
379
+ throw new Error("Terminal daemon is updating");
365
380
  result = createOrAttach(params);
366
381
  break;
367
382
  case "write": {
@@ -399,6 +414,8 @@ export async function runTerminalDaemon(configPath) {
399
414
  result = { ok: true };
400
415
  break;
401
416
  case "structuredSpawn":
417
+ if (retiring)
418
+ throw new Error("Terminal daemon is updating");
402
419
  result = structuredSpawn(params);
403
420
  break;
404
421
  case "structuredAttach": {
@@ -425,6 +442,8 @@ export async function runTerminalDaemon(configPath) {
425
442
  structuredForget(String(params.runId ?? ""));
426
443
  result = { ok: true };
427
444
  break;
445
+ default:
446
+ throw new Error("Unsupported terminal daemon method");
428
447
  }
429
448
  send(client.socket, { kind: "response", id: request.id, ok: true, result });
430
449
  }
@@ -68,6 +68,8 @@ export interface TerminalHost {
68
68
  attach(sessionId: string, afterSeq?: number): TerminalAttachResult | null;
69
69
  createOrAttach(request: TerminalSpawnRequest, afterSeq?: number): Promise<TerminalAttachResult>;
70
70
  forget(sessionId: string): void;
71
+ /** Publish already-read bytes before synchronous snapshots or owner teardown. */
72
+ flush?(): void;
71
73
  disconnect(): void;
72
74
  }
73
75
  /** Legacy/local adapter and deterministic test adapter for the TerminalHost seam. */
@@ -81,3 +83,5 @@ export declare class InProcessTerminalHost implements TerminalHost {
81
83
  }
82
84
  /** Keep a bounded replay log while preserving chunk sequence numbers. */
83
85
  export declare function appendTerminalChunkWindow(chunks: readonly TerminalDataEvent[], event: TerminalDataEvent): TerminalDataEvent[];
86
+ /** Same replay window, computed once for a batch instead of per wire frame. */
87
+ export declare function appendTerminalChunkBatch(chunks: readonly TerminalDataEvent[], events: readonly TerminalDataEvent[]): TerminalDataEvent[];
@@ -103,10 +103,13 @@ function emptyRunningState(process, cols, rows, launchMarkerToken) {
103
103
  }
104
104
  /** Keep a bounded replay log while preserving chunk sequence numbers. */
105
105
  export function appendTerminalChunkWindow(chunks, event) {
106
- const boundedEvent = event.data.length > PTY_OUTPUT_MAX_SIZE
107
- ? { ...event, data: event.data.slice(-PTY_OUTPUT_MAX_SIZE) }
108
- : event;
109
- const next = [...chunks, boundedEvent];
106
+ return appendTerminalChunkBatch(chunks, [event]);
107
+ }
108
+ /** Same replay window, computed once for a batch instead of per wire frame. */
109
+ export function appendTerminalChunkBatch(chunks, events) {
110
+ const next = [...chunks, ...events.map((event) => event.data.length > PTY_OUTPUT_MAX_SIZE
111
+ ? { ...event, data: event.data.slice(-PTY_OUTPUT_MAX_SIZE) }
112
+ : event)];
110
113
  let size = 0;
111
114
  let first = next.length;
112
115
  while (first > 0) {
@@ -783,7 +783,7 @@ function staleDaemonNote(configPath) {
783
783
  if (daemonVersion && compareSemver(daemonVersion, binaryVersion) >= 0)
784
784
  return null;
785
785
  const shown = daemonVersion ? `Render ${daemonVersion}` : "Render daemon";
786
- return `;但 ${shown} 仍在跑(装好的二进制是 ${binaryVersion}),它要重启才会用上新代码:./start.sh --restart-daemons`;
786
+ return `;底层组件将在所有会话结束后自动更新(${shown} → ${binaryVersion})。等待期间部分功能可能暂时异常,无需操作`;
787
787
  }
788
788
  /** daemon 端点状态摘要,给服务命令的 detail 用。 */
789
789
  function describeDaemonEndpoints(configPath) {
package/dist/types.d.ts CHANGED
@@ -144,6 +144,11 @@ export interface WandConfig {
144
144
  defaultProvider?: SessionProvider;
145
145
  /** 新建会话时默认使用的承载类型。 */
146
146
  defaultSessionKind?: SessionKind;
147
+ /**
148
+ * 新建会话时默认的执行引擎;只对支持多引擎的 provider 有意义(目前只有 pi):
149
+ * `cli` = Pi CLI,`sdk` = Wand Agent(进程内 SDK)。缺省视为 CLI。
150
+ */
151
+ defaultEngine?: "cli" | "sdk";
147
152
  /** 新建任务时是否默认开启独立 worktree。 */
148
153
  defaultTaskWorktree?: boolean;
149
154
  defaultMode: ExecutionMode;
@@ -615,6 +620,8 @@ export interface ConversationAuthor {
615
620
  avatar?: string;
616
621
  leader?: boolean;
617
622
  provider?: SessionProvider;
623
+ /** 本条发言实际候选的执行引擎;历史缺省仍按 CLI 显示。 */
624
+ engine?: "cli" | "sdk";
618
625
  /**
619
626
  * 这条发言**实际使用候选**的模型 id(真值,不是展示文案;`"default"` 表示跟随该 provider
620
627
  * 的服务端默认模型)。老数据没有这个字段,读端要能只按 provider 显示。
@@ -761,10 +768,15 @@ export interface SessionSnapshot extends SessionCompletionState {
761
768
  /**
762
769
  * PTY 会话本轮是否正在生成回复(provider CLI busy)。语义对齐 structured 的
763
770
  * structuredState.inFlight:turn 开始置 true,回复结束/进程退出清 false。
764
- * 运行时信号,不持久化,服务重启后视为 false。目前仅 Claude PTY(bridge)能精确判定;
765
- * 其余 provider 不产生该信号(undefined),客户端应回落到旧行为。
771
+ * 运行时信号,不持久化;Claude 使用 bridge,其余 provider 使用前台资格与输出静默窗口。
766
772
  */
767
773
  ptyBusy?: boolean;
774
+ /**
775
+ * 空白 PTY 中有前台命令(如手动启动 CLI / 构建),由内核前台进程组采样得出。
776
+ * 与 provider 回合无关,不产生成功完成回执或回合计时;无输出也可为 true。
777
+ * 仅运行时,不持久化;未探测、不支持的平台、提示符与已结束终端均为 false。
778
+ */
779
+ ptyCommandRunning?: boolean;
768
780
  /**
769
781
  * PTY 本轮的时间锚点,语义与 structuredState.turnStartedAt / lastActivityAt 完全一致,
770
782
  * 是 ptyBusy 的配套读数:turn 开始(或静默窗口被输出续期)时写入,ptyBusy 清 false
@@ -0,0 +1,23 @@
1
+ /** Small transport pages; the browser fills them to a measured viewport buffer. */
2
+ export declare const CHAT_INITIAL_MESSAGE_COUNT = 8;
3
+ export declare const CHAT_HISTORY_BLOCK_BUDGET = 12;
4
+ export declare const CHAT_HISTORY_BYTE_BUDGET: number;
5
+ export declare const CHAT_HISTORY_BUFFER_SCREENS = 2;
6
+ export interface ChatScrollGeometry {
7
+ scrollHeight: number;
8
+ clientHeight: number;
9
+ scrollTop: number;
10
+ }
11
+ /** The chat scroller is column-reverse (latest at scrollTop=0). */
12
+ export declare function chatHistoryBufferRemaining(geometry: ChatScrollGeometry): number;
13
+ export declare function needsChatHistoryBuffer(geometry: ChatScrollGeometry): boolean;
14
+ export declare function chatHistoryRootMargin(height: number): string;
15
+ /** One attempt per cursor until progress or an explicit retry. Height decides
16
+ * how much to prefetch, never whether a successful page made progress:
17
+ * scrollHeight stays at clientHeight while the screen is underfilled, and
18
+ * folded tool pages may add no visible height at all. */
19
+ export declare class ChatHistoryPrefetchGate {
20
+ private attempt;
21
+ canAttempt(scope: string, cursor: string, manual?: boolean): boolean;
22
+ attempted(scope: string, cursor: string): void;
23
+ }
@@ -0,0 +1,29 @@
1
+ /** Small transport pages; the browser fills them to a measured viewport buffer. */
2
+ export const CHAT_INITIAL_MESSAGE_COUNT = 8;
3
+ export const CHAT_HISTORY_BLOCK_BUDGET = 12;
4
+ export const CHAT_HISTORY_BYTE_BUDGET = 96 * 1024;
5
+ export const CHAT_HISTORY_BUFFER_SCREENS = 2;
6
+ /** The chat scroller is column-reverse (latest at scrollTop=0). */
7
+ export function chatHistoryBufferRemaining(geometry) {
8
+ return Math.max(0, geometry.scrollHeight - geometry.clientHeight - Math.abs(geometry.scrollTop));
9
+ }
10
+ export function needsChatHistoryBuffer(geometry) {
11
+ return geometry.clientHeight > 0
12
+ && chatHistoryBufferRemaining(geometry) < geometry.clientHeight * CHAT_HISTORY_BUFFER_SCREENS;
13
+ }
14
+ export function chatHistoryRootMargin(height) {
15
+ return `${Math.max(0, Math.ceil(height * CHAT_HISTORY_BUFFER_SCREENS))}px 0px 0px 0px`;
16
+ }
17
+ /** One attempt per cursor until progress or an explicit retry. Height decides
18
+ * how much to prefetch, never whether a successful page made progress:
19
+ * scrollHeight stays at clientHeight while the screen is underfilled, and
20
+ * folded tool pages may add no visible height at all. */
21
+ export class ChatHistoryPrefetchGate {
22
+ attempt = "";
23
+ canAttempt(scope, cursor, manual = false) {
24
+ return manual || this.attempt !== `${scope}\0${cursor}`;
25
+ }
26
+ attempted(scope, cursor) {
27
+ this.attempt = `${scope}\0${cursor}`;
28
+ }
29
+ }