@pentoshi/clai 4.9.0 → 4.10.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.
Files changed (68) hide show
  1. package/README.md +22 -1
  2. package/dist/agent/compaction-executor.d.ts +3 -5
  3. package/dist/agent/compaction-executor.js +165 -28
  4. package/dist/agent/compaction-executor.js.map +1 -1
  5. package/dist/app/commands/catalog.js +5 -0
  6. package/dist/app/commands/catalog.js.map +1 -1
  7. package/dist/app/commands/registry.js +1 -0
  8. package/dist/app/commands/registry.js.map +1 -1
  9. package/dist/classic/bootstrap/start-classic.d.ts +1 -0
  10. package/dist/classic/bootstrap/start-classic.js +13 -0
  11. package/dist/classic/bootstrap/start-classic.js.map +1 -1
  12. package/dist/index.js +60 -7
  13. package/dist/index.js.map +1 -1
  14. package/dist/interactive-session/transport-node-pty.d.ts +10 -1
  15. package/dist/interactive-session/transport-node-pty.js +36 -12
  16. package/dist/interactive-session/transport-node-pty.js.map +1 -1
  17. package/dist/llm/operation-ledger.js +6 -2
  18. package/dist/llm/operation-ledger.js.map +1 -1
  19. package/dist/session-runtime/binding.d.ts +5 -0
  20. package/dist/session-runtime/binding.js +28 -0
  21. package/dist/session-runtime/binding.js.map +1 -0
  22. package/dist/session-runtime/child-bridge.d.ts +31 -0
  23. package/dist/session-runtime/child-bridge.js +169 -0
  24. package/dist/session-runtime/child-bridge.js.map +1 -0
  25. package/dist/session-runtime/client.d.ts +8 -0
  26. package/dist/session-runtime/client.js +448 -0
  27. package/dist/session-runtime/client.js.map +1 -0
  28. package/dist/session-runtime/discovery.d.ts +6 -0
  29. package/dist/session-runtime/discovery.js +100 -0
  30. package/dist/session-runtime/discovery.js.map +1 -0
  31. package/dist/session-runtime/host.d.ts +77 -0
  32. package/dist/session-runtime/host.js +758 -0
  33. package/dist/session-runtime/host.js.map +1 -0
  34. package/dist/session-runtime/launch.d.ts +11 -0
  35. package/dist/session-runtime/launch.js +68 -0
  36. package/dist/session-runtime/launch.js.map +1 -0
  37. package/dist/session-runtime/paths.d.ts +8 -0
  38. package/dist/session-runtime/paths.js +92 -0
  39. package/dist/session-runtime/paths.js.map +1 -0
  40. package/dist/session-runtime/protocol.d.ts +22 -0
  41. package/dist/session-runtime/protocol.js +149 -0
  42. package/dist/session-runtime/protocol.js.map +1 -0
  43. package/dist/session-runtime/replay-buffer.d.ts +12 -0
  44. package/dist/session-runtime/replay-buffer.js +50 -0
  45. package/dist/session-runtime/replay-buffer.js.map +1 -0
  46. package/dist/session-runtime/store.d.ts +14 -0
  47. package/dist/session-runtime/store.js +143 -0
  48. package/dist/session-runtime/store.js.map +1 -0
  49. package/dist/session-runtime/types.d.ts +102 -0
  50. package/dist/session-runtime/types.js +2 -0
  51. package/dist/session-runtime/types.js.map +1 -0
  52. package/dist/tui-v2/bootstrap/start-tui-v2.d.ts +1 -0
  53. package/dist/tui-v2/bootstrap/start-tui-v2.js +13 -0
  54. package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -1
  55. package/dist/ui-core/bootstrap/composition-root.d.ts +4 -0
  56. package/dist/ui-core/bootstrap/composition-root.js +2 -0
  57. package/dist/ui-core/bootstrap/composition-root.js.map +1 -1
  58. package/dist/ui-core/commands/command-handlers.js +3 -2
  59. package/dist/ui-core/commands/command-handlers.js.map +1 -1
  60. package/dist/ui-core/commands/config-commands.d.ts +1 -0
  61. package/dist/ui-core/commands/config-commands.js +6 -0
  62. package/dist/ui-core/commands/config-commands.js.map +1 -1
  63. package/dist/ui-core/commands/picker-commands.js +68 -25
  64. package/dist/ui-core/commands/picker-commands.js.map +1 -1
  65. package/dist/version.generated.d.ts +2 -2
  66. package/dist/version.generated.js +2 -2
  67. package/dist/version.generated.js.map +1 -1
  68. package/package.json +1 -1
@@ -0,0 +1,758 @@
1
+ import { randomUUID, timingSafeEqual } from "node:crypto";
2
+ import { chmod, rm } from "node:fs/promises";
3
+ import { createServer } from "node:net";
4
+ import { processIdentityTracker } from "../os/process-identity.js";
5
+ import { processAlive } from "../os/process-tree.js";
6
+ import { startPtyProcess } from "../interactive-session/transport-node-pty.js";
7
+ import { RUNTIME_CHILD_ENV, RUNTIME_HOST_ENV, RUNTIME_SESSION_ENV, RUNTIME_SOCKET_ENV, RUNTIME_TOKEN_ENV, decodeRuntimeHostPayload, } from "./launch.js";
8
+ import { isRuntimeSocketPath, runtimeSocketPath } from "./paths.js";
9
+ import { JsonFrameChannel, readFirstFrame, sendFrame, } from "./protocol.js";
10
+ import { TerminalReplayBuffer } from "./replay-buffer.js";
11
+ import { createRuntimeToken, deleteRuntimeMetadata, reapStaleRuntimeLock, tryAcquireRuntimeLease, writeRuntimeMetadata, } from "./store.js";
12
+ import { RUNTIME_PROTOCOL_VERSION, } from "./types.js";
13
+ const CLIENT_BACKPRESSURE_BYTES = 1024 * 1024;
14
+ const METADATA_DEBOUNCE_MS = 50;
15
+ const FINAL_OUTPUT_DRAIN_MS = 3_000;
16
+ const FORCE_STOP_MS = 8_000;
17
+ const ATTACH_RESET = Buffer.from("\u001b[?1049h\u001b[2J\u001b[H", "utf8");
18
+ function tokensEqual(candidate, expected) {
19
+ if (!/^[a-f0-9]{64}$/i.test(candidate) || !/^[a-f0-9]{64}$/i.test(expected)) {
20
+ return false;
21
+ }
22
+ return timingSafeEqual(Buffer.from(candidate, "hex"), Buffer.from(expected, "hex"));
23
+ }
24
+ function errorText(error) {
25
+ const value = error instanceof Error ? error.message : String(error);
26
+ return value.replace(/\s+/g, " ").trim().slice(0, 300) || "runtime host failed";
27
+ }
28
+ function authFrame(value) {
29
+ if (!value || typeof value !== "object")
30
+ return undefined;
31
+ const frame = value;
32
+ if (frame.version !== RUNTIME_PROTOCOL_VERSION ||
33
+ frame.type !== "auth" ||
34
+ !["probe", "client-control", "client-terminal", "child"].includes(String(frame.role)) ||
35
+ typeof frame.token !== "string") {
36
+ return undefined;
37
+ }
38
+ return frame;
39
+ }
40
+ function dimensionsOf(value) {
41
+ if (value.type !== "resize")
42
+ return undefined;
43
+ const columns = Math.floor(value.columns);
44
+ const rows = Math.floor(value.rows);
45
+ if (columns < 20 || columns > 1_000 || rows < 5 || rows > 500) {
46
+ return undefined;
47
+ }
48
+ return { columns, rows };
49
+ }
50
+ function clientFrame(value) {
51
+ if (!value || typeof value !== "object")
52
+ return undefined;
53
+ const frame = value;
54
+ if (frame.type === "ping" || frame.type === "detach") {
55
+ return frame;
56
+ }
57
+ if (frame.type === "resize" &&
58
+ typeof frame.columns === "number" &&
59
+ typeof frame.rows === "number") {
60
+ return frame;
61
+ }
62
+ return undefined;
63
+ }
64
+ function childFrame(value) {
65
+ if (!value || typeof value !== "object")
66
+ return undefined;
67
+ const frame = value;
68
+ if (frame.type === "minimise")
69
+ return frame;
70
+ if (frame.type === "exiting" &&
71
+ typeof frame.exitCode === "number" &&
72
+ Number.isSafeInteger(frame.exitCode)) {
73
+ return frame;
74
+ }
75
+ if (frame.type === "switch" &&
76
+ typeof frame.sessionId === "string" &&
77
+ typeof frame.closeCurrent === "boolean") {
78
+ return frame;
79
+ }
80
+ if (frame.type === "status" &&
81
+ typeof frame.sessionId === "string" &&
82
+ frame.sessionId.length <= 256 &&
83
+ typeof frame.cwd === "string" &&
84
+ frame.cwd.length <= 4096 &&
85
+ typeof frame.busy === "boolean" &&
86
+ (frame.title === undefined ||
87
+ (typeof frame.title === "string" && frame.title.length <= 256))) {
88
+ return frame;
89
+ }
90
+ return undefined;
91
+ }
92
+ function listen(server, path) {
93
+ return new Promise((resolve, reject) => {
94
+ const onError = (error) => {
95
+ server.off("listening", onListening);
96
+ reject(error);
97
+ };
98
+ const onListening = () => {
99
+ server.off("error", onError);
100
+ resolve();
101
+ };
102
+ server.once("error", onError);
103
+ server.once("listening", onListening);
104
+ server.listen(path);
105
+ });
106
+ }
107
+ function closeServer(server) {
108
+ if (!server?.listening)
109
+ return Promise.resolve();
110
+ return new Promise((resolve) => {
111
+ let settled = false;
112
+ const finish = () => {
113
+ if (settled)
114
+ return;
115
+ settled = true;
116
+ clearTimeout(timer);
117
+ resolve();
118
+ };
119
+ const timer = setTimeout(finish, 1_000);
120
+ timer.unref?.();
121
+ server.close(finish);
122
+ });
123
+ }
124
+ export class SessionRuntimeHost {
125
+ payload;
126
+ token = createRuntimeToken();
127
+ replay = new TerminalReplayBuffer();
128
+ startedAt = new Date().toISOString();
129
+ sessionId;
130
+ cwd;
131
+ socketPath;
132
+ title;
133
+ busy = true;
134
+ attached = false;
135
+ phase = "starting";
136
+ error;
137
+ lease;
138
+ server;
139
+ connections = new Set();
140
+ transport;
141
+ client;
142
+ childSocket;
143
+ childChannel;
144
+ idleTimer;
145
+ forceTimer;
146
+ livenessTimer;
147
+ livenessMisses = 0;
148
+ expectedExitCode;
149
+ metadataTimer;
150
+ metadataDirty = false;
151
+ outputPausedFor;
152
+ outputDrainHandler;
153
+ metadataWrites = Promise.resolve();
154
+ inputWrites = Promise.resolve();
155
+ statusWrites = Promise.resolve();
156
+ closing = false;
157
+ cleaning = false;
158
+ exitOutcome;
159
+ resolveExit;
160
+ exitPromise = new Promise((resolve) => {
161
+ this.resolveExit = resolve;
162
+ });
163
+ signalHandlers = new Map();
164
+ constructor(payload) {
165
+ this.payload = payload;
166
+ this.sessionId = payload.sessionId;
167
+ this.cwd = payload.cwd;
168
+ this.socketPath = runtimeSocketPath(payload.sessionId);
169
+ }
170
+ async run() {
171
+ this.lease = await tryAcquireRuntimeLease(this.sessionId);
172
+ if (!this.lease && (await reapStaleRuntimeLock(this.sessionId))) {
173
+ this.lease = await tryAcquireRuntimeLease(this.sessionId);
174
+ }
175
+ if (!this.lease)
176
+ return;
177
+ try {
178
+ await this.startServer();
179
+ await this.writeMetadataNow();
180
+ this.installSignals();
181
+ await this.startChild();
182
+ this.phase = "running";
183
+ this.queueMetadata();
184
+ const outcome = await this.exitPromise;
185
+ this.exitOutcome = outcome;
186
+ await this.flushTerminalOutput();
187
+ this.sendToClient({
188
+ type: "exit",
189
+ exitCode: outcome.exitCode ?? 1,
190
+ ...(outcome.signal ? { signal: outcome.signal } : {}),
191
+ });
192
+ await new Promise((resolve) => setTimeout(resolve, 300));
193
+ }
194
+ catch (error) {
195
+ this.phase = "failed";
196
+ this.error = errorText(error);
197
+ await this.writeMetadataNow().catch(() => undefined);
198
+ await new Promise((resolve) => setTimeout(resolve, 1_500));
199
+ }
200
+ finally {
201
+ await this.cleanup();
202
+ }
203
+ }
204
+ async startServer() {
205
+ if (process.platform !== "win32" && isRuntimeSocketPath(this.socketPath)) {
206
+ await rm(this.socketPath, { force: true }).catch(() => undefined);
207
+ }
208
+ const server = createServer((socket) => void this.accept(socket));
209
+ this.server = server;
210
+ await listen(server, this.socketPath);
211
+ if (process.platform !== "win32") {
212
+ await chmod(this.socketPath, 0o600).catch(() => undefined);
213
+ }
214
+ }
215
+ async startChild() {
216
+ const env = { ...process.env };
217
+ delete env[RUNTIME_HOST_ENV];
218
+ env[RUNTIME_CHILD_ENV] = "1";
219
+ env[RUNTIME_SOCKET_ENV] = this.socketPath;
220
+ env[RUNTIME_TOKEN_ENV] = this.token;
221
+ env[RUNTIME_SESSION_ENV] = this.sessionId;
222
+ const result = await startPtyProcess({
223
+ file: this.payload.launch.file,
224
+ args: this.payload.launch.args,
225
+ cwd: this.payload.cwd,
226
+ env,
227
+ dimensions: {
228
+ columns: this.payload.columns,
229
+ rows: this.payload.rows,
230
+ },
231
+ });
232
+ this.transport = result.transport;
233
+ this.transport.onOutput((event) => this.publishOutput(event.bytes));
234
+ this.transport.onExit((outcome) => {
235
+ if (this.exitOutcome)
236
+ return;
237
+ this.resolveExit?.(outcome);
238
+ });
239
+ this.armChildLiveness();
240
+ }
241
+ armChildLiveness() {
242
+ if (this.exitOutcome || !this.transport)
243
+ return;
244
+ this.livenessTimer = setTimeout(() => {
245
+ this.livenessTimer = undefined;
246
+ const transport = this.transport;
247
+ if (!transport || this.exitOutcome)
248
+ return;
249
+ const expected = transport.identity;
250
+ const comparison = expected
251
+ ? processIdentityTracker.compare(transport.pid, expected)
252
+ : processAlive(transport.pid)
253
+ ? "unknown"
254
+ : "gone";
255
+ const provenDead = comparison === "gone" || comparison === "mismatch";
256
+ this.livenessMisses = provenDead ? this.livenessMisses + 1 : 0;
257
+ if (this.livenessMisses >= 2) {
258
+ this.resolveExit?.({
259
+ endedAt: Date.now(),
260
+ exitCode: this.expectedExitCode ?? 1,
261
+ });
262
+ return;
263
+ }
264
+ this.armChildLiveness();
265
+ }, 500);
266
+ this.livenessTimer.unref?.();
267
+ }
268
+ async accept(socket) {
269
+ this.connections.add(socket);
270
+ socket.on("error", () => socket.destroy());
271
+ socket.once("close", () => this.connections.delete(socket));
272
+ socket.setNoDelay(true);
273
+ try {
274
+ const first = await readFirstFrame(socket);
275
+ const auth = authFrame(first.value);
276
+ if (!auth || !tokensEqual(auth.token, this.token)) {
277
+ sendFrame(socket, {
278
+ version: RUNTIME_PROTOCOL_VERSION,
279
+ type: "error",
280
+ message: "authentication failed",
281
+ });
282
+ socket.end();
283
+ return;
284
+ }
285
+ if (auth.role === "probe") {
286
+ this.acknowledge(socket);
287
+ socket.end();
288
+ return;
289
+ }
290
+ if (auth.role === "client-control") {
291
+ this.acceptClientControl(socket, auth, first.rest);
292
+ return;
293
+ }
294
+ if (auth.role === "client-terminal") {
295
+ this.acceptClientTerminal(socket, auth, first.rest);
296
+ return;
297
+ }
298
+ this.acceptChild(socket, first.rest);
299
+ }
300
+ catch {
301
+ socket.destroy();
302
+ }
303
+ }
304
+ acknowledge(socket) {
305
+ sendFrame(socket, {
306
+ version: RUNTIME_PROTOCOL_VERSION,
307
+ type: "ack",
308
+ sessionId: this.sessionId,
309
+ });
310
+ }
311
+ acceptClientControl(socket, auth, rest) {
312
+ const clientId = auth.clientId?.trim();
313
+ if (!clientId || clientId.length > 128) {
314
+ socket.destroy();
315
+ return;
316
+ }
317
+ if (this.client) {
318
+ this.disconnectClient("taken-over");
319
+ }
320
+ const client = { id: clientId, control: socket };
321
+ this.client = client;
322
+ this.acknowledge(socket);
323
+ client.channel = new JsonFrameChannel(socket, (value) => this.handleClientFrame(client, value), () => socket.destroy(), rest);
324
+ socket.once("close", () => this.clientClosed(client, socket));
325
+ }
326
+ acceptClientTerminal(socket, auth, rest) {
327
+ const client = this.client;
328
+ if (!client || !auth.clientId || client.id !== auth.clientId) {
329
+ socket.destroy();
330
+ return;
331
+ }
332
+ if (client.terminal) {
333
+ this.releaseOutputBackpressure(client.terminal);
334
+ client.terminal.destroy();
335
+ }
336
+ client.terminal = socket;
337
+ this.attached = true;
338
+ this.cancelIdleTimer();
339
+ this.queueMetadata();
340
+ this.acknowledge(socket);
341
+ this.writeToTerminal(socket, ATTACH_RESET);
342
+ const replay = this.replay.snapshot();
343
+ if (replay.length > 0)
344
+ this.writeToTerminal(socket, replay);
345
+ if (rest.length > 0)
346
+ this.queueInput(rest);
347
+ socket.on("data", (bytes) => {
348
+ if (this.client === client && client.terminal === socket) {
349
+ this.queueInput(bytes);
350
+ }
351
+ });
352
+ socket.once("close", () => this.clientClosed(client, socket));
353
+ socket.resume();
354
+ void this.resize({
355
+ columns: this.payload.columns,
356
+ rows: this.payload.rows,
357
+ });
358
+ }
359
+ acceptChild(socket, rest) {
360
+ this.childChannel?.dispose();
361
+ this.childSocket?.destroy();
362
+ this.childSocket = socket;
363
+ this.acknowledge(socket);
364
+ this.childChannel = new JsonFrameChannel(socket, (value) => this.handleChildFrame(value), () => socket.destroy(), rest);
365
+ socket.once("close", () => {
366
+ if (this.childSocket !== socket)
367
+ return;
368
+ this.childChannel?.dispose();
369
+ this.childChannel = undefined;
370
+ this.childSocket = undefined;
371
+ });
372
+ }
373
+ handleClientFrame(client, value) {
374
+ if (this.client !== client)
375
+ return;
376
+ const frame = clientFrame(value);
377
+ if (!frame)
378
+ return;
379
+ if (frame.type === "ping") {
380
+ client.channel?.send({ type: "pong" });
381
+ return;
382
+ }
383
+ if (frame.type === "detach") {
384
+ this.disconnectClient("requested");
385
+ return;
386
+ }
387
+ const dimensions = dimensionsOf(frame);
388
+ if (dimensions)
389
+ void this.resize(dimensions);
390
+ }
391
+ handleChildFrame(value) {
392
+ const frame = childFrame(value);
393
+ if (!frame)
394
+ return;
395
+ if (frame.type === "minimise") {
396
+ this.disconnectClient("minimise");
397
+ return;
398
+ }
399
+ if (frame.type === "exiting") {
400
+ this.expectedExitCode = frame.exitCode;
401
+ return;
402
+ }
403
+ if (frame.type === "switch") {
404
+ const target = frame.sessionId.trim();
405
+ if (!target || target.length > 256)
406
+ return;
407
+ this.sendToClient({ type: "switch", sessionId: target });
408
+ this.closeClientSockets();
409
+ if (frame.closeCurrent) {
410
+ setTimeout(() => this.requestGracefulStop(), 80).unref?.();
411
+ }
412
+ return;
413
+ }
414
+ this.statusWrites = this.statusWrites
415
+ .then(() => this.applyStatus(frame))
416
+ .catch(() => undefined);
417
+ }
418
+ async applyStatus(frame) {
419
+ let changed = false;
420
+ const nextId = frame.sessionId.trim();
421
+ if (nextId && nextId !== this.sessionId && nextId.length <= 256) {
422
+ const previousId = this.sessionId;
423
+ await this.rebind(nextId);
424
+ changed ||= this.sessionId !== previousId;
425
+ }
426
+ const nextCwd = frame.cwd || this.cwd;
427
+ const nextTitle = frame.title?.trim() || undefined;
428
+ changed ||=
429
+ nextCwd !== this.cwd ||
430
+ nextTitle !== this.title ||
431
+ frame.busy !== this.busy;
432
+ if (!changed)
433
+ return;
434
+ this.cwd = nextCwd;
435
+ this.title = nextTitle;
436
+ this.busy = frame.busy;
437
+ this.queueMetadata();
438
+ this.scheduleIdleTimer();
439
+ }
440
+ async rebind(nextId) {
441
+ let nextLease = await tryAcquireRuntimeLease(nextId);
442
+ if (!nextLease && (await reapStaleRuntimeLock(nextId))) {
443
+ nextLease = await tryAcquireRuntimeLease(nextId);
444
+ }
445
+ if (!nextLease)
446
+ return;
447
+ const previousId = this.sessionId;
448
+ const previousLease = this.lease;
449
+ this.sessionId = nextId;
450
+ this.lease = nextLease;
451
+ await this.writeMetadataNow();
452
+ await deleteRuntimeMetadata(previousId);
453
+ await previousLease?.release();
454
+ }
455
+ publishOutput(bytes) {
456
+ this.replay.append(bytes);
457
+ const terminal = this.client?.terminal;
458
+ if (terminal)
459
+ this.writeToTerminal(terminal, bytes);
460
+ }
461
+ writeToTerminal(terminal, bytes) {
462
+ if (!terminal.writable || terminal.destroyed)
463
+ return;
464
+ const writable = terminal.write(bytes);
465
+ if (!writable ||
466
+ terminal.writableLength >= CLIENT_BACKPRESSURE_BYTES) {
467
+ this.applyOutputBackpressure(terminal);
468
+ }
469
+ }
470
+ applyOutputBackpressure(terminal) {
471
+ if (this.outputPausedFor === terminal)
472
+ return;
473
+ this.releaseOutputBackpressure();
474
+ this.outputPausedFor = terminal;
475
+ this.transport?.pauseOutput();
476
+ const drain = () => this.releaseOutputBackpressure(terminal);
477
+ this.outputDrainHandler = drain;
478
+ terminal.once("drain", drain);
479
+ }
480
+ releaseOutputBackpressure(terminal) {
481
+ if (!this.outputPausedFor)
482
+ return;
483
+ if (terminal && this.outputPausedFor !== terminal)
484
+ return;
485
+ if (this.outputDrainHandler) {
486
+ this.outputPausedFor.off("drain", this.outputDrainHandler);
487
+ }
488
+ this.outputPausedFor = undefined;
489
+ this.outputDrainHandler = undefined;
490
+ this.transport?.resumeOutput();
491
+ }
492
+ async flushTerminalOutput() {
493
+ const deadline = Date.now() + FINAL_OUTPUT_DRAIN_MS;
494
+ while (Date.now() < deadline) {
495
+ const paused = this.outputPausedFor;
496
+ if (paused) {
497
+ await new Promise((resolve) => {
498
+ let settled = false;
499
+ const finish = () => {
500
+ if (settled)
501
+ return;
502
+ settled = true;
503
+ clearTimeout(timeout);
504
+ paused.off("drain", finish);
505
+ resolve();
506
+ };
507
+ const timeout = setTimeout(finish, Math.max(1, Math.min(100, deadline - Date.now())));
508
+ timeout.unref?.();
509
+ paused.once("drain", finish);
510
+ });
511
+ continue;
512
+ }
513
+ const remaining = Math.max(1, deadline - Date.now());
514
+ await Promise.race([
515
+ this.transport?.waitForOutputDrain?.() ?? Promise.resolve(),
516
+ new Promise((resolve) => {
517
+ const timeout = setTimeout(resolve, remaining);
518
+ timeout.unref?.();
519
+ }),
520
+ ]);
521
+ if (!this.outputPausedFor)
522
+ break;
523
+ }
524
+ this.releaseOutputBackpressure();
525
+ const terminal = this.client?.terminal;
526
+ if (!terminal ||
527
+ terminal.destroyed ||
528
+ terminal.writableEnded ||
529
+ !terminal.writable)
530
+ return;
531
+ await new Promise((resolve) => {
532
+ let settled = false;
533
+ const finish = () => {
534
+ if (settled)
535
+ return;
536
+ settled = true;
537
+ clearTimeout(timeout);
538
+ resolve();
539
+ };
540
+ const timeout = setTimeout(finish, 1_000);
541
+ timeout.unref?.();
542
+ terminal.once("close", finish);
543
+ terminal.end(finish);
544
+ });
545
+ }
546
+ queueInput(bytes) {
547
+ const copy = Buffer.from(bytes);
548
+ this.inputWrites = this.inputWrites
549
+ .then(() => this.transport?.write(copy))
550
+ .catch(() => undefined);
551
+ }
552
+ async resize(dimensions) {
553
+ await this.transport?.resize?.(dimensions).catch(() => undefined);
554
+ }
555
+ clientClosed(client, socket) {
556
+ if (client.terminal === socket) {
557
+ client.terminal = undefined;
558
+ this.releaseOutputBackpressure(socket);
559
+ if (this.client === client) {
560
+ this.attached = false;
561
+ this.queueMetadata();
562
+ this.scheduleIdleTimer();
563
+ }
564
+ }
565
+ if (client.control === socket)
566
+ client.channel?.dispose();
567
+ if (this.client !== client)
568
+ return;
569
+ if (!client.control.destroyed || client.terminal)
570
+ return;
571
+ this.client = undefined;
572
+ this.attached = false;
573
+ this.queueMetadata();
574
+ this.scheduleIdleTimer();
575
+ }
576
+ sendToClient(frame) {
577
+ this.client?.channel?.send(frame);
578
+ }
579
+ disconnectClient(reason) {
580
+ this.sendToClient({
581
+ type: "detached",
582
+ reason,
583
+ sessionId: this.sessionId,
584
+ });
585
+ this.closeClientSockets();
586
+ }
587
+ closeClientSockets() {
588
+ const client = this.client;
589
+ if (!client)
590
+ return;
591
+ this.client = undefined;
592
+ this.attached = false;
593
+ this.releaseOutputBackpressure(client.terminal);
594
+ client.channel?.dispose();
595
+ client.control.end();
596
+ setTimeout(() => client.terminal?.end(), 50).unref?.();
597
+ setTimeout(() => {
598
+ client.terminal?.destroy();
599
+ client.control.destroy();
600
+ }, 250).unref?.();
601
+ this.queueMetadata();
602
+ this.scheduleIdleTimer();
603
+ }
604
+ scheduleIdleTimer() {
605
+ this.cancelIdleTimer();
606
+ if (this.closing || this.attached || this.busy)
607
+ return;
608
+ this.idleTimer = setTimeout(() => this.requestGracefulStop(), this.payload.idleTimeoutMs);
609
+ this.idleTimer.unref?.();
610
+ }
611
+ cancelIdleTimer() {
612
+ if (!this.idleTimer)
613
+ return;
614
+ clearTimeout(this.idleTimer);
615
+ this.idleTimer = undefined;
616
+ }
617
+ requestGracefulStop() {
618
+ if (this.closing)
619
+ return;
620
+ this.closing = true;
621
+ this.phase = "stopping";
622
+ this.cancelIdleTimer();
623
+ this.queueMetadata();
624
+ if (!this.childChannel?.send({ type: "shutdown" })) {
625
+ void this.transport?.requestTreeTermination("graceful");
626
+ }
627
+ this.forceTimer = setTimeout(() => {
628
+ void this.transport?.requestTreeTermination("forceful");
629
+ }, FORCE_STOP_MS);
630
+ this.forceTimer.unref?.();
631
+ }
632
+ metadataSnapshot() {
633
+ const now = new Date().toISOString();
634
+ const childPid = this.transport?.pid;
635
+ const childIdentity = this.transport?.identity;
636
+ return {
637
+ version: RUNTIME_PROTOCOL_VERSION,
638
+ sessionId: this.sessionId,
639
+ hostPid: process.pid,
640
+ ...(processIdentityTracker.capture(process.pid)
641
+ ? { hostIdentity: processIdentityTracker.capture(process.pid) }
642
+ : {}),
643
+ ...(childPid ? { childPid } : {}),
644
+ ...(childIdentity ? { childIdentity } : {}),
645
+ socketPath: this.socketPath,
646
+ token: this.token,
647
+ cwd: this.cwd,
648
+ ...(this.title ? { title: this.title } : {}),
649
+ startedAt: this.startedAt,
650
+ updatedAt: now,
651
+ phase: this.phase,
652
+ busy: this.busy,
653
+ attached: this.attached,
654
+ ...(this.error ? { error: this.error } : {}),
655
+ };
656
+ }
657
+ queueMetadata() {
658
+ if (this.cleaning || this.phase === "failed")
659
+ return;
660
+ this.metadataDirty = true;
661
+ if (this.metadataTimer)
662
+ return;
663
+ this.metadataTimer = setTimeout(() => {
664
+ this.metadataTimer = undefined;
665
+ this.flushQueuedMetadata();
666
+ }, METADATA_DEBOUNCE_MS);
667
+ this.metadataTimer.unref?.();
668
+ }
669
+ flushQueuedMetadata() {
670
+ if (!this.metadataDirty || this.phase === "failed")
671
+ return;
672
+ this.metadataDirty = false;
673
+ const snapshot = this.metadataSnapshot();
674
+ this.metadataWrites = this.metadataWrites
675
+ .then(() => writeRuntimeMetadata(snapshot))
676
+ .catch(() => undefined);
677
+ }
678
+ async writeMetadataNow() {
679
+ if (this.metadataTimer) {
680
+ clearTimeout(this.metadataTimer);
681
+ this.metadataTimer = undefined;
682
+ }
683
+ this.metadataDirty = false;
684
+ const snapshot = this.metadataSnapshot();
685
+ const write = this.metadataWrites
686
+ .catch(() => undefined)
687
+ .then(() => writeRuntimeMetadata(snapshot));
688
+ this.metadataWrites = write.catch(() => undefined);
689
+ await write;
690
+ }
691
+ installSignals() {
692
+ for (const signal of ["SIGINT", "SIGTERM"]) {
693
+ const handler = () => this.requestGracefulStop();
694
+ this.signalHandlers.set(signal, handler);
695
+ process.on(signal, handler);
696
+ }
697
+ if (process.platform !== "win32") {
698
+ const handler = () => undefined;
699
+ this.signalHandlers.set("SIGHUP", handler);
700
+ process.on("SIGHUP", handler);
701
+ }
702
+ }
703
+ removeSignals() {
704
+ for (const [signal, handler] of this.signalHandlers) {
705
+ process.off(signal, handler);
706
+ }
707
+ this.signalHandlers.clear();
708
+ }
709
+ async cleanup() {
710
+ if (this.cleaning)
711
+ return;
712
+ this.cleaning = true;
713
+ this.closing = true;
714
+ this.cancelIdleTimer();
715
+ if (this.metadataTimer)
716
+ clearTimeout(this.metadataTimer);
717
+ this.metadataTimer = undefined;
718
+ this.metadataDirty = false;
719
+ if (this.forceTimer)
720
+ clearTimeout(this.forceTimer);
721
+ if (this.livenessTimer)
722
+ clearTimeout(this.livenessTimer);
723
+ this.releaseOutputBackpressure();
724
+ this.removeSignals();
725
+ this.closeClientSockets();
726
+ this.childChannel?.dispose();
727
+ this.childSocket?.destroy();
728
+ for (const socket of this.connections)
729
+ socket.destroy();
730
+ this.connections.clear();
731
+ await closeServer(this.server);
732
+ if (!this.exitOutcome && this.transport) {
733
+ await this.transport.requestTreeTermination("forceful").catch(() => undefined);
734
+ }
735
+ await this.transport?.dispose().catch(() => undefined);
736
+ await this.metadataWrites.catch(() => undefined);
737
+ await deleteRuntimeMetadata(this.sessionId);
738
+ await this.lease?.release();
739
+ if (process.platform !== "win32" && isRuntimeSocketPath(this.socketPath)) {
740
+ await rm(this.socketPath, { force: true }).catch(() => undefined);
741
+ }
742
+ }
743
+ }
744
+ export async function runRuntimeHostFromEnvironment() {
745
+ const encoded = process.env[RUNTIME_HOST_ENV];
746
+ if (!encoded)
747
+ return false;
748
+ delete process.env[RUNTIME_HOST_ENV];
749
+ const payload = decodeRuntimeHostPayload(encoded);
750
+ if (!payload)
751
+ throw new Error("invalid session runtime host payload");
752
+ await new SessionRuntimeHost(payload).run();
753
+ return true;
754
+ }
755
+ export function runtimeHostClientId() {
756
+ return `${process.pid}-${randomUUID()}`;
757
+ }
758
+ //# sourceMappingURL=host.js.map