@rynx-ai/runtime 0.1.11-beta.33 → 0.1.11-beta.35
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-hook-main.js +1 -0
- package/dist/claude/native-integration.d.ts +10 -0
- package/dist/claude/native-integration.js +100 -6
- package/dist/codex-app-server/forwarder.js +23 -1
- package/dist/codex-app-server/mapping.js +34 -10
- 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/codex-home.d.ts +35 -3
- package/dist/codex-home.js +321 -14
- package/dist/host.d.ts +11 -25
- package/dist/host.js +147 -132
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/runner/child.d.ts +25 -3
- package/dist/runner/child.js +359 -64
- 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 +18 -15
- package/dist/terminal/registry.js +43 -23
- 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 +675 -200
- 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,10 +120,13 @@ 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();
|
|
129
|
+
codexStartupMonitors = new Map();
|
|
126
130
|
terminalWatchers = new Map();
|
|
127
131
|
/** Opens are async; a close received before attach resolves tombstones the id. */
|
|
128
132
|
pendingTerminalOpens = new Set();
|
|
@@ -133,7 +137,9 @@ export class RunnerSession {
|
|
|
133
137
|
mirrorImageAcks = new Map();
|
|
134
138
|
/** Provider name retained for asynchronous Terminal-exit diagnostics. */
|
|
135
139
|
liveRuntimes = new Map();
|
|
140
|
+
terminalStatuses = new Map();
|
|
136
141
|
shuttingDown = false;
|
|
142
|
+
shutdownPromise = null;
|
|
137
143
|
constructor({ transport, executor, onShutdown }) {
|
|
138
144
|
this.transport = transport;
|
|
139
145
|
this.executor = executor;
|
|
@@ -177,7 +183,9 @@ export class RunnerSession {
|
|
|
177
183
|
void this.runCap(msg.capId, msg.name, msg.args);
|
|
178
184
|
return;
|
|
179
185
|
case "term.open":
|
|
180
|
-
if (this.pendingTerminalOpens.has(msg.attachId) ||
|
|
186
|
+
if (this.pendingTerminalOpens.has(msg.attachId) ||
|
|
187
|
+
this.preparations.has(msg.attachId) ||
|
|
188
|
+
this.attachments.has(msg.attachId)) {
|
|
181
189
|
this.transport.send({
|
|
182
190
|
t: "term.error",
|
|
183
191
|
attachId: msg.attachId,
|
|
@@ -192,29 +200,35 @@ export class RunnerSession {
|
|
|
192
200
|
this.cancelledTerminalOpens.delete(msg.attachId);
|
|
193
201
|
});
|
|
194
202
|
return;
|
|
203
|
+
case "term.seed.read":
|
|
204
|
+
void this.readTerminalSeed(msg);
|
|
205
|
+
return;
|
|
206
|
+
case "term.start":
|
|
207
|
+
void this.startTerminal(msg);
|
|
208
|
+
return;
|
|
209
|
+
case "term.read":
|
|
210
|
+
void this.readTerminalOutput(msg);
|
|
211
|
+
return;
|
|
195
212
|
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
|
-
}
|
|
213
|
+
void this.writeTerminalInput(msg);
|
|
207
214
|
return;
|
|
208
215
|
case "term.resize":
|
|
209
|
-
this.
|
|
216
|
+
void this.resizeTerminal(msg);
|
|
210
217
|
return;
|
|
211
218
|
case "term.close": {
|
|
212
219
|
if (this.pendingTerminalOpens.has(msg.attachId)) {
|
|
213
220
|
this.cancelledTerminalOpens.add(msg.attachId);
|
|
214
221
|
}
|
|
222
|
+
const pending = this.preparations.get(msg.attachId);
|
|
223
|
+
if (pending) {
|
|
224
|
+
clearTimeout(pending.timer);
|
|
225
|
+
this.preparations.delete(msg.attachId);
|
|
226
|
+
pending.preparation.kill();
|
|
227
|
+
}
|
|
215
228
|
const attachment = this.attachments.get(msg.attachId);
|
|
216
229
|
this.attachments.delete(msg.attachId);
|
|
217
230
|
this.attachmentThreadIds.delete(msg.attachId);
|
|
231
|
+
this.attachmentRoles.delete(msg.attachId);
|
|
218
232
|
attachment?.kill();
|
|
219
233
|
return;
|
|
220
234
|
}
|
|
@@ -234,8 +248,11 @@ export class RunnerSession {
|
|
|
234
248
|
case "live.interrupt":
|
|
235
249
|
void this.interruptLive(msg);
|
|
236
250
|
return;
|
|
251
|
+
case "terminal.reap":
|
|
252
|
+
void this.reapTerminal(msg);
|
|
253
|
+
return;
|
|
237
254
|
case "shutdown":
|
|
238
|
-
this.shutdown();
|
|
255
|
+
void this.shutdown();
|
|
239
256
|
return;
|
|
240
257
|
}
|
|
241
258
|
}
|
|
@@ -269,14 +286,19 @@ export class RunnerSession {
|
|
|
269
286
|
const target = { id: localThreadId };
|
|
270
287
|
const emit = (event) => {
|
|
271
288
|
const sessionId = target.id;
|
|
289
|
+
if (event.type === "session.status") {
|
|
290
|
+
this.terminalStatuses.set(sessionId, event.status);
|
|
291
|
+
}
|
|
272
292
|
this.enqueueMirror(() => this.sendMirroredEvent(sessionId, event));
|
|
273
293
|
};
|
|
274
294
|
const retarget = (newId, meta) => {
|
|
295
|
+
const previousId = target.id;
|
|
296
|
+
this.transferNativeSessionResources(previousId, newId);
|
|
275
297
|
target.id = newId;
|
|
276
298
|
this.enqueueMirror(() => {
|
|
277
299
|
this.transport.send({
|
|
278
300
|
t: "rotate",
|
|
279
|
-
from:
|
|
301
|
+
from: previousId,
|
|
280
302
|
to: newId,
|
|
281
303
|
kind: meta.kind,
|
|
282
304
|
workspace: meta.workspace,
|
|
@@ -287,11 +309,41 @@ export class RunnerSession {
|
|
|
287
309
|
};
|
|
288
310
|
return { emit, retarget };
|
|
289
311
|
}
|
|
312
|
+
transferNativeSessionResources(sourceId, targetId) {
|
|
313
|
+
if (sourceId === targetId)
|
|
314
|
+
return;
|
|
315
|
+
const sourceTerminalId = `${sourceId}-main`;
|
|
316
|
+
const targetTerminalId = `${targetId}-main`;
|
|
317
|
+
this.terminals.transfer(sourceTerminalId, targetTerminalId);
|
|
318
|
+
const watcher = this.terminalWatchers.get(sourceId);
|
|
319
|
+
if (watcher)
|
|
320
|
+
clearInterval(watcher);
|
|
321
|
+
this.terminalWatchers.delete(sourceId);
|
|
322
|
+
const runtime = this.liveRuntimes.get(sourceId);
|
|
323
|
+
if (runtime) {
|
|
324
|
+
this.liveRuntimes.delete(sourceId);
|
|
325
|
+
this.liveRuntimes.set(targetId, runtime);
|
|
326
|
+
}
|
|
327
|
+
const status = this.terminalStatuses.get(sourceId);
|
|
328
|
+
if (status) {
|
|
329
|
+
this.terminalStatuses.delete(sourceId);
|
|
330
|
+
this.terminalStatuses.set(targetId, status);
|
|
331
|
+
}
|
|
332
|
+
if (this.liveIds.delete(sourceId))
|
|
333
|
+
this.liveIds.add(targetId);
|
|
334
|
+
for (const [attachId, sessionId] of this.attachmentThreadIds) {
|
|
335
|
+
if (sessionId === sourceId)
|
|
336
|
+
this.attachmentThreadIds.set(attachId, targetId);
|
|
337
|
+
}
|
|
338
|
+
const terminal = this.terminals.get(targetTerminalId);
|
|
339
|
+
if (terminal)
|
|
340
|
+
this.watchNativeTerminal(targetId, targetTerminalId, terminal);
|
|
341
|
+
}
|
|
290
342
|
enqueueMirror(operation) {
|
|
291
343
|
this.mirrorQueue = this.mirrorQueue.then(operation).catch((error) => {
|
|
292
344
|
if (!this.shuttingDown) {
|
|
293
345
|
this.rejectMirrorImageAcks(error instanceof Error ? error : new Error(String(error)));
|
|
294
|
-
this.shutdown();
|
|
346
|
+
void this.shutdown();
|
|
295
347
|
}
|
|
296
348
|
});
|
|
297
349
|
}
|
|
@@ -477,10 +529,31 @@ export class RunnerSession {
|
|
|
477
529
|
}
|
|
478
530
|
}
|
|
479
531
|
monitorCodexThreadStartup(localThreadId, runtime, provider) {
|
|
532
|
+
// Starting (or declining to start) a new monitor invalidates every callback
|
|
533
|
+
// owned by the previous launch before it can touch the replacement state.
|
|
534
|
+
this.codexStartupMonitors.delete(localThreadId);
|
|
480
535
|
if (!provider.waitLiveReady)
|
|
481
536
|
return;
|
|
537
|
+
if (provider.liveSessionFailure?.(localThreadId)?.code === "login_required")
|
|
538
|
+
return;
|
|
539
|
+
const expectedTerminal = this.terminals.get(`${localThreadId}-main`);
|
|
540
|
+
const monitor = Symbol(localThreadId);
|
|
541
|
+
this.codexStartupMonitors.set(localThreadId, monitor);
|
|
542
|
+
const claimCurrentLaunch = () => {
|
|
543
|
+
if (this.codexStartupMonitors.get(localThreadId) !== monitor ||
|
|
544
|
+
this.terminals.get(`${localThreadId}-main`) !== expectedTerminal)
|
|
545
|
+
return false;
|
|
546
|
+
this.codexStartupMonitors.delete(localThreadId);
|
|
547
|
+
return true;
|
|
548
|
+
};
|
|
482
549
|
void provider.waitLiveReady(localThreadId, CODEX_THREAD_START_TIMEOUT_MS).then((ready) => {
|
|
483
|
-
if (ready)
|
|
550
|
+
if (ready) {
|
|
551
|
+
if (this.codexStartupMonitors.get(localThreadId) === monitor) {
|
|
552
|
+
this.codexStartupMonitors.delete(localThreadId);
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (!claimCurrentLaunch())
|
|
484
557
|
return;
|
|
485
558
|
const terminal = this.terminals.get(`${localThreadId}-main`);
|
|
486
559
|
const paneFailure = terminal && !terminal.isAlive()
|
|
@@ -499,27 +572,30 @@ export class RunnerSession {
|
|
|
499
572
|
: `${runtime === "traex" ? "Traex" : "Codex"} TUI did not publish thread/started within ${CODEX_THREAD_START_TIMEOUT_MS / 1_000}s after app-server and observer readiness`);
|
|
500
573
|
this.failCodexThreadStartup(localThreadId, provider, new AgentRuntimeError(detail, 503, paneFailure
|
|
501
574
|
? "native_terminal_exited_before_session"
|
|
502
|
-
: "native_thread_discovery_timeout"));
|
|
575
|
+
: "native_thread_discovery_timeout"), expectedTerminal);
|
|
503
576
|
}).catch((error) => {
|
|
577
|
+
if (!claimCurrentLaunch())
|
|
578
|
+
return;
|
|
504
579
|
this.failCodexThreadStartup(localThreadId, provider, error instanceof AgentRuntimeError
|
|
505
580
|
? error
|
|
506
|
-
: nativePhaseError(runtime, "native_thread_discovery_failed", "native session discovery failed", error));
|
|
581
|
+
: nativePhaseError(runtime, "native_thread_discovery_failed", "native session discovery failed", error), expectedTerminal);
|
|
507
582
|
});
|
|
508
583
|
}
|
|
509
584
|
/** Discovery failure is terminal for this partial native launch. Publish the
|
|
510
585
|
* exact cause first so an in-flight injection wakes, then drop the observer
|
|
511
586
|
* and TUI so a later task retry creates a clean launch instead of reusing an
|
|
512
587
|
* already-rejected readiness promise. */
|
|
513
|
-
failCodexThreadStartup(localThreadId, provider, error) {
|
|
588
|
+
failCodexThreadStartup(localThreadId, provider, error, expectedTerminal) {
|
|
514
589
|
if (!provider.failLiveStartup?.(localThreadId, error))
|
|
515
590
|
return false;
|
|
591
|
+
this.codexStartupMonitors.delete(localThreadId);
|
|
516
592
|
const watcher = this.terminalWatchers.get(localThreadId);
|
|
517
593
|
if (watcher)
|
|
518
594
|
clearInterval(watcher);
|
|
519
595
|
this.terminalWatchers.delete(localThreadId);
|
|
520
596
|
this.cancelTraexStartupWatcher(localThreadId);
|
|
521
597
|
try {
|
|
522
|
-
this.terminals.close(`${localThreadId}-main
|
|
598
|
+
this.terminals.close(`${localThreadId}-main`, expectedTerminal);
|
|
523
599
|
}
|
|
524
600
|
catch (closeError) {
|
|
525
601
|
console.warn(`[runner] session=${localThreadId} failed to close native Terminal after startup failure: ${closeError instanceof Error ? closeError.message : String(closeError)}`);
|
|
@@ -614,6 +690,43 @@ export class RunnerSession {
|
|
|
614
690
|
...(error ? { error } : {}),
|
|
615
691
|
});
|
|
616
692
|
}
|
|
693
|
+
async reapTerminal(msg) {
|
|
694
|
+
let error;
|
|
695
|
+
try {
|
|
696
|
+
const lifecycle = this.terminals.lifecycle(msg.terminalId);
|
|
697
|
+
const watcher = this.terminalWatchers.get(msg.localThreadId);
|
|
698
|
+
if (watcher)
|
|
699
|
+
clearInterval(watcher);
|
|
700
|
+
this.terminalWatchers.delete(msg.localThreadId);
|
|
701
|
+
this.cancelTraexStartupWatcher(msg.localThreadId);
|
|
702
|
+
for (const [attachId, sessionId] of this.attachmentThreadIds) {
|
|
703
|
+
if (sessionId !== msg.localThreadId)
|
|
704
|
+
continue;
|
|
705
|
+
this.attachments.get(attachId)?.kill();
|
|
706
|
+
this.attachments.delete(attachId);
|
|
707
|
+
this.attachmentThreadIds.delete(attachId);
|
|
708
|
+
this.attachmentRoles.delete(attachId);
|
|
709
|
+
}
|
|
710
|
+
this.terminals.close(msg.terminalId);
|
|
711
|
+
if (lifecycle === "auxiliary") {
|
|
712
|
+
await this.liveProvider.teardownAuxiliaryTerminalRuntime?.(msg.localThreadId);
|
|
713
|
+
this.liveIds.delete(msg.localThreadId);
|
|
714
|
+
this.liveRuntimes.delete(msg.localThreadId);
|
|
715
|
+
this.terminalStatuses.delete(msg.localThreadId);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
catch (cause) {
|
|
719
|
+
error = toWireError(cause);
|
|
720
|
+
}
|
|
721
|
+
this.transport.send({
|
|
722
|
+
t: "terminal.reaped",
|
|
723
|
+
reqId: msg.reqId,
|
|
724
|
+
localThreadId: msg.localThreadId,
|
|
725
|
+
terminalId: msg.terminalId,
|
|
726
|
+
ok: error === undefined,
|
|
727
|
+
...(error ? { error } : {}),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
617
730
|
/** Launch (idempotently) the session's codex TUI pane from the executor's
|
|
618
731
|
* `codexTerminalSpec`. Shares the `${id}-main` terminal id with `term.open`,
|
|
619
732
|
* so the web attach reuses the same detached pane. */
|
|
@@ -623,12 +736,23 @@ export class RunnerSession {
|
|
|
623
736
|
return;
|
|
624
737
|
const terminalId = `${localThreadId}-main`;
|
|
625
738
|
const term = this.terminals.getOrCreate(terminalId, {
|
|
739
|
+
lifecycle: spec.lifecycle,
|
|
626
740
|
cwd: spec.cwd,
|
|
627
741
|
command: spec.command,
|
|
628
742
|
args: spec.args,
|
|
629
743
|
cols: cols ?? 120,
|
|
630
744
|
rows: rows ?? 40,
|
|
631
745
|
...(spec.env ? { env: spec.env } : {}),
|
|
746
|
+
...(spec.scrollback === undefined ? {} : { scrollback: spec.scrollback }),
|
|
747
|
+
...(spec.tmuxAllowPassthrough === undefined
|
|
748
|
+
? {}
|
|
749
|
+
: { tmuxAllowPassthrough: spec.tmuxAllowPassthrough }),
|
|
750
|
+
...(spec.tmuxStartOnAttach === undefined
|
|
751
|
+
? {}
|
|
752
|
+
: { tmuxStartOnAttach: spec.tmuxStartOnAttach }),
|
|
753
|
+
...(spec.keepAliveAfterExit === undefined
|
|
754
|
+
? {}
|
|
755
|
+
: { keepAliveAfterExit: spec.keepAliveAfterExit }),
|
|
632
756
|
});
|
|
633
757
|
if (spec.skipTraexStartupPrompts) {
|
|
634
758
|
const watcher = Symbol(localThreadId);
|
|
@@ -671,30 +795,39 @@ export class RunnerSession {
|
|
|
671
795
|
this.terminalWatchers.delete(localThreadId);
|
|
672
796
|
this.cancelTraexStartupWatcher(localThreadId);
|
|
673
797
|
const runtime = this.liveRuntimes.get(localThreadId);
|
|
798
|
+
const lifecycle = this.terminals.lifecycle(terminalId);
|
|
674
799
|
const paneFailure = terminalFailureDetail(terminal.capturePane?.() ?? "");
|
|
675
|
-
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
|
-
}
|
|
800
|
+
const startupFailed = this.failCodexThreadStartup(localThreadId, this.liveProvider, nativePhaseError(runtime, "native_terminal_exited_before_session", "Terminal exited before native session discovery completed", paneFailure || undefined), terminal);
|
|
801
|
+
if (startupFailed || lifecycle === undefined)
|
|
695
802
|
return;
|
|
803
|
+
try {
|
|
804
|
+
this.terminals.close(terminalId, terminal);
|
|
805
|
+
}
|
|
806
|
+
catch (closeError) {
|
|
807
|
+
console.warn(`[runner] session=${localThreadId} failed to close exited native Terminal: ${closeError instanceof Error ? closeError.message : String(closeError)}`);
|
|
808
|
+
}
|
|
809
|
+
const status = this.terminalStatuses.get(localThreadId) ?? "unknown";
|
|
810
|
+
if (lifecycle === "auxiliary") {
|
|
811
|
+
console.warn(`[runner] session=${localThreadId} ${providerDisplayName(runtime)} auxiliary Terminal exited`);
|
|
696
812
|
}
|
|
697
|
-
|
|
813
|
+
else {
|
|
814
|
+
if (status !== "idle") {
|
|
815
|
+
this.liveProvider.failLiveSession?.(localThreadId, nativePhaseError(runtime, "required_terminal_exited", "Required Terminal exited unexpectedly", paneFailure || undefined));
|
|
816
|
+
}
|
|
817
|
+
this.liveProvider.stopLiveCodexSession?.(localThreadId);
|
|
818
|
+
this.liveIds.delete(localThreadId);
|
|
819
|
+
this.liveRuntimes.delete(localThreadId);
|
|
820
|
+
this.terminalStatuses.delete(localThreadId);
|
|
821
|
+
}
|
|
822
|
+
this.enqueueMirror(() => {
|
|
823
|
+
this.transport.send({
|
|
824
|
+
t: "terminal.lifecycle.ended",
|
|
825
|
+
localThreadId,
|
|
826
|
+
terminalId,
|
|
827
|
+
lifecycle,
|
|
828
|
+
status,
|
|
829
|
+
});
|
|
830
|
+
});
|
|
698
831
|
}).catch((error) => {
|
|
699
832
|
console.warn(`[runner] session=${localThreadId} native Terminal liveness probe failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
700
833
|
}).finally(() => {
|
|
@@ -835,6 +968,7 @@ export class RunnerSession {
|
|
|
835
968
|
for (const timer of this.terminalWatchers.values())
|
|
836
969
|
clearInterval(timer);
|
|
837
970
|
this.terminalWatchers.clear();
|
|
971
|
+
this.codexStartupMonitors.clear();
|
|
838
972
|
for (const id of this.liveIds) {
|
|
839
973
|
this.liveProvider.stopLiveCodexSession?.(id, {
|
|
840
974
|
deferClaudeInteractionCleanup: true,
|
|
@@ -846,19 +980,31 @@ export class RunnerSession {
|
|
|
846
980
|
/** Stop event forwarding, kill native terminals/hooks, then synchronously
|
|
847
981
|
* scrub provider handoff files before the child process is allowed to exit. */
|
|
848
982
|
shutdown() {
|
|
849
|
-
if (this.
|
|
850
|
-
return;
|
|
983
|
+
if (this.shutdownPromise)
|
|
984
|
+
return this.shutdownPromise;
|
|
851
985
|
this.shuttingDown = true;
|
|
852
|
-
this.
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
986
|
+
this.shutdownPromise = (async () => {
|
|
987
|
+
this.rejectMirrorImageAcks(new Error("runner is shutting down"));
|
|
988
|
+
this.stopLive();
|
|
989
|
+
for (const pending of this.preparations.values()) {
|
|
990
|
+
clearTimeout(pending.timer);
|
|
991
|
+
pending.preparation.kill();
|
|
992
|
+
}
|
|
993
|
+
this.preparations.clear();
|
|
994
|
+
for (const attachment of this.attachments.values())
|
|
995
|
+
attachment.kill();
|
|
996
|
+
this.terminals.closeAll();
|
|
997
|
+
this.attachments.clear();
|
|
998
|
+
this.attachmentThreadIds.clear();
|
|
999
|
+
this.attachmentRoles.clear();
|
|
1000
|
+
await this.liveProvider.finalizeStoppedLiveSessions?.();
|
|
1001
|
+
this.onShutdown();
|
|
1002
|
+
})();
|
|
1003
|
+
return this.shutdownPromise;
|
|
857
1004
|
}
|
|
858
1005
|
async openTerminal(msg) {
|
|
859
1006
|
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
|
|
1007
|
+
// codex/claude-native session (no explicit command): DUMB ATTACH. A tab switch ONLY
|
|
862
1008
|
// attaches an already-live pane; it NEVER ensures the forwarder or relaunches a
|
|
863
1009
|
// dead pane. Creation/relaunch happens on message-send (`live.ensure`) or an
|
|
864
1010
|
// explicit restart. A missing/dead pane → `term.error`, so the web shows the
|
|
@@ -880,6 +1026,7 @@ export class RunnerSession {
|
|
|
880
1026
|
}
|
|
881
1027
|
// Login-shell terminal (explicit command): create-or-reuse as before.
|
|
882
1028
|
this.terminals.getOrCreate(msg.terminalId, {
|
|
1029
|
+
lifecycle: "auxiliary",
|
|
883
1030
|
cwd: msg.cwd,
|
|
884
1031
|
command: msg.command,
|
|
885
1032
|
args: msg.args,
|
|
@@ -897,14 +1044,14 @@ export class RunnerSession {
|
|
|
897
1044
|
});
|
|
898
1045
|
}
|
|
899
1046
|
}
|
|
900
|
-
/**
|
|
1047
|
+
/** Prepare captured history without starting the live control client. */
|
|
901
1048
|
async attachExisting(msg) {
|
|
902
|
-
const {
|
|
1049
|
+
const { preparation, role } = await this.terminals.prepare(msg.terminalId, msg.role, {
|
|
903
1050
|
cols: msg.cols,
|
|
904
1051
|
rows: msg.rows,
|
|
905
1052
|
});
|
|
906
1053
|
if (this.cancelledTerminalOpens.has(msg.attachId)) {
|
|
907
|
-
|
|
1054
|
+
preparation.kill();
|
|
908
1055
|
this.transport.send({
|
|
909
1056
|
t: "term.error",
|
|
910
1057
|
attachId: msg.attachId,
|
|
@@ -913,20 +1060,168 @@ export class RunnerSession {
|
|
|
913
1060
|
});
|
|
914
1061
|
return;
|
|
915
1062
|
}
|
|
916
|
-
|
|
1063
|
+
const timer = setTimeout(() => {
|
|
1064
|
+
const pending = this.preparations.get(msg.attachId);
|
|
1065
|
+
if (!pending || pending.preparation !== preparation)
|
|
1066
|
+
return;
|
|
1067
|
+
this.preparations.delete(msg.attachId);
|
|
1068
|
+
this.attachmentThreadIds.delete(msg.attachId);
|
|
1069
|
+
this.attachmentRoles.delete(msg.attachId);
|
|
1070
|
+
preparation.kill();
|
|
1071
|
+
}, TERMINAL_PREPARATION_TTL_MS);
|
|
1072
|
+
timer.unref?.();
|
|
1073
|
+
this.preparations.set(msg.attachId, { preparation, timer });
|
|
1074
|
+
this.attachmentRoles.set(msg.attachId, role);
|
|
917
1075
|
if (msg.localThreadId)
|
|
918
1076
|
this.attachmentThreadIds.set(msg.attachId, msg.localThreadId);
|
|
919
|
-
|
|
920
|
-
t: "term.
|
|
1077
|
+
this.transport.send({
|
|
1078
|
+
t: "term.prepared",
|
|
921
1079
|
attachId: msg.attachId,
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1080
|
+
role,
|
|
1081
|
+
seedBytes: preparation.seedBytes,
|
|
1082
|
+
...(preparation.dimensions ?? {}),
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
async readTerminalSeed(msg) {
|
|
1086
|
+
const pending = this.preparations.get(msg.attachId);
|
|
1087
|
+
if (!pending) {
|
|
1088
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, "terminal is not prepared");
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
try {
|
|
1092
|
+
const result = await pending.preparation.readSeed(msg.offset, msg.maxBytes);
|
|
1093
|
+
await this.transport.sendAndDrain({
|
|
1094
|
+
t: "term.seed.chunk",
|
|
1095
|
+
attachId: msg.attachId,
|
|
1096
|
+
reqId: msg.reqId,
|
|
1097
|
+
dataB64: Buffer.from(result.data).toString("base64"),
|
|
1098
|
+
nextOffset: result.nextOffset,
|
|
1099
|
+
done: result.done,
|
|
1100
|
+
...(result.finalOffset === undefined ? {} : { finalOffset: result.finalOffset }),
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, error);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
async startTerminal(msg) {
|
|
1108
|
+
const pending = this.preparations.get(msg.attachId);
|
|
1109
|
+
if (!pending) {
|
|
1110
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, "terminal is not prepared");
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
clearTimeout(pending.timer);
|
|
1114
|
+
this.preparations.delete(msg.attachId);
|
|
1115
|
+
try {
|
|
1116
|
+
const attachment = await pending.preparation.start();
|
|
1117
|
+
this.attachments.set(msg.attachId, attachment);
|
|
1118
|
+
attachment.onResize(({ cols, rows }) => {
|
|
1119
|
+
this.transport.send({ t: "term.dimensions", attachId: msg.attachId, cols, rows });
|
|
1120
|
+
});
|
|
1121
|
+
await this.transport.sendAndDrain({
|
|
1122
|
+
t: "term.started",
|
|
1123
|
+
attachId: msg.attachId,
|
|
1124
|
+
reqId: msg.reqId,
|
|
1125
|
+
});
|
|
1126
|
+
void attachment.readerDone.then(async (info) => {
|
|
1127
|
+
await this.transport.sendAndDrain({
|
|
1128
|
+
t: "term.reader.done",
|
|
1129
|
+
attachId: msg.attachId,
|
|
1130
|
+
finalOffset: info.finalOffset,
|
|
1131
|
+
reason: info.reason,
|
|
1132
|
+
exitCode: info.exitCode,
|
|
1133
|
+
}).catch(() => undefined);
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
catch (error) {
|
|
1137
|
+
pending.preparation.kill();
|
|
926
1138
|
this.attachmentThreadIds.delete(msg.attachId);
|
|
927
|
-
this.
|
|
1139
|
+
this.attachmentRoles.delete(msg.attachId);
|
|
1140
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, error);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
async readTerminalOutput(msg) {
|
|
1144
|
+
const attachment = this.attachments.get(msg.attachId);
|
|
1145
|
+
if (!attachment) {
|
|
1146
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, "terminal has not started");
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
const result = await attachment.read(msg.offset, msg.maxBytes);
|
|
1151
|
+
await this.transport.sendAndDrain({
|
|
1152
|
+
t: "term.chunk",
|
|
1153
|
+
attachId: msg.attachId,
|
|
1154
|
+
reqId: msg.reqId,
|
|
1155
|
+
dataB64: Buffer.from(result.data).toString("base64"),
|
|
1156
|
+
nextOffset: result.nextOffset,
|
|
1157
|
+
done: result.done,
|
|
1158
|
+
...(result.finalOffset === undefined ? {} : { finalOffset: result.finalOffset }),
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, error);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async writeTerminalInput(msg) {
|
|
1166
|
+
if (this.attachmentRoles.get(msg.attachId) !== "owner") {
|
|
1167
|
+
await this.transport.sendAndDrain({
|
|
1168
|
+
t: "term.ack",
|
|
1169
|
+
attachId: msg.attachId,
|
|
1170
|
+
reqId: msg.reqId,
|
|
1171
|
+
operation: "input",
|
|
1172
|
+
});
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
const attachment = this.attachments.get(msg.attachId);
|
|
1176
|
+
if (!attachment) {
|
|
1177
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, "terminal has not started");
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const localThreadId = this.attachmentThreadIds.get(msg.attachId);
|
|
1181
|
+
const input = Buffer.from(msg.dataB64, "base64");
|
|
1182
|
+
if (!isTerminalProtocolResponse(input.toString("utf8"))) {
|
|
1183
|
+
this.cancelTraexStartupWatcher(localThreadId);
|
|
1184
|
+
}
|
|
1185
|
+
try {
|
|
1186
|
+
await attachment.write(input);
|
|
1187
|
+
await this.transport.sendAndDrain({
|
|
1188
|
+
t: "term.ack",
|
|
1189
|
+
attachId: msg.attachId,
|
|
1190
|
+
reqId: msg.reqId,
|
|
1191
|
+
operation: "input",
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
catch (error) {
|
|
1195
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, error);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
async resizeTerminal(msg) {
|
|
1199
|
+
const attachment = this.attachments.get(msg.attachId);
|
|
1200
|
+
if (!attachment) {
|
|
1201
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, "terminal has not started");
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
try {
|
|
1205
|
+
await attachment.resize(msg.cols, msg.rows);
|
|
1206
|
+
await this.transport.sendAndDrain({
|
|
1207
|
+
t: "term.ack",
|
|
1208
|
+
attachId: msg.attachId,
|
|
1209
|
+
reqId: msg.reqId,
|
|
1210
|
+
operation: "resize",
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
catch (error) {
|
|
1214
|
+
this.sendTerminalOperationError(msg.attachId, msg.reqId, error);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
sendTerminalOperationError(attachId, reqId, error) {
|
|
1218
|
+
this.transport.send({
|
|
1219
|
+
t: "term.error",
|
|
1220
|
+
attachId,
|
|
1221
|
+
reqId,
|
|
1222
|
+
code: "terminal_open_failed",
|
|
1223
|
+
message: error instanceof Error ? error.message : String(error),
|
|
928
1224
|
});
|
|
929
|
-
this.transport.send({ t: "term.opened", attachId: msg.attachId, role });
|
|
930
1225
|
}
|
|
931
1226
|
async runCap(capId, name, args) {
|
|
932
1227
|
try {
|