@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.
Files changed (60) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/config.js +2 -0
  3. package/dist/daemon-connection.d.ts +9 -0
  4. package/dist/daemon-connection.js +86 -0
  5. package/dist/distribution-manager.d.ts +5 -0
  6. package/dist/distribution-manager.js +79 -1
  7. package/dist/git-quick-commit.js +12 -0
  8. package/dist/native/darwin-arm64/wand-render +0 -0
  9. package/dist/native/darwin-arm64/wand-render.sha256 +1 -1
  10. package/dist/native/darwin-arm64/wand-render.version +1 -1
  11. package/dist/native/darwin-x64/wand-render +0 -0
  12. package/dist/native/darwin-x64/wand-render.sha256 +1 -0
  13. package/dist/native/darwin-x64/wand-render.version +1 -0
  14. package/dist/native/linux-arm64/wand-render +0 -0
  15. package/dist/native/linux-arm64/wand-render.sha256 +1 -0
  16. package/dist/native/linux-arm64/wand-render.version +1 -0
  17. package/dist/native/linux-x64/wand-render +0 -0
  18. package/dist/native/linux-x64/wand-render.sha256 +1 -0
  19. package/dist/native/linux-x64/wand-render.version +1 -0
  20. package/dist/process-manager.d.ts +10 -0
  21. package/dist/process-manager.js +73 -4
  22. package/dist/render-daemon-client.d.ts +20 -2
  23. package/dist/render-daemon-client.js +264 -52
  24. package/dist/render-host.d.ts +6 -1
  25. package/dist/render-host.js +47 -16
  26. package/dist/render-structured-client.d.ts +80 -0
  27. package/dist/render-structured-client.js +513 -0
  28. package/dist/render-structured-host.d.ts +31 -0
  29. package/dist/render-structured-host.js +218 -0
  30. package/dist/render-structured-protocol.d.ts +101 -0
  31. package/dist/render-structured-protocol.js +35 -0
  32. package/dist/server-session-routes.js +30 -3
  33. package/dist/server-task-routes.js +52 -3
  34. package/dist/server-update-routes.d.ts +2 -0
  35. package/dist/server-update-routes.js +44 -0
  36. package/dist/server.js +21 -22
  37. package/dist/storage.d.ts +2 -1
  38. package/dist/storage.js +14 -5
  39. package/dist/structured-client-protocol.js +39 -22
  40. package/dist/structured-exec-host.js +7 -1
  41. package/dist/structured-exec-pump.js +6 -2
  42. package/dist/structured-session-manager.d.ts +2 -0
  43. package/dist/structured-session-manager.js +25 -1
  44. package/dist/task-types.d.ts +2 -0
  45. package/dist/terminal-daemon-client.d.ts +9 -2
  46. package/dist/terminal-daemon-client.js +119 -46
  47. package/dist/terminal-daemon-server.js +20 -4
  48. package/dist/terminal-host.d.ts +6 -0
  49. package/dist/terminal-host.js +3 -0
  50. package/dist/tui/ipc-server.js +12 -0
  51. package/dist/types.d.ts +7 -1
  52. package/dist/unix-socket-keepalive.d.ts +3 -0
  53. package/dist/unix-socket-keepalive.js +94 -0
  54. package/dist/web-ui/content/scripts.js +68 -68
  55. package/dist/web-ui/content/styles.css +1 -1
  56. package/dist/web-ui/embedded-assets.js +2 -2
  57. package/dist/ws-broadcast.d.ts +1 -1
  58. package/dist/ws-broadcast.js +13 -1
  59. package/package.json +2 -1
  60. package/scripts/stage-render-binaries.js +52 -6
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "52f54afb01f38180e2bce8b9a7ba35275772cacd",
3
- "builtAt": "2026-09-23T03:06:35.233Z",
4
- "version": "4.72.2-beta.g52f54af",
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) {
@@ -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
  }
@@ -1 +1 @@
1
- a8b46e0ed51802d1220434b033693eb900a32442631f60d5380001a71418dab4 wand-render
1
+ 6855db8c0423d995dbd57567076cdf019a4e3306a465d0828cca80ee2f85f34c wand-render
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
@@ -0,0 +1 @@
1
+ 783bd070dad27f2ec879ed5853f813f7ffdb3914d719d3ed947ec86af12a7b88 wand-render
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1 @@
1
+ 7acb6e92e8f049ac87d8159905115dabe5f9a33f9d310e3c98f38b798c4302e5 wand-render
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -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
  /**
@@ -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.onExit(({ exitCode }) => this.handleTerminalExit(record, child, exitCode));
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>;