@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.
- package/README.md +4 -0
- package/dist/agent-dispatch.js +5 -3
- package/dist/ai-team-runner.js +1 -0
- package/dist/ai-team-types.d.ts +1 -0
- package/dist/ai-team-types.js +6 -2
- package/dist/build-info.json +4 -4
- package/dist/cli.js +12 -6
- package/dist/config.d.ts +1 -1
- package/dist/config.js +33 -0
- package/dist/core-runner.js +1 -1
- package/dist/daemon-maintenance-state.d.ts +6 -0
- package/dist/daemon-maintenance-state.js +9 -0
- package/dist/daemon-maintenance-targets.d.ts +20 -0
- package/dist/daemon-maintenance-targets.js +183 -0
- package/dist/daemon-maintenance.d.ts +53 -0
- package/dist/daemon-maintenance.js +164 -0
- package/dist/decision-expert-employee.d.ts +6 -0
- package/dist/decision-expert-employee.js +22 -0
- package/dist/decision-expert-identity.d.ts +7 -0
- package/dist/decision-expert-identity.js +7 -0
- package/dist/decision-hardware.d.ts +19 -0
- package/dist/decision-hardware.js +19 -0
- package/dist/decision-service.d.ts +6 -1
- package/dist/decision-service.js +60 -1
- package/dist/decision-types.d.ts +1 -0
- package/dist/decision-types.js +12 -0
- package/dist/git-quick-commit.js +1 -1
- package/dist/laya-model-manifest.d.ts +11 -0
- package/dist/laya-model-manifest.js +18 -0
- package/dist/local-model-setup.d.ts +46 -0
- package/dist/local-model-setup.js +248 -0
- package/dist/local-model-types.d.ts +34 -0
- package/dist/local-model-types.js +1 -0
- package/dist/message-truncator.d.ts +4 -2
- package/dist/message-truncator.js +20 -6
- package/dist/missions.js +6 -3
- package/dist/model-file-store.d.ts +13 -0
- package/dist/model-file-store.js +108 -0
- package/dist/model-groups.js +2 -2
- package/dist/model-setup-process.d.ts +2 -0
- package/dist/model-setup-process.js +80 -0
- package/dist/models.js +3 -3
- package/dist/process-manager.js +21 -8
- package/dist/provider-catalog.d.ts +13 -1
- package/dist/provider-catalog.js +14 -2
- package/dist/render-binary.d.ts +1 -1
- package/dist/render-binary.js +4 -4
- package/dist/render-daemon-client.d.ts +12 -1
- package/dist/render-daemon-client.js +72 -4
- package/dist/render-host.d.ts +1 -0
- package/dist/render-host.js +9 -56
- package/dist/render-structured-host.d.ts +1 -0
- package/dist/render-structured-host.js +2 -2
- package/dist/server-employee-routes.js +3 -3
- package/dist/server-local-model-routes.d.ts +8 -0
- package/dist/server-local-model-routes.js +62 -0
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-session-routes.js +34 -8
- package/dist/server-settings-routes.js +2 -0
- package/dist/server-speech-routes.d.ts +8 -0
- package/dist/server-speech-routes.js +87 -0
- package/dist/server-task-routes.js +3 -1
- package/dist/server-workspace-routes.js +2 -0
- package/dist/server.js +63 -18
- package/dist/session-completion-state.d.ts +1 -0
- package/dist/session-completion-state.js +1 -0
- package/dist/session-transport.js +1 -0
- package/dist/silicon-employee-dispatch.js +5 -2
- package/dist/silicon-employee-draft.js +1 -1
- package/dist/speech-models.d.ts +24 -0
- package/dist/speech-models.js +16 -0
- package/dist/speech-service.d.ts +57 -0
- package/dist/speech-service.js +399 -0
- package/dist/speech-types.d.ts +47 -0
- package/dist/speech-types.js +3 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +13 -0
- package/dist/structured-failure.js +1 -1
- package/dist/structured-session-manager.d.ts +15 -2
- package/dist/structured-session-manager.js +58 -19
- package/dist/terminal-daemon-build.d.ts +4 -0
- package/dist/terminal-daemon-build.js +54 -0
- package/dist/terminal-daemon-protocol.d.ts +1 -1
- package/dist/terminal-daemon-server.js +20 -1
- package/dist/terminal-host.d.ts +4 -0
- package/dist/terminal-host.js +7 -4
- package/dist/tui/commands.js +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/web-ui/chat-history-window.d.ts +23 -0
- package/dist/web-ui/chat-history-window.js +29 -0
- package/dist/web-ui/content/ai-teams.js +12 -12
- package/dist/web-ui/content/scripts.js +316 -233
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/provider-identity.d.ts +28 -1
- package/dist/web-ui/provider-identity.js +31 -2
- package/dist/web-ui/session-activity.d.ts +1 -0
- package/dist/web-ui/session-activity.js +5 -2
- package/dist/web-ui/speech-audio.d.ts +2 -0
- package/dist/web-ui/speech-audio.js +41 -0
- package/dist/web-ui/vendor-loader.d.ts +2 -0
- package/dist/web-ui/vendor-loader.js +50 -0
- package/dist/ws-broadcast.js +10 -4
- package/package.json +7 -3
- package/scripts/install-laya-runtime.js +67 -0
- package/scripts/install-speech-runtime.js +82 -0
package/dist/process-manager.js
CHANGED
|
@@ -723,24 +723,32 @@ export class ProcessManager extends EventEmitter {
|
|
|
723
723
|
&& record.ptyProcess !== null
|
|
724
724
|
&& !record.ptyBridge
|
|
725
725
|
&& typeof record.processId === "number"
|
|
726
|
-
&& record.processId > 0));
|
|
726
|
+
&& record.processId > 0)).map((record) => ({ record, pid: record.processId, process: record.ptyProcess }));
|
|
727
727
|
if (candidates.length === 0)
|
|
728
728
|
return;
|
|
729
729
|
this.ptyForegroundSampling = true;
|
|
730
730
|
try {
|
|
731
|
-
const samples = await this.probePtyForegrounds(candidates.map((
|
|
731
|
+
const samples = await this.probePtyForegrounds(candidates.map(({ pid }) => pid));
|
|
732
732
|
if (!samples || this.disposed)
|
|
733
733
|
return;
|
|
734
|
-
for (const record of candidates) {
|
|
735
|
-
|
|
734
|
+
for (const { record, pid, process } of candidates) {
|
|
735
|
+
// ps is asynchronous: a stopped/replaced terminal must not inherit an old probe.
|
|
736
|
+
if (this.sessions.get(record.id) !== record || record.status !== "running"
|
|
737
|
+
|| record.ptyProcess !== process || record.processId !== pid)
|
|
736
738
|
continue;
|
|
737
|
-
const next = samples.get(
|
|
739
|
+
const next = samples.get(pid);
|
|
738
740
|
if (next === undefined || record.ptyForeground === next)
|
|
739
741
|
continue;
|
|
742
|
+
const wasForeground = record.ptyForeground === true;
|
|
740
743
|
record.ptyForeground = next;
|
|
741
744
|
// CLI 退回提示符:这一轮立刻结束,不必再等静默窗口。
|
|
742
745
|
if (!next)
|
|
743
746
|
this.closePtyTurn(record);
|
|
747
|
+
// 空白终端里的手动命令也有运行事实,但没有 provider 回合边界。
|
|
748
|
+
// 不复用 ptyBusy,避免一次前台切换冒充 AI 成功完成;静默命令也保持活动。
|
|
749
|
+
if (!record.provider && wasForeground !== next) {
|
|
750
|
+
this.emitEvent({ type: "status", sessionId: record.id, data: { ptyCommandRunning: next } });
|
|
751
|
+
}
|
|
744
752
|
}
|
|
745
753
|
}
|
|
746
754
|
catch {
|
|
@@ -1099,6 +1107,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1099
1107
|
dispose() {
|
|
1100
1108
|
if (this.disposed)
|
|
1101
1109
|
return;
|
|
1110
|
+
this.terminalHost.flush?.();
|
|
1102
1111
|
this.disposed = true;
|
|
1103
1112
|
// 静默心跳不跨服务关闭存活;每个会话的 timer 也在 cleanupRecord 里单独撤销。
|
|
1104
1113
|
this.ptyHeartbeat.dispose();
|
|
@@ -1195,7 +1204,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1195
1204
|
if (this.disposed)
|
|
1196
1205
|
throw new Error("ProcessManager has been disposed.");
|
|
1197
1206
|
if (isOpenRouterFreeSelector(opts?.model))
|
|
1198
|
-
throw new Error("免费分组仅支持
|
|
1207
|
+
throw new Error("免费分组仅支持 Pi 结构化对话。");
|
|
1199
1208
|
if (!opts?.interactiveShell)
|
|
1200
1209
|
this.assertCommandAllowed(command);
|
|
1201
1210
|
const groupProvider = opts?.interactiveShell ? undefined : opts?.provider ?? inferProviderFromCommand(command);
|
|
@@ -1239,7 +1248,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1239
1248
|
preferDefault: !requested || requested === "default",
|
|
1240
1249
|
}) : [requested ?? ""];
|
|
1241
1250
|
if (preferredModels.some(isOpenRouterFreeSelector))
|
|
1242
|
-
throw new Error("免费模型分组仅支持
|
|
1251
|
+
throw new Error("免费模型分组仅支持 Pi 结构化对话。");
|
|
1243
1252
|
const baseCwd = resolveSessionCwd(cwd, this.config.defaultCwd);
|
|
1244
1253
|
const id = opts?.reuseId || randomUUID();
|
|
1245
1254
|
// When a session is being resumed under the same id, capture its prior
|
|
@@ -1729,6 +1738,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1729
1738
|
}
|
|
1730
1739
|
/** Return only a session owned by this manager, without the SQLite fallback used by get(). */
|
|
1731
1740
|
getOwned(id) {
|
|
1741
|
+
this.terminalHost.flush?.();
|
|
1732
1742
|
const record = this.sessions.get(id);
|
|
1733
1743
|
if (!record)
|
|
1734
1744
|
return null;
|
|
@@ -1754,7 +1764,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1754
1764
|
preferDefault: !normalized || normalized === "default",
|
|
1755
1765
|
});
|
|
1756
1766
|
if (members.some(isOpenRouterFreeSelector))
|
|
1757
|
-
throw new Error("免费分组仅支持
|
|
1767
|
+
throw new Error("免费分组仅支持 Pi 结构化对话。");
|
|
1758
1768
|
record.selectedModel = normalized;
|
|
1759
1769
|
if (record.providerCliActive && record.provider === "claude" && record.status === "running" && record.ptyProcess) {
|
|
1760
1770
|
const value = members[0] && members[0] !== "default" ? members[0] : "default";
|
|
@@ -1922,6 +1932,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1922
1932
|
return this.snapshot(record);
|
|
1923
1933
|
}
|
|
1924
1934
|
getTerminalState(id) {
|
|
1935
|
+
this.terminalHost.flush?.();
|
|
1925
1936
|
const record = this.sessions.get(id);
|
|
1926
1937
|
if (!record)
|
|
1927
1938
|
return null;
|
|
@@ -2215,6 +2226,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
2215
2226
|
providerCliActive: record.providerCliActive,
|
|
2216
2227
|
providerCliExitCode: record.providerCliExitCode,
|
|
2217
2228
|
ptyBusy: record.ptyBridge ? record.ptyBridge.isResponding() : record.ptyBusy === true,
|
|
2229
|
+
ptyCommandRunning: record.status === "running" && record.ptyProcess !== null
|
|
2230
|
+
&& !record.provider && record.ptyForeground === true,
|
|
2218
2231
|
// 与 ptyBusy 同生命周期的本轮锚点。运行时事实:存储没有对应列,重启后为 null。
|
|
2219
2232
|
ptyTurnStartedAt: record.ptyTurnStartedAt ?? null,
|
|
2220
2233
|
ptyLastActivityAt: record.ptyLastActivityAt ?? null,
|
|
@@ -21,8 +21,20 @@ export declare function isNativeThinkingEffort(value: unknown): boolean;
|
|
|
21
21
|
export declare function isSessionProvider(value: unknown): value is SessionProvider;
|
|
22
22
|
/** provider 对应的 CLI 可执行文件名(Qoder 的 CLI 叫 qodercli)。 */
|
|
23
23
|
export declare function providerCliCommand(provider: SessionProvider): string;
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* provider 的展示名。缺省不认识的字符串一律回落到 "AI"。
|
|
26
|
+
*
|
|
27
|
+
* `pi` 指的是 Pi CLI(结构化 JSON / PTY 终端),与 Wand 自带的 Agent 是两条独立执行路径:
|
|
28
|
+
* 后者走进程内 SDK,展示名用 `WAND_AGENT_LABEL`,不要拿 provider 名字冒充它。
|
|
29
|
+
*/
|
|
25
30
|
export declare const PROVIDER_LABELS: Readonly<Record<SessionProvider, string>>;
|
|
31
|
+
/**
|
|
32
|
+
* Wand 自带 Agent(进程内 `core` harness / SDK)的展示名。
|
|
33
|
+
*
|
|
34
|
+
* 它和 `pi` provider 不是同一个东西:`pi` 起 Pi CLI 进程,Wand Agent 在 Wand 进程内用 SDK 跑 agent loop,
|
|
35
|
+
* 因此能力边界(会话级 Skills / MCP、CodeMode、持久化恢复)也不一样。
|
|
36
|
+
*/
|
|
37
|
+
export declare const WAND_AGENT_LABEL = "Wand Agent";
|
|
26
38
|
/**
|
|
27
39
|
* provider id 别名表:既接受用户/客户端可能写出的别名(`anthropic`、`open-code`),
|
|
28
40
|
* 也接受历史 runner 名(`claude-cli-print`、`codex-cli-exec` …),
|
package/dist/provider-catalog.js
CHANGED
|
@@ -28,16 +28,28 @@ export function isSessionProvider(value) {
|
|
|
28
28
|
export function providerCliCommand(provider) {
|
|
29
29
|
return provider === "qoder" ? "qodercli" : provider;
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* provider 的展示名。缺省不认识的字符串一律回落到 "AI"。
|
|
33
|
+
*
|
|
34
|
+
* `pi` 指的是 Pi CLI(结构化 JSON / PTY 终端),与 Wand 自带的 Agent 是两条独立执行路径:
|
|
35
|
+
* 后者走进程内 SDK,展示名用 `WAND_AGENT_LABEL`,不要拿 provider 名字冒充它。
|
|
36
|
+
*/
|
|
32
37
|
export const PROVIDER_LABELS = {
|
|
33
38
|
claude: "Claude",
|
|
34
39
|
codex: "Codex",
|
|
35
40
|
opencode: "OpenCode",
|
|
36
41
|
grok: "Grok",
|
|
37
42
|
qoder: "Qoder",
|
|
38
|
-
pi: "
|
|
43
|
+
pi: "Pi",
|
|
39
44
|
gemini: "Gemini",
|
|
40
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* Wand 自带 Agent(进程内 `core` harness / SDK)的展示名。
|
|
48
|
+
*
|
|
49
|
+
* 它和 `pi` provider 不是同一个东西:`pi` 起 Pi CLI 进程,Wand Agent 在 Wand 进程内用 SDK 跑 agent loop,
|
|
50
|
+
* 因此能力边界(会话级 Skills / MCP、CodeMode、持久化恢复)也不一样。
|
|
51
|
+
*/
|
|
52
|
+
export const WAND_AGENT_LABEL = "Wand Agent";
|
|
41
53
|
/**
|
|
42
54
|
* provider id 别名表:既接受用户/客户端可能写出的别名(`anthropic`、`open-code`),
|
|
43
55
|
* 也接受历史 runner 名(`claude-cli-print`、`codex-cli-exec` …),
|
package/dist/render-binary.d.ts
CHANGED
|
@@ -56,6 +56,6 @@ export declare function resolveRenderBinaryPath(configPath: string): string | nu
|
|
|
56
56
|
* `wand-render --version` —— 启动路径上不该无谓地执行一个进程。
|
|
57
57
|
* 两种方式都失败时返回 null:调用方只要一个展示用版本号,拿不到不能影响启动。
|
|
58
58
|
*/
|
|
59
|
-
export declare function readRenderBinaryVersion(binaryPath: string): Promise<string | null>;
|
|
59
|
+
export declare function readRenderBinaryVersion(binaryPath: string, versionFile?: string): Promise<string | null>;
|
|
60
60
|
export declare function isExecutableFile(candidate: string): boolean;
|
|
61
61
|
export {};
|
package/dist/render-binary.js
CHANGED
|
@@ -158,8 +158,8 @@ export function resolveRenderBinaryPath(configPath) {
|
|
|
158
158
|
* `wand-render --version` —— 启动路径上不该无谓地执行一个进程。
|
|
159
159
|
* 两种方式都失败时返回 null:调用方只要一个展示用版本号,拿不到不能影响启动。
|
|
160
160
|
*/
|
|
161
|
-
export function readRenderBinaryVersion(binaryPath) {
|
|
162
|
-
const sidecar = readVersionSidecar(
|
|
161
|
+
export function readRenderBinaryVersion(binaryPath, versionFile = path.join(path.dirname(binaryPath), "wand-render.version")) {
|
|
162
|
+
const sidecar = readVersionSidecar(versionFile);
|
|
163
163
|
if (sidecar)
|
|
164
164
|
return Promise.resolve(sidecar);
|
|
165
165
|
return new Promise((resolve) => {
|
|
@@ -174,9 +174,9 @@ export function readRenderBinaryVersion(binaryPath) {
|
|
|
174
174
|
});
|
|
175
175
|
});
|
|
176
176
|
}
|
|
177
|
-
function readVersionSidecar(
|
|
177
|
+
function readVersionSidecar(versionFile) {
|
|
178
178
|
try {
|
|
179
|
-
const raw = readFileSync(
|
|
179
|
+
const raw = readFileSync(versionFile, "utf8").trim();
|
|
180
180
|
const version = /^(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.]+)?)$/.exec(raw);
|
|
181
181
|
return version ? version[1] : null;
|
|
182
182
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RenderMethod } from "./render-protocol.js";
|
|
1
|
+
import { type RenderHelloResult, type RenderMethod } from "./render-protocol.js";
|
|
2
2
|
import { type TerminalAttachResult, type TerminalHost, type TerminalSessionState, type TerminalSpawnRequest } from "./terminal-host.js";
|
|
3
3
|
/**
|
|
4
4
|
* 鉴权层面的失败(与网络抖动区分)。
|
|
@@ -59,6 +59,8 @@ export declare class RenderDaemonClient implements TerminalHost {
|
|
|
59
59
|
private bodyParts;
|
|
60
60
|
private bodyBytes;
|
|
61
61
|
private frameLength;
|
|
62
|
+
private liveDataBatch;
|
|
63
|
+
private liveFlushTimer;
|
|
62
64
|
private disposed;
|
|
63
65
|
private reconnectTimer;
|
|
64
66
|
private reconnectDelayMs;
|
|
@@ -100,6 +102,12 @@ export declare class RenderDaemonClient implements TerminalHost {
|
|
|
100
102
|
* 调用它会杀掉用户的 shell。
|
|
101
103
|
*/
|
|
102
104
|
requestShutdownNow(): Promise<void>;
|
|
105
|
+
/** Fresh, authenticated inventory; never infer idleness from reconnect caches. */
|
|
106
|
+
maintenanceSnapshot(includeInventory?: boolean): Promise<{
|
|
107
|
+
hello: RenderHelloResult;
|
|
108
|
+
sessions: TerminalSessionState[] | null;
|
|
109
|
+
}>;
|
|
110
|
+
requestShutdownDrain(): Promise<void>;
|
|
103
111
|
connect(): Promise<void>;
|
|
104
112
|
private open;
|
|
105
113
|
/**
|
|
@@ -142,6 +150,9 @@ export declare class RenderDaemonClient implements TerminalHost {
|
|
|
142
150
|
private handleFrame;
|
|
143
151
|
private handleResponse;
|
|
144
152
|
private routeEvent;
|
|
153
|
+
private queueLiveData;
|
|
154
|
+
flush(): void;
|
|
155
|
+
private flushLiveData;
|
|
145
156
|
private queuePendingEvent;
|
|
146
157
|
/** Repair a bounded event-buffer overflow without replaying a partial stream. */
|
|
147
158
|
private refreshSession;
|
|
@@ -5,7 +5,7 @@ import { startDaemonHeartbeat, waitForDaemonSocket } from "./daemon-connection.j
|
|
|
5
5
|
import { getErrorMessage } from "./error-utils.js";
|
|
6
6
|
import { appendWindow, PTY_OUTPUT_MAX_SIZE } from "./pty-text-utils.js";
|
|
7
7
|
import { MAX_FRAME_BYTES, RENDER_PROTOCOL_VERSION, encodeRenderFrame, renderPaths, } from "./render-protocol.js";
|
|
8
|
-
import { appendTerminalChunkWindow, } from "./terminal-host.js";
|
|
8
|
+
import { appendTerminalChunkBatch, appendTerminalChunkWindow, } from "./terminal-host.js";
|
|
9
9
|
const RECONNECT_INITIAL_MS = 500;
|
|
10
10
|
const RECONNECT_MAX_MS = 10_000;
|
|
11
11
|
/**
|
|
@@ -19,6 +19,10 @@ const AUTH_FAILURE_RETRY_MS = 60_000;
|
|
|
19
19
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
20
20
|
/** 未挂上 listener 时的缓存上限,与 legacy 客户端保持一致。 */
|
|
21
21
|
const MAX_PENDING_EVENTS = 512;
|
|
22
|
+
/** Tiny fixed window bounds projection cost even when IPC reads arrive one by one. */
|
|
23
|
+
const LIVE_BATCH_FLUSH_MS = 4;
|
|
24
|
+
const LIVE_BATCH_MAX_CHARS = 64 * 1024;
|
|
25
|
+
const LIVE_BATCH_MAX_EVENTS = 1024;
|
|
22
26
|
/**
|
|
23
27
|
* 鉴权层面的失败(与网络抖动区分)。
|
|
24
28
|
*
|
|
@@ -164,6 +168,8 @@ export class RenderDaemonClient {
|
|
|
164
168
|
bodyParts = [];
|
|
165
169
|
bodyBytes = 0;
|
|
166
170
|
frameLength = null;
|
|
171
|
+
liveDataBatch = null;
|
|
172
|
+
liveFlushTimer = null;
|
|
167
173
|
disposed = false;
|
|
168
174
|
reconnectTimer = null;
|
|
169
175
|
reconnectDelayMs = RECONNECT_INITIAL_MS;
|
|
@@ -218,6 +224,15 @@ export class RenderDaemonClient {
|
|
|
218
224
|
async requestShutdownNow() {
|
|
219
225
|
await this.request("shutdown", { mode: "now" });
|
|
220
226
|
}
|
|
227
|
+
/** Fresh, authenticated inventory; never infer idleness from reconnect caches. */
|
|
228
|
+
async maintenanceSnapshot(includeInventory = true) {
|
|
229
|
+
const hello = await this.request("hello");
|
|
230
|
+
const sessions = includeInventory ? await this.listSessions() : null;
|
|
231
|
+
return { hello, sessions };
|
|
232
|
+
}
|
|
233
|
+
async requestShutdownDrain() {
|
|
234
|
+
await this.request("shutdown", { mode: "drain" });
|
|
235
|
+
}
|
|
221
236
|
connect() {
|
|
222
237
|
if (this.disposed)
|
|
223
238
|
return Promise.reject(new Error("Render client disposed"));
|
|
@@ -367,6 +382,7 @@ export class RenderDaemonClient {
|
|
|
367
382
|
state.rows = rows;
|
|
368
383
|
}
|
|
369
384
|
attach(sessionId, afterSeq = 0) {
|
|
385
|
+
this.flushLiveData();
|
|
370
386
|
const state = this.inventory.get(sessionId);
|
|
371
387
|
if (!state)
|
|
372
388
|
return null;
|
|
@@ -409,6 +425,7 @@ export class RenderDaemonClient {
|
|
|
409
425
|
* 这是 Server / Render 分离的核心 —— 下一次启动 attach 回来仍拿到同一批会话。
|
|
410
426
|
*/
|
|
411
427
|
disconnect() {
|
|
428
|
+
this.flushLiveData();
|
|
412
429
|
this.disposed = true;
|
|
413
430
|
this.stopHeartbeat?.();
|
|
414
431
|
this.stopHeartbeat = null;
|
|
@@ -618,6 +635,7 @@ export class RenderDaemonClient {
|
|
|
618
635
|
this.routeEvent(message);
|
|
619
636
|
}
|
|
620
637
|
handleResponse(message) {
|
|
638
|
+
this.flushLiveData();
|
|
621
639
|
if (typeof message.id !== "number")
|
|
622
640
|
return;
|
|
623
641
|
const pending = this.pending.get(message.id);
|
|
@@ -634,6 +652,8 @@ export class RenderDaemonClient {
|
|
|
634
652
|
pending.reject(new Error(`Render ${pending.method} failed (${code})${detail ? `: ${detail}` : ""}`));
|
|
635
653
|
}
|
|
636
654
|
routeEvent(event) {
|
|
655
|
+
if (event.event !== "data")
|
|
656
|
+
this.flushLiveData();
|
|
637
657
|
if (this.reconciling) {
|
|
638
658
|
if (event.event === "reconcile") {
|
|
639
659
|
this.reconcileDuringSync = event;
|
|
@@ -670,13 +690,16 @@ export class RenderDaemonClient {
|
|
|
670
690
|
if (event.seq <= state.seq)
|
|
671
691
|
return;
|
|
672
692
|
if (event.seq > state.seq + 1) {
|
|
693
|
+
this.flushLiveData();
|
|
673
694
|
this.queuePendingEvent(sessionId, event);
|
|
674
695
|
void this.refreshSession(sessionId, handle);
|
|
675
696
|
return;
|
|
676
697
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
state.
|
|
698
|
+
// Validate every wire sequence independently. Only local projection is
|
|
699
|
+
// batched; attach/reconnect still retain the original per-frame cursors.
|
|
700
|
+
state.seq = event.seq;
|
|
701
|
+
this.queueLiveData(state, handle, { data: event.data, seq: event.seq });
|
|
702
|
+
return;
|
|
680
703
|
}
|
|
681
704
|
else if (event.event === "exit") {
|
|
682
705
|
state.status = "exited";
|
|
@@ -684,6 +707,7 @@ export class RenderDaemonClient {
|
|
|
684
707
|
}
|
|
685
708
|
}
|
|
686
709
|
if (event.event === "data" && typeof event.data === "string" && typeof event.seq === "number") {
|
|
710
|
+
this.flushLiveData();
|
|
687
711
|
handle.acceptData({ data: event.data, seq: event.seq });
|
|
688
712
|
}
|
|
689
713
|
else if (event.event === "exit") {
|
|
@@ -691,6 +715,49 @@ export class RenderDaemonClient {
|
|
|
691
715
|
this.handles.delete(sessionId);
|
|
692
716
|
}
|
|
693
717
|
}
|
|
718
|
+
queueLiveData(state, handle, event) {
|
|
719
|
+
if (this.liveDataBatch && (this.liveDataBatch.state !== state || this.liveDataBatch.handle !== handle)) {
|
|
720
|
+
this.flushLiveData();
|
|
721
|
+
}
|
|
722
|
+
const batch = this.liveDataBatch ?? { state, handle, events: [], chars: 0 };
|
|
723
|
+
this.liveDataBatch = batch;
|
|
724
|
+
batch.events.push(event);
|
|
725
|
+
batch.chars += event.data.length;
|
|
726
|
+
if (batch.chars >= LIVE_BATCH_MAX_CHARS || batch.events.length >= LIVE_BATCH_MAX_EVENTS) {
|
|
727
|
+
this.flushLiveData();
|
|
728
|
+
}
|
|
729
|
+
else if (!this.liveFlushTimer) {
|
|
730
|
+
// IPC can deliver many tiny reads on successive turns. A check-phase
|
|
731
|
+
// flush alone still projects each read on a busy installed Server. Keep
|
|
732
|
+
// the first read's deadline (never reset on more data): continuous output
|
|
733
|
+
// cannot starve, while idle echo pays at most one 4ms window.
|
|
734
|
+
this.liveFlushTimer = setTimeout(() => {
|
|
735
|
+
this.liveFlushTimer = null;
|
|
736
|
+
this.flushLiveData();
|
|
737
|
+
}, LIVE_BATCH_FLUSH_MS);
|
|
738
|
+
this.liveFlushTimer.unref();
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
flush() {
|
|
742
|
+
this.flushLiveData();
|
|
743
|
+
}
|
|
744
|
+
flushLiveData() {
|
|
745
|
+
if (this.liveFlushTimer) {
|
|
746
|
+
clearTimeout(this.liveFlushTimer);
|
|
747
|
+
this.liveFlushTimer = null;
|
|
748
|
+
}
|
|
749
|
+
const batch = this.liveDataBatch;
|
|
750
|
+
if (!batch)
|
|
751
|
+
return;
|
|
752
|
+
this.liveDataBatch = null;
|
|
753
|
+
if (this.disposed || this.handles.get(batch.state.sessionId) !== batch.handle
|
|
754
|
+
|| this.inventory.get(batch.state.sessionId) !== batch.state)
|
|
755
|
+
return;
|
|
756
|
+
const data = batch.events.map(event => event.data).join("");
|
|
757
|
+
batch.state.output = appendWindow(batch.state.output, data, PTY_OUTPUT_MAX_SIZE);
|
|
758
|
+
batch.state.chunks = appendTerminalChunkBatch(batch.state.chunks, batch.events);
|
|
759
|
+
batch.handle.acceptData({ data, seq: batch.events[batch.events.length - 1].seq });
|
|
760
|
+
}
|
|
694
761
|
queuePendingEvent(sessionId, event) {
|
|
695
762
|
const buffered = this.pendingEvents.get(sessionId) ?? [];
|
|
696
763
|
buffered.push(event);
|
|
@@ -770,6 +837,7 @@ export class RenderDaemonClient {
|
|
|
770
837
|
handleDisconnect(origin) {
|
|
771
838
|
if (this.socket !== origin)
|
|
772
839
|
return;
|
|
840
|
+
this.flushLiveData();
|
|
773
841
|
this.socket = null;
|
|
774
842
|
this.stopHeartbeat?.();
|
|
775
843
|
this.stopHeartbeat = null;
|
package/dist/render-host.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export declare class CompositeTerminalHost implements TerminalHost {
|
|
|
47
47
|
attach(sessionId: string, afterSeq?: number): TerminalAttachResult | null;
|
|
48
48
|
createOrAttach(request: TerminalSpawnRequest, afterSeq?: number): Promise<TerminalAttachResult>;
|
|
49
49
|
forget(sessionId: string): void;
|
|
50
|
+
flush(): void;
|
|
50
51
|
/**
|
|
51
52
|
* 两侧都只是解绑:legacy daemon 可能还持有升级前的旧 PTY,Render 更是必须
|
|
52
53
|
* 跨 Server 重启继续活着。**这里绝不能 kill 任何东西。**
|
package/dist/render-host.js
CHANGED
|
@@ -13,8 +13,6 @@ import { InProcessTerminalHost, } from "./terminal-host.js";
|
|
|
13
13
|
const RENDER_READY_TIMEOUT_MS = 5_000;
|
|
14
14
|
/** 自愈 revive 的最小间隔:重连退避可能很密集,窗口内只 spawn 一次。 */
|
|
15
15
|
const RENDER_REVIVE_BACKOFF_MS = 5_000;
|
|
16
|
-
/** 空闲 daemon 升级时等它退出的上限(超过就当升级失败,交给上层决定)。 */
|
|
17
|
-
const RENDER_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
18
16
|
/** 陈留 socket 的二次确认间隔(避开「刚 bind、还没 accept」的启动窗口)。 */
|
|
19
17
|
const RENDER_STALE_PROBE_DELAY_MS = 250;
|
|
20
18
|
/**
|
|
@@ -109,6 +107,10 @@ export class CompositeTerminalHost {
|
|
|
109
107
|
this.legacy?.forget(sessionId);
|
|
110
108
|
this.render.forget(sessionId);
|
|
111
109
|
}
|
|
110
|
+
flush() {
|
|
111
|
+
this.legacy?.flush?.();
|
|
112
|
+
this.render.flush?.();
|
|
113
|
+
}
|
|
112
114
|
/**
|
|
113
115
|
* 两侧都只是解绑:legacy daemon 可能还持有升级前的旧 PTY,Render 更是必须
|
|
114
116
|
* 跨 Server 重启继续活着。**这里绝不能 kill 任何东西。**
|
|
@@ -234,8 +236,8 @@ export async function createUpgradeAwareTerminalHost(configPath, options = {}) {
|
|
|
234
236
|
*/
|
|
235
237
|
export async function createRenderTerminalHost(configPath, options = {}) {
|
|
236
238
|
const binaryPath = options.binaryPath ?? resolveRenderBinaryPath(configPath);
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
+
// Adoption never restarts an owner. The server maintenance worker waits for
|
|
240
|
+
// every execution family and closes admission before replacing idle daemons.
|
|
239
241
|
const paths = renderPaths(configPath);
|
|
240
242
|
const endpointExists = process.platform !== "win32" && existsSync(paths.socketPath);
|
|
241
243
|
// 已经 adopt 过的客户端需要能自己把 daemon 拉回来:adopt 完 binaryPath 就丢了,
|
|
@@ -244,12 +246,12 @@ export async function createRenderTerminalHost(configPath, options = {}) {
|
|
|
244
246
|
if (endpointExists) {
|
|
245
247
|
const adopted = await connectExistingRenderClient(configPath, options.knownSessionIds, reviveDaemon);
|
|
246
248
|
if (adopted)
|
|
247
|
-
return
|
|
249
|
+
return adopted;
|
|
248
250
|
if (await isSocketListening(paths.socketPath)) {
|
|
249
251
|
// 有人监听却拒绝 adopt(协议版本不符 / token 不符):这是错误配置,必须报出来而不是绕开。
|
|
250
252
|
const starting = await waitForRender(configPath, RENDER_READY_TIMEOUT_MS, options.knownSessionIds, reviveDaemon);
|
|
251
253
|
if (starting)
|
|
252
|
-
return
|
|
254
|
+
return starting;
|
|
253
255
|
throw new Error(`A Render daemon is listening on ${paths.socketPath} but rejected adoption. Refusing to spawn a competing Render; ` +
|
|
254
256
|
"check the protocol version/token of the running binary, or stop it first.");
|
|
255
257
|
}
|
|
@@ -268,7 +270,7 @@ export async function createRenderTerminalHost(configPath, options = {}) {
|
|
|
268
270
|
if (livePid !== null) {
|
|
269
271
|
const starting = await waitForRender(configPath, RENDER_READY_TIMEOUT_MS, options.knownSessionIds, reviveDaemon);
|
|
270
272
|
if (starting)
|
|
271
|
-
return
|
|
273
|
+
return starting;
|
|
272
274
|
throw new Error(`Render process ${livePid} is alive but its socket ${paths.socketPath} is unavailable; refusing to spawn a second daemon.`);
|
|
273
275
|
}
|
|
274
276
|
if (!binaryPath)
|
|
@@ -286,55 +288,6 @@ export function shouldUpgradeRenderDaemon(daemonVersion, binaryVersion, sessionC
|
|
|
286
288
|
return false;
|
|
287
289
|
return sessionCount === 0;
|
|
288
290
|
}
|
|
289
|
-
/**
|
|
290
|
-
* 装好的二进制比正在跑的 daemon 新时怎么办。
|
|
291
|
-
*
|
|
292
|
-
* npm 升级 / `./start.sh` 都不会重启正在跑的 daemon(不能杀别人的 PTY),所以升级后
|
|
293
|
-
* 一直在跑旧代码 —— 2026-09-24 的线上事故就是这么来的:端点自愈是 9/23 21:26 补上的,
|
|
294
|
-
* 但 terminald 是 9/19 启的老进程、Render 是 9/23 10:49 的老二进制,socket 被临时目录
|
|
295
|
-
* 清理器删掉后永远回不来。
|
|
296
|
-
*
|
|
297
|
-
* 所以:daemon 空闲(没有 PTY)就直接把它换到新二进制上 —— 升级完就好;
|
|
298
|
-
* 还有会话就只大声告警,让人在会话结束后跑 `./start.sh --restart-daemons`。
|
|
299
|
-
*/
|
|
300
|
-
async function upgradeRenderDaemonIfIdle(configPath, binaryPath, knownSessionIds, client) {
|
|
301
|
-
if (!binaryPath)
|
|
302
|
-
return client;
|
|
303
|
-
const daemonVersion = client.version;
|
|
304
|
-
if (!daemonVersion)
|
|
305
|
-
return client;
|
|
306
|
-
const binaryVersion = await readRenderBinaryVersion(binaryPath);
|
|
307
|
-
if (!binaryVersion || compareSemver(daemonVersion, binaryVersion) >= 0)
|
|
308
|
-
return client;
|
|
309
|
-
const sessions = client.sessionCount;
|
|
310
|
-
if (!shouldUpgradeRenderDaemon(daemonVersion, binaryVersion, sessions)) {
|
|
311
|
-
process.stderr.write(`[wand] WARNING: Render daemon ${daemonVersion} is older than the installed binary ${binaryVersion} but still owns ` +
|
|
312
|
-
`${sessions} PTY session(s); it keeps running the old code. Restart it when those sessions are done: ./start.sh --restart-daemons\n`);
|
|
313
|
-
return client;
|
|
314
|
-
}
|
|
315
|
-
try {
|
|
316
|
-
await client.requestShutdownNow();
|
|
317
|
-
client.disconnect();
|
|
318
|
-
await waitForRenderProcessExit(renderPaths(configPath).pidPath, RENDER_SHUTDOWN_TIMEOUT_MS);
|
|
319
|
-
const upgraded = await spawnDetachedRender(configPath, binaryPath, knownSessionIds);
|
|
320
|
-
process.stderr.write(`[wand] Render daemon upgraded ${daemonVersion} -> ${binaryVersion} (it was idle).\n`);
|
|
321
|
-
return upgraded;
|
|
322
|
-
}
|
|
323
|
-
catch (error) {
|
|
324
|
-
// 已经让它收摊了:回不去旧 client,报清楚并让上层(auto 模式)回退 legacy。
|
|
325
|
-
throw new Error(`Render daemon ${daemonVersion} was stopped for an upgrade to ${binaryVersion} but the replacement did not come up: ` +
|
|
326
|
-
getErrorMessage(error));
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
/** 等旧 daemon 真的退出(新 daemon 会因为 pid 活着而拒绝启动)。 */
|
|
330
|
-
async function waitForRenderProcessExit(pidPath, timeoutMs) {
|
|
331
|
-
const deadline = Date.now() + timeoutMs;
|
|
332
|
-
while (Date.now() < deadline) {
|
|
333
|
-
if (readLiveRenderPid(pidPath) === null)
|
|
334
|
-
return;
|
|
335
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
291
|
/**
|
|
339
292
|
* detached spawn(stdio 忽略、unref),返回子进程 pid。
|
|
340
293
|
*
|
|
@@ -3,6 +3,7 @@ import type { StructuredExecHost, StructuredExecProcess, StructuredRunState, Str
|
|
|
3
3
|
type Owner = "legacy" | "rust";
|
|
4
4
|
/** Resolve the separate v2 executable; do not confuse it with PTY v1 wand-render. */
|
|
5
5
|
export declare function resolveStructuredRenderBinary(configPath: string): string | null;
|
|
6
|
+
export declare function startStructuredRenderHost(configPath: string): Promise<RenderStructuredClient>;
|
|
6
7
|
/**
|
|
7
8
|
* A stable runId can belong to exactly one persistent daemon. Configuration
|
|
8
9
|
* chooses only new runs; existing owners are always resolved by inventories.
|
|
@@ -81,7 +81,7 @@ async function connectExisting(configPath) {
|
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
async function
|
|
84
|
+
export async function startStructuredRenderHost(configPath) {
|
|
85
85
|
const existing = await connectExisting(configPath);
|
|
86
86
|
if (existing)
|
|
87
87
|
return existing;
|
|
@@ -211,7 +211,7 @@ export async function createUpgradeAwareStructuredHost(configPath, legacy, proce
|
|
|
211
211
|
if (process.env.WAND_TEST_MODE === "1" || process.env.NODE_TEST_CONTEXT) {
|
|
212
212
|
return { host: legacy ?? undefined, rustClient: null };
|
|
213
213
|
}
|
|
214
|
-
const rustClient = processHost === "rust" ? await
|
|
214
|
+
const rustClient = processHost === "rust" ? await startStructuredRenderHost(configPath) : await connectExisting(configPath);
|
|
215
215
|
if (!rustClient && !legacy)
|
|
216
216
|
return { host: undefined, rustClient: null };
|
|
217
217
|
return { host: new CompositeStructuredExecHost(legacy, rustClient, processHost), rustClient };
|
|
@@ -107,7 +107,7 @@ export function parseSystemEmployeeAgents(value, existing) {
|
|
|
107
107
|
const agents = body.agents.map((rawAgent) => {
|
|
108
108
|
const parsed = parseTaskAgent(rawAgent ?? {});
|
|
109
109
|
if (!parsed)
|
|
110
|
-
throw new Error("请选择有效的 CLI 工具或
|
|
110
|
+
throw new Error("请选择有效的 CLI 工具或 Wand Agent。");
|
|
111
111
|
if (parsed.kind !== "structured")
|
|
112
112
|
throw new Error("硅基员工只支持结构化会话。");
|
|
113
113
|
return parsed;
|
|
@@ -136,7 +136,7 @@ export function parseSiliconEmployeeInput(value, existing, now) {
|
|
|
136
136
|
agents = body.agents.map((rawAgent) => {
|
|
137
137
|
const parsed = parseTaskAgent(rawAgent ?? {});
|
|
138
138
|
if (!parsed)
|
|
139
|
-
throw new Error("请选择有效的 CLI 工具或
|
|
139
|
+
throw new Error("请选择有效的 CLI 工具或 Wand Agent。");
|
|
140
140
|
if (parsed.kind !== "structured")
|
|
141
141
|
throw new Error("硅基员工只支持结构化会话。");
|
|
142
142
|
return parsed;
|
|
@@ -148,7 +148,7 @@ export function parseSiliconEmployeeInput(value, existing, now) {
|
|
|
148
148
|
else if (body.agent !== undefined && body.agent !== null) {
|
|
149
149
|
const singleAgent = parseTaskAgent(body.agent);
|
|
150
150
|
if (!singleAgent)
|
|
151
|
-
throw new Error("请选择有效的 CLI 工具或
|
|
151
|
+
throw new Error("请选择有效的 CLI 工具或 Wand Agent。");
|
|
152
152
|
if (singleAgent.kind !== "structured")
|
|
153
153
|
throw new Error("硅基员工只支持结构化会话。");
|
|
154
154
|
agents = [singleAgent];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Express, RequestHandler } from "express";
|
|
2
|
+
import type { LocalModelSetupService } from "./local-model-setup.js";
|
|
3
|
+
/** Behind generic /api authentication; inference-only capabilities cannot install dependencies. */
|
|
4
|
+
export declare function registerLocalModelRoutes(app: Express, deps: {
|
|
5
|
+
models: LocalModelSetupService;
|
|
6
|
+
requireSessions: RequestHandler;
|
|
7
|
+
requireAdmin: RequestHandler;
|
|
8
|
+
}): void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DecisionError } from "./decision-types.js";
|
|
2
|
+
import { SpeechError } from "./speech-service.js";
|
|
3
|
+
/** Behind generic /api authentication; inference-only capabilities cannot install dependencies. */
|
|
4
|
+
export function registerLocalModelRoutes(app, deps) {
|
|
5
|
+
app.use("/api/local-models", deps.requireSessions, (_req, res, next) => { res.set("Cache-Control", "no-store"); next(); });
|
|
6
|
+
const kind = (req, res, next) => {
|
|
7
|
+
if (req.params.kind !== "laya" && req.params.kind !== "speech") {
|
|
8
|
+
res.status(404).json({ error: "未知本地模型。" });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
next();
|
|
12
|
+
};
|
|
13
|
+
app.get("/api/local-models/status", async (_req, res, next) => {
|
|
14
|
+
try {
|
|
15
|
+
res.json(await deps.models.status());
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
next(error);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
for (const action of ["download", "initialize"]) {
|
|
22
|
+
app.post(`/api/local-models/:kind/${action}`, deps.requireAdmin, kind, async (req, res, next) => {
|
|
23
|
+
try {
|
|
24
|
+
deps.models.start(req.params.kind, action, req.body ?? {});
|
|
25
|
+
res.status(202).json(await deps.models.status());
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
next(error);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
app.post("/api/local-models/:kind/cancel", deps.requireAdmin, kind, async (req, res, next) => {
|
|
33
|
+
try {
|
|
34
|
+
if (req.body && Object.keys(req.body).length)
|
|
35
|
+
throw new DecisionError("INVALID_REQUEST", "取消操作不接受命令参数。");
|
|
36
|
+
deps.models.cancel(req.params.kind);
|
|
37
|
+
res.status(202).json(await deps.models.status());
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
next(error);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
app.patch("/api/local-models/laya/settings", deps.requireAdmin, async (req, res, next) => {
|
|
44
|
+
try {
|
|
45
|
+
if (!req.body || Object.keys(req.body).length !== 1 || typeof req.body.enabled !== "boolean")
|
|
46
|
+
throw new DecisionError("INVALID_REQUEST", "仅允许修改 LAYA 启用状态;路径由受信任安装入口确定。");
|
|
47
|
+
deps.models.setLayaEnabled(req.body.enabled);
|
|
48
|
+
res.json(await deps.models.status());
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
next(error);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const errors = (error, _req, res, _next) => {
|
|
55
|
+
if (error instanceof DecisionError || error instanceof SpeechError) {
|
|
56
|
+
res.status(error.status).json({ error: error.message, code: error.code });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
res.status(500).json({ error: "本地模型管理失败,请刷新状态后重试。", code: "SETUP_FAILED" });
|
|
60
|
+
};
|
|
61
|
+
app.use("/api/local-models", errors);
|
|
62
|
+
}
|