@co0ontty/wand 4.72.2-beta.g52f54af → 4.72.2-beta.g7ff2192
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/config.js +2 -0
- package/dist/daemon-connection.d.ts +9 -0
- package/dist/daemon-connection.js +86 -0
- package/dist/distribution-manager.d.ts +5 -0
- package/dist/distribution-manager.js +79 -1
- package/dist/git-quick-commit.js +12 -0
- package/dist/native/darwin-arm64/wand-render +0 -0
- package/dist/native/darwin-arm64/wand-render.sha256 +1 -1
- package/dist/native/darwin-arm64/wand-render.version +1 -1
- package/dist/native/darwin-x64/wand-render +0 -0
- package/dist/native/darwin-x64/wand-render.sha256 +1 -0
- package/dist/native/darwin-x64/wand-render.version +1 -0
- package/dist/native/linux-arm64/wand-render +0 -0
- package/dist/native/linux-arm64/wand-render.sha256 +1 -0
- package/dist/native/linux-arm64/wand-render.version +1 -0
- package/dist/native/linux-x64/wand-render +0 -0
- package/dist/native/linux-x64/wand-render.sha256 +1 -0
- package/dist/native/linux-x64/wand-render.version +1 -0
- package/dist/process-manager.d.ts +10 -0
- package/dist/process-manager.js +73 -4
- package/dist/render-daemon-client.d.ts +20 -2
- package/dist/render-daemon-client.js +264 -52
- package/dist/render-host.d.ts +6 -1
- package/dist/render-host.js +47 -16
- package/dist/render-structured-client.d.ts +80 -0
- package/dist/render-structured-client.js +513 -0
- package/dist/render-structured-host.d.ts +31 -0
- package/dist/render-structured-host.js +218 -0
- package/dist/render-structured-protocol.d.ts +101 -0
- package/dist/render-structured-protocol.js +35 -0
- package/dist/server-session-routes.js +30 -3
- package/dist/server-task-routes.js +52 -3
- package/dist/server-update-routes.d.ts +2 -0
- package/dist/server-update-routes.js +44 -0
- package/dist/server.js +21 -22
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +14 -5
- package/dist/structured-client-protocol.js +39 -22
- package/dist/structured-exec-host.js +7 -1
- package/dist/structured-exec-pump.js +6 -2
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +25 -1
- package/dist/task-types.d.ts +2 -0
- package/dist/terminal-daemon-client.d.ts +9 -2
- package/dist/terminal-daemon-client.js +119 -46
- package/dist/terminal-daemon-server.js +20 -4
- package/dist/terminal-host.d.ts +6 -0
- package/dist/terminal-host.js +3 -0
- package/dist/tui/ipc-server.js +12 -0
- package/dist/types.d.ts +7 -1
- package/dist/unix-socket-keepalive.d.ts +3 -0
- package/dist/unix-socket-keepalive.js +94 -0
- package/dist/web-ui/content/scripts.js +68 -68
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.d.ts +1 -1
- package/dist/ws-broadcast.js +13 -1
- package/package.json +2 -1
- package/scripts/stage-render-binaries.js +52 -6
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-09-
|
|
4
|
-
"version": "4.72.2-beta.
|
|
2
|
+
"commit": "7ff219242659cdf0ec264845fbba73b7bbf8561f",
|
|
3
|
+
"builtAt": "2026-09-23T13:28:16.101Z",
|
|
4
|
+
"version": "4.72.2-beta.g7ff2192",
|
|
5
5
|
"channel": "beta"
|
|
6
6
|
}
|
package/dist/config.js
CHANGED
|
@@ -92,6 +92,7 @@ export const defaultConfig = () => ({
|
|
|
92
92
|
macos: defaultMacosDmgConfig(),
|
|
93
93
|
ios: defaultIosIpaConfig(),
|
|
94
94
|
render: defaultRenderConfig(),
|
|
95
|
+
structured: { processHost: "legacy" },
|
|
95
96
|
cardDefaults: defaultCardExpandDefaults(),
|
|
96
97
|
defaultModel: "",
|
|
97
98
|
defaultCodexModel: "",
|
|
@@ -755,6 +756,7 @@ function mergeWithDefaults(input) {
|
|
|
755
756
|
macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
|
|
756
757
|
ios: normalizeIosIpaConfig(input.ios) ?? defaults.ios,
|
|
757
758
|
render: normalizeRenderConfig(input.render) ?? defaults.render,
|
|
759
|
+
structured: { processHost: input.structured?.processHost === "rust" ? "rust" : "legacy" },
|
|
758
760
|
cardDefaults: normalizeCardDefaults(input.cardDefaults),
|
|
759
761
|
defaultProvider: isSessionProvider(input.defaultProvider) ? input.defaultProvider : "claude",
|
|
760
762
|
defaultSessionKind: input.defaultSessionKind === "pty" ? "pty" : "structured",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type net from "node:net";
|
|
2
|
+
/** Bound connect attempts, including sockets that close before the handshake. */
|
|
3
|
+
export declare function waitForDaemonSocket(socket: net.Socket): Promise<void>;
|
|
4
|
+
/** Probe the application, not just the fd: a half-open socket may still be writable. */
|
|
5
|
+
export declare function startDaemonHeartbeat(socket: net.Socket, probe: () => Promise<unknown>, options?: {
|
|
6
|
+
socketPath?: string;
|
|
7
|
+
intervalMs?: number;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}): () => void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { lstatSync, utimesSync } from "node:fs";
|
|
2
|
+
const CONNECT_TIMEOUT_MS = 10_000;
|
|
3
|
+
const HEARTBEAT_INTERVAL_MS = 15_000;
|
|
4
|
+
const HEARTBEAT_TIMEOUT_MS = 5_000;
|
|
5
|
+
/** Bound connect attempts, including sockets that close before the handshake. */
|
|
6
|
+
export function waitForDaemonSocket(socket) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const cleanup = () => {
|
|
9
|
+
clearTimeout(timer);
|
|
10
|
+
socket.off("connect", onConnect);
|
|
11
|
+
socket.off("error", onError);
|
|
12
|
+
socket.off("close", onClose);
|
|
13
|
+
};
|
|
14
|
+
const onConnect = () => { cleanup(); resolve(); };
|
|
15
|
+
const onError = (error) => { cleanup(); socket.destroy(); reject(error); };
|
|
16
|
+
const onClose = () => onError(new Error("Daemon socket closed while connecting"));
|
|
17
|
+
const timer = setTimeout(() => onError(new Error("Daemon socket connect timed out")), CONNECT_TIMEOUT_MS);
|
|
18
|
+
socket.once("connect", onConnect);
|
|
19
|
+
socket.once("error", onError);
|
|
20
|
+
socket.once("close", onClose);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** Probe the application, not just the fd: a half-open socket may still be writable. */
|
|
24
|
+
export function startDaemonHeartbeat(socket, probe, options = {}) {
|
|
25
|
+
const { socketPath, intervalMs = HEARTBEAT_INTERVAL_MS, timeoutMs = HEARTBEAT_TIMEOUT_MS } = options;
|
|
26
|
+
// Old daemons have no listener watchdog. Refresh their existing endpoint's
|
|
27
|
+
// timestamps after a successful probe so idle /tmp cleanup won't remove it.
|
|
28
|
+
const identity = (() => {
|
|
29
|
+
if (!socketPath || process.platform === "win32")
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const stat = lstatSync(socketPath);
|
|
33
|
+
return stat.isSocket() && (typeof process.getuid !== "function" || stat.uid === process.getuid())
|
|
34
|
+
? stat : null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
let lastTouch = 0;
|
|
41
|
+
let stopped = false;
|
|
42
|
+
let inFlight = false;
|
|
43
|
+
let deadline = null;
|
|
44
|
+
const stop = () => {
|
|
45
|
+
stopped = true;
|
|
46
|
+
clearInterval(timer);
|
|
47
|
+
if (deadline)
|
|
48
|
+
clearTimeout(deadline);
|
|
49
|
+
socket.off("close", stop);
|
|
50
|
+
};
|
|
51
|
+
const fail = () => {
|
|
52
|
+
if (stopped)
|
|
53
|
+
return;
|
|
54
|
+
stop();
|
|
55
|
+
// Destroy this generation only; its close handler performs normal replay
|
|
56
|
+
// and reconnect. Never retry mutating RPCs or terminate the daemon.
|
|
57
|
+
socket.destroy(new Error("Daemon heartbeat failed; reconnecting"));
|
|
58
|
+
};
|
|
59
|
+
const timer = setInterval(() => {
|
|
60
|
+
if (stopped || inFlight || socket.destroyed)
|
|
61
|
+
return;
|
|
62
|
+
inFlight = true;
|
|
63
|
+
deadline = setTimeout(fail, timeoutMs);
|
|
64
|
+
deadline.unref();
|
|
65
|
+
void Promise.resolve().then(() => stopped ? undefined : probe()).then(() => {
|
|
66
|
+
if (deadline)
|
|
67
|
+
clearTimeout(deadline);
|
|
68
|
+
deadline = null;
|
|
69
|
+
inFlight = false;
|
|
70
|
+
if (stopped || !identity || !socketPath || Date.now() - lastTouch < 60_000)
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
const stat = lstatSync(socketPath);
|
|
74
|
+
if (stat.dev === identity.dev && stat.ino === identity.ino) {
|
|
75
|
+
const now = new Date();
|
|
76
|
+
utimesSync(socketPath, now, now);
|
|
77
|
+
lastTouch = Date.now();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch { /* The owning daemon repairs a missing path; never replace it here. */ }
|
|
81
|
+
}, fail);
|
|
82
|
+
}, intervalMs);
|
|
83
|
+
timer.unref();
|
|
84
|
+
socket.once("close", stop);
|
|
85
|
+
return stop;
|
|
86
|
+
}
|
|
@@ -82,6 +82,11 @@ export declare class DistributionManager {
|
|
|
82
82
|
deleted: string[];
|
|
83
83
|
freedBytes: number;
|
|
84
84
|
}>;
|
|
85
|
+
/** Beta 仅从当前服务端的 macOS 分发目录选择,绝不回退 GitHub。 */
|
|
86
|
+
resolveLatestMacosBeta(): Promise<ResolvedDistributionAsset | null>;
|
|
87
|
+
/** 固定下载文件名,避免检查与下载之间新增包后下载到另一个版本。 */
|
|
88
|
+
resolveMacosBetaDownload(fileName: string): Promise<LocalDistributionAsset | null>;
|
|
89
|
+
private resolveMacosBetaAsset;
|
|
85
90
|
resolveLatestDmg(): Promise<ResolvedDistributionAsset | null>;
|
|
86
91
|
resolveMacosDownload(): Promise<LocalDistributionAsset | null>;
|
|
87
92
|
resolveLatestIpa(): Promise<ResolvedDistributionAsset | null>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
|
-
import { mkdir, readdir, readFile, stat, unlink } from "node:fs/promises";
|
|
3
|
+
import { lstat, mkdir, readdir, readFile, stat, unlink } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { compareApkInstallOrder, compareSemver, compareWandInstallOrder, extractSemver } from "./version-utils.js";
|
|
6
6
|
const GITHUB_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
7
|
+
const MACOS_UPDATE_FILE_PATTERN = /^wand-v\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?(?:\+[A-Za-z0-9.-]+)?\.(?:zip|dmg)$/i;
|
|
7
8
|
/**
|
|
8
9
|
* GitHub Release 正文还包含 Android/macOS/iOS 的安装指引;它们属于发布页,
|
|
9
10
|
* 不该出现在 Android 的更新弹窗。保留分隔线前的变更摘要,并兼容旧版正文。
|
|
@@ -211,6 +212,83 @@ export class DistributionManager {
|
|
|
211
212
|
}
|
|
212
213
|
return { deleted, freedBytes };
|
|
213
214
|
}
|
|
215
|
+
/** Beta 仅从当前服务端的 macOS 分发目录选择,绝不回退 GitHub。 */
|
|
216
|
+
async resolveLatestMacosBeta() {
|
|
217
|
+
const asset = await this.resolveMacosBetaAsset();
|
|
218
|
+
if (!asset?.version || asset.size <= 0)
|
|
219
|
+
return null;
|
|
220
|
+
const sha256 = await this.computeAssetSha256(asset);
|
|
221
|
+
if (!sha256)
|
|
222
|
+
return null;
|
|
223
|
+
return {
|
|
224
|
+
version: asset.version.split("+")[0],
|
|
225
|
+
downloadUrl: `/macos/update-download?fileName=${encodeURIComponent(asset.fileName)}`,
|
|
226
|
+
fileName: asset.fileName,
|
|
227
|
+
size: asset.size,
|
|
228
|
+
source: "local",
|
|
229
|
+
sha256,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** 固定下载文件名,避免检查与下载之间新增包后下载到另一个版本。 */
|
|
233
|
+
async resolveMacosBetaDownload(fileName) {
|
|
234
|
+
if (!MACOS_UPDATE_FILE_PATTERN.test(fileName))
|
|
235
|
+
return null;
|
|
236
|
+
await this.refreshConfig();
|
|
237
|
+
const { config, configDir } = this.options;
|
|
238
|
+
if (config.macos?.enabled !== true)
|
|
239
|
+
return null;
|
|
240
|
+
const directory = resolveConfiguredDir(configDir, config.macos.dmgDir, "macos");
|
|
241
|
+
const filePath = path.join(directory, fileName);
|
|
242
|
+
try {
|
|
243
|
+
if (!(await lstat(filePath)).isFile())
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
return this.readLocalAsset(filePath, {
|
|
250
|
+
extension: fileName.toLowerCase().endsWith(".zip") ? ".zip" : ".dmg",
|
|
251
|
+
downloadUrl: `/macos/update-download?fileName=${encodeURIComponent(fileName)}`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async resolveMacosBetaAsset() {
|
|
255
|
+
await this.refreshConfig();
|
|
256
|
+
const { config, configDir } = this.options;
|
|
257
|
+
if (config.macos?.enabled !== true)
|
|
258
|
+
return null;
|
|
259
|
+
const directory = resolveConfiguredDir(configDir, config.macos.dmgDir, "macos");
|
|
260
|
+
await mkdir(directory, { recursive: true });
|
|
261
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
262
|
+
const candidates = await Promise.all(entries
|
|
263
|
+
.filter((entry) => entry.isFile() && MACOS_UPDATE_FILE_PATTERN.test(entry.name))
|
|
264
|
+
.map(async (entry) => ({
|
|
265
|
+
name: entry.name,
|
|
266
|
+
filePath: path.join(directory, entry.name),
|
|
267
|
+
fileStat: await stat(path.join(directory, entry.name)),
|
|
268
|
+
})));
|
|
269
|
+
candidates.sort((a, b) => {
|
|
270
|
+
const aVersion = extractArtifactVersion(a.name, path.extname(a.name));
|
|
271
|
+
const bVersion = extractArtifactVersion(b.name, path.extname(b.name));
|
|
272
|
+
const order = compareWandInstallOrder(aVersion ?? "", bVersion ?? "");
|
|
273
|
+
if (order !== 0)
|
|
274
|
+
return -order;
|
|
275
|
+
// 同版本优先 ZIP,避免 DMG 挂载;同格式多构建按最新修改时间挑选。
|
|
276
|
+
const format = Number(b.name.toLowerCase().endsWith(".zip")) - Number(a.name.toLowerCase().endsWith(".zip"));
|
|
277
|
+
return format || b.fileStat.mtimeMs - a.fileStat.mtimeMs;
|
|
278
|
+
});
|
|
279
|
+
const selected = candidates[0];
|
|
280
|
+
if (!selected)
|
|
281
|
+
return null;
|
|
282
|
+
return {
|
|
283
|
+
fileName: selected.name,
|
|
284
|
+
filePath: selected.filePath,
|
|
285
|
+
size: selected.fileStat.size,
|
|
286
|
+
updatedAt: selected.fileStat.mtime.toISOString(),
|
|
287
|
+
version: extractArtifactVersion(selected.name, path.extname(selected.name)),
|
|
288
|
+
downloadUrl: `/macos/update-download?fileName=${encodeURIComponent(selected.name)}`,
|
|
289
|
+
source: "local",
|
|
290
|
+
};
|
|
291
|
+
}
|
|
214
292
|
async resolveLatestDmg() {
|
|
215
293
|
const localDmg = await this.resolveMacosDownload();
|
|
216
294
|
if (localDmg?.version) {
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -494,6 +494,18 @@ function runCliText(command, args, prompt, opts) {
|
|
|
494
494
|
}
|
|
495
495
|
reject(new QuickCommitError(`${command} CLI 失败:${(stderr || stdout).trim() || `exit ${code}`}`, "CLAUDE_CLI_FAILED"));
|
|
496
496
|
});
|
|
497
|
+
// A CLI may exit before reading stdin (e.g. invalid credentials/argv).
|
|
498
|
+
// The writable stream emits EPIPE independently of ChildProcess.error;
|
|
499
|
+
// without a listener that becomes an uncaught exception after callers
|
|
500
|
+
// have already finished their turn or closed storage.
|
|
501
|
+
child.stdin?.on("error", (error) => {
|
|
502
|
+
if (settled)
|
|
503
|
+
return;
|
|
504
|
+
settled = true;
|
|
505
|
+
clearTimeout(timeout);
|
|
506
|
+
child.kill("SIGTERM");
|
|
507
|
+
reject(new QuickCommitError(`${command} CLI stdin failed: ${error.code ?? "write error"}`, "CLAUDE_CLI_FAILED"));
|
|
508
|
+
});
|
|
497
509
|
child.stdin?.end(prompt);
|
|
498
510
|
});
|
|
499
511
|
}
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
6855db8c0423d995dbd57567076cdf019a4e3306a465d0828cca80ee2f85f34c wand-render
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.1
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
783bd070dad27f2ec879ed5853f813f7ffdb3914d719d3ed947ec86af12a7b88 wand-render
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.1
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
7acb6e92e8f049ac87d8159905115dabe5f9a33f9d310e3c98f38b798c4302e5 wand-render
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.1
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
7814590a02ae2606fedc395ee1708921aec3b16332edb46fa2369e43e9a589f5 wand-render
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.1
|
|
@@ -15,6 +15,11 @@ export declare class SessionInputError extends Error {
|
|
|
15
15
|
readonly sessionStatus?: SessionSnapshot["status"] | undefined;
|
|
16
16
|
constructor(message: string, code: "SESSION_NOT_FOUND" | "SESSION_NOT_RUNNING" | "SESSION_NO_PTY", sessionId: string, sessionStatus?: SessionSnapshot["status"] | undefined);
|
|
17
17
|
}
|
|
18
|
+
/** Write outcome is unknown to the caller; do not record this as accepted input. */
|
|
19
|
+
export declare class PtyInputDeliveryError extends Error {
|
|
20
|
+
readonly sessionId: string;
|
|
21
|
+
constructor(sessionId: string, cause: unknown);
|
|
22
|
+
}
|
|
18
23
|
export declare class ProcessManager extends EventEmitter {
|
|
19
24
|
private readonly config;
|
|
20
25
|
private readonly storage;
|
|
@@ -39,6 +44,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
39
44
|
private bindRestoredTerminal;
|
|
40
45
|
private initializeClaudeBridge;
|
|
41
46
|
private bindTerminalProcess;
|
|
47
|
+
private handleTerminalResync;
|
|
42
48
|
private handleTerminalExit;
|
|
43
49
|
private handleTerminalData;
|
|
44
50
|
on(_event: "process", listener: ProcessEventHandler): this;
|
|
@@ -107,6 +113,10 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
107
113
|
*/
|
|
108
114
|
setSessionMode(id: string, mode: ExecutionMode): SessionSnapshot;
|
|
109
115
|
sendInput(id: string, input: string, view?: "chat" | "terminal", shortcutKey?: string, trackUserInput?: boolean): SessionSnapshot;
|
|
116
|
+
sendInputConfirmed(id: string, input: string, view?: "chat" | "terminal", shortcutKey?: string, trackUserInput?: boolean): Promise<SessionSnapshot>;
|
|
117
|
+
private preparePtyInput;
|
|
118
|
+
private validatePtyInput;
|
|
119
|
+
private trackPtyInput;
|
|
110
120
|
resize(id: string, cols: number, rows: number): SessionSnapshot;
|
|
111
121
|
getTerminalState(id: string): PtyTerminalSnapshot | null;
|
|
112
122
|
/**
|
package/dist/process-manager.js
CHANGED
|
@@ -150,6 +150,15 @@ export class SessionInputError extends Error {
|
|
|
150
150
|
this.name = "SessionInputError";
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
+
/** Write outcome is unknown to the caller; do not record this as accepted input. */
|
|
154
|
+
export class PtyInputDeliveryError extends Error {
|
|
155
|
+
sessionId;
|
|
156
|
+
constructor(sessionId, cause) {
|
|
157
|
+
super("PTY input delivery could not be confirmed.", { cause });
|
|
158
|
+
this.sessionId = sessionId;
|
|
159
|
+
this.name = "PtyInputDeliveryError";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
153
162
|
const REAL_CONVERSATION_MIN_LINES = 2;
|
|
154
163
|
const DISCOVERY_RECENT_WINDOW_MS = 10 * 60 * 1000;
|
|
155
164
|
const START_TIME_SKEW_MS = 30 * 1000;
|
|
@@ -736,8 +745,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
736
745
|
});
|
|
737
746
|
}
|
|
738
747
|
bindTerminalProcess(record, child, onVisibleChunk) {
|
|
739
|
-
child.
|
|
748
|
+
child.onResync?.((state) => this.handleTerminalResync(record, child, state));
|
|
740
749
|
child.onData((event) => this.handleTerminalData(record, child, event, onVisibleChunk));
|
|
750
|
+
child.onExit(({ exitCode }) => this.handleTerminalExit(record, child, exitCode));
|
|
741
751
|
if (record.ptyBridge) {
|
|
742
752
|
record.ptyBridge.setPtyWrite((input) => {
|
|
743
753
|
if (this.sessions.get(record.id) !== record || record.ptyProcess !== child)
|
|
@@ -746,6 +756,31 @@ export class ProcessManager extends EventEmitter {
|
|
|
746
756
|
});
|
|
747
757
|
}
|
|
748
758
|
}
|
|
759
|
+
handleTerminalResync(record, child, state) {
|
|
760
|
+
const current = this.sessions.get(record.id);
|
|
761
|
+
if (current !== record || current.ptyProcess !== child)
|
|
762
|
+
return;
|
|
763
|
+
current.terminalState?.dispose();
|
|
764
|
+
current.terminalState = restoreTerminalState(state);
|
|
765
|
+
current.output = state.output;
|
|
766
|
+
current.storedOutput = state.output;
|
|
767
|
+
current.ptyOutputSeq = state.seq;
|
|
768
|
+
current.ptyCols = state.cols;
|
|
769
|
+
current.ptyRows = state.rows;
|
|
770
|
+
current.ptyLaunchMarkerToken = state.launchMarkerToken;
|
|
771
|
+
current.providerShellMarker = state.launchMarkerToken
|
|
772
|
+
? new PtyCliExitMarker(state.launchMarkerToken)
|
|
773
|
+
: null;
|
|
774
|
+
if (current.ptyBridge)
|
|
775
|
+
current.messages = current.ptyBridge.getMessages();
|
|
776
|
+
this.initializeClaudeBridge(current, state.output);
|
|
777
|
+
current.ptyBridge?.setPtyWrite((input) => {
|
|
778
|
+
if (this.sessions.get(record.id) === current && current.ptyProcess === child)
|
|
779
|
+
child.write(input);
|
|
780
|
+
});
|
|
781
|
+
this.flushPersist(current, true);
|
|
782
|
+
this.emitEvent({ type: "resync", sessionId: record.id, data: { reason: "render_replay_gap" } });
|
|
783
|
+
}
|
|
749
784
|
handleTerminalExit(record, child, exitCode) {
|
|
750
785
|
const current = this.sessions.get(record.id);
|
|
751
786
|
// A stopped session can be resumed under the same public id before the old
|
|
@@ -1483,6 +1518,39 @@ export class ProcessManager extends EventEmitter {
|
|
|
1483
1518
|
return this.snapshot(record);
|
|
1484
1519
|
}
|
|
1485
1520
|
sendInput(id, input, view, shortcutKey, trackUserInput = true) {
|
|
1521
|
+
const record = this.preparePtyInput(id, input, view, shortcutKey, trackUserInput);
|
|
1522
|
+
record.ptyProcess.write(input);
|
|
1523
|
+
this.persist(record);
|
|
1524
|
+
return this.snapshot(record);
|
|
1525
|
+
}
|
|
1526
|
+
async sendInputConfirmed(id, input, view, shortcutKey, trackUserInput = true) {
|
|
1527
|
+
const record = this.validatePtyInput(id);
|
|
1528
|
+
const terminal = record.ptyProcess;
|
|
1529
|
+
try {
|
|
1530
|
+
if (terminal.writeConfirmed)
|
|
1531
|
+
await terminal.writeConfirmed(input);
|
|
1532
|
+
else
|
|
1533
|
+
terminal.write(input);
|
|
1534
|
+
}
|
|
1535
|
+
catch (cause) {
|
|
1536
|
+
throw new PtyInputDeliveryError(id, cause);
|
|
1537
|
+
}
|
|
1538
|
+
const current = this.sessions.get(id);
|
|
1539
|
+
if (current !== record || record.ptyProcess !== terminal) {
|
|
1540
|
+
// The write reached an old PTY, but the caller cannot treat it as input
|
|
1541
|
+
// accepted by the current incarnation or persist the stale record.
|
|
1542
|
+
throw new PtyInputDeliveryError(id, new Error("PTY incarnation changed during write"));
|
|
1543
|
+
}
|
|
1544
|
+
this.trackPtyInput(record, input, view, shortcutKey, trackUserInput);
|
|
1545
|
+
this.persist(record);
|
|
1546
|
+
return this.snapshot(record);
|
|
1547
|
+
}
|
|
1548
|
+
preparePtyInput(id, input, view, shortcutKey, trackUserInput = true) {
|
|
1549
|
+
const record = this.validatePtyInput(id);
|
|
1550
|
+
this.trackPtyInput(record, input, view, shortcutKey, trackUserInput);
|
|
1551
|
+
return record;
|
|
1552
|
+
}
|
|
1553
|
+
validatePtyInput(id) {
|
|
1486
1554
|
if (this.disposed)
|
|
1487
1555
|
throw new Error("ProcessManager has been disposed.");
|
|
1488
1556
|
const record = this.mustGet(id);
|
|
@@ -1495,6 +1563,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
1495
1563
|
console.error(`[ProcessManager] Rejecting input: session ${id} has no PTY`);
|
|
1496
1564
|
throw new SessionInputError("Session is not running.", "SESSION_NO_PTY", id, record.status);
|
|
1497
1565
|
}
|
|
1566
|
+
return record;
|
|
1567
|
+
}
|
|
1568
|
+
trackPtyInput(record, input, view, shortcutKey, trackUserInput = true) {
|
|
1569
|
+
const id = record.id;
|
|
1498
1570
|
if (!record.ptyTopicDraft)
|
|
1499
1571
|
record.ptyTopicDraft = createPtyTopicLineBuffer();
|
|
1500
1572
|
const topicInput = consumePtyInputForTopic(record.ptyTopicDraft, input, view, shortcutKey);
|
|
@@ -1516,9 +1588,6 @@ export class ProcessManager extends EventEmitter {
|
|
|
1516
1588
|
if (record.ptyBridge && trackUserInput) {
|
|
1517
1589
|
record.ptyBridge.onUserInput(input);
|
|
1518
1590
|
}
|
|
1519
|
-
record.ptyProcess.write(input);
|
|
1520
|
-
this.persist(record);
|
|
1521
|
-
return this.snapshot(record);
|
|
1522
1591
|
}
|
|
1523
1592
|
resize(id, cols, rows) {
|
|
1524
1593
|
const record = this.mustGet(id);
|
|
@@ -26,6 +26,8 @@ export declare class RenderDaemonClient implements TerminalHost {
|
|
|
26
26
|
* (现象是 `createOrAttach` 全部失败、错误只显示 unavailable,旧会话被永久报成 running)。
|
|
27
27
|
*/
|
|
28
28
|
private readonly tokenPath;
|
|
29
|
+
/** Persisted PTY IDs used only if v1 `list` exceeds its aggregate frame limit. */
|
|
30
|
+
private readonly knownSessionIds;
|
|
29
31
|
readonly persistent = true;
|
|
30
32
|
private socket;
|
|
31
33
|
private nextRequestId;
|
|
@@ -33,6 +35,11 @@ export declare class RenderDaemonClient implements TerminalHost {
|
|
|
33
35
|
private readonly inventory;
|
|
34
36
|
private readonly handles;
|
|
35
37
|
private readonly pendingEvents;
|
|
38
|
+
private readonly pendingEventOverflows;
|
|
39
|
+
private readonly refreshingSessions;
|
|
40
|
+
private reconciling;
|
|
41
|
+
private eventsDuringReconcile;
|
|
42
|
+
private reconcileDuringSync;
|
|
36
43
|
private headerParts;
|
|
37
44
|
private headerBytes;
|
|
38
45
|
private bodyParts;
|
|
@@ -54,15 +61,20 @@ export declare class RenderDaemonClient implements TerminalHost {
|
|
|
54
61
|
private tokenReadFailureLogged;
|
|
55
62
|
/** 当前 socket 上已解出的帧数,用来区分「未鉴权被直接关闭」与网络错误。 */
|
|
56
63
|
private framesOnCurrentSocket;
|
|
64
|
+
private connecting;
|
|
65
|
+
private stopHeartbeat;
|
|
57
66
|
constructor(socketPath: string, token: string,
|
|
58
67
|
/**
|
|
59
68
|
* token 文件路径。给了它就在**每次 connect 时重读**:Render 崩溃/升级重启后
|
|
60
69
|
* 会 `generate_token()` 轮换 token,只缓存构造期的那一把会让重连永远鉴权失败
|
|
61
70
|
* (现象是 `createOrAttach` 全部失败、错误只显示 unavailable,旧会话被永久报成 running)。
|
|
62
71
|
*/
|
|
63
|
-
tokenPath?: string | null
|
|
72
|
+
tokenPath?: string | null,
|
|
73
|
+
/** Persisted PTY IDs used only if v1 `list` exceeds its aggregate frame limit. */
|
|
74
|
+
knownSessionIds?: readonly string[]);
|
|
64
75
|
get version(): string;
|
|
65
76
|
connect(): Promise<void>;
|
|
77
|
+
private open;
|
|
66
78
|
/**
|
|
67
79
|
* 对端一个字节都没回就断开:daemon 对 token/协议不符的处理是直接 `close()`
|
|
68
80
|
* (`wand-renderd` 的 `server.rs`),所以这是鉴权/版本问题的特征,不是网络抖动。
|
|
@@ -91,8 +103,11 @@ export declare class RenderDaemonClient implements TerminalHost {
|
|
|
91
103
|
* 否则一次 PTY 抖动会顺着 ProcessManager 把整个会话路径打断。
|
|
92
104
|
*/
|
|
93
105
|
fireOperation(sessionId: string, method: RenderMethod, params?: Record<string, unknown>): void;
|
|
106
|
+
confirmWrite(sessionId: string, data: string): Promise<void>;
|
|
94
107
|
reportOperationError(sessionId: string, method: RenderMethod, error: unknown): void;
|
|
95
108
|
private listSessions;
|
|
109
|
+
/** `list` locates sessions; each `attach` is the authoritative recovery state. */
|
|
110
|
+
private loadAuthoritativeSessions;
|
|
96
111
|
private parseHello;
|
|
97
112
|
private request;
|
|
98
113
|
private consume;
|
|
@@ -100,6 +115,9 @@ export declare class RenderDaemonClient implements TerminalHost {
|
|
|
100
115
|
private handleFrame;
|
|
101
116
|
private handleResponse;
|
|
102
117
|
private routeEvent;
|
|
118
|
+
private queuePendingEvent;
|
|
119
|
+
/** Repair a bounded event-buffer overflow without replaying a partial stream. */
|
|
120
|
+
private refreshSession;
|
|
103
121
|
/**
|
|
104
122
|
* Render 主动告知当前会话集合(例如自身清掉了已 forget 的会话)。不在
|
|
105
123
|
* 集合里的会话要给既有句柄补一个退出事件,否则 ProcessManager 会永远停在 running。
|
|
@@ -150,4 +168,4 @@ export declare function normalizeRenderSessionState(value: unknown): TerminalSes
|
|
|
150
168
|
* 连接一个已经存在的 Render(socket + token 都在)。**不 spawn**,找不到就返回 null,
|
|
151
169
|
* 这样调用方可以区分「已被别处 adopt」和「需要自己拉起」。
|
|
152
170
|
*/
|
|
153
|
-
export declare function connectExistingRenderClient(configPath: string): Promise<RenderDaemonClient | null>;
|
|
171
|
+
export declare function connectExistingRenderClient(configPath: string, knownSessionIds?: readonly string[]): Promise<RenderDaemonClient | null>;
|