@co0ontty/wand 4.72.2 → 4.75.0-beta.g1d477a2

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 (101) hide show
  1. package/dist/build-info.json +4 -4
  2. package/dist/claude-sdk-runner.d.ts +2 -3
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +48 -0
  5. package/dist/daemon-connection.d.ts +9 -0
  6. package/dist/daemon-connection.js +86 -0
  7. package/dist/distribution-manager.d.ts +5 -0
  8. package/dist/distribution-manager.js +79 -1
  9. package/dist/git-quick-commit.js +12 -0
  10. package/dist/message-truncator.js +9 -3
  11. package/dist/models.d.ts +39 -2
  12. package/dist/models.js +302 -9
  13. package/dist/native/darwin-arm64/wand-render +0 -0
  14. package/dist/native/darwin-arm64/wand-render.sha256 +1 -0
  15. package/dist/native/darwin-arm64/wand-render.version +1 -0
  16. package/dist/native/darwin-arm64/wand-structured-renderd +0 -0
  17. package/dist/native/darwin-arm64/wand-structured-renderd.sha256 +1 -0
  18. package/dist/native/darwin-arm64/wand-structured-renderd.version +1 -0
  19. package/dist/native/darwin-x64/wand-render +0 -0
  20. package/dist/native/darwin-x64/wand-render.sha256 +1 -0
  21. package/dist/native/darwin-x64/wand-render.version +1 -0
  22. package/dist/native/darwin-x64/wand-structured-renderd +0 -0
  23. package/dist/native/darwin-x64/wand-structured-renderd.sha256 +1 -0
  24. package/dist/native/darwin-x64/wand-structured-renderd.version +1 -0
  25. package/dist/native/linux-arm64/wand-render +0 -0
  26. package/dist/native/linux-arm64/wand-render.sha256 +1 -0
  27. package/dist/native/linux-arm64/wand-render.version +1 -0
  28. package/dist/native/linux-arm64/wand-structured-renderd +0 -0
  29. package/dist/native/linux-arm64/wand-structured-renderd.sha256 +1 -0
  30. package/dist/native/linux-arm64/wand-structured-renderd.version +1 -0
  31. package/dist/native/linux-x64/wand-render +0 -0
  32. package/dist/native/linux-x64/wand-render.sha256 +1 -0
  33. package/dist/native/linux-x64/wand-render.version +1 -0
  34. package/dist/native/linux-x64/wand-structured-renderd +0 -0
  35. package/dist/native/linux-x64/wand-structured-renderd.sha256 +1 -0
  36. package/dist/native/linux-x64/wand-structured-renderd.version +1 -0
  37. package/dist/process-manager.d.ts +12 -1
  38. package/dist/process-manager.js +88 -7
  39. package/dist/pty-terminal-state.d.ts +17 -0
  40. package/dist/pty-terminal-state.js +51 -1
  41. package/dist/render-binary.d.ts +61 -0
  42. package/dist/render-binary.js +204 -0
  43. package/dist/render-daemon-client.d.ts +202 -0
  44. package/dist/render-daemon-client.js +1164 -0
  45. package/dist/render-host.d.ts +88 -0
  46. package/dist/render-host.js +440 -0
  47. package/dist/render-protocol.d.ts +128 -0
  48. package/dist/render-protocol.js +99 -0
  49. package/dist/render-structured-client.d.ts +80 -0
  50. package/dist/render-structured-client.js +513 -0
  51. package/dist/render-structured-host.d.ts +31 -0
  52. package/dist/render-structured-host.js +218 -0
  53. package/dist/render-structured-protocol.d.ts +101 -0
  54. package/dist/render-structured-protocol.js +35 -0
  55. package/dist/server-session-routes.js +114 -12
  56. package/dist/server-task-routes.js +55 -6
  57. package/dist/server-update-routes.d.ts +2 -0
  58. package/dist/server-update-routes.js +44 -0
  59. package/dist/server.js +43 -13
  60. package/dist/session-ai-context.d.ts +1 -1
  61. package/dist/session-ai-context.js +15 -4
  62. package/dist/session-logger.d.ts +2 -0
  63. package/dist/session-logger.js +44 -1
  64. package/dist/session-transport.js +1 -1
  65. package/dist/storage.d.ts +2 -1
  66. package/dist/storage.js +14 -5
  67. package/dist/structured-client-protocol.d.ts +3 -1
  68. package/dist/structured-client-protocol.js +85 -25
  69. package/dist/structured-content.d.ts +20 -3
  70. package/dist/structured-content.js +80 -1
  71. package/dist/structured-exec-host.js +7 -1
  72. package/dist/structured-exec-pump.js +6 -2
  73. package/dist/structured-grok-adapter.js +22 -0
  74. package/dist/structured-pi-adapter.js +18 -2
  75. package/dist/structured-provider-common.d.ts +11 -9
  76. package/dist/structured-provider-common.js +60 -18
  77. package/dist/structured-session-manager.d.ts +2 -0
  78. package/dist/structured-session-manager.js +27 -3
  79. package/dist/task-types.d.ts +4 -1
  80. package/dist/terminal-daemon-client.d.ts +9 -2
  81. package/dist/terminal-daemon-client.js +119 -46
  82. package/dist/terminal-daemon-server.js +47 -8
  83. package/dist/terminal-host.d.ts +6 -0
  84. package/dist/terminal-host.js +3 -0
  85. package/dist/tui/commands.js +183 -10
  86. package/dist/tui/ipc-server.js +12 -0
  87. package/dist/types.d.ts +30 -10
  88. package/dist/unix-socket-keepalive.d.ts +3 -0
  89. package/dist/unix-socket-keepalive.js +94 -0
  90. package/dist/wand-task-sync.js +2 -1
  91. package/dist/web-ui/content/scripts.js +79 -73
  92. package/dist/web-ui/content/styles.css +1 -1
  93. package/dist/web-ui/content/tailwind.css +21 -0
  94. package/dist/web-ui/embedded-assets.js +2 -2
  95. package/dist/web-ui/thinking-efforts.d.ts +24 -0
  96. package/dist/web-ui/thinking-efforts.js +81 -0
  97. package/dist/ws-broadcast.d.ts +1 -1
  98. package/dist/ws-broadcast.js +28 -9
  99. package/package.json +8 -3
  100. package/scripts/install-render-binary.js +499 -0
  101. package/scripts/stage-render-binaries.js +626 -0
@@ -0,0 +1,99 @@
1
+ import { createHash } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ /**
6
+ * Render 协议 v1 的 TS 镜像。Rust 单一真源在
7
+ * `render/crates/wand-render-protocol/src/lib.rs`,契约文档在
8
+ * `render/docs/render-protocol.md`(本仓库 `docs/render-protocol.md` 只是指针)。
9
+ *
10
+ * 两侧常量与字段名必须逐一对齐;任一侧改动都要同时提升版本号并更新文档。
11
+ */
12
+ export const RENDER_PROTOCOL_VERSION = 1;
13
+ /** 单帧上限:`u32` 大端长度前缀 + UTF-8 JSON。 */
14
+ export const MAX_FRAME_BYTES = 64 * 1024 * 1024;
15
+ /** 与 `PTY_OUTPUT_MAX_SIZE` 对齐(chunk 窗口 / output 有界)。 */
16
+ export const RENDER_PTY_OUTPUT_MAX_CHARS = 200_000;
17
+ /**
18
+ * config 路径的 canonical 形式:`realpathSync` 解析符号链接,失败(文件尚未创建)
19
+ * 才回退纯词法的 `path.resolve`。
20
+ *
21
+ * 两侧必须对同一 config 派生出**同一套**路径,而「同一份 config」在用户那里常常是
22
+ * 经过符号链接的路径(`~/.wand` → 别的卷、`/tmp` → `/private/tmp`、容器挂载点)。
23
+ * 一侧做 realpath、另一侧只做 resolve,就会算出两个 suffix:同一个 config 出现两个
24
+ * Render、两套 PTY 所有权 —— 这正是要靠寻址归一化防住的故障。
25
+ */
26
+ function canonicalConfigPath(configPath) {
27
+ const resolved = path.resolve(configPath);
28
+ try {
29
+ return realpathSync(resolved);
30
+ }
31
+ catch {
32
+ return resolved;
33
+ }
34
+ }
35
+ /**
36
+ * Render 的按 config 隔离寻址。
37
+ *
38
+ * 与 legacy `terminald`(`wand-terminald-<uid>-<suffix>.sock` /
39
+ * `.terminald-<suffix>.token`)**刻意使用不同文件名**:升级期两套并存,
40
+ * 但永不互相领养。
41
+ */
42
+ export function renderPaths(configPath) {
43
+ const resolved = canonicalConfigPath(configPath);
44
+ const suffix = createHash("sha256").update(resolved).digest("hex").slice(0, 12);
45
+ const dir = path.dirname(resolved);
46
+ const uid = (() => {
47
+ try {
48
+ return os.userInfo().uid;
49
+ }
50
+ catch {
51
+ return 0;
52
+ }
53
+ })();
54
+ return {
55
+ // macOS 的 Unix socket 路径上限约 100 字节,所以 socket 放 /tmp 保持短。
56
+ socketPath: path.join("/tmp", `wand-render-${uid}-${suffix}.sock`),
57
+ tokenPath: path.join(dir, `.render-${suffix}.token`),
58
+ pidPath: path.join(dir, `.render-${suffix}.pid`),
59
+ metaPath: path.join(dir, `.render-${suffix}.json`),
60
+ };
61
+ }
62
+ /** 编码一帧:`u32` 大端长度 + UTF-8 JSON。 */
63
+ export function encodeRenderFrame(value) {
64
+ const body = Buffer.from(JSON.stringify(value), "utf8");
65
+ if (body.length > MAX_FRAME_BYTES) {
66
+ throw new Error(`Render frame of ${body.length} bytes exceeds the ${MAX_FRAME_BYTES} byte limit`);
67
+ }
68
+ const header = Buffer.allocUnsafe(4);
69
+ header.writeUInt32BE(body.length, 0);
70
+ return Buffer.concat([header, body]);
71
+ }
72
+ /**
73
+ * 从累积缓冲区解出所有完整帧。
74
+ *
75
+ * 返回剩余(不完整)字节。长度越界时抛错,调用方应关闭连接。
76
+ */
77
+ export function decodeRenderFrames(buffer) {
78
+ const frames = [];
79
+ let offset = 0;
80
+ while (buffer.length - offset >= 4) {
81
+ const length = buffer.readUInt32BE(offset);
82
+ if (length > MAX_FRAME_BYTES) {
83
+ throw new Error(`Render frame length ${length} exceeds the ${MAX_FRAME_BYTES} byte limit`);
84
+ }
85
+ if (buffer.length - offset - 4 < length)
86
+ break;
87
+ const body = buffer.subarray(offset + 4, offset + 4 + length);
88
+ frames.push(JSON.parse(body.toString("utf8")));
89
+ offset += 4 + length;
90
+ }
91
+ return { frames, rest: offset === 0 ? buffer : buffer.subarray(offset) };
92
+ }
93
+ /** 是否为 protocol v1 事件帧(无 `id`,带 `event`)。 */
94
+ export function isRenderEvent(value) {
95
+ if (!value || typeof value !== "object")
96
+ return false;
97
+ const record = value;
98
+ return typeof record.event === "string" && record.id === undefined;
99
+ }
@@ -0,0 +1,80 @@
1
+ import { type StructuredRenderEvent, type StructuredRenderMethod } from "./render-structured-protocol.js";
2
+ import type { StructuredExecHost, StructuredExecProcess, StructuredExitEvent, StructuredRunState, StructuredSpawnRequest, StructuredStreamEvent } from "./structured-exec-host.js";
3
+ type RunEvent = Extract<StructuredRenderEvent, {
4
+ event: "stream" | "exit";
5
+ }>;
6
+ declare class RemoteRun implements StructuredExecProcess {
7
+ readonly runId: string;
8
+ readonly incarnationId: string;
9
+ readonly pid: number;
10
+ private readonly client;
11
+ private readonly streamListeners;
12
+ private readonly exitListeners;
13
+ private readonly beforeListener;
14
+ private pendingExit;
15
+ readonly seq: {
16
+ stdout: number;
17
+ stderr: number;
18
+ };
19
+ readonly queued: RunEvent[];
20
+ syncing: boolean;
21
+ overflow: boolean;
22
+ get exited(): boolean;
23
+ constructor(runId: string, incarnationId: string, pid: number, client: RenderStructuredClient);
24
+ interrupt(signal?: string): void;
25
+ onStream(listener: (event: StructuredStreamEvent) => void): {
26
+ dispose(): void;
27
+ };
28
+ onExit(listener: (event: StructuredExitEvent) => void): {
29
+ dispose(): void;
30
+ };
31
+ acceptStream(event: StructuredStreamEvent): void;
32
+ acceptExit(event: StructuredExitEvent): void;
33
+ fail(): void;
34
+ }
35
+ /** v2-only StructuredExecHost. It never accesses the PTY v1 or terminald sockets. */
36
+ export declare class RenderStructuredClient implements StructuredExecHost {
37
+ private readonly configPath;
38
+ readonly persistent = true;
39
+ private socket;
40
+ private buffer;
41
+ private token;
42
+ private requestId;
43
+ private readonly pending;
44
+ private readonly inventory;
45
+ private readonly observed;
46
+ private readonly handles;
47
+ private readonly orphanEvents;
48
+ private readonly orphanOverflow;
49
+ private connecting;
50
+ private reconnectTimer;
51
+ private disposed;
52
+ private connected;
53
+ private fatalProtocol;
54
+ private stopHeartbeat;
55
+ private reconnectDelayMs;
56
+ constructor(configPath: string);
57
+ connect(): Promise<void>;
58
+ private open;
59
+ request(method: StructuredRenderMethod, params?: Record<string, unknown>): Promise<unknown>;
60
+ private consume;
61
+ private acceptEvent;
62
+ private queue;
63
+ private catchUp;
64
+ private replayStream;
65
+ private collect;
66
+ /** Events can overtake the spawn response. Feed the contiguous prefix before
67
+ * paging so a fast >8 MiB child does not falsely lose its already-received head. */
68
+ private primeBuffered;
69
+ spawnStructured(request: StructuredSpawnRequest): Promise<StructuredExecProcess>;
70
+ attachRun(runId: string): Promise<StructuredRunState | null>;
71
+ adoptRun(runId: string): Promise<StructuredExecProcess | null>;
72
+ listRuns(): Promise<StructuredRunState[]>;
73
+ forgetRun(runId: string): void;
74
+ dropHandle(runId: string, handle: RemoteRun): void;
75
+ disconnect(): void;
76
+ private onDisconnect;
77
+ private scheduleReconnect;
78
+ private rejectPending;
79
+ }
80
+ export {};
@@ -0,0 +1,513 @@
1
+ import { lstatSync, readFileSync } from "node:fs";
2
+ import net from "node:net";
3
+ import { startDaemonHeartbeat, waitForDaemonSocket } from "./daemon-connection.js";
4
+ import { getErrorMessage } from "./error-utils.js";
5
+ import { STRUCTURED_RENDER_PROTOCOL_VERSION, decodeRenderFrames, encodeRenderFrame, structuredRenderPaths, } from "./render-structured-protocol.js";
6
+ const REQUEST_TIMEOUT_MS = 10_000;
7
+ const MAX_QUEUED_EVENTS = 2048;
8
+ function checkSocket(path) {
9
+ const stat = lstatSync(path);
10
+ if (!stat.isSocket() || (typeof process.getuid === "function" && stat.uid !== process.getuid())
11
+ || (stat.mode & 0o777) !== 0o600) {
12
+ throw new Error("Structured Render socket is not a private socket owned by this user");
13
+ }
14
+ }
15
+ function toState(state, stdoutLog = "", stderrLog = "") {
16
+ return { ...state, stdoutLog, stderrLog };
17
+ }
18
+ class RemoteRun {
19
+ runId;
20
+ incarnationId;
21
+ pid;
22
+ client;
23
+ streamListeners = new Set();
24
+ exitListeners = new Set();
25
+ beforeListener = [];
26
+ pendingExit = null;
27
+ seq = { stdout: 0, stderr: 0 };
28
+ queued = [];
29
+ syncing = false;
30
+ overflow = false;
31
+ get exited() { return this.pendingExit !== null; }
32
+ constructor(runId, incarnationId, pid, client) {
33
+ this.runId = runId;
34
+ this.incarnationId = incarnationId;
35
+ this.pid = pid;
36
+ this.client = client;
37
+ }
38
+ interrupt(signal) {
39
+ void this.client.request("interrupt", { runId: this.runId, signal }).catch((error) => process.stderr.write(`[wand] structured Render interrupt failed: ${getErrorMessage(error)}\n`));
40
+ }
41
+ onStream(listener) {
42
+ this.streamListeners.add(listener);
43
+ const pending = this.beforeListener.splice(0);
44
+ if (pending.length > 0)
45
+ queueMicrotask(() => {
46
+ if (this.streamListeners.has(listener))
47
+ for (const event of pending)
48
+ listener(event);
49
+ });
50
+ return { dispose: () => this.streamListeners.delete(listener) };
51
+ }
52
+ onExit(listener) {
53
+ this.exitListeners.add(listener);
54
+ const event = this.pendingExit;
55
+ if (event)
56
+ queueMicrotask(() => { if (this.exitListeners.has(listener))
57
+ listener(event); });
58
+ return { dispose: () => this.exitListeners.delete(listener) };
59
+ }
60
+ acceptStream(event) {
61
+ const last = this.seq[event.stream];
62
+ if (event.seq <= last || this.pendingExit)
63
+ return;
64
+ if (event.seq !== last + 1)
65
+ throw new Error("non-contiguous structured Render stream");
66
+ this.seq[event.stream] = event.seq;
67
+ if (this.streamListeners.size === 0) {
68
+ this.beforeListener.push(event);
69
+ // Never silently lose bytes when a fast process finishes before its
70
+ // consumer installs listeners: fail rather than dropping the head.
71
+ if (this.beforeListener.length > MAX_QUEUED_EVENTS)
72
+ this.fail();
73
+ return;
74
+ }
75
+ for (const listener of this.streamListeners)
76
+ listener(event);
77
+ }
78
+ acceptExit(event) {
79
+ if (this.pendingExit)
80
+ return;
81
+ this.pendingExit = event;
82
+ for (const listener of this.exitListeners)
83
+ listener(event);
84
+ }
85
+ fail() {
86
+ this.client.dropHandle(this.runId, this);
87
+ // Explicit failure is preferable to a successful but incomplete turn.
88
+ this.acceptExit({ exitCode: 1, signal: null });
89
+ }
90
+ }
91
+ /** v2-only StructuredExecHost. It never accesses the PTY v1 or terminald sockets. */
92
+ export class RenderStructuredClient {
93
+ configPath;
94
+ persistent = true;
95
+ socket = null;
96
+ buffer = Buffer.alloc(0);
97
+ token = "";
98
+ requestId = 0;
99
+ pending = new Map();
100
+ inventory = new Map();
101
+ observed = new Map();
102
+ handles = new Map();
103
+ orphanEvents = new Map();
104
+ orphanOverflow = new Set();
105
+ connecting = null;
106
+ reconnectTimer = null;
107
+ disposed = false;
108
+ connected = false;
109
+ fatalProtocol = null;
110
+ stopHeartbeat = null;
111
+ reconnectDelayMs = 500;
112
+ constructor(configPath) {
113
+ this.configPath = configPath;
114
+ }
115
+ connect() {
116
+ if (this.disposed)
117
+ return Promise.reject(new Error("Structured Render client disposed"));
118
+ if (this.fatalProtocol)
119
+ return Promise.reject(this.fatalProtocol);
120
+ if (this.connected && this.socket && !this.socket.destroyed)
121
+ return Promise.resolve();
122
+ if (this.connecting)
123
+ return this.connecting;
124
+ const connecting = this.open();
125
+ this.connecting = connecting;
126
+ void connecting.finally(() => { if (this.connecting === connecting)
127
+ this.connecting = null; })
128
+ .catch(() => this.scheduleReconnect());
129
+ return connecting;
130
+ }
131
+ async open() {
132
+ const paths = structuredRenderPaths(this.configPath);
133
+ checkSocket(paths.socketPath);
134
+ this.token = readFileSync(paths.tokenPath, "utf8").trim();
135
+ if (!this.token)
136
+ throw new Error("Structured Render token missing");
137
+ const socket = net.createConnection(paths.socketPath);
138
+ socket.setNoDelay(true);
139
+ this.socket = socket;
140
+ try {
141
+ await waitForDaemonSocket(socket);
142
+ }
143
+ catch (error) {
144
+ socket.destroy();
145
+ if (this.socket === socket)
146
+ this.socket = null;
147
+ throw error;
148
+ }
149
+ this.buffer = Buffer.alloc(0);
150
+ socket.on("data", (data) => {
151
+ if (this.socket === socket)
152
+ this.consume(data);
153
+ });
154
+ socket.on("close", () => this.onDisconnect(socket));
155
+ socket.on("error", (error) => this.onDisconnect(socket, error));
156
+ try {
157
+ const hello = await this.request("hello");
158
+ if (hello.protocolVersion !== STRUCTURED_RENDER_PROTOCOL_VERSION) {
159
+ this.fatalProtocol = new Error("Structured Render protocol mismatch; refusing degraded mode");
160
+ throw this.fatalProtocol;
161
+ }
162
+ const result = await this.request("list");
163
+ this.inventory.clear();
164
+ for (const state of result.runs)
165
+ this.inventory.set(state.runId, state);
166
+ for (const [runId, handle] of this.handles) {
167
+ const state = this.inventory.get(runId);
168
+ if (!state || state.incarnationId !== handle.incarnationId) {
169
+ handle.fail();
170
+ continue;
171
+ }
172
+ await this.catchUp(handle);
173
+ }
174
+ if (this.disposed || this.socket !== socket)
175
+ throw new Error("Structured Render disconnected during adoption");
176
+ this.connected = true;
177
+ this.reconnectDelayMs = 500;
178
+ this.stopHeartbeat?.();
179
+ this.stopHeartbeat = startDaemonHeartbeat(socket, () => this.request("ping"), { socketPath: paths.socketPath });
180
+ }
181
+ catch (error) {
182
+ if (/protocolMismatch/.test(getErrorMessage(error))) {
183
+ this.fatalProtocol = new Error("Structured Render v2 protocol mismatch; refusing degraded mode");
184
+ }
185
+ socket.destroy();
186
+ this.onDisconnect(socket);
187
+ throw this.fatalProtocol ?? error;
188
+ }
189
+ }
190
+ async request(method, params) {
191
+ const socket = this.socket;
192
+ if (!socket || socket.destroyed)
193
+ throw new Error("Structured Render unavailable");
194
+ const id = ++this.requestId;
195
+ const frame = encodeRenderFrame({ id, token: this.token,
196
+ protocolVersion: STRUCTURED_RENDER_PROTOCOL_VERSION, method, params });
197
+ return new Promise((resolve, reject) => {
198
+ const timer = setTimeout(() => {
199
+ this.pending.delete(id);
200
+ reject(new Error(`Structured Render ${method} timed out`));
201
+ }, REQUEST_TIMEOUT_MS);
202
+ this.pending.set(id, { resolve, reject, timer });
203
+ socket.write(frame, (error) => {
204
+ if (error) {
205
+ const pending = this.pending.get(id);
206
+ if (pending) {
207
+ clearTimeout(pending.timer);
208
+ this.pending.delete(id);
209
+ pending.reject(error);
210
+ }
211
+ }
212
+ });
213
+ });
214
+ }
215
+ consume(data) {
216
+ try {
217
+ this.buffer = Buffer.concat([this.buffer, data]);
218
+ const decoded = decodeRenderFrames(this.buffer);
219
+ this.buffer = decoded.rest;
220
+ for (const frame of decoded.frames) {
221
+ if ("event" in frame) {
222
+ this.acceptEvent(frame);
223
+ continue;
224
+ }
225
+ const pending = this.pending.get(frame.id);
226
+ if (!pending)
227
+ continue;
228
+ this.pending.delete(frame.id);
229
+ clearTimeout(pending.timer);
230
+ if (frame.ok)
231
+ pending.resolve(frame.result);
232
+ else
233
+ pending.reject(new Error(`Structured Render ${frame.error?.code ?? "error"}: ${frame.error?.message ?? "unknown"}`));
234
+ }
235
+ }
236
+ catch (error) {
237
+ this.socket?.destroy(error instanceof Error ? error : new Error(String(error)));
238
+ }
239
+ }
240
+ acceptEvent(event) {
241
+ if (event.event === "reconcile")
242
+ return;
243
+ const runId = event.runId;
244
+ const handle = this.handles.get(runId);
245
+ if (handle && handle.incarnationId === event.incarnationId) {
246
+ if (handle.syncing || !this.connected) {
247
+ this.queue(handle.queued, event, runId);
248
+ if (handle.queued.length >= MAX_QUEUED_EVENTS)
249
+ handle.overflow = true;
250
+ }
251
+ else if (event.event === "exit") {
252
+ void this.catchUp(handle).then(() => handle.acceptExit(event)).catch(() => handle.fail());
253
+ }
254
+ else if (event.seq !== handle.seq[event.stream] + 1) {
255
+ void this.catchUp(handle).catch(() => handle.fail());
256
+ this.queue(handle.queued, event, runId);
257
+ }
258
+ else {
259
+ handle.acceptStream(event);
260
+ }
261
+ return;
262
+ }
263
+ const pending = this.orphanEvents.get(runId) ?? [];
264
+ this.queue(pending, event, runId);
265
+ this.orphanEvents.set(runId, pending);
266
+ }
267
+ queue(queue, event, runId) {
268
+ if (queue.length >= MAX_QUEUED_EVENTS) {
269
+ queue.shift();
270
+ this.orphanOverflow.add(runId);
271
+ }
272
+ queue.push(event);
273
+ }
274
+ async catchUp(handle) {
275
+ if (handle.syncing)
276
+ return;
277
+ handle.syncing = true;
278
+ try {
279
+ for (let pages = 0; pages < 2048; pages++) {
280
+ const page = await this.request("attach", { runId: handle.runId,
281
+ afterStdoutSeq: handle.seq.stdout, afterStderrSeq: handle.seq.stderr });
282
+ if (page.state.incarnationId !== handle.incarnationId)
283
+ throw new Error("structured owner changed");
284
+ this.replayStream(handle, "stdout", page.stdout);
285
+ this.replayStream(handle, "stderr", page.stderr);
286
+ this.inventory.set(handle.runId, page.state);
287
+ if (page.stdout.complete && page.stderr.complete)
288
+ break;
289
+ if (pages === 2047)
290
+ throw new Error("structured replay page cap exceeded");
291
+ }
292
+ if (handle.overflow || this.orphanOverflow.has(handle.runId)) {
293
+ handle.overflow = false;
294
+ this.orphanOverflow.delete(handle.runId);
295
+ // A bounded event queue overflow is recoverable from the authoritative
296
+ // replay. Recheck the tail before draining any surviving events.
297
+ handle.queued.splice(0);
298
+ for (let pages = 0; pages < 2048; pages++) {
299
+ const tail = await this.request("attach", { runId: handle.runId,
300
+ afterStdoutSeq: handle.seq.stdout, afterStderrSeq: handle.seq.stderr });
301
+ this.replayStream(handle, "stdout", tail.stdout);
302
+ this.replayStream(handle, "stderr", tail.stderr);
303
+ this.inventory.set(handle.runId, tail.state);
304
+ if (tail.stdout.complete && tail.stderr.complete)
305
+ break;
306
+ if (pages === 2047)
307
+ throw new Error("structured overflow replay page cap exceeded");
308
+ }
309
+ }
310
+ if (handle.overflow || this.orphanOverflow.has(handle.runId)) {
311
+ throw new Error("structured event queue overflowed again during replay");
312
+ }
313
+ const latest = this.inventory.get(handle.runId);
314
+ const queued = handle.queued.splice(0);
315
+ for (const event of queued) {
316
+ if (event.incarnationId !== handle.incarnationId)
317
+ continue;
318
+ if (event.event === "stream") {
319
+ if (event.seq <= handle.seq[event.stream])
320
+ continue;
321
+ if (event.seq !== handle.seq[event.stream] + 1)
322
+ throw new Error("structured live event gap");
323
+ handle.acceptStream(event);
324
+ }
325
+ else if (event.event === "exit")
326
+ handle.acceptExit(event);
327
+ }
328
+ if (latest?.status === "exited") {
329
+ handle.acceptExit({ exitCode: latest.exitCode, signal: latest.signal });
330
+ }
331
+ }
332
+ catch (error) {
333
+ process.stderr.write(`[wand] structured Render replay failed for ${handle.runId}: ${getErrorMessage(error)}\n`);
334
+ handle.fail();
335
+ throw error;
336
+ }
337
+ finally {
338
+ handle.syncing = false;
339
+ }
340
+ }
341
+ replayStream(handle, stream, replay) {
342
+ if (replay.resetRequired && handle.seq[stream] !== 0) {
343
+ throw new Error("structured replay window lost a live stream segment");
344
+ }
345
+ // A new adoption with a truncated head is handled by the recovery manager;
346
+ // an active handle may not quietly skip that head.
347
+ if (replay.resetRequired && replay.chunks[0]?.seq !== 1) {
348
+ throw new Error("structured replay cannot rebuild an active turn from a truncated head");
349
+ }
350
+ for (const chunk of replay.chunks) {
351
+ if (chunk.seq > handle.seq[stream])
352
+ handle.acceptStream({ stream, ...chunk });
353
+ }
354
+ }
355
+ async collect(runId) {
356
+ const logs = { stdout: [], stderr: [] };
357
+ const cursor = { stdout: 0, stderr: 0 };
358
+ for (let pages = 0; pages < 2048; pages++) {
359
+ let page;
360
+ try {
361
+ page = await this.request("attach", { runId, afterStdoutSeq: cursor.stdout,
362
+ afterStderrSeq: cursor.stderr });
363
+ }
364
+ catch (error) {
365
+ if (/notFound/.test(getErrorMessage(error)))
366
+ return null;
367
+ throw error;
368
+ }
369
+ for (const stream of ["stdout", "stderr"]) {
370
+ const replay = page[stream];
371
+ if (replay.resetRequired && cursor[stream] !== 0)
372
+ throw new Error("structured snapshot changed during replay");
373
+ if (replay.resetRequired && !page.state[`${stream}Truncated`]) {
374
+ throw new Error("structured replay cursor gap without truncation");
375
+ }
376
+ for (const chunk of replay.chunks)
377
+ logs[stream].push(chunk.data);
378
+ cursor[stream] = replay.nextSeq;
379
+ }
380
+ if (page.stdout.complete && page.stderr.complete) {
381
+ const state = toState(page.state, logs.stdout.join(""), logs.stderr.join(""));
382
+ this.inventory.set(runId, page.state);
383
+ this.observed.set(runId, { incarnationId: state.incarnationId,
384
+ stdoutSeq: state.stdoutSeq, stderrSeq: state.stderrSeq });
385
+ return state;
386
+ }
387
+ }
388
+ throw new Error("structured snapshot exceeded replay page cap");
389
+ }
390
+ /** Events can overtake the spawn response. Feed the contiguous prefix before
391
+ * paging so a fast >8 MiB child does not falsely lose its already-received head. */
392
+ primeBuffered(handle) {
393
+ const buffered = this.orphanEvents.get(handle.runId) ?? [];
394
+ this.orphanEvents.delete(handle.runId);
395
+ for (const event of buffered) {
396
+ if (event.incarnationId !== handle.incarnationId)
397
+ continue;
398
+ if (event.event === "exit") {
399
+ handle.queued.push(event);
400
+ continue;
401
+ }
402
+ if (event.seq <= handle.seq[event.stream])
403
+ continue;
404
+ if (event.seq !== handle.seq[event.stream] + 1) {
405
+ handle.queued.push(event);
406
+ continue;
407
+ }
408
+ handle.acceptStream(event);
409
+ }
410
+ }
411
+ async spawnStructured(request) {
412
+ await this.connect();
413
+ const existing = this.handles.get(request.runId);
414
+ if (existing && !existing.exited)
415
+ return existing;
416
+ if (existing)
417
+ this.handles.delete(request.runId);
418
+ const env = Object.fromEntries(Object.entries(request.env).filter((entry) => typeof entry[1] === "string"));
419
+ const result = await this.request("spawn", { ...request, env });
420
+ const handle = new RemoteRun(request.runId, result.state.incarnationId, result.state.pid, this);
421
+ this.handles.set(request.runId, handle);
422
+ this.primeBuffered(handle);
423
+ try {
424
+ await this.catchUp(handle);
425
+ if (result.state.status === "exited")
426
+ handle.acceptExit({ exitCode: result.state.exitCode, signal: result.state.signal });
427
+ return handle;
428
+ }
429
+ catch (error) {
430
+ this.dropHandle(request.runId, handle);
431
+ throw error;
432
+ }
433
+ }
434
+ async attachRun(runId) {
435
+ await this.connect();
436
+ return this.collect(runId);
437
+ }
438
+ async adoptRun(runId) {
439
+ await this.connect();
440
+ const prior = this.observed.get(runId);
441
+ const known = this.inventory.get(runId);
442
+ if (!known || known.status !== "running")
443
+ return null;
444
+ const handle = new RemoteRun(runId, known.incarnationId, known.pid, this);
445
+ if (prior?.incarnationId === known.incarnationId) {
446
+ handle.seq.stdout = prior.stdoutSeq;
447
+ handle.seq.stderr = prior.stderrSeq;
448
+ }
449
+ this.handles.set(runId, handle);
450
+ this.primeBuffered(handle);
451
+ await this.catchUp(handle);
452
+ return handle;
453
+ }
454
+ async listRuns() {
455
+ await this.connect();
456
+ const result = await this.request("list");
457
+ for (const state of result.runs)
458
+ this.inventory.set(state.runId, state);
459
+ return result.runs.map((state) => toState(state));
460
+ }
461
+ forgetRun(runId) {
462
+ this.handles.delete(runId);
463
+ this.inventory.delete(runId);
464
+ this.observed.delete(runId);
465
+ this.orphanEvents.delete(runId);
466
+ this.orphanOverflow.delete(runId);
467
+ void this.request("forget", { runId }).catch(() => { });
468
+ }
469
+ dropHandle(runId, handle) {
470
+ if (this.handles.get(runId) === handle)
471
+ this.handles.delete(runId);
472
+ }
473
+ disconnect() {
474
+ this.disposed = true;
475
+ this.stopHeartbeat?.();
476
+ this.stopHeartbeat = null;
477
+ if (this.reconnectTimer)
478
+ clearTimeout(this.reconnectTimer);
479
+ this.socket?.destroy();
480
+ this.socket = null;
481
+ this.rejectPending(new Error("Structured Render client disconnected"));
482
+ this.handles.clear();
483
+ }
484
+ onDisconnect(socket, error) {
485
+ if (this.socket !== socket)
486
+ return;
487
+ this.connected = false;
488
+ this.socket = null;
489
+ this.stopHeartbeat?.();
490
+ this.stopHeartbeat = null;
491
+ socket.destroy();
492
+ this.buffer = Buffer.alloc(0);
493
+ this.rejectPending(error ?? new Error("Structured Render disconnected"));
494
+ this.scheduleReconnect();
495
+ }
496
+ scheduleReconnect() {
497
+ if (this.disposed || this.fatalProtocol || this.reconnectTimer)
498
+ return;
499
+ this.reconnectTimer = setTimeout(() => {
500
+ this.reconnectTimer = null;
501
+ void this.connect().catch(() => { });
502
+ }, this.reconnectDelayMs);
503
+ this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, 10_000);
504
+ this.reconnectTimer.unref?.();
505
+ }
506
+ rejectPending(error) {
507
+ for (const pending of this.pending.values()) {
508
+ clearTimeout(pending.timer);
509
+ pending.reject(error);
510
+ }
511
+ this.pending.clear();
512
+ }
513
+ }