@rynx-ai/runtime 0.1.11-beta.21 → 0.1.11-beta.23
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-integration.d.ts +7 -0
- package/dist/claude/native-integration.js +25 -2
- package/dist/codex-app-server/client.d.ts +4 -3
- package/dist/codex-app-server/client.js +35 -12
- package/dist/codex-app-server/forwarder.d.ts +2 -7
- package/dist/codex-app-server/forwarder.js +12 -50
- package/dist/codex-app-server/mapping.d.ts +1 -0
- package/dist/codex-app-server/mapping.js +8 -1
- package/dist/codex-app-server/protocol.d.ts +1 -2
- package/dist/host.d.ts +36 -25
- package/dist/host.js +384 -441
- package/dist/index.d.ts +1 -1
- package/dist/runner/child.d.ts +18 -7
- package/dist/runner/child.js +169 -45
- package/dist/runner/manager.d.ts +14 -15
- package/dist/runner/manager.js +90 -98
- package/dist/runner/protocol.d.ts +16 -15
- package/dist/runner/startup-policy.d.ts +3 -0
- package/dist/runner/startup-policy.js +5 -0
- package/dist/terminal/registry.js +3 -2
- package/dist/terminal/tmux.d.ts +29 -5
- package/dist/terminal/tmux.js +98 -52
- package/package.json +2 -2
package/dist/runner/manager.js
CHANGED
|
@@ -24,7 +24,7 @@ import { listRuntimeModels } from "../models-catalog.js";
|
|
|
24
24
|
import { probeRuntimeStatus } from "../runtime-status.js";
|
|
25
25
|
import { terminateTmuxServer, tmuxHasAttachedClient, tmuxWindowActivityAt, } from "../terminal/tmux.js";
|
|
26
26
|
import { fromWireError, } from "./protocol.js";
|
|
27
|
-
import { isManagedNativeProvider } from "./startup-policy.js";
|
|
27
|
+
import { isCodexLineageProvider, isManagedNativeProvider } from "./startup-policy.js";
|
|
28
28
|
import { StdioRunnerTransport } from "./transport.js";
|
|
29
29
|
/** Routing key for the shared capability runner (slash-command RPCs). */
|
|
30
30
|
const CAP_KEY = "__cap__";
|
|
@@ -34,9 +34,8 @@ const STDERR_TAIL_LINES = 40;
|
|
|
34
34
|
const DEFAULT_SHUTDOWN_GRACE_MS = 5_000;
|
|
35
35
|
/** Time allowed for exit after SIGKILL before shutdown reports failure. */
|
|
36
36
|
const DEFAULT_SHUTDOWN_KILL_GRACE_MS = 5_000;
|
|
37
|
-
/**
|
|
38
|
-
*
|
|
39
|
-
* own 10s gate so scheduling/IPC overhead cannot win the same deadline. */
|
|
37
|
+
/** Legacy setup-pane acknowledgement deadline for providers that do not own
|
|
38
|
+
* phase-specific startup errors. Codex/Traex are deliberately excluded. */
|
|
40
39
|
const DEFAULT_LIVE_START_TIMEOUT_MS = 30_000;
|
|
41
40
|
/** Legacy fallback for a provider without a native startup policy. */
|
|
42
41
|
const DEFAULT_LIVE_READY_TIMEOUT_MS = 25_000;
|
|
@@ -48,9 +47,6 @@ const DEFAULT_NATIVE_LIVE_START_TIMEOUT_MS = 60_000;
|
|
|
48
47
|
* quickly. If it does not, the runner is fenced by a verified process-tree
|
|
49
48
|
* shutdown before maintenance may treat the submission as settled. */
|
|
50
49
|
const DEFAULT_TERMINAL_INPUT_HANDOFF_TIMEOUT_MS = 30_000;
|
|
51
|
-
/** Retry transient terminal-server cleanup without spinning forever. */
|
|
52
|
-
const DEFAULT_TERMINAL_INPUT_CLEANUP_RETRY_MS = 1_000;
|
|
53
|
-
const MAX_TERMINAL_INPUT_CLEANUP_RETRY_MS = 30_000;
|
|
54
50
|
/** Full idle window before an inactive native pane becomes reapable. */
|
|
55
51
|
const DEFAULT_NATIVE_PANE_IDLE_TTL_MS = 60 * 60_000;
|
|
56
52
|
/** tmux output this recent independently proves that a native pane is busy. */
|
|
@@ -181,7 +177,6 @@ export class RunnerManager {
|
|
|
181
177
|
nativeLiveStartTimeoutMs;
|
|
182
178
|
liveInterruptTimeoutMs;
|
|
183
179
|
terminalInputHandoffTimeoutMs;
|
|
184
|
-
terminalInputCleanupRetryMs;
|
|
185
180
|
reapPromise = null;
|
|
186
181
|
stopping = false;
|
|
187
182
|
stopPromise;
|
|
@@ -237,7 +232,6 @@ export class RunnerManager {
|
|
|
237
232
|
this.nativeLiveStartTimeoutMs = Math.max(1, opts.nativeLiveStartTimeoutMs ?? DEFAULT_NATIVE_LIVE_START_TIMEOUT_MS);
|
|
238
233
|
this.liveInterruptTimeoutMs = Math.max(1, opts.liveInterruptTimeoutMs ?? 10_000);
|
|
239
234
|
this.terminalInputHandoffTimeoutMs = Math.max(1, opts.terminalInputHandoffTimeoutMs ?? DEFAULT_TERMINAL_INPUT_HANDOFF_TIMEOUT_MS);
|
|
240
|
-
this.terminalInputCleanupRetryMs = Math.max(1, opts.terminalInputCleanupRetryMs ?? DEFAULT_TERMINAL_INPUT_CLEANUP_RETRY_MS);
|
|
241
235
|
const reapIntervalMs = opts.reapIntervalMs ?? 60_000;
|
|
242
236
|
if (reapIntervalMs > 0) {
|
|
243
237
|
this.reapTimer = setInterval(() => void this.reapIdle(), reapIntervalMs);
|
|
@@ -457,11 +451,6 @@ export class RunnerManager {
|
|
|
457
451
|
this.terminalInputHandoffs.delete(handle);
|
|
458
452
|
}
|
|
459
453
|
finishAllTerminalInputHandoffs(handle) {
|
|
460
|
-
if (handle.terminalCleanupRetryTimer) {
|
|
461
|
-
clearTimeout(handle.terminalCleanupRetryTimer);
|
|
462
|
-
delete handle.terminalCleanupRetryTimer;
|
|
463
|
-
}
|
|
464
|
-
handle.terminalCleanupRetryFailures = 0;
|
|
465
454
|
for (const handoff of [...(this.terminalInputHandoffs.get(handle) ?? [])]) {
|
|
466
455
|
this.finishTerminalInputHandoff(handle, handoff);
|
|
467
456
|
}
|
|
@@ -476,22 +465,6 @@ export class RunnerManager {
|
|
|
476
465
|
handoff.reservation = undefined;
|
|
477
466
|
}
|
|
478
467
|
}
|
|
479
|
-
scheduleTerminalInputCleanupRetry(handle) {
|
|
480
|
-
if (handle.terminalCleanupRetryTimer ||
|
|
481
|
-
!this.terminalInputHandoffs.has(handle))
|
|
482
|
-
return;
|
|
483
|
-
const exponent = Math.min(handle.terminalCleanupRetryFailures - 1, 10);
|
|
484
|
-
const delay = Math.min(this.terminalInputCleanupRetryMs * (2 ** Math.max(0, exponent)), MAX_TERMINAL_INPUT_CLEANUP_RETRY_MS);
|
|
485
|
-
handle.terminalCleanupRetryTimer = setTimeout(() => {
|
|
486
|
-
delete handle.terminalCleanupRetryTimer;
|
|
487
|
-
if (!this.terminalInputHandoffs.has(handle))
|
|
488
|
-
return;
|
|
489
|
-
void this.terminateHandle(handle, "retrying unproven terminal input cleanup").catch((error) => {
|
|
490
|
-
logTerminationFailure(handle, error);
|
|
491
|
-
});
|
|
492
|
-
}, delay);
|
|
493
|
-
handle.terminalCleanupRetryTimer.unref?.();
|
|
494
|
-
}
|
|
495
468
|
expireTerminalInputHandoffs(handle) {
|
|
496
469
|
const handoffs = this.terminalInputHandoffs.get(handle);
|
|
497
470
|
if (!handoffs?.length)
|
|
@@ -509,9 +482,10 @@ export class RunnerManager {
|
|
|
509
482
|
/**
|
|
510
483
|
* Eagerly bring up a session's codex-native live view (persistent forwarder +
|
|
511
484
|
* detached `codex --remote` TUI) in its runner child, spawning the runner if
|
|
512
|
-
* needed. Idempotent. For Codex-lineage
|
|
513
|
-
*
|
|
514
|
-
*
|
|
485
|
+
* needed. Idempotent. For Codex-lineage sessions, resolves after backend
|
|
486
|
+
* preload and pane launch; known-thread observer attach and fresh-thread
|
|
487
|
+
* discovery continue in the background. Other providers retain their
|
|
488
|
+
* readiness gate.
|
|
515
489
|
*/
|
|
516
490
|
ensureLiveSession(localThreadId, opts) {
|
|
517
491
|
const admission = this.reserveAdmission();
|
|
@@ -550,55 +524,66 @@ export class RunnerManager {
|
|
|
550
524
|
this.liveSessionKeys.add(localThreadId);
|
|
551
525
|
const reqId = randomUUID();
|
|
552
526
|
return new Promise((resolve) => {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
527
|
+
const provider = opts.execution.provider;
|
|
528
|
+
const timeoutMs = isCodexLineageProvider(provider)
|
|
529
|
+
? null
|
|
530
|
+
: !waitForReady
|
|
531
|
+
? this.liveStartTimeoutMs
|
|
532
|
+
: provider === "claude"
|
|
533
|
+
? this.nativeLiveStartTimeoutMs
|
|
559
534
|
: this.liveReadyTimeoutMs;
|
|
560
|
-
const timeout =
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
? `${provider === "traex" ? "Traex" : "Codex"} app-server/observer/Terminal launch was not acknowledged within ${Math.round(timeoutMs / 1_000)}s; the runner process was reset before retry`
|
|
535
|
+
const timeout = timeoutMs === null
|
|
536
|
+
? undefined
|
|
537
|
+
: setTimeout(() => {
|
|
538
|
+
if (!handle.live.delete(reqId))
|
|
539
|
+
return;
|
|
540
|
+
const finishTimeout = () => {
|
|
541
|
+
const reason = !waitForReady
|
|
542
|
+
? `runner did not acknowledge terminal start within ${timeoutMs}ms`
|
|
543
|
+
: provider === "claude"
|
|
544
|
+
? `Claude SessionStart was not observed within ${Math.round(timeoutMs / 1_000)}s; the runner process was reset before retry`
|
|
571
545
|
: `runner did not acknowledge live readiness within ${timeoutMs}ms`;
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
546
|
+
this.liveErrors.set(localThreadId, {
|
|
547
|
+
message: reason,
|
|
548
|
+
code: provider === "claude"
|
|
549
|
+
? "claude_session_start_timeout"
|
|
550
|
+
: "native_runner_ack_timeout",
|
|
551
|
+
statusCode: 503,
|
|
552
|
+
});
|
|
553
|
+
this.liveSessionKeys.delete(localThreadId);
|
|
554
|
+
// Legacy providers without phase-owned startup errors retain the
|
|
555
|
+
// bounded control guard. Codex/Traex never enter this branch.
|
|
556
|
+
void this.terminateHandle(handle, reason).catch((error) => {
|
|
557
|
+
logTerminationFailure(handle, error);
|
|
558
|
+
});
|
|
559
|
+
return false;
|
|
560
|
+
};
|
|
561
|
+
const reservation = allowReservedForkTarget
|
|
562
|
+
? undefined
|
|
563
|
+
: this.forkReservations.get(localThreadId);
|
|
564
|
+
if (reservation) {
|
|
565
|
+
void reservation
|
|
566
|
+
.then(() => finishTimeout())
|
|
567
|
+
.then(resolve, () => resolve(false));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
resolve(finishTimeout());
|
|
571
|
+
}, timeoutMs);
|
|
572
|
+
timeout?.unref?.();
|
|
593
573
|
handle.live.set(reqId, (res) => {
|
|
594
|
-
|
|
574
|
+
if (timeout)
|
|
575
|
+
clearTimeout(timeout);
|
|
595
576
|
const ok = res.ok ?? false;
|
|
596
577
|
const finish = () => {
|
|
597
578
|
if (ok) {
|
|
598
579
|
this.liveErrors.delete(localThreadId);
|
|
599
580
|
}
|
|
600
581
|
else {
|
|
601
|
-
this.liveErrors.set(localThreadId, res.error ??
|
|
582
|
+
this.liveErrors.set(localThreadId, res.error ?? {
|
|
583
|
+
message: "live session did not become ready",
|
|
584
|
+
code: "native_readiness_failed",
|
|
585
|
+
statusCode: 503,
|
|
586
|
+
});
|
|
602
587
|
}
|
|
603
588
|
return ok;
|
|
604
589
|
};
|
|
@@ -626,7 +611,11 @@ export class RunnerManager {
|
|
|
626
611
|
});
|
|
627
612
|
}
|
|
628
613
|
lastLiveSessionError(localThreadId) {
|
|
629
|
-
return this.liveErrors.get(localThreadId);
|
|
614
|
+
return this.liveErrors.get(localThreadId)?.message;
|
|
615
|
+
}
|
|
616
|
+
lastLiveSessionFailure(localThreadId) {
|
|
617
|
+
const failure = this.liveErrors.get(localThreadId);
|
|
618
|
+
return failure ? { ...failure } : undefined;
|
|
630
619
|
}
|
|
631
620
|
/**
|
|
632
621
|
* Inject a user turn into a session's live codex thread — reference implementation's
|
|
@@ -653,21 +642,29 @@ export class RunnerManager {
|
|
|
653
642
|
const reqId = randomUUID();
|
|
654
643
|
return new Promise((resolve) => {
|
|
655
644
|
handle.live.set(reqId, (res) => {
|
|
656
|
-
const
|
|
645
|
+
const result = res.result ?? { outcome: "failed" };
|
|
657
646
|
const finish = () => {
|
|
658
|
-
if (outcome === "injected" || outcome === "steered") {
|
|
647
|
+
if (result.outcome === "injected" || result.outcome === "steered") {
|
|
659
648
|
this.liveErrors.delete(localThreadId);
|
|
660
649
|
}
|
|
661
650
|
else {
|
|
662
|
-
this.liveErrors.set(localThreadId, res.error ??
|
|
651
|
+
this.liveErrors.set(localThreadId, res.error ?? {
|
|
652
|
+
message: `live injection ${result.outcome}`,
|
|
653
|
+
code: result.outcome === "notReady"
|
|
654
|
+
? "native_thread_not_ready"
|
|
655
|
+
: result.outcome === "notLive"
|
|
656
|
+
? "native_session_not_live"
|
|
657
|
+
: "native_message_injection_failed",
|
|
658
|
+
statusCode: 503,
|
|
659
|
+
});
|
|
663
660
|
}
|
|
664
|
-
return
|
|
661
|
+
return result;
|
|
665
662
|
};
|
|
666
663
|
const reservation = this.forkReservations.get(localThreadId);
|
|
667
664
|
if (reservation) {
|
|
668
665
|
void reservation
|
|
669
666
|
.then(() => finish())
|
|
670
|
-
.then(resolve, () => resolve("failed"));
|
|
667
|
+
.then(resolve, () => resolve({ outcome: "failed" }));
|
|
671
668
|
return;
|
|
672
669
|
}
|
|
673
670
|
resolve(finish());
|
|
@@ -714,7 +711,7 @@ export class RunnerManager {
|
|
|
714
711
|
handle.live.set(reqId, (reply) => {
|
|
715
712
|
resolve(reply.interactionResult ?? {
|
|
716
713
|
disposition: "invalid",
|
|
717
|
-
message: reply.error ?? "runner did not return an interaction result",
|
|
714
|
+
message: reply.error?.message ?? "runner did not return an interaction result",
|
|
718
715
|
});
|
|
719
716
|
});
|
|
720
717
|
handle.transport.send({
|
|
@@ -1188,7 +1185,6 @@ export class RunnerManager {
|
|
|
1188
1185
|
activeResponseIds: new Set(),
|
|
1189
1186
|
dead: false,
|
|
1190
1187
|
completion,
|
|
1191
|
-
terminalCleanupRetryFailures: 0,
|
|
1192
1188
|
processGroup,
|
|
1193
1189
|
...(sessionContext ? { sessionContext } : {}),
|
|
1194
1190
|
caps: new Map(),
|
|
@@ -1275,7 +1271,7 @@ export class RunnerManager {
|
|
|
1275
1271
|
const resolve = handle.live.get(msg.reqId);
|
|
1276
1272
|
handle.live.delete(msg.reqId);
|
|
1277
1273
|
resolve?.(msg.t === "injected"
|
|
1278
|
-
? {
|
|
1274
|
+
? { result: msg.result, error: msg.error }
|
|
1279
1275
|
: { ok: msg.ok, error: msg.error });
|
|
1280
1276
|
return;
|
|
1281
1277
|
}
|
|
@@ -1318,7 +1314,10 @@ export class RunnerManager {
|
|
|
1318
1314
|
terminal._fail(message);
|
|
1319
1315
|
}
|
|
1320
1316
|
for (const resolve of handle.live.values()) {
|
|
1321
|
-
resolve({
|
|
1317
|
+
resolve({
|
|
1318
|
+
ok: false,
|
|
1319
|
+
error: { message, code: "runner_crashed", statusCode: 500 },
|
|
1320
|
+
});
|
|
1322
1321
|
}
|
|
1323
1322
|
handle.caps.clear();
|
|
1324
1323
|
handle.terminals.clear();
|
|
@@ -1328,10 +1327,6 @@ export class RunnerManager {
|
|
|
1328
1327
|
terminateHandle(handle, reason) {
|
|
1329
1328
|
if (handle.termination)
|
|
1330
1329
|
return handle.termination;
|
|
1331
|
-
if (handle.terminalCleanupRetryTimer) {
|
|
1332
|
-
clearTimeout(handle.terminalCleanupRetryTimer);
|
|
1333
|
-
delete handle.terminalCleanupRetryTimer;
|
|
1334
|
-
}
|
|
1335
1330
|
const attempt = (async () => {
|
|
1336
1331
|
if (!handle.dead) {
|
|
1337
1332
|
try {
|
|
@@ -1348,15 +1343,14 @@ export class RunnerManager {
|
|
|
1348
1343
|
this.signalChild(handle.child, "SIGKILL", handle.processGroup);
|
|
1349
1344
|
childExited = await waitForChildExit(handle.completion, this.shutdownKillGraceMs);
|
|
1350
1345
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1346
|
+
if (handle.key !== CAP_KEY) {
|
|
1347
|
+
await Promise.resolve(this.terminateTerminalServer(`${handle.key}-main`)).catch((error) => {
|
|
1348
|
+
console.warn(`[runner-manager] best-effort terminal close failed for ${handle.key}-main: ${error instanceof Error ? error.message : String(error)}`);
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1354
1351
|
if (!childExited) {
|
|
1355
1352
|
throw new Error(`runner child ${handle.child.pid ?? handle.key} did not exit after SIGKILL`);
|
|
1356
1353
|
}
|
|
1357
|
-
if (!terminalStopped) {
|
|
1358
|
-
throw new Error(`runner terminal ${handle.key}-main remained live after child exit`);
|
|
1359
|
-
}
|
|
1360
1354
|
this.childHandles.delete(handle);
|
|
1361
1355
|
})();
|
|
1362
1356
|
handle.termination = attempt;
|
|
@@ -1365,12 +1359,9 @@ export class RunnerManager {
|
|
|
1365
1359
|
}, () => {
|
|
1366
1360
|
if (handle.termination === attempt)
|
|
1367
1361
|
delete handle.termination;
|
|
1368
|
-
|
|
1369
|
-
//
|
|
1370
|
-
// submission in the daemon activity snapshot, but release its gate
|
|
1371
|
-
// reservation so maintenance returns a structured busy result.
|
|
1362
|
+
// A child that survives SIGKILL is still real activity. Release the
|
|
1363
|
+
// gate reservation but retain the handoff until a later explicit stop.
|
|
1372
1364
|
this.preserveTerminalInputHandoffsAsActivity(handle);
|
|
1373
|
-
this.scheduleTerminalInputCleanupRetry(handle);
|
|
1374
1365
|
});
|
|
1375
1366
|
return attempt;
|
|
1376
1367
|
}
|
|
@@ -1503,6 +1494,7 @@ export class RunnerManager {
|
|
|
1503
1494
|
return;
|
|
1504
1495
|
case "response.completed":
|
|
1505
1496
|
case "response.failed":
|
|
1497
|
+
case "session.interrupted":
|
|
1506
1498
|
handle.activeResponseIds.delete(event.responseId);
|
|
1507
1499
|
return;
|
|
1508
1500
|
case "session.status":
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* (`live.ensure` / `inject` / `live.interrupt`) plus per-thread capabilities and
|
|
7
7
|
* terminal attachment; the reply channels mirror each request's `reqId`.
|
|
8
8
|
*/
|
|
9
|
-
import { AgentRuntimeError, type
|
|
10
|
-
export type { InjectOutcome } from "@rynx-ai/core";
|
|
9
|
+
import { AgentRuntimeError, type InjectResult, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
|
|
10
|
+
export type { InjectOutcome, InjectResult } from "@rynx-ai/core";
|
|
11
11
|
import type { ResolveInteractionResult } from "../interactions.js";
|
|
12
12
|
/** A runtime error flattened for the wire; rebuilt parent-side as `AgentRuntimeError`. */
|
|
13
13
|
export interface WireError {
|
|
@@ -64,9 +64,9 @@ export type ToChild = {
|
|
|
64
64
|
}
|
|
65
65
|
/** Eagerly bring up a session's live codex TUI + forwarder (codex-native), so
|
|
66
66
|
* its turns mirror to the bus regardless of whether the web has attached the
|
|
67
|
-
* terminal.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
67
|
+
* terminal. Fresh sessions connect the discovery listener before TUI launch;
|
|
68
|
+
* known-thread resumes launch the TUI after backend preload and attach their
|
|
69
|
+
* observer in the background. */
|
|
70
70
|
| {
|
|
71
71
|
t: "live.ensure";
|
|
72
72
|
reqId: string;
|
|
@@ -155,25 +155,26 @@ export type FromChild = {
|
|
|
155
155
|
execution: ResolvedExecutionSnapshot;
|
|
156
156
|
parentSessionId?: string;
|
|
157
157
|
}
|
|
158
|
-
/** Result of a `live.ensure`: for Codex-lineage sessions `ok` means
|
|
159
|
-
* and
|
|
160
|
-
*
|
|
158
|
+
/** Result of a `live.ensure`: for Codex-lineage sessions `ok` means backend
|
|
159
|
+
* preload and pane launch completed. A resume observer and fresh-thread
|
|
160
|
+
* discovery may continue in the background. Other Providers retain their
|
|
161
|
+
* own readiness gate. */
|
|
161
162
|
| {
|
|
162
163
|
t: "live.ready";
|
|
163
164
|
reqId: string;
|
|
164
165
|
localThreadId: string;
|
|
165
166
|
ok: boolean;
|
|
166
|
-
error?:
|
|
167
|
+
error?: WireError;
|
|
167
168
|
}
|
|
168
|
-
/** Result of an `inject`: `outcome` distinguishes a new turn, an
|
|
169
|
-
*
|
|
170
|
-
*
|
|
169
|
+
/** Result of an `inject`: `result.outcome` distinguishes a new turn, an
|
|
170
|
+
* active-turn steer, and failures. Successful results also carry the
|
|
171
|
+
* runtime-owned canonical Response identity. Echoes the request `reqId`. */
|
|
171
172
|
| {
|
|
172
173
|
t: "injected";
|
|
173
174
|
reqId: string;
|
|
174
175
|
localThreadId: string;
|
|
175
|
-
|
|
176
|
-
error?:
|
|
176
|
+
result: InjectResult;
|
|
177
|
+
error?: WireError;
|
|
177
178
|
}
|
|
178
179
|
/** Result of a `live.interrupt`: `ok` when an active turn was interrupted (false
|
|
179
180
|
* when there was none). Echoes the request `reqId`. */
|
|
@@ -182,7 +183,7 @@ export type FromChild = {
|
|
|
182
183
|
reqId: string;
|
|
183
184
|
localThreadId: string;
|
|
184
185
|
ok: boolean;
|
|
185
|
-
error?:
|
|
186
|
+
error?: WireError;
|
|
186
187
|
} | {
|
|
187
188
|
t: "interaction.resolved";
|
|
188
189
|
reqId: string;
|
|
@@ -2,3 +2,6 @@ import type { AgentRuntimeId } from "@rynx-ai/core";
|
|
|
2
2
|
/** Current live providers are native CLI sessions. Keep this explicit so a
|
|
3
3
|
* future non-native provider does not silently inherit their startup budget. */
|
|
4
4
|
export declare function isManagedNativeProvider(provider: AgentRuntimeId | undefined): provider is AgentRuntimeId;
|
|
5
|
+
/** Codex-lineage providers own app-server, thread-discovery, and injection
|
|
6
|
+
* phase errors. Claude instead retains its parent-owned SessionStart gate. */
|
|
7
|
+
export declare function isCodexLineageProvider(provider: AgentRuntimeId | undefined): provider is Exclude<AgentRuntimeId, "claude">;
|
|
@@ -3,3 +3,8 @@
|
|
|
3
3
|
export function isManagedNativeProvider(provider) {
|
|
4
4
|
return provider === "codex" || provider === "traex" || provider === "claude";
|
|
5
5
|
}
|
|
6
|
+
/** Codex-lineage providers own app-server, thread-discovery, and injection
|
|
7
|
+
* phase errors. Claude instead retains its parent-owned SessionStart gate. */
|
|
8
|
+
export function isCodexLineageProvider(provider) {
|
|
9
|
+
return provider === "codex" || provider === "traex";
|
|
10
|
+
}
|
|
@@ -31,13 +31,13 @@ export class TerminalRegistry {
|
|
|
31
31
|
return existing;
|
|
32
32
|
if (existing) {
|
|
33
33
|
// Dead pane (its TUI exited) — kill the husk + drop it before relaunching.
|
|
34
|
+
this.terminals.delete(id);
|
|
34
35
|
try {
|
|
35
36
|
existing.kill();
|
|
36
37
|
}
|
|
37
38
|
catch {
|
|
38
39
|
// already gone — fine
|
|
39
40
|
}
|
|
40
|
-
this.terminals.delete(id);
|
|
41
41
|
}
|
|
42
42
|
const terminal = new TmuxTerminal({ ...opts, name: id });
|
|
43
43
|
terminal.start();
|
|
@@ -59,8 +59,9 @@ export class TerminalRegistry {
|
|
|
59
59
|
const terminal = this.terminals.get(id);
|
|
60
60
|
if (!terminal)
|
|
61
61
|
return;
|
|
62
|
-
|
|
62
|
+
// Omnigent removes the resource from its registry before best-effort close.
|
|
63
63
|
this.terminals.delete(id);
|
|
64
|
+
terminal.kill();
|
|
64
65
|
}
|
|
65
66
|
/** Kill every terminal (runner shutdown). */
|
|
66
67
|
closeAll() {
|
package/dist/terminal/tmux.d.ts
CHANGED
|
@@ -11,6 +11,11 @@ export interface TerminalAttachment {
|
|
|
11
11
|
/** Detach this client. The tmux server + pane keep running. */
|
|
12
12
|
kill(): void;
|
|
13
13
|
}
|
|
14
|
+
/** Result of probing the inner tmux pane. `unknown` is deliberately distinct
|
|
15
|
+
* from `dead`: a timed-out/unexecutable control command is inconclusive;
|
|
16
|
+
* `dead` requires an explicit dead pane. The lifecycle watcher separately
|
|
17
|
+
* mirrors Omnigent's capture failure path for a vanished server/session. */
|
|
18
|
+
export type TerminalLiveness = "alive" | "dead" | "unknown";
|
|
14
19
|
/** Minimal node-pty surface (kept local so this module has no type dep on it). */
|
|
15
20
|
interface PtyProcess {
|
|
16
21
|
onData(cb: (data: string) => void): void;
|
|
@@ -62,9 +67,11 @@ export declare function tmuxWindowActivityAt(name: string, tmuxBin?: string): Pr
|
|
|
62
67
|
* This remains authoritative if a parent-side attachment bookkeeping edge was
|
|
63
68
|
* missed during a transport failure. */
|
|
64
69
|
export declare function tmuxHasAttachedClient(name: string, tmuxBin?: string): Promise<boolean>;
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
* command
|
|
70
|
+
/** Best-effort close of one private tmux server. Mirrors Omnigent's bounded
|
|
71
|
+
* `TerminalInstance.close`: attempt `kill-server`, then retire the private
|
|
72
|
+
* socket regardless of command outcome. The registry has already forgotten
|
|
73
|
+
* the resource, so cleanup failure is diagnostic rather than a second
|
|
74
|
+
* lifecycle state. */
|
|
68
75
|
export declare function terminateTmuxServer(name: string, tmuxBin?: string): boolean;
|
|
69
76
|
/** Is a usable `tmux` on PATH? Cheap probe for the capability gate. */
|
|
70
77
|
export declare function isTmuxAvailable(tmuxBin?: string): boolean;
|
|
@@ -80,6 +87,14 @@ export declare class TmuxTerminal {
|
|
|
80
87
|
private readonly tmuxBin;
|
|
81
88
|
private readonly injectedSpawn?;
|
|
82
89
|
private started;
|
|
90
|
+
private lastPaneSnapshot;
|
|
91
|
+
/** Shared by every attachment watcher and by the lifecycle watcher's
|
|
92
|
+
* pane-dead stage. One Terminal must never fan the same tmux control probe
|
|
93
|
+
* out once per attached client. */
|
|
94
|
+
private paneLivenessFlight?;
|
|
95
|
+
/** Shared by lifecycle callers so capture + pane-dead remains one ordered
|
|
96
|
+
* Omnigent-style observation per Terminal. */
|
|
97
|
+
private lifecycleLivenessFlight?;
|
|
83
98
|
constructor(opts: TmuxTerminalOptions);
|
|
84
99
|
/** tmux argv prefix targeting this terminal's private server. */
|
|
85
100
|
private base;
|
|
@@ -123,8 +138,17 @@ export declare class TmuxTerminal {
|
|
|
123
138
|
/** Async pane-liveness probe — MUST NOT block the event loop. The attach
|
|
124
139
|
* pane-death watcher polls this on an interval; a synchronous `execFileSync`
|
|
125
140
|
* there stalls the runner child's event loop (freezing the PTY stream → the
|
|
126
|
-
* terminal appears "stuck").
|
|
127
|
-
*
|
|
141
|
+
* terminal appears "stuck"). Exactly like Omnigent's definitive pane probe,
|
|
142
|
+
* every command error is `unknown`; only `#{pane_dead}=1` is `dead`. */
|
|
143
|
+
livenessAsync(): Promise<TerminalLiveness>;
|
|
144
|
+
/** Omnigent's always-on terminal lifecycle watcher first captures the pane:
|
|
145
|
+
* a control command that ran and reports the target missing is terminal exit;
|
|
146
|
+
* a probe that cannot spawn is inconclusive. If capture succeeds, the normal
|
|
147
|
+
* definitive `pane_dead` probe distinguishes live from exited. */
|
|
148
|
+
lifecycleLivenessAsync(): Promise<TerminalLiveness>;
|
|
149
|
+
/** Compatibility boolean for callers that cannot represent an inconclusive
|
|
150
|
+
* probe. Unknown must remain live so a transient tmux failure cannot tear down
|
|
151
|
+
* a healthy native Session. */
|
|
128
152
|
isAliveAsync(): Promise<boolean>;
|
|
129
153
|
/** PID of the process currently owning the pane. */
|
|
130
154
|
panePid(): number | undefined;
|