@rynx-ai/runtime 0.1.11-beta.32 → 0.1.11-beta.34
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/claude/native-bridge.d.ts +16 -1
- package/dist/claude/native-bridge.js +91 -17
- package/dist/claude/native-integration.d.ts +10 -0
- package/dist/claude/native-integration.js +100 -6
- package/dist/codex-app-server/forwarder.js +17 -1
- package/dist/codex-app-server/process-registry.d.ts +36 -0
- package/dist/codex-app-server/process-registry.js +320 -0
- package/dist/codex-app-server/ws-channel.d.ts +7 -0
- package/dist/codex-app-server/ws-channel.js +85 -9
- package/dist/host.d.ts +9 -1
- package/dist/host.js +48 -14
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/runner/child.d.ts +24 -3
- package/dist/runner/child.js +327 -58
- package/dist/runner/manager.d.ts +33 -18
- package/dist/runner/manager.js +229 -49
- package/dist/runner/protocol.d.ts +79 -7
- package/dist/runner/transport.d.ts +9 -2
- package/dist/runner/transport.js +55 -3
- package/dist/runner-main.js +8 -3
- package/dist/terminal/codex-tui.d.ts +4 -0
- package/dist/terminal/codex-tui.js +5 -0
- package/dist/terminal/control-parser.d.ts +39 -0
- package/dist/terminal/control-parser.js +172 -0
- package/dist/terminal/registry.d.ts +15 -13
- package/dist/terminal/registry.js +38 -20
- package/dist/terminal/spool.d.ts +47 -0
- package/dist/terminal/spool.js +231 -0
- package/dist/terminal/tmux.d.ts +93 -84
- package/dist/terminal/tmux.js +666 -199
- package/package.json +3 -4
package/dist/runner/manager.js
CHANGED
|
@@ -72,11 +72,16 @@ class ManagedTerminal {
|
|
|
72
72
|
sendMsg;
|
|
73
73
|
onClose;
|
|
74
74
|
ready;
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
readerDone;
|
|
76
|
+
resizeListener;
|
|
77
|
+
dimensions;
|
|
78
|
+
role = "read-only";
|
|
79
|
+
seedBytes = 0;
|
|
77
80
|
resolveReady;
|
|
78
81
|
rejectReady;
|
|
82
|
+
resolveReaderDone;
|
|
79
83
|
state = "opening";
|
|
84
|
+
pending = new Map();
|
|
80
85
|
constructor(attachId, sendMsg, onClose) {
|
|
81
86
|
this.attachId = attachId;
|
|
82
87
|
this.sendMsg = sendMsg;
|
|
@@ -85,20 +90,75 @@ class ManagedTerminal {
|
|
|
85
90
|
this.resolveReady = resolve;
|
|
86
91
|
this.rejectReady = reject;
|
|
87
92
|
});
|
|
93
|
+
this.readerDone = new Promise((resolve) => {
|
|
94
|
+
this.resolveReaderDone = resolve;
|
|
95
|
+
});
|
|
88
96
|
// Never surface an unhandled rejection if no caller awaits `ready`.
|
|
89
97
|
void this.ready.catch(() => undefined);
|
|
90
98
|
}
|
|
91
|
-
|
|
92
|
-
this.
|
|
99
|
+
onResize(listener) {
|
|
100
|
+
this.resizeListener = listener;
|
|
101
|
+
if (this.dimensions)
|
|
102
|
+
queueMicrotask(() => listener(this.dimensions));
|
|
103
|
+
}
|
|
104
|
+
readSeed(offset, maxBytes) {
|
|
105
|
+
if (this.state !== "prepared")
|
|
106
|
+
return Promise.reject(new Error("terminal is not prepared"));
|
|
107
|
+
return this.requestRead("seed", {
|
|
108
|
+
t: "term.seed.read",
|
|
109
|
+
attachId: this.attachId,
|
|
110
|
+
reqId: randomUUID(),
|
|
111
|
+
offset,
|
|
112
|
+
maxBytes,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
start() {
|
|
116
|
+
if (this.state !== "prepared")
|
|
117
|
+
return Promise.reject(new Error("terminal is not prepared"));
|
|
118
|
+
this.state = "starting";
|
|
119
|
+
const reqId = randomUUID();
|
|
120
|
+
return this.request("start", { t: "term.start", attachId: this.attachId, reqId })
|
|
121
|
+
.then(() => {
|
|
122
|
+
if (this.state === "starting")
|
|
123
|
+
this.state = "started";
|
|
124
|
+
});
|
|
93
125
|
}
|
|
94
|
-
|
|
95
|
-
this.
|
|
126
|
+
read(offset, maxBytes) {
|
|
127
|
+
if (this.state !== "started")
|
|
128
|
+
return Promise.reject(new Error("terminal has not started"));
|
|
129
|
+
return this.requestRead("read", {
|
|
130
|
+
t: "term.read",
|
|
131
|
+
attachId: this.attachId,
|
|
132
|
+
reqId: randomUUID(),
|
|
133
|
+
offset,
|
|
134
|
+
maxBytes,
|
|
135
|
+
});
|
|
96
136
|
}
|
|
97
137
|
write(data) {
|
|
98
|
-
this.
|
|
138
|
+
if (this.state !== "started" || this.role !== "owner")
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
|
|
141
|
+
if (bytes.byteLength === 0)
|
|
142
|
+
return Promise.resolve();
|
|
143
|
+
const reqId = randomUUID();
|
|
144
|
+
return this.request("input", {
|
|
145
|
+
t: "term.input",
|
|
146
|
+
attachId: this.attachId,
|
|
147
|
+
reqId,
|
|
148
|
+
dataB64: bytes.toString("base64"),
|
|
149
|
+
}).then(() => undefined);
|
|
99
150
|
}
|
|
100
151
|
resize(cols, rows) {
|
|
101
|
-
this.
|
|
152
|
+
if (this.state !== "started")
|
|
153
|
+
return Promise.resolve();
|
|
154
|
+
const reqId = randomUUID();
|
|
155
|
+
return this.request("resize", {
|
|
156
|
+
t: "term.resize",
|
|
157
|
+
attachId: this.attachId,
|
|
158
|
+
reqId,
|
|
159
|
+
cols,
|
|
160
|
+
rows,
|
|
161
|
+
}).then(() => undefined);
|
|
102
162
|
}
|
|
103
163
|
close() {
|
|
104
164
|
if (this.state === "closed")
|
|
@@ -107,43 +167,96 @@ class ManagedTerminal {
|
|
|
107
167
|
this.state = "closed";
|
|
108
168
|
this.sendMsg({ t: "term.close", attachId: this.attachId });
|
|
109
169
|
this.onClose();
|
|
170
|
+
this.rejectPending(new Error("terminal attachment was closed"));
|
|
110
171
|
if (wasOpening) {
|
|
111
172
|
this.rejectReady(new TerminalOpenError("terminal attachment was closed while opening", "terminal_open_failed"));
|
|
112
173
|
}
|
|
174
|
+
this.resolveReaderDone({ finalOffset: 0, reason: "client_closed", exitCode: 0 });
|
|
113
175
|
}
|
|
114
176
|
// ── internal (driven by onChildMessage) ──
|
|
115
|
-
|
|
177
|
+
_prepared(role, seedBytes, cols, rows) {
|
|
116
178
|
if (this.state !== "opening")
|
|
117
179
|
return;
|
|
118
|
-
this.state = "
|
|
119
|
-
this.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.
|
|
180
|
+
this.state = "prepared";
|
|
181
|
+
this.role = role;
|
|
182
|
+
this.seedBytes = seedBytes;
|
|
183
|
+
this.dimensions = cols !== undefined && rows !== undefined ? { cols, rows } : undefined;
|
|
184
|
+
this.resolveReady({
|
|
185
|
+
role,
|
|
186
|
+
seedBytes,
|
|
187
|
+
...(this.dimensions ? { dimensions: this.dimensions } : {}),
|
|
188
|
+
});
|
|
123
189
|
}
|
|
124
|
-
|
|
190
|
+
_dimensions(cols, rows) {
|
|
125
191
|
if (this.state === "closed")
|
|
126
192
|
return;
|
|
127
|
-
|
|
128
|
-
this.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
193
|
+
this.dimensions = { cols, rows };
|
|
194
|
+
this.resizeListener?.(this.dimensions);
|
|
195
|
+
}
|
|
196
|
+
_started(reqId) {
|
|
197
|
+
this.resolvePending(reqId, "start", undefined);
|
|
198
|
+
}
|
|
199
|
+
_chunk(reqId, operation, dataB64, nextOffset, done, finalOffset) {
|
|
200
|
+
this.resolvePending(reqId, operation, {
|
|
201
|
+
data: Uint8Array.from(Buffer.from(dataB64, "base64")),
|
|
202
|
+
nextOffset,
|
|
203
|
+
done,
|
|
204
|
+
...(finalOffset === undefined ? {} : { finalOffset }),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
_ack(reqId, operation) {
|
|
208
|
+
this.resolvePending(reqId, operation, undefined);
|
|
135
209
|
}
|
|
136
|
-
|
|
210
|
+
_readerDone(info) {
|
|
211
|
+
this.resolveReaderDone(info);
|
|
212
|
+
}
|
|
213
|
+
_fail(message, code = "terminal_open_failed", reqId) {
|
|
137
214
|
if (this.state === "closed")
|
|
138
215
|
return;
|
|
216
|
+
if (reqId) {
|
|
217
|
+
const pending = this.pending.get(reqId);
|
|
218
|
+
if (pending) {
|
|
219
|
+
this.pending.delete(reqId);
|
|
220
|
+
pending.reject(new TerminalOpenError(message, code));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
139
224
|
const wasOpening = this.state === "opening";
|
|
140
225
|
this.state = "closed";
|
|
226
|
+
this.rejectPending(new TerminalOpenError(message, code));
|
|
141
227
|
if (wasOpening) {
|
|
142
228
|
this.rejectReady(new TerminalOpenError(message, code));
|
|
143
229
|
}
|
|
144
|
-
|
|
145
|
-
|
|
230
|
+
this.resolveReaderDone({ finalOffset: 0, reason: "internal", exitCode: 1 });
|
|
231
|
+
}
|
|
232
|
+
requestRead(operation, message) {
|
|
233
|
+
return this.request(operation, message);
|
|
234
|
+
}
|
|
235
|
+
request(operation, message) {
|
|
236
|
+
return new Promise((resolve, reject) => {
|
|
237
|
+
this.pending.set(message.reqId, { operation, resolve, reject });
|
|
238
|
+
try {
|
|
239
|
+
this.sendMsg(message);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
this.pending.delete(message.reqId);
|
|
243
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
resolvePending(reqId, operation, value) {
|
|
248
|
+
const pending = this.pending.get(reqId);
|
|
249
|
+
if (!pending || pending.operation !== operation) {
|
|
250
|
+
this._fail(`terminal response ${reqId} is duplicate or out of order`);
|
|
251
|
+
return;
|
|
146
252
|
}
|
|
253
|
+
this.pending.delete(reqId);
|
|
254
|
+
pending.resolve(value);
|
|
255
|
+
}
|
|
256
|
+
rejectPending(error) {
|
|
257
|
+
for (const pending of this.pending.values())
|
|
258
|
+
pending.reject(error);
|
|
259
|
+
this.pending.clear();
|
|
147
260
|
}
|
|
148
261
|
}
|
|
149
262
|
export class RunnerManager {
|
|
@@ -227,9 +340,12 @@ export class RunnerManager {
|
|
|
227
340
|
this.shutdownGraceMs = Math.max(0, opts.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS);
|
|
228
341
|
this.shutdownKillGraceMs = Math.max(0, opts.shutdownKillGraceMs ?? DEFAULT_SHUTDOWN_KILL_GRACE_MS);
|
|
229
342
|
this.signalChild = opts.signalChild ?? signalRunnerChild;
|
|
230
|
-
this.terminateTerminalServer = opts.terminateTerminalServer ??
|
|
231
|
-
|
|
232
|
-
this.
|
|
343
|
+
this.terminateTerminalServer = opts.terminateTerminalServer ??
|
|
344
|
+
((name, ownerPid) => terminateTmuxServer(name, undefined, ownerPid));
|
|
345
|
+
this.terminalWindowActivityAt = opts.terminalWindowActivityAt ??
|
|
346
|
+
((name, ownerPid) => tmuxWindowActivityAt(name, undefined, ownerPid));
|
|
347
|
+
this.terminalHasAttachedClient = opts.terminalHasAttachedClient ??
|
|
348
|
+
((name, ownerPid) => tmuxHasAttachedClient(name, undefined, ownerPid));
|
|
233
349
|
this.liveStartTimeoutMs = Math.max(1, opts.liveStartTimeoutMs ?? DEFAULT_LIVE_START_TIMEOUT_MS);
|
|
234
350
|
this.liveReadyTimeoutMs = Math.max(1, opts.liveReadyTimeoutMs ?? DEFAULT_LIVE_READY_TIMEOUT_MS);
|
|
235
351
|
this.nativeLiveStartTimeoutMs = Math.max(1, opts.nativeLiveStartTimeoutMs ?? DEFAULT_NATIVE_LIVE_START_TIMEOUT_MS);
|
|
@@ -248,9 +364,8 @@ export class RunnerManager {
|
|
|
248
364
|
/**
|
|
249
365
|
* Open a live terminal on the session's runner child (spawning it if needed).
|
|
250
366
|
* Phase C hosts one terminal ("main") per session; the returned handle is a
|
|
251
|
-
* single attach client
|
|
252
|
-
*
|
|
253
|
-
* bridge calls it directly.
|
|
367
|
+
* single attach client with the requested `owner` (read-write) or `read-only`
|
|
368
|
+
* role. Not part of `AgentExecutor`; the WS bridge calls it directly.
|
|
254
369
|
*/
|
|
255
370
|
openTerminal(localThreadId, opts) {
|
|
256
371
|
const handle = this.getOrSpawn(localThreadId);
|
|
@@ -1279,8 +1394,14 @@ export class RunnerManager {
|
|
|
1279
1394
|
}
|
|
1280
1395
|
});
|
|
1281
1396
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1397
|
+
const failAndReap = (reason) => {
|
|
1398
|
+
this.failHandle(handle, reason);
|
|
1399
|
+
void this.terminateHandle(handle, reason).catch((error) => {
|
|
1400
|
+
logTerminationFailure(handle, error);
|
|
1401
|
+
});
|
|
1402
|
+
};
|
|
1403
|
+
child.on("error", (error) => failAndReap(error.message));
|
|
1404
|
+
child.on("exit", (code, signal) => failAndReap(`runner exited (code=${code ?? "null"}${signal ? `, signal=${signal}` : ""})`));
|
|
1284
1405
|
return handle;
|
|
1285
1406
|
}
|
|
1286
1407
|
onChildMessage(handle, msg) {
|
|
@@ -1301,22 +1422,36 @@ export class RunnerManager {
|
|
|
1301
1422
|
}
|
|
1302
1423
|
return;
|
|
1303
1424
|
}
|
|
1304
|
-
case "term.
|
|
1305
|
-
handle.terminals.get(msg.attachId)?.
|
|
1425
|
+
case "term.prepared":
|
|
1426
|
+
handle.terminals.get(msg.attachId)?._prepared(msg.role, msg.seedBytes, msg.cols, msg.rows);
|
|
1306
1427
|
return;
|
|
1307
|
-
case "term.
|
|
1308
|
-
handle.terminals.get(msg.attachId)?.
|
|
1428
|
+
case "term.seed.chunk":
|
|
1429
|
+
handle.terminals.get(msg.attachId)?._chunk(msg.reqId, "seed", msg.dataB64, msg.nextOffset, msg.done, msg.finalOffset);
|
|
1309
1430
|
return;
|
|
1310
|
-
case "term.
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1431
|
+
case "term.started":
|
|
1432
|
+
handle.terminals.get(msg.attachId)?._started(msg.reqId);
|
|
1433
|
+
return;
|
|
1434
|
+
case "term.chunk":
|
|
1435
|
+
handle.terminals.get(msg.attachId)?._chunk(msg.reqId, "read", msg.dataB64, msg.nextOffset, msg.done, msg.finalOffset);
|
|
1436
|
+
return;
|
|
1437
|
+
case "term.dimensions":
|
|
1438
|
+
handle.terminals.get(msg.attachId)?._dimensions(msg.cols, msg.rows);
|
|
1439
|
+
return;
|
|
1440
|
+
case "term.reader.done":
|
|
1441
|
+
handle.terminals.get(msg.attachId)?._readerDone({
|
|
1442
|
+
finalOffset: msg.finalOffset,
|
|
1443
|
+
reason: msg.reason,
|
|
1444
|
+
exitCode: msg.exitCode,
|
|
1445
|
+
});
|
|
1446
|
+
return;
|
|
1447
|
+
case "term.ack":
|
|
1448
|
+
handle.terminals.get(msg.attachId)?._ack(msg.reqId, msg.operation);
|
|
1314
1449
|
return;
|
|
1315
|
-
}
|
|
1316
1450
|
case "term.error": {
|
|
1317
1451
|
const terminal = handle.terminals.get(msg.attachId);
|
|
1318
|
-
|
|
1319
|
-
|
|
1452
|
+
if (!msg.reqId)
|
|
1453
|
+
handle.terminals.delete(msg.attachId);
|
|
1454
|
+
terminal?._fail(msg.message, msg.code, msg.reqId);
|
|
1320
1455
|
return;
|
|
1321
1456
|
}
|
|
1322
1457
|
case "mirror":
|
|
@@ -1409,6 +1544,18 @@ export class RunnerManager {
|
|
|
1409
1544
|
this.deliverRotateMessage(handle, msg);
|
|
1410
1545
|
return;
|
|
1411
1546
|
}
|
|
1547
|
+
case "terminal.lifecycle.ended": {
|
|
1548
|
+
if (msg.lifecycle === "required") {
|
|
1549
|
+
void this.terminateHandle(handle, `required Terminal exited with status ${msg.status}`).catch((error) => logTerminationFailure(handle, error));
|
|
1550
|
+
}
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
case "terminal.reaped": {
|
|
1554
|
+
const resolve = handle.live.get(msg.reqId);
|
|
1555
|
+
handle.live.delete(msg.reqId);
|
|
1556
|
+
resolve?.({ ok: msg.ok, error: msg.error });
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1412
1559
|
case "live.ready":
|
|
1413
1560
|
case "interrupted":
|
|
1414
1561
|
case "injected": {
|
|
@@ -1499,7 +1646,7 @@ export class RunnerManager {
|
|
|
1499
1646
|
childExited = await waitForChildExit(handle.completion, this.shutdownKillGraceMs);
|
|
1500
1647
|
}
|
|
1501
1648
|
if (handle.key !== CAP_KEY) {
|
|
1502
|
-
await Promise.resolve(this.terminateTerminalServer(`${handle.key}-main
|
|
1649
|
+
await Promise.resolve(this.terminateTerminalServer(`${handle.key}-main`, handle.child.pid)).catch((error) => {
|
|
1503
1650
|
console.warn(`[runner-manager] best-effort terminal close failed for ${handle.key}-main: ${error instanceof Error ? error.message : String(error)}`);
|
|
1504
1651
|
});
|
|
1505
1652
|
}
|
|
@@ -1584,7 +1731,40 @@ export class RunnerManager {
|
|
|
1584
1731
|
// A failed teardown must re-arm on the next scan instead of retrying on
|
|
1585
1732
|
// every sweep forever.
|
|
1586
1733
|
delete handle.nativePaneLastBusyAt;
|
|
1587
|
-
await this.
|
|
1734
|
+
await this.reapNativeTerminal(handle).catch((error) => {
|
|
1735
|
+
logTerminationFailure(handle, error);
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
reapNativeTerminal(handle) {
|
|
1739
|
+
const localThreadId = this.currentTerminalSessionId(handle, handle.key);
|
|
1740
|
+
const terminalId = `${localThreadId}-main`;
|
|
1741
|
+
const reqId = randomUUID();
|
|
1742
|
+
return new Promise((resolve, reject) => {
|
|
1743
|
+
const timeout = setTimeout(() => {
|
|
1744
|
+
if (!handle.live.delete(reqId))
|
|
1745
|
+
return;
|
|
1746
|
+
reject(new Error(`runner did not acknowledge idle terminal reap within ${this.liveReadyTimeoutMs}ms`));
|
|
1747
|
+
}, this.liveReadyTimeoutMs);
|
|
1748
|
+
timeout.unref?.();
|
|
1749
|
+
handle.live.set(reqId, (reply) => {
|
|
1750
|
+
clearTimeout(timeout);
|
|
1751
|
+
if (reply.ok) {
|
|
1752
|
+
resolve();
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
reject(fromWireError(reply.error ?? {
|
|
1756
|
+
message: "idle terminal reap failed",
|
|
1757
|
+
code: "terminal_reap_failed",
|
|
1758
|
+
statusCode: 500,
|
|
1759
|
+
}));
|
|
1760
|
+
});
|
|
1761
|
+
handle.transport.send({
|
|
1762
|
+
t: "terminal.reap",
|
|
1763
|
+
reqId,
|
|
1764
|
+
localThreadId,
|
|
1765
|
+
terminalId,
|
|
1766
|
+
});
|
|
1767
|
+
});
|
|
1588
1768
|
}
|
|
1589
1769
|
async isNativePaneBusy(handle) {
|
|
1590
1770
|
if (handle.activeResponseIds.size > 0 ||
|
|
@@ -1595,7 +1775,7 @@ export class RunnerManager {
|
|
|
1595
1775
|
}
|
|
1596
1776
|
const terminalName = `${handle.key}-main`;
|
|
1597
1777
|
try {
|
|
1598
|
-
if (await this.terminalHasAttachedClient(terminalName))
|
|
1778
|
+
if (await this.terminalHasAttachedClient(terminalName, handle.child.pid))
|
|
1599
1779
|
return true;
|
|
1600
1780
|
}
|
|
1601
1781
|
catch {
|
|
@@ -1603,7 +1783,7 @@ export class RunnerManager {
|
|
|
1603
1783
|
}
|
|
1604
1784
|
let activityAt = null;
|
|
1605
1785
|
try {
|
|
1606
|
-
activityAt = await this.terminalWindowActivityAt(terminalName);
|
|
1786
|
+
activityAt = await this.terminalWindowActivityAt(terminalName, handle.child.pid);
|
|
1607
1787
|
}
|
|
1608
1788
|
catch {
|
|
1609
1789
|
// A failed probe contributes no busy evidence.
|
|
@@ -23,7 +23,7 @@ export type TerminalRole = "owner" | "read-only";
|
|
|
23
23
|
/** Stable reason for a terminal attach failure. `terminal_not_live` is an
|
|
24
24
|
* expected, retryable absence; `terminal_open_failed` is an infrastructure error. */
|
|
25
25
|
export type TerminalOpenErrorCode = "terminal_not_live" | "terminal_open_failed";
|
|
26
|
-
/** Parent → child.
|
|
26
|
+
/** Parent → child. Raw terminal bytes ride `term.input` base64-encoded so the
|
|
27
27
|
* NDJSON line framing stays intact (raw bytes contain newlines). */
|
|
28
28
|
export type ToChild = {
|
|
29
29
|
t: "mirror.image.ack";
|
|
@@ -51,13 +51,31 @@ export type ToChild = {
|
|
|
51
51
|
rows: number;
|
|
52
52
|
command?: string;
|
|
53
53
|
args?: string[];
|
|
54
|
+
} | {
|
|
55
|
+
t: "term.seed.read";
|
|
56
|
+
attachId: string;
|
|
57
|
+
reqId: string;
|
|
58
|
+
offset: number;
|
|
59
|
+
maxBytes: number;
|
|
60
|
+
} | {
|
|
61
|
+
t: "term.start";
|
|
62
|
+
attachId: string;
|
|
63
|
+
reqId: string;
|
|
64
|
+
} | {
|
|
65
|
+
t: "term.read";
|
|
66
|
+
attachId: string;
|
|
67
|
+
reqId: string;
|
|
68
|
+
offset: number;
|
|
69
|
+
maxBytes: number;
|
|
54
70
|
} | {
|
|
55
71
|
t: "term.input";
|
|
56
72
|
attachId: string;
|
|
73
|
+
reqId: string;
|
|
57
74
|
dataB64: string;
|
|
58
75
|
} | {
|
|
59
76
|
t: "term.resize";
|
|
60
77
|
attachId: string;
|
|
78
|
+
reqId: string;
|
|
61
79
|
cols: number;
|
|
62
80
|
rows: number;
|
|
63
81
|
} | {
|
|
@@ -119,10 +137,18 @@ export type ToChild = {
|
|
|
119
137
|
t: "live.interrupt";
|
|
120
138
|
reqId: string;
|
|
121
139
|
localThreadId: string;
|
|
140
|
+
}
|
|
141
|
+
/** Pane-scoped idle cleanup. The child removes the terminal resource and
|
|
142
|
+
* performs the lifecycle adapter's explicit cleanup before acknowledging. */
|
|
143
|
+
| {
|
|
144
|
+
t: "terminal.reap";
|
|
145
|
+
reqId: string;
|
|
146
|
+
localThreadId: string;
|
|
147
|
+
terminalId: string;
|
|
122
148
|
} | {
|
|
123
149
|
t: "shutdown";
|
|
124
150
|
};
|
|
125
|
-
/** Child → parent. `term.data` carries
|
|
151
|
+
/** Child → parent. `term.data` carries raw pane output as base64 (line-safe NDJSON). */
|
|
126
152
|
export type FromChild = {
|
|
127
153
|
t: "ready";
|
|
128
154
|
} | {
|
|
@@ -136,20 +162,52 @@ export type FromChild = {
|
|
|
136
162
|
ok: false;
|
|
137
163
|
error: WireError;
|
|
138
164
|
} | {
|
|
139
|
-
t: "term.
|
|
165
|
+
t: "term.prepared";
|
|
140
166
|
attachId: string;
|
|
141
167
|
role: TerminalRole;
|
|
168
|
+
seedBytes: number;
|
|
169
|
+
cols?: number;
|
|
170
|
+
rows?: number;
|
|
171
|
+
} | {
|
|
172
|
+
t: "term.seed.chunk";
|
|
173
|
+
attachId: string;
|
|
174
|
+
reqId: string;
|
|
175
|
+
dataB64: string;
|
|
176
|
+
nextOffset: number;
|
|
177
|
+
done: boolean;
|
|
178
|
+
finalOffset?: number;
|
|
179
|
+
} | {
|
|
180
|
+
t: "term.started";
|
|
181
|
+
attachId: string;
|
|
182
|
+
reqId: string;
|
|
142
183
|
} | {
|
|
143
|
-
t: "term.
|
|
184
|
+
t: "term.chunk";
|
|
144
185
|
attachId: string;
|
|
186
|
+
reqId: string;
|
|
145
187
|
dataB64: string;
|
|
188
|
+
nextOffset: number;
|
|
189
|
+
done: boolean;
|
|
190
|
+
finalOffset?: number;
|
|
146
191
|
} | {
|
|
147
|
-
t: "term.
|
|
192
|
+
t: "term.reader.done";
|
|
148
193
|
attachId: string;
|
|
194
|
+
finalOffset: number;
|
|
195
|
+
reason: "terminal_exited" | "client_closed" | "backpressure" | "internal";
|
|
149
196
|
exitCode: number;
|
|
197
|
+
} | {
|
|
198
|
+
t: "term.ack";
|
|
199
|
+
attachId: string;
|
|
200
|
+
reqId: string;
|
|
201
|
+
operation: "input" | "resize";
|
|
202
|
+
} | {
|
|
203
|
+
t: "term.dimensions";
|
|
204
|
+
attachId: string;
|
|
205
|
+
cols: number;
|
|
206
|
+
rows: number;
|
|
150
207
|
} | {
|
|
151
208
|
t: "term.error";
|
|
152
209
|
attachId: string;
|
|
210
|
+
reqId?: string;
|
|
153
211
|
code: TerminalOpenErrorCode;
|
|
154
212
|
message: string;
|
|
155
213
|
}
|
|
@@ -177,6 +235,19 @@ export type FromChild = {
|
|
|
177
235
|
t: "mirror.image.commit";
|
|
178
236
|
transferId: string;
|
|
179
237
|
seq: number;
|
|
238
|
+
} | {
|
|
239
|
+
t: "terminal.lifecycle.ended";
|
|
240
|
+
localThreadId: string;
|
|
241
|
+
terminalId: string;
|
|
242
|
+
lifecycle: "required" | "auxiliary";
|
|
243
|
+
status: "idle" | "running" | "failed" | "unknown";
|
|
244
|
+
} | {
|
|
245
|
+
t: "terminal.reaped";
|
|
246
|
+
reqId: string;
|
|
247
|
+
localThreadId: string;
|
|
248
|
+
terminalId: string;
|
|
249
|
+
ok: boolean;
|
|
250
|
+
error?: WireError;
|
|
180
251
|
}
|
|
181
252
|
/** The session rotated to a fresh machine-session (claude `/clear`·`/fork`): the
|
|
182
253
|
* child re-pointed its mirror to `to` and asks the daemon to alias the runner
|
|
@@ -202,8 +273,9 @@ export type FromChild = {
|
|
|
202
273
|
error?: WireError;
|
|
203
274
|
}
|
|
204
275
|
/** Result of an `inject`: `result.outcome` distinguishes a new turn, an
|
|
205
|
-
* active-turn steer, and failures.
|
|
206
|
-
*
|
|
276
|
+
* active-turn steer, and failures. RPC-backed success carries the canonical
|
|
277
|
+
* Response identity immediately; transcript-backed success may publish it on
|
|
278
|
+
* the mirrored user record shortly afterward. Echoes the request `reqId`. */
|
|
207
279
|
| {
|
|
208
280
|
t: "injected";
|
|
209
281
|
reqId: string;
|
|
@@ -3,9 +3,13 @@ import { type FromChild, type ToChild } from "./protocol.js";
|
|
|
3
3
|
export interface RunnerTransport<TSend, TRecv> {
|
|
4
4
|
/** Send one message. Best-effort: a dead peer is surfaced via {@link onClose}. */
|
|
5
5
|
send(msg: TSend): void;
|
|
6
|
+
/** Send one bounded message and resolve only after the writable accepts and
|
|
7
|
+
* drains it. Terminal pull responses use this to keep stdio end-to-end
|
|
8
|
+
* demand-driven. */
|
|
9
|
+
sendAndDrain(msg: TSend): Promise<void>;
|
|
6
10
|
/** Register the single message handler. */
|
|
7
11
|
onMessage(cb: (msg: TRecv) => void): void;
|
|
8
|
-
/** Register a close/error
|
|
12
|
+
/** Register a close/error observer (peer stream ended). */
|
|
9
13
|
onClose(cb: (error?: Error) => void): void;
|
|
10
14
|
/** Stop reading and release the readline interface. */
|
|
11
15
|
close(): void;
|
|
@@ -25,12 +29,15 @@ export declare class StdioRunnerTransport<TSend, TRecv> implements RunnerTranspo
|
|
|
25
29
|
private readonly output;
|
|
26
30
|
private readonly rl;
|
|
27
31
|
private messageHandler;
|
|
28
|
-
private
|
|
32
|
+
private readonly closeHandlers;
|
|
29
33
|
private closed;
|
|
34
|
+
private writeTail;
|
|
30
35
|
constructor(input: Readable, output: Writable);
|
|
31
36
|
send(msg: TSend): void;
|
|
37
|
+
sendAndDrain(msg: TSend): Promise<void>;
|
|
32
38
|
onMessage(cb: (msg: TRecv) => void): void;
|
|
33
39
|
onClose(cb: (error?: Error) => void): void;
|
|
34
40
|
close(): void;
|
|
35
41
|
private handleClose;
|
|
42
|
+
private writeMessage;
|
|
36
43
|
}
|
package/dist/runner/transport.js
CHANGED
|
@@ -22,8 +22,9 @@ export class StdioRunnerTransport {
|
|
|
22
22
|
output;
|
|
23
23
|
rl;
|
|
24
24
|
messageHandler = null;
|
|
25
|
-
|
|
25
|
+
closeHandlers = new Set();
|
|
26
26
|
closed = false;
|
|
27
|
+
writeTail = Promise.resolve();
|
|
27
28
|
constructor(input, output) {
|
|
28
29
|
this.input = input;
|
|
29
30
|
this.output = output;
|
|
@@ -52,11 +53,16 @@ export class StdioRunnerTransport {
|
|
|
52
53
|
}
|
|
53
54
|
this.output.write(encodeMessage(msg));
|
|
54
55
|
}
|
|
56
|
+
sendAndDrain(msg) {
|
|
57
|
+
const operation = this.writeTail.then(() => this.writeMessage(msg));
|
|
58
|
+
this.writeTail = operation.catch(() => undefined);
|
|
59
|
+
return operation;
|
|
60
|
+
}
|
|
55
61
|
onMessage(cb) {
|
|
56
62
|
this.messageHandler = cb;
|
|
57
63
|
}
|
|
58
64
|
onClose(cb) {
|
|
59
|
-
this.
|
|
65
|
+
this.closeHandlers.add(cb);
|
|
60
66
|
}
|
|
61
67
|
close() {
|
|
62
68
|
this.closed = true;
|
|
@@ -67,6 +73,52 @@ export class StdioRunnerTransport {
|
|
|
67
73
|
return;
|
|
68
74
|
}
|
|
69
75
|
this.closed = true;
|
|
70
|
-
this.
|
|
76
|
+
for (const handler of this.closeHandlers)
|
|
77
|
+
handler(error);
|
|
78
|
+
}
|
|
79
|
+
writeMessage(msg) {
|
|
80
|
+
if (this.closed)
|
|
81
|
+
return Promise.reject(new Error("runner transport is closed"));
|
|
82
|
+
const encoded = encodeMessage(msg);
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
let callbackDone = false;
|
|
85
|
+
let drainDone = false;
|
|
86
|
+
let settled = false;
|
|
87
|
+
const cleanup = () => {
|
|
88
|
+
this.output.off("drain", onDrain);
|
|
89
|
+
this.output.off("error", onError);
|
|
90
|
+
};
|
|
91
|
+
const finish = () => {
|
|
92
|
+
if (settled || !callbackDone || !drainDone)
|
|
93
|
+
return;
|
|
94
|
+
settled = true;
|
|
95
|
+
cleanup();
|
|
96
|
+
resolve();
|
|
97
|
+
};
|
|
98
|
+
const onDrain = () => {
|
|
99
|
+
drainDone = true;
|
|
100
|
+
finish();
|
|
101
|
+
};
|
|
102
|
+
const onError = (error) => {
|
|
103
|
+
if (settled)
|
|
104
|
+
return;
|
|
105
|
+
settled = true;
|
|
106
|
+
cleanup();
|
|
107
|
+
reject(error);
|
|
108
|
+
};
|
|
109
|
+
this.output.once("error", onError);
|
|
110
|
+
const accepted = this.output.write(encoded, (error) => {
|
|
111
|
+
if (error) {
|
|
112
|
+
onError(error);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
callbackDone = true;
|
|
116
|
+
finish();
|
|
117
|
+
});
|
|
118
|
+
drainDone = accepted;
|
|
119
|
+
if (!accepted)
|
|
120
|
+
this.output.once("drain", onDrain);
|
|
121
|
+
finish();
|
|
122
|
+
});
|
|
71
123
|
}
|
|
72
124
|
}
|