@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/child.js
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
11
|
import { AgentRuntimeError, } from "@rynx-ai/core";
|
|
12
|
-
import { TerminalRegistry } from "../terminal/registry.js";
|
|
12
|
+
import { TerminalRegistry, } from "../terminal/registry.js";
|
|
13
13
|
import { RUNNER_IMAGE_CHUNK_CHARS, RUNNER_IMAGE_MAX_RESULT_CHARS, toWireError, } from "./protocol.js";
|
|
14
14
|
import { isCodexLineageProvider } from "./startup-policy.js";
|
|
15
|
+
const TERMINAL_PREPARATION_TTL_MS = 30_000;
|
|
15
16
|
/** Maximum time for a fresh Codex-lineage TUI to publish its native thread. */
|
|
16
17
|
const CODEX_THREAD_START_TIMEOUT_MS = 30_000;
|
|
17
18
|
function providerDisplayName(runtime) {
|
|
@@ -119,8 +120,10 @@ export class RunnerSession {
|
|
|
119
120
|
onShutdown;
|
|
120
121
|
/** Live terminals hosted by this session, and per-attach client handles. */
|
|
121
122
|
terminals = new TerminalRegistry();
|
|
123
|
+
preparations = new Map();
|
|
122
124
|
attachments = new Map();
|
|
123
125
|
attachmentThreadIds = new Map();
|
|
126
|
+
attachmentRoles = new Map();
|
|
124
127
|
traexStartupWatchers = new Map();
|
|
125
128
|
traexStartupGates = new Map();
|
|
126
129
|
terminalWatchers = new Map();
|
|
@@ -133,7 +136,9 @@ export class RunnerSession {
|
|
|
133
136
|
mirrorImageAcks = new Map();
|
|
134
137
|
/** Provider name retained for asynchronous Terminal-exit diagnostics. */
|
|
135
138
|
liveRuntimes = new Map();
|
|
139
|
+
terminalStatuses = new Map();
|
|
136
140
|
shuttingDown = false;
|
|
141
|
+
shutdownPromise = null;
|
|
137
142
|
constructor({ transport, executor, onShutdown }) {
|
|
138
143
|
this.transport = transport;
|
|
139
144
|
this.executor = executor;
|
|
@@ -177,7 +182,9 @@ export class RunnerSession {
|
|
|
177
182
|
void this.runCap(msg.capId, msg.name, msg.args);
|
|
178
183
|
return;
|
|
179
184
|
case "term.open":
|
|
180
|
-
if (this.pendingTerminalOpens.has(msg.attachId) ||
|
|
185
|
+
if (this.pendingTerminalOpens.has(msg.attachId) ||
|
|
186
|
+
this.preparations.has(msg.attachId) ||
|
|
187
|
+
this.attachments.has(msg.attachId)) {
|
|
181
188
|
this.transport.send({
|
|
182
189
|
t: "term.error",
|
|
183
190
|
attachId: msg.attachId,
|
|
@@ -192,29 +199,35 @@ export class RunnerSession {
|
|
|
192
199
|
this.cancelledTerminalOpens.delete(msg.attachId);
|
|
193
200
|
});
|
|
194
201
|
return;
|
|
202
|
+
case "term.seed.read":
|
|
203
|
+
void this.readTerminalSeed(msg);
|
|
204
|
+
return;
|
|
205
|
+
case "term.start":
|
|
206
|
+
void this.startTerminal(msg);
|
|
207
|
+
return;
|
|
208
|
+
case "term.read":
|
|
209
|
+
void this.readTerminalOutput(msg);
|
|
210
|
+
return;
|
|
195
211
|
case "term.input":
|
|
196
|
-
|
|
197
|
-
const localThreadId = this.attachmentThreadIds.get(msg.attachId);
|
|
198
|
-
const input = Buffer.from(msg.dataB64, "base64").toString("utf8");
|
|
199
|
-
// xterm sends device/focus reports through the same onData channel as
|
|
200
|
-
// keystrokes. They must reach the TUI without pretending the user has
|
|
201
|
-
// taken over startup prompt handling.
|
|
202
|
-
if (!isTerminalProtocolResponse(input)) {
|
|
203
|
-
this.cancelTraexStartupWatcher(localThreadId);
|
|
204
|
-
}
|
|
205
|
-
this.attachments.get(msg.attachId)?.write(input);
|
|
206
|
-
}
|
|
212
|
+
void this.writeTerminalInput(msg);
|
|
207
213
|
return;
|
|
208
214
|
case "term.resize":
|
|
209
|
-
this.
|
|
215
|
+
void this.resizeTerminal(msg);
|
|
210
216
|
return;
|
|
211
217
|
case "term.close": {
|
|
212
218
|
if (this.pendingTerminalOpens.has(msg.attachId)) {
|
|
213
219
|
this.cancelledTerminalOpens.add(msg.attachId);
|
|
214
220
|
}
|
|
221
|
+
const pending = this.preparations.get(msg.attachId);
|
|
222
|
+
if (pending) {
|
|
223
|
+
clearTimeout(pending.timer);
|
|
224
|
+
this.preparations.delete(msg.attachId);
|
|
225
|
+
pending.preparation.kill();
|
|
226
|
+
}
|
|
215
227
|
const attachment = this.attachments.get(msg.attachId);
|
|
216
228
|
this.attachments.delete(msg.attachId);
|
|
217
229
|
this.attachmentThreadIds.delete(msg.attachId);
|
|
230
|
+
this.attachmentRoles.delete(msg.attachId);
|
|
218
231
|
attachment?.kill();
|
|
219
232
|
return;
|
|
220
233
|
}
|
|
@@ -234,8 +247,11 @@ export class RunnerSession {
|
|
|
234
247
|
case "live.interrupt":
|
|
235
248
|
void this.interruptLive(msg);
|
|
236
249
|
return;
|
|
250
|
+
case "terminal.reap":
|
|
251
|
+
void this.reapTerminal(msg);
|
|
252
|
+
return;
|
|
237
253
|
case "shutdown":
|
|
238
|
-
this.shutdown();
|
|
254
|
+
void this.shutdown();
|
|
239
255
|
return;
|
|
240
256
|
}
|
|
241
257
|
}
|
|
@@ -269,14 +285,19 @@ export class RunnerSession {
|
|
|
269
285
|
const target = { id: localThreadId };
|
|
270
286
|
const emit = (event) => {
|
|
271
287
|
const sessionId = target.id;
|
|
288
|
+
if (event.type === "session.status") {
|
|
289
|
+
this.terminalStatuses.set(sessionId, event.status);
|
|
290
|
+
}
|
|
272
291
|
this.enqueueMirror(() => this.sendMirroredEvent(sessionId, event));
|
|
273
292
|
};
|
|
274
293
|
const retarget = (newId, meta) => {
|
|
294
|
+
const previousId = target.id;
|
|
295
|
+
this.transferNativeSessionResources(previousId, newId);
|
|
275
296
|
target.id = newId;
|
|
276
297
|
this.enqueueMirror(() => {
|
|
277
298
|
this.transport.send({
|
|
278
299
|
t: "rotate",
|
|
279
|
-
from:
|
|
300
|
+
from: previousId,
|
|
280
301
|
to: newId,
|
|
281
302
|
kind: meta.kind,
|
|
282
303
|
workspace: meta.workspace,
|
|
@@ -287,11 +308,41 @@ export class RunnerSession {
|
|
|
287
308
|
};
|
|
288
309
|
return { emit, retarget };
|
|
289
310
|
}
|
|
311
|
+
transferNativeSessionResources(sourceId, targetId) {
|
|
312
|
+
if (sourceId === targetId)
|
|
313
|
+
return;
|
|
314
|
+
const sourceTerminalId = `${sourceId}-main`;
|
|
315
|
+
const targetTerminalId = `${targetId}-main`;
|
|
316
|
+
this.terminals.transfer(sourceTerminalId, targetTerminalId);
|
|
317
|
+
const watcher = this.terminalWatchers.get(sourceId);
|
|
318
|
+
if (watcher)
|
|
319
|
+
clearInterval(watcher);
|
|
320
|
+
this.terminalWatchers.delete(sourceId);
|
|
321
|
+
const runtime = this.liveRuntimes.get(sourceId);
|
|
322
|
+
if (runtime) {
|
|
323
|
+
this.liveRuntimes.delete(sourceId);
|
|
324
|
+
this.liveRuntimes.set(targetId, runtime);
|
|
325
|
+
}
|
|
326
|
+
const status = this.terminalStatuses.get(sourceId);
|
|
327
|
+
if (status) {
|
|
328
|
+
this.terminalStatuses.delete(sourceId);
|
|
329
|
+
this.terminalStatuses.set(targetId, status);
|
|
330
|
+
}
|
|
331
|
+
if (this.liveIds.delete(sourceId))
|
|
332
|
+
this.liveIds.add(targetId);
|
|
333
|
+
for (const [attachId, sessionId] of this.attachmentThreadIds) {
|
|
334
|
+
if (sessionId === sourceId)
|
|
335
|
+
this.attachmentThreadIds.set(attachId, targetId);
|
|
336
|
+
}
|
|
337
|
+
const terminal = this.terminals.get(targetTerminalId);
|
|
338
|
+
if (terminal)
|
|
339
|
+
this.watchNativeTerminal(targetId, targetTerminalId, terminal);
|
|
340
|
+
}
|
|
290
341
|
enqueueMirror(operation) {
|
|
291
342
|
this.mirrorQueue = this.mirrorQueue.then(operation).catch((error) => {
|
|
292
343
|
if (!this.shuttingDown) {
|
|
293
344
|
this.rejectMirrorImageAcks(error instanceof Error ? error : new Error(String(error)));
|
|
294
|
-
this.shutdown();
|
|
345
|
+
void this.shutdown();
|
|
295
346
|
}
|
|
296
347
|
});
|
|
297
348
|
}
|
|
@@ -614,6 +665,43 @@ export class RunnerSession {
|
|
|
614
665
|
...(error ? { error } : {}),
|
|
615
666
|
});
|
|
616
667
|
}
|
|
668
|
+
async reapTerminal(msg) {
|
|
669
|
+
let error;
|
|
670
|
+
try {
|
|
671
|
+
const lifecycle = this.terminals.lifecycle(msg.terminalId);
|
|
672
|
+
const watcher = this.terminalWatchers.get(msg.localThreadId);
|
|
673
|
+
if (watcher)
|
|
674
|
+
clearInterval(watcher);
|
|
675
|
+
this.terminalWatchers.delete(msg.localThreadId);
|
|
676
|
+
this.cancelTraexStartupWatcher(msg.localThreadId);
|
|
677
|
+
for (const [attachId, sessionId] of this.attachmentThreadIds) {
|
|
678
|
+
if (sessionId !== msg.localThreadId)
|
|
679
|
+
continue;
|
|
680
|
+
this.attachments.get(attachId)?.kill();
|
|
681
|
+
this.attachments.delete(attachId);
|
|
682
|
+
this.attachmentThreadIds.delete(attachId);
|
|
683
|
+
this.attachmentRoles.delete(attachId);
|
|
684
|
+
}
|
|
685
|
+
this.terminals.close(msg.terminalId);
|
|
686
|
+
if (lifecycle === "auxiliary") {
|
|
687
|
+
await this.liveProvider.teardownAuxiliaryTerminalRuntime?.(msg.localThreadId);
|
|
688
|
+
this.liveIds.delete(msg.localThreadId);
|
|
689
|
+
this.liveRuntimes.delete(msg.localThreadId);
|
|
690
|
+
this.terminalStatuses.delete(msg.localThreadId);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
catch (cause) {
|
|
694
|
+
error = toWireError(cause);
|
|
695
|
+
}
|
|
696
|
+
this.transport.send({
|
|
697
|
+
t: "terminal.reaped",
|
|
698
|
+
reqId: msg.reqId,
|
|
699
|
+
localThreadId: msg.localThreadId,
|
|
700
|
+
terminalId: msg.terminalId,
|
|
701
|
+
ok: error === undefined,
|
|
702
|
+
...(error ? { error } : {}),
|
|
703
|
+
});
|
|
704
|
+
}
|
|
617
705
|
/** Launch (idempotently) the session's codex TUI pane from the executor's
|
|
618
706
|
* `codexTerminalSpec`. Shares the `${id}-main` terminal id with `term.open`,
|
|
619
707
|
* so the web attach reuses the same detached pane. */
|
|
@@ -623,12 +711,23 @@ export class RunnerSession {
|
|
|
623
711
|
return;
|
|
624
712
|
const terminalId = `${localThreadId}-main`;
|
|
625
713
|
const term = this.terminals.getOrCreate(terminalId, {
|
|
714
|
+
lifecycle: spec.lifecycle,
|
|
626
715
|
cwd: spec.cwd,
|
|
627
716
|
command: spec.command,
|
|
628
717
|
args: spec.args,
|
|
629
718
|
cols: cols ?? 120,
|
|
630
719
|
rows: rows ?? 40,
|
|
631
720
|
...(spec.env ? { env: spec.env } : {}),
|
|
721
|
+
...(spec.scrollback === undefined ? {} : { scrollback: spec.scrollback }),
|
|
722
|
+
...(spec.tmuxAllowPassthrough === undefined
|
|
723
|
+
? {}
|
|
724
|
+
: { tmuxAllowPassthrough: spec.tmuxAllowPassthrough }),
|
|
725
|
+
...(spec.tmuxStartOnAttach === undefined
|
|
726
|
+
? {}
|
|
727
|
+
: { tmuxStartOnAttach: spec.tmuxStartOnAttach }),
|
|
728
|
+
...(spec.keepAliveAfterExit === undefined
|
|
729
|
+
? {}
|
|
730
|
+
: { keepAliveAfterExit: spec.keepAliveAfterExit }),
|
|
632
731
|
});
|
|
633
732
|
if (spec.skipTraexStartupPrompts) {
|
|
634
733
|
const watcher = Symbol(localThreadId);
|
|
@@ -671,30 +770,39 @@ export class RunnerSession {
|
|
|
671
770
|
this.terminalWatchers.delete(localThreadId);
|
|
672
771
|
this.cancelTraexStartupWatcher(localThreadId);
|
|
673
772
|
const runtime = this.liveRuntimes.get(localThreadId);
|
|
773
|
+
const lifecycle = this.terminals.lifecycle(terminalId);
|
|
674
774
|
const paneFailure = terminalFailureDetail(terminal.capturePane?.() ?? "");
|
|
675
775
|
const startupFailed = this.failCodexThreadStartup(localThreadId, this.liveProvider, nativePhaseError(runtime, "native_terminal_exited_before_session", "Terminal exited before native session discovery completed", paneFailure || undefined));
|
|
676
|
-
if (
|
|
677
|
-
if (!startupFailed) {
|
|
678
|
-
try {
|
|
679
|
-
this.terminals.close(terminalId);
|
|
680
|
-
}
|
|
681
|
-
catch (closeError) {
|
|
682
|
-
console.warn(`[runner] session=${localThreadId} failed to close exited native Terminal: ${closeError instanceof Error ? closeError.message : String(closeError)}`);
|
|
683
|
-
}
|
|
684
|
-
const exitError = nativePhaseError(runtime, "native_terminal_exited", "Terminal exited unexpectedly", paneFailure || undefined);
|
|
685
|
-
if (this.liveProvider.teardownLiveCodexSession) {
|
|
686
|
-
this.liveProvider.teardownLiveCodexSession(localThreadId, exitError);
|
|
687
|
-
}
|
|
688
|
-
else {
|
|
689
|
-
this.liveProvider.failLiveSession?.(localThreadId, exitError);
|
|
690
|
-
this.liveProvider.stopLiveCodexSession?.(localThreadId);
|
|
691
|
-
}
|
|
692
|
-
this.liveIds.delete(localThreadId);
|
|
693
|
-
console.warn(`[runner] session=${localThreadId} ${providerDisplayName(runtime)} auxiliary Terminal exited; native runtime torn down for cold resume`);
|
|
694
|
-
}
|
|
776
|
+
if (startupFailed || lifecycle === undefined)
|
|
695
777
|
return;
|
|
778
|
+
try {
|
|
779
|
+
this.terminals.close(terminalId);
|
|
696
780
|
}
|
|
697
|
-
|
|
781
|
+
catch (closeError) {
|
|
782
|
+
console.warn(`[runner] session=${localThreadId} failed to close exited native Terminal: ${closeError instanceof Error ? closeError.message : String(closeError)}`);
|
|
783
|
+
}
|
|
784
|
+
const status = this.terminalStatuses.get(localThreadId) ?? "unknown";
|
|
785
|
+
if (lifecycle === "auxiliary") {
|
|
786
|
+
console.warn(`[runner] session=${localThreadId} ${providerDisplayName(runtime)} auxiliary Terminal exited`);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
if (status !== "idle") {
|
|
790
|
+
this.liveProvider.failLiveSession?.(localThreadId, nativePhaseError(runtime, "required_terminal_exited", "Required Terminal exited unexpectedly", paneFailure || undefined));
|
|
791
|
+
}
|
|
792
|
+
this.liveProvider.stopLiveCodexSession?.(localThreadId);
|
|
793
|
+
this.liveIds.delete(localThreadId);
|
|
794
|
+
this.liveRuntimes.delete(localThreadId);
|
|
795
|
+
this.terminalStatuses.delete(localThreadId);
|
|
796
|
+
}
|
|
797
|
+
this.enqueueMirror(() => {
|
|
798
|
+
this.transport.send({
|
|
799
|
+
t: "terminal.lifecycle.ended",
|
|
800
|
+
localThreadId,
|
|
801
|
+
terminalId,
|
|
802
|
+
lifecycle,
|
|
803
|
+
status,
|
|
804
|
+
});
|
|
805
|
+
});
|
|
698
806
|
}).catch((error) => {
|
|
699
807
|
console.warn(`[runner] session=${localThreadId} native Terminal liveness probe failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
700
808
|
}).finally(() => {
|
|
@@ -846,19 +954,31 @@ export class RunnerSession {
|
|
|
846
954
|
/** Stop event forwarding, kill native terminals/hooks, then synchronously
|
|
847
955
|
* scrub provider handoff files before the child process is allowed to exit. */
|
|
848
956
|
shutdown() {
|
|
849
|
-
if (this.
|
|
850
|
-
return;
|
|
957
|
+
if (this.shutdownPromise)
|
|
958
|
+
return this.shutdownPromise;
|
|
851
959
|
this.shuttingDown = true;
|
|
852
|
-
this.
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
960
|
+
this.shutdownPromise = (async () => {
|
|
961
|
+
this.rejectMirrorImageAcks(new Error("runner is shutting down"));
|
|
962
|
+
this.stopLive();
|
|
963
|
+
for (const pending of this.preparations.values()) {
|
|
964
|
+
clearTimeout(pending.timer);
|
|
965
|
+
pending.preparation.kill();
|
|
966
|
+
}
|
|
967
|
+
this.preparations.clear();
|
|
968
|
+
for (const attachment of this.attachments.values())
|
|
969
|
+
attachment.kill();
|
|
970
|
+
this.terminals.closeAll();
|
|
971
|
+
this.attachments.clear();
|
|
972
|
+
this.attachmentThreadIds.clear();
|
|
973
|
+
this.attachmentRoles.clear();
|
|
974
|
+
await this.liveProvider.finalizeStoppedLiveSessions?.();
|
|
975
|
+
this.onShutdown();
|
|
976
|
+
})();
|
|
977
|
+
return this.shutdownPromise;
|
|
857
978
|
}
|
|
858
979
|
async openTerminal(msg) {
|
|
859
980
|
try {
|
|
860
|
-
// codex/claude-native session (no explicit command): DUMB ATTACH
|
|
861
|
-
// reattach (codex_native.py:905-942, `app_server=None`). A tab switch ONLY
|
|
981
|
+
// codex/claude-native session (no explicit command): DUMB ATTACH. A tab switch ONLY
|
|
862
982
|
// attaches an already-live pane; it NEVER ensures the forwarder or relaunches a
|
|
863
983
|
// dead pane. Creation/relaunch happens on message-send (`live.ensure`) or an
|
|
864
984
|
// explicit restart. A missing/dead pane → `term.error`, so the web shows the
|
|
@@ -880,6 +1000,7 @@ export class RunnerSession {
|
|
|
880
1000
|
}
|
|
881
1001
|
// Login-shell terminal (explicit command): create-or-reuse as before.
|
|
882
1002
|
this.terminals.getOrCreate(msg.terminalId, {
|
|
1003
|
+
lifecycle: "auxiliary",
|
|
883
1004
|
cwd: msg.cwd,
|
|
884
1005
|
command: msg.command,
|
|
885
1006
|
args: msg.args,
|
|
@@ -897,14 +1018,14 @@ export class RunnerSession {
|
|
|
897
1018
|
});
|
|
898
1019
|
}
|
|
899
1020
|
}
|
|
900
|
-
/**
|
|
1021
|
+
/** Prepare captured history without starting the live control client. */
|
|
901
1022
|
async attachExisting(msg) {
|
|
902
|
-
const {
|
|
1023
|
+
const { preparation, role } = await this.terminals.prepare(msg.terminalId, msg.role, {
|
|
903
1024
|
cols: msg.cols,
|
|
904
1025
|
rows: msg.rows,
|
|
905
1026
|
});
|
|
906
1027
|
if (this.cancelledTerminalOpens.has(msg.attachId)) {
|
|
907
|
-
|
|
1028
|
+
preparation.kill();
|
|
908
1029
|
this.transport.send({
|
|
909
1030
|
t: "term.error",
|
|
910
1031
|
attachId: msg.attachId,
|
|
@@ -913,20 +1034,168 @@ export class RunnerSession {
|
|
|
913
1034
|
});
|
|
914
1035
|
return;
|
|
915
1036
|
}
|
|
916
|
-
|
|
1037
|
+
const timer = setTimeout(() => {
|
|
1038
|
+
const pending = this.preparations.get(msg.attachId);
|
|
1039
|
+
if (!pending || pending.preparation !== preparation)
|
|
1040
|
+
return;
|
|
1041
|
+
this.preparations.delete(msg.attachId);
|
|
1042
|
+
this.attachmentThreadIds.delete(msg.attachId);
|
|
1043
|
+
this.attachmentRoles.delete(msg.attachId);
|
|
1044
|
+
preparation.kill();
|
|
1045
|
+
}, TERMINAL_PREPARATION_TTL_MS);
|
|
1046
|
+
timer.unref?.();
|
|
1047
|
+
this.preparations.set(msg.attachId, { preparation, timer });
|
|
1048
|
+
this.attachmentRoles.set(msg.attachId, role);
|
|
917
1049
|
if (msg.localThreadId)
|
|
918
1050
|
this.attachmentThreadIds.set(msg.attachId, msg.localThreadId);
|
|
919
|
-
|
|
920
|
-
t: "term.
|
|
1051
|
+
this.transport.send({
|
|
1052
|
+
t: "term.prepared",
|
|
921
1053
|
attachId: msg.attachId,
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1054
|
+
role,
|
|
1055
|
+
seedBytes: preparation.seedBytes,
|
|
1056
|
+
...(preparation.dimensions ?? {}),
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
async readTerminalSeed(msg) {
|
|
1060
|
+
const pending = this.preparations.get(msg.attachId);
|
|
1061
|
+
if (!pending) {
|
|
1062
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, "terminal is not prepared");
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
const result = await pending.preparation.readSeed(msg.offset, msg.maxBytes);
|
|
1067
|
+
await this.transport.sendAndDrain({
|
|
1068
|
+
t: "term.seed.chunk",
|
|
1069
|
+
attachId: msg.attachId,
|
|
1070
|
+
reqId: msg.reqId,
|
|
1071
|
+
dataB64: Buffer.from(result.data).toString("base64"),
|
|
1072
|
+
nextOffset: result.nextOffset,
|
|
1073
|
+
done: result.done,
|
|
1074
|
+
...(result.finalOffset === undefined ? {} : { finalOffset: result.finalOffset }),
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, error);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async startTerminal(msg) {
|
|
1082
|
+
const pending = this.preparations.get(msg.attachId);
|
|
1083
|
+
if (!pending) {
|
|
1084
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, "terminal is not prepared");
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
clearTimeout(pending.timer);
|
|
1088
|
+
this.preparations.delete(msg.attachId);
|
|
1089
|
+
try {
|
|
1090
|
+
const attachment = await pending.preparation.start();
|
|
1091
|
+
this.attachments.set(msg.attachId, attachment);
|
|
1092
|
+
attachment.onResize(({ cols, rows }) => {
|
|
1093
|
+
this.transport.send({ t: "term.dimensions", attachId: msg.attachId, cols, rows });
|
|
1094
|
+
});
|
|
1095
|
+
await this.transport.sendAndDrain({
|
|
1096
|
+
t: "term.started",
|
|
1097
|
+
attachId: msg.attachId,
|
|
1098
|
+
reqId: msg.reqId,
|
|
1099
|
+
});
|
|
1100
|
+
void attachment.readerDone.then(async (info) => {
|
|
1101
|
+
await this.transport.sendAndDrain({
|
|
1102
|
+
t: "term.reader.done",
|
|
1103
|
+
attachId: msg.attachId,
|
|
1104
|
+
finalOffset: info.finalOffset,
|
|
1105
|
+
reason: info.reason,
|
|
1106
|
+
exitCode: info.exitCode,
|
|
1107
|
+
}).catch(() => undefined);
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
pending.preparation.kill();
|
|
926
1112
|
this.attachmentThreadIds.delete(msg.attachId);
|
|
927
|
-
this.
|
|
1113
|
+
this.attachmentRoles.delete(msg.attachId);
|
|
1114
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, error);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
async readTerminalOutput(msg) {
|
|
1118
|
+
const attachment = this.attachments.get(msg.attachId);
|
|
1119
|
+
if (!attachment) {
|
|
1120
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, "terminal has not started");
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
const result = await attachment.read(msg.offset, msg.maxBytes);
|
|
1125
|
+
await this.transport.sendAndDrain({
|
|
1126
|
+
t: "term.chunk",
|
|
1127
|
+
attachId: msg.attachId,
|
|
1128
|
+
reqId: msg.reqId,
|
|
1129
|
+
dataB64: Buffer.from(result.data).toString("base64"),
|
|
1130
|
+
nextOffset: result.nextOffset,
|
|
1131
|
+
done: result.done,
|
|
1132
|
+
...(result.finalOffset === undefined ? {} : { finalOffset: result.finalOffset }),
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
catch (error) {
|
|
1136
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, error);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
async writeTerminalInput(msg) {
|
|
1140
|
+
if (this.attachmentRoles.get(msg.attachId) !== "owner") {
|
|
1141
|
+
await this.transport.sendAndDrain({
|
|
1142
|
+
t: "term.ack",
|
|
1143
|
+
attachId: msg.attachId,
|
|
1144
|
+
reqId: msg.reqId,
|
|
1145
|
+
operation: "input",
|
|
1146
|
+
});
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
const attachment = this.attachments.get(msg.attachId);
|
|
1150
|
+
if (!attachment) {
|
|
1151
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, "terminal has not started");
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const localThreadId = this.attachmentThreadIds.get(msg.attachId);
|
|
1155
|
+
const input = Buffer.from(msg.dataB64, "base64");
|
|
1156
|
+
if (!isTerminalProtocolResponse(input.toString("utf8"))) {
|
|
1157
|
+
this.cancelTraexStartupWatcher(localThreadId);
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
await attachment.write(input);
|
|
1161
|
+
await this.transport.sendAndDrain({
|
|
1162
|
+
t: "term.ack",
|
|
1163
|
+
attachId: msg.attachId,
|
|
1164
|
+
reqId: msg.reqId,
|
|
1165
|
+
operation: "input",
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
catch (error) {
|
|
1169
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, error);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async resizeTerminal(msg) {
|
|
1173
|
+
const attachment = this.attachments.get(msg.attachId);
|
|
1174
|
+
if (!attachment) {
|
|
1175
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, "terminal has not started");
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
await attachment.resize(msg.cols, msg.rows);
|
|
1180
|
+
await this.transport.sendAndDrain({
|
|
1181
|
+
t: "term.ack",
|
|
1182
|
+
attachId: msg.attachId,
|
|
1183
|
+
reqId: msg.reqId,
|
|
1184
|
+
operation: "resize",
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
catch (error) {
|
|
1188
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, error);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
sendTerminalOperationError(attachId, reqId, error) {
|
|
1192
|
+
this.transport.send({
|
|
1193
|
+
t: "term.error",
|
|
1194
|
+
attachId,
|
|
1195
|
+
reqId,
|
|
1196
|
+
code: "terminal_open_failed",
|
|
1197
|
+
message: error instanceof Error ? error.message : String(error),
|
|
928
1198
|
});
|
|
929
|
-
this.transport.send({ t: "term.opened", attachId: msg.attachId, role });
|
|
930
1199
|
}
|
|
931
1200
|
async runCap(capId, name, args) {
|
|
932
1201
|
try {
|
package/dist/runner/manager.d.ts
CHANGED
|
@@ -74,11 +74,11 @@ export interface RunnerManagerOptions {
|
|
|
74
74
|
signalChild?: (child: ChildProcess, signal: NodeJS.Signals, processGroup: boolean) => void;
|
|
75
75
|
/** Injected for tests. Best-effort close of the deterministic private tmux
|
|
76
76
|
* server owned by a Session runner; reference implementation does not verify/retry close. */
|
|
77
|
-
terminateTerminalServer?: (terminalName: string) => boolean | Promise<boolean>;
|
|
77
|
+
terminateTerminalServer?: (terminalName: string, ownerPid?: number) => boolean | Promise<boolean>;
|
|
78
78
|
/** Injected tmux `#{window_activity}` reader. Returns epoch seconds. */
|
|
79
|
-
terminalWindowActivityAt?: (terminalName: string) => number | null | Promise<number | null>;
|
|
79
|
+
terminalWindowActivityAt?: (terminalName: string, ownerPid?: number) => number | null | Promise<number | null>;
|
|
80
80
|
/** Injected tmux attached-client probe. */
|
|
81
|
-
terminalHasAttachedClient?: (terminalName: string) => boolean | Promise<boolean>;
|
|
81
|
+
terminalHasAttachedClient?: (terminalName: string, ownerPid?: number) => boolean | Promise<boolean>;
|
|
82
82
|
now?: () => number;
|
|
83
83
|
/** Wall clock used to compare tmux's epoch timestamp. */
|
|
84
84
|
wallNow?: () => number;
|
|
@@ -116,24 +116,39 @@ export interface OpenTerminalOptions {
|
|
|
116
116
|
command?: string;
|
|
117
117
|
args?: string[];
|
|
118
118
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
export interface ParentTerminalRead {
|
|
120
|
+
data: Uint8Array;
|
|
121
|
+
nextOffset: number;
|
|
122
|
+
done: boolean;
|
|
123
|
+
finalOffset?: number;
|
|
124
|
+
}
|
|
125
|
+
export interface ParentTerminalReaderDone {
|
|
126
|
+
finalOffset: number;
|
|
127
|
+
reason: "terminal_exited" | "client_closed" | "backpressure" | "internal";
|
|
128
|
+
exitCode: number;
|
|
129
|
+
}
|
|
130
|
+
/** Parent-side demand-driven terminal handle. `ready` publishes only captured
|
|
131
|
+
* history metadata; the caller must pull every seed byte before `start`. */
|
|
126
132
|
export interface ParentTerminal {
|
|
127
133
|
readonly attachId: string;
|
|
128
134
|
readonly ready: Promise<{
|
|
129
135
|
role: TerminalRole;
|
|
136
|
+
seedBytes: number;
|
|
137
|
+
dimensions?: {
|
|
138
|
+
cols: number;
|
|
139
|
+
rows: number;
|
|
140
|
+
};
|
|
130
141
|
}>;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
readonly readerDone: Promise<ParentTerminalReaderDone>;
|
|
143
|
+
readSeed(offset: number, maxBytes: number): Promise<ParentTerminalRead>;
|
|
144
|
+
start(): Promise<void>;
|
|
145
|
+
read(offset: number, maxBytes: number): Promise<ParentTerminalRead>;
|
|
146
|
+
onResize?(listener: (dimensions: {
|
|
147
|
+
cols: number;
|
|
148
|
+
rows: number;
|
|
134
149
|
}) => void): void;
|
|
135
|
-
write(data: string): void
|
|
136
|
-
resize(cols: number, rows: number): void
|
|
150
|
+
write(data: string | Uint8Array): Promise<void>;
|
|
151
|
+
resize(cols: number, rows: number): Promise<void>;
|
|
137
152
|
close(): void;
|
|
138
153
|
}
|
|
139
154
|
/** A typed attach failure so transport adapters can distinguish an expected
|
|
@@ -210,9 +225,8 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
210
225
|
/**
|
|
211
226
|
* Open a live terminal on the session's runner child (spawning it if needed).
|
|
212
227
|
* Phase C hosts one terminal ("main") per session; the returned handle is a
|
|
213
|
-
* single attach client
|
|
214
|
-
*
|
|
215
|
-
* bridge calls it directly.
|
|
228
|
+
* single attach client with the requested `owner` (read-write) or `read-only`
|
|
229
|
+
* role. Not part of `AgentExecutor`; the WS bridge calls it directly.
|
|
216
230
|
*/
|
|
217
231
|
openTerminal(localThreadId: string, opts: OpenTerminalOptions): ParentTerminal;
|
|
218
232
|
/**
|
|
@@ -345,6 +359,7 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
345
359
|
* and tmux's own activity clock are independent busy evidence. There is no
|
|
346
360
|
* "user was inactive for an hour" override for an active turn. */
|
|
347
361
|
private reapNativePane;
|
|
362
|
+
private reapNativeTerminal;
|
|
348
363
|
private isNativePaneBusy;
|
|
349
364
|
private isManagedNativeHandle;
|
|
350
365
|
private failActiveResponses;
|