@co0ontty/wand 4.72.2-beta.gcc5d57e → 4.73.0
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 +4 -4
- package/dist/config.js +20 -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/message-truncator.js +6 -0
- package/dist/native/darwin-arm64/wand-render +0 -0
- package/dist/native/darwin-arm64/wand-render.sha256 +1 -0
- package/dist/native/darwin-arm64/wand-render.version +1 -0
- 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-binary.d.ts +61 -0
- package/dist/render-binary.js +204 -0
- package/dist/render-daemon-client.d.ts +171 -0
- package/dist/render-daemon-client.js +1114 -0
- package/dist/render-host.d.ts +64 -0
- package/dist/render-host.js +301 -0
- package/dist/render-protocol.d.ts +128 -0
- package/dist/render-protocol.js +99 -0
- 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 +72 -9
- 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 +31 -9
- package/dist/session-transport.js +1 -1
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +14 -5
- package/dist/structured-client-protocol.d.ts +3 -1
- package/dist/structured-client-protocol.js +85 -25
- package/dist/structured-content.d.ts +20 -3
- package/dist/structured-content.js +80 -1
- package/dist/structured-exec-host.js +7 -1
- package/dist/structured-exec-pump.js +6 -2
- package/dist/structured-pi-adapter.js +18 -2
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +27 -3
- 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 +19 -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/content/tailwind.css +12 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.d.ts +1 -1
- package/dist/ws-broadcast.js +17 -5
- package/package.json +8 -3
- package/scripts/install-render-binary.js +499 -0
- package/scripts/stage-render-binaries.js +626 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { RenderStructuredClient } from "./render-structured-client.js";
|
|
2
|
+
import type { StructuredExecHost, StructuredExecProcess, StructuredRunState, StructuredSpawnRequest } from "./structured-exec-host.js";
|
|
3
|
+
type Owner = "legacy" | "rust";
|
|
4
|
+
/** Resolve the separate v2 executable; do not confuse it with PTY v1 wand-render. */
|
|
5
|
+
export declare function resolveStructuredRenderBinary(configPath: string): string | null;
|
|
6
|
+
/**
|
|
7
|
+
* A stable runId can belong to exactly one persistent daemon. Configuration
|
|
8
|
+
* chooses only new runs; existing owners are always resolved by inventories.
|
|
9
|
+
*/
|
|
10
|
+
export declare class CompositeStructuredExecHost implements StructuredExecHost {
|
|
11
|
+
private readonly legacy;
|
|
12
|
+
private readonly rust;
|
|
13
|
+
private readonly newOwner;
|
|
14
|
+
readonly persistent = true;
|
|
15
|
+
private readonly owners;
|
|
16
|
+
private readonly starting;
|
|
17
|
+
constructor(legacy: StructuredExecHost | null, rust: StructuredExecHost | null, newOwner: Owner);
|
|
18
|
+
private getHost;
|
|
19
|
+
listRuns(): Promise<StructuredRunState[]>;
|
|
20
|
+
private ownerOf;
|
|
21
|
+
spawnStructured(request: StructuredSpawnRequest): Promise<StructuredExecProcess>;
|
|
22
|
+
attachRun(runId: string): Promise<StructuredRunState | null>;
|
|
23
|
+
adoptRun(runId: string): Promise<StructuredExecProcess | null>;
|
|
24
|
+
forgetRun(runId: string): void;
|
|
25
|
+
}
|
|
26
|
+
/** Default legacy. v2 is adopted even on rollback when it still owns runs. */
|
|
27
|
+
export declare function createUpgradeAwareStructuredHost(configPath: string, legacy: StructuredExecHost | null, processHost?: "legacy" | "rust"): Promise<{
|
|
28
|
+
host: StructuredExecHost | undefined;
|
|
29
|
+
rustClient: RenderStructuredClient | null;
|
|
30
|
+
}>;
|
|
31
|
+
export {};
|