@rynx-ai/runtime 0.1.11-beta.22 → 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/forwarder.d.ts +2 -0
- package/dist/codex-app-server/forwarder.js +12 -2
- package/dist/codex-app-server/mapping.d.ts +1 -0
- package/dist/codex-app-server/mapping.js +8 -1
- package/dist/host.d.ts +9 -4
- package/dist/host.js +153 -71
- package/dist/index.d.ts +1 -1
- package/dist/runner/child.d.ts +6 -2
- package/dist/runner/child.js +59 -14
- package/dist/runner/manager.d.ts +5 -9
- package/dist/runner/manager.js +16 -50
- package/dist/runner/protocol.d.ts +6 -6
- 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/child.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { type AgentCapabilities, type LiveSessionFailure, type ResolvedExecution
|
|
|
11
11
|
import type { SessionEvent } from "@rynx-ai/core";
|
|
12
12
|
import type { TerminalInjector } from "../claude/native-integration.js";
|
|
13
13
|
import type { ResolveInteractionResult } from "../interactions.js";
|
|
14
|
-
import { type
|
|
14
|
+
import { type InjectResult } from "./protocol.js";
|
|
15
15
|
import type { ChildTransport } from "./transport.js";
|
|
16
16
|
/** Re-target the mirror to a freshly minted rynx session (claude `/clear`·`/fork`)
|
|
17
17
|
* + record the terminal transfer with the daemon. The child owns the transport,
|
|
@@ -50,7 +50,11 @@ interface LiveCodexProvider {
|
|
|
50
50
|
liveSessionFailure?(localThreadId: string): LiveSessionFailure | undefined;
|
|
51
51
|
failLiveStartup?(localThreadId: string, error: Error): boolean;
|
|
52
52
|
failLiveSession?(localThreadId: string, error: Error): boolean;
|
|
53
|
-
|
|
53
|
+
/** Codex/Traex auxiliary Terminal lifecycle: settle the current Turn, stop
|
|
54
|
+
* observer/forwarder, and evict the owned app-server while preserving the
|
|
55
|
+
* durable native-session binding for a later cold resume. */
|
|
56
|
+
teardownLiveCodexSession?(localThreadId: string, error?: Error): boolean;
|
|
57
|
+
injectMessage?(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectResult>;
|
|
54
58
|
interruptLive?(localThreadId: string): Promise<boolean>;
|
|
55
59
|
stopLiveCodexSession?(localThreadId: string, opts?: {
|
|
56
60
|
deferClaudeInteractionCleanup?: boolean;
|
package/dist/runner/child.js
CHANGED
|
@@ -30,10 +30,15 @@ function nativePhaseError(runtime, code, phase, cause) {
|
|
|
30
30
|
return new AgentRuntimeError(`${providerDisplayName(runtime)} ${phase}${detail}`, 503, code);
|
|
31
31
|
}
|
|
32
32
|
function terminalFailureDetail(pane) {
|
|
33
|
+
// A captured pane is a screen snapshot, not stderr. Do not present ordinary
|
|
34
|
+
// TUI chrome ("Working", shortcuts, model/status bars) as the process's exit
|
|
35
|
+
// cause. Retain only lines that plausibly carry an actual startup/runtime
|
|
36
|
+
// failure; the structured error still reports the proven pane exit itself.
|
|
33
37
|
return pane
|
|
34
38
|
.split("\n")
|
|
35
39
|
.map((line) => line.trim())
|
|
36
40
|
.filter(Boolean)
|
|
41
|
+
.filter((line) => /\b(?:error|fatal|failed|failure|panic|refused|denied|unauthorized|invalid)\b/i.test(line))
|
|
37
42
|
.slice(-6)
|
|
38
43
|
.join(" ")
|
|
39
44
|
.slice(-1_000);
|
|
@@ -347,7 +352,7 @@ export class RunnerSession {
|
|
|
347
352
|
* already-rejected readiness promise. */
|
|
348
353
|
failCodexThreadStartup(localThreadId, provider, error) {
|
|
349
354
|
if (!provider.failLiveStartup?.(localThreadId, error))
|
|
350
|
-
return;
|
|
355
|
+
return false;
|
|
351
356
|
const watcher = this.terminalWatchers.get(localThreadId);
|
|
352
357
|
if (watcher)
|
|
353
358
|
clearInterval(watcher);
|
|
@@ -359,27 +364,34 @@ export class RunnerSession {
|
|
|
359
364
|
catch (closeError) {
|
|
360
365
|
console.warn(`[runner] session=${localThreadId} failed to close native Terminal after startup failure: ${closeError instanceof Error ? closeError.message : String(closeError)}`);
|
|
361
366
|
}
|
|
362
|
-
provider.
|
|
367
|
+
if (provider.teardownLiveCodexSession) {
|
|
368
|
+
provider.teardownLiveCodexSession(localThreadId);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
provider.stopLiveCodexSession?.(localThreadId);
|
|
372
|
+
}
|
|
363
373
|
this.liveIds.delete(localThreadId);
|
|
374
|
+
return true;
|
|
364
375
|
}
|
|
365
376
|
async inject(msg) {
|
|
366
377
|
const provider = this.liveProvider;
|
|
367
378
|
try {
|
|
368
379
|
const input = msg.input ?? msg.text;
|
|
369
|
-
const
|
|
380
|
+
const result = (await provider.injectMessage?.(msg.localThreadId, input))
|
|
381
|
+
?? { outcome: "notLive" };
|
|
370
382
|
// App-server injection is independent of the Terminal TUI startup, so it
|
|
371
383
|
// must not cancel prompt handling for the pane that is still starting.
|
|
372
|
-
const error = outcome === "injected" || outcome === "steered"
|
|
384
|
+
const error = result.outcome === "injected" || result.outcome === "steered"
|
|
373
385
|
? undefined
|
|
374
386
|
: providerFailure(provider, msg.localThreadId, {
|
|
375
|
-
code: outcome === "notReady"
|
|
387
|
+
code: result.outcome === "notReady"
|
|
376
388
|
? "native_thread_not_ready"
|
|
377
|
-
: outcome === "notLive"
|
|
389
|
+
: result.outcome === "notLive"
|
|
378
390
|
? "native_session_not_live"
|
|
379
391
|
: "native_message_injection_failed",
|
|
380
|
-
message: outcome === "notReady"
|
|
392
|
+
message: result.outcome === "notReady"
|
|
381
393
|
? "Native session was not ready for message injection"
|
|
382
|
-
: outcome === "notLive"
|
|
394
|
+
: result.outcome === "notLive"
|
|
383
395
|
? "Native session was not live for message injection"
|
|
384
396
|
: "Native message injection failed",
|
|
385
397
|
});
|
|
@@ -387,7 +399,7 @@ export class RunnerSession {
|
|
|
387
399
|
t: "injected",
|
|
388
400
|
reqId: msg.reqId,
|
|
389
401
|
localThreadId: msg.localThreadId,
|
|
390
|
-
|
|
402
|
+
result,
|
|
391
403
|
...(error ? { error } : {}),
|
|
392
404
|
});
|
|
393
405
|
}
|
|
@@ -396,7 +408,7 @@ export class RunnerSession {
|
|
|
396
408
|
t: "injected",
|
|
397
409
|
reqId: msg.reqId,
|
|
398
410
|
localThreadId: msg.localThreadId,
|
|
399
|
-
outcome: "failed",
|
|
411
|
+
result: { outcome: "failed" },
|
|
400
412
|
error: toWireError(error),
|
|
401
413
|
});
|
|
402
414
|
}
|
|
@@ -452,17 +464,50 @@ export class RunnerSession {
|
|
|
452
464
|
if (checking || this.shuttingDown || this.terminals.get(terminalId) !== terminal)
|
|
453
465
|
return;
|
|
454
466
|
checking = true;
|
|
455
|
-
const probe = terminal.
|
|
456
|
-
|
|
457
|
-
|
|
467
|
+
const probe = terminal.lifecycleLivenessAsync
|
|
468
|
+
? terminal.lifecycleLivenessAsync()
|
|
469
|
+
: terminal.livenessAsync
|
|
470
|
+
? terminal.livenessAsync()
|
|
471
|
+
: terminal.isAliveAsync
|
|
472
|
+
? terminal.isAliveAsync().then((alive) => alive ? "alive" : "dead")
|
|
473
|
+
: Promise.resolve(terminal.isAlive() ? "alive" : "dead");
|
|
474
|
+
void probe.then((liveness) => {
|
|
475
|
+
// A failed pane probe is inconclusive. Only Omnigent's lifecycle
|
|
476
|
+
// evidence — capture target gone or explicit pane_dead — may fail it.
|
|
477
|
+
if (liveness !== "dead" ||
|
|
478
|
+
this.shuttingDown ||
|
|
479
|
+
this.terminals.get(terminalId) !== terminal)
|
|
458
480
|
return;
|
|
459
481
|
clearInterval(timer);
|
|
460
482
|
this.terminalWatchers.delete(localThreadId);
|
|
461
483
|
this.cancelTraexStartupWatcher(localThreadId);
|
|
462
484
|
const runtime = this.liveRuntimes.get(localThreadId);
|
|
463
485
|
const paneFailure = terminalFailureDetail(terminal.capturePane?.() ?? "");
|
|
486
|
+
const startupFailed = this.failCodexThreadStartup(localThreadId, this.liveProvider, nativePhaseError(runtime, "native_terminal_exited_before_session", "Terminal exited before native session discovery completed", paneFailure || undefined));
|
|
487
|
+
if (isCodexLineageProvider(runtime)) {
|
|
488
|
+
if (!startupFailed) {
|
|
489
|
+
try {
|
|
490
|
+
this.terminals.close(terminalId);
|
|
491
|
+
}
|
|
492
|
+
catch (closeError) {
|
|
493
|
+
console.warn(`[runner] session=${localThreadId} failed to close exited native Terminal: ${closeError instanceof Error ? closeError.message : String(closeError)}`);
|
|
494
|
+
}
|
|
495
|
+
const exitError = nativePhaseError(runtime, "native_terminal_exited", "Terminal exited unexpectedly", paneFailure || undefined);
|
|
496
|
+
if (this.liveProvider.teardownLiveCodexSession) {
|
|
497
|
+
this.liveProvider.teardownLiveCodexSession(localThreadId, exitError);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
this.liveProvider.failLiveSession?.(localThreadId, exitError);
|
|
501
|
+
this.liveProvider.stopLiveCodexSession?.(localThreadId);
|
|
502
|
+
}
|
|
503
|
+
this.liveIds.delete(localThreadId);
|
|
504
|
+
console.warn(`[runner] session=${localThreadId} ${providerDisplayName(runtime)} auxiliary Terminal exited; native runtime torn down for cold resume`);
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
464
508
|
this.liveProvider.failLiveSession?.(localThreadId, nativePhaseError(runtime, "native_terminal_exited", "Terminal exited unexpectedly", paneFailure || undefined));
|
|
465
|
-
|
|
509
|
+
}).catch((error) => {
|
|
510
|
+
console.warn(`[runner] session=${localThreadId} native Terminal liveness probe failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
466
511
|
}).finally(() => {
|
|
467
512
|
checking = false;
|
|
468
513
|
});
|
package/dist/runner/manager.d.ts
CHANGED
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
* channel.
|
|
17
17
|
*/
|
|
18
18
|
import { spawn as nodeSpawn, type ChildProcess } from "node:child_process";
|
|
19
|
-
import { type AdmissionReservation, type AgentCapabilities, type AgentRuntimeId, type AppConfig, type CapabilityResult, type ModelListResponse, type LiveSessionFailure, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type SessionWorkspaceSnapshot, type ThreadGoal } from "@rynx-ai/core";
|
|
19
|
+
import { type AdmissionReservation, type AgentCapabilities, type AgentRuntimeId, type AppConfig, type CapabilityResult, type ModelListResponse, type LiveSessionFailure, type InjectResult, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type SessionWorkspaceSnapshot, type ThreadGoal } from "@rynx-ai/core";
|
|
20
20
|
import { type CodexSessionStore } from "../host.js";
|
|
21
21
|
import type { ResolveInteractionResult } from "../interactions.js";
|
|
22
|
-
import { type
|
|
22
|
+
import { type TerminalOpenErrorCode, type TerminalRole } from "./protocol.js";
|
|
23
23
|
/** Runtime-local context attached to one Session runner process. The provider is
|
|
24
24
|
* intentionally generic: package-specific resources stay in the composition
|
|
25
25
|
* root that implements this port. */
|
|
@@ -67,15 +67,13 @@ export interface RunnerManagerOptions {
|
|
|
67
67
|
/** Max wait for an accepted owner TUI submission to become observable as a
|
|
68
68
|
* mirrored response or a published native rotation. */
|
|
69
69
|
terminalInputHandoffTimeoutMs?: number;
|
|
70
|
-
/** Initial exponential backoff for retrying unproven terminal cleanup. */
|
|
71
|
-
terminalInputCleanupRetryMs?: number;
|
|
72
70
|
/** Injected for tests. */
|
|
73
71
|
spawn?: typeof nodeSpawn;
|
|
74
72
|
/** Injected for tests. Defaults to signaling the whole POSIX process group
|
|
75
73
|
* created for the runner child, with direct-child fallback. */
|
|
76
74
|
signalChild?: (child: ChildProcess, signal: NodeJS.Signals, processGroup: boolean) => void;
|
|
77
|
-
/** Injected for tests.
|
|
78
|
-
* server owned by a Session runner. */
|
|
75
|
+
/** Injected for tests. Best-effort close of the deterministic private tmux
|
|
76
|
+
* server owned by a Session runner; Omnigent does not verify/retry close. */
|
|
79
77
|
terminateTerminalServer?: (terminalName: string) => boolean | Promise<boolean>;
|
|
80
78
|
/** Injected tmux `#{window_activity}` reader. Returns epoch seconds. */
|
|
81
79
|
terminalWindowActivityAt?: (terminalName: string) => number | null | Promise<number | null>;
|
|
@@ -176,7 +174,6 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
176
174
|
private readonly nativeLiveStartTimeoutMs;
|
|
177
175
|
private readonly liveInterruptTimeoutMs;
|
|
178
176
|
private readonly terminalInputHandoffTimeoutMs;
|
|
179
|
-
private readonly terminalInputCleanupRetryMs;
|
|
180
177
|
private reapPromise;
|
|
181
178
|
private stopping;
|
|
182
179
|
private stopPromise;
|
|
@@ -243,7 +240,6 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
243
240
|
private finishTerminalInputHandoff;
|
|
244
241
|
private finishAllTerminalInputHandoffs;
|
|
245
242
|
private preserveTerminalInputHandoffsAsActivity;
|
|
246
|
-
private scheduleTerminalInputCleanupRetry;
|
|
247
243
|
private expireTerminalInputHandoffs;
|
|
248
244
|
/**
|
|
249
245
|
* Eagerly bring up a session's codex-native live view (persistent forwarder +
|
|
@@ -277,7 +273,7 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
277
273
|
* all output. Resolves true when the app-server accepted the turn, false when
|
|
278
274
|
* the session has no live forwarder (caller falls back to the run path).
|
|
279
275
|
*/
|
|
280
|
-
injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<
|
|
276
|
+
injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectResult>;
|
|
281
277
|
private injectMessageAdmitted;
|
|
282
278
|
/**
|
|
283
279
|
* Interrupt a session's active live turn — the web Stop button (codex
|
package/dist/runner/manager.js
CHANGED
|
@@ -47,9 +47,6 @@ const DEFAULT_NATIVE_LIVE_START_TIMEOUT_MS = 60_000;
|
|
|
47
47
|
* quickly. If it does not, the runner is fenced by a verified process-tree
|
|
48
48
|
* shutdown before maintenance may treat the submission as settled. */
|
|
49
49
|
const DEFAULT_TERMINAL_INPUT_HANDOFF_TIMEOUT_MS = 30_000;
|
|
50
|
-
/** Retry transient terminal-server cleanup without spinning forever. */
|
|
51
|
-
const DEFAULT_TERMINAL_INPUT_CLEANUP_RETRY_MS = 1_000;
|
|
52
|
-
const MAX_TERMINAL_INPUT_CLEANUP_RETRY_MS = 30_000;
|
|
53
50
|
/** Full idle window before an inactive native pane becomes reapable. */
|
|
54
51
|
const DEFAULT_NATIVE_PANE_IDLE_TTL_MS = 60 * 60_000;
|
|
55
52
|
/** tmux output this recent independently proves that a native pane is busy. */
|
|
@@ -180,7 +177,6 @@ export class RunnerManager {
|
|
|
180
177
|
nativeLiveStartTimeoutMs;
|
|
181
178
|
liveInterruptTimeoutMs;
|
|
182
179
|
terminalInputHandoffTimeoutMs;
|
|
183
|
-
terminalInputCleanupRetryMs;
|
|
184
180
|
reapPromise = null;
|
|
185
181
|
stopping = false;
|
|
186
182
|
stopPromise;
|
|
@@ -236,7 +232,6 @@ export class RunnerManager {
|
|
|
236
232
|
this.nativeLiveStartTimeoutMs = Math.max(1, opts.nativeLiveStartTimeoutMs ?? DEFAULT_NATIVE_LIVE_START_TIMEOUT_MS);
|
|
237
233
|
this.liveInterruptTimeoutMs = Math.max(1, opts.liveInterruptTimeoutMs ?? 10_000);
|
|
238
234
|
this.terminalInputHandoffTimeoutMs = Math.max(1, opts.terminalInputHandoffTimeoutMs ?? DEFAULT_TERMINAL_INPUT_HANDOFF_TIMEOUT_MS);
|
|
239
|
-
this.terminalInputCleanupRetryMs = Math.max(1, opts.terminalInputCleanupRetryMs ?? DEFAULT_TERMINAL_INPUT_CLEANUP_RETRY_MS);
|
|
240
235
|
const reapIntervalMs = opts.reapIntervalMs ?? 60_000;
|
|
241
236
|
if (reapIntervalMs > 0) {
|
|
242
237
|
this.reapTimer = setInterval(() => void this.reapIdle(), reapIntervalMs);
|
|
@@ -456,11 +451,6 @@ export class RunnerManager {
|
|
|
456
451
|
this.terminalInputHandoffs.delete(handle);
|
|
457
452
|
}
|
|
458
453
|
finishAllTerminalInputHandoffs(handle) {
|
|
459
|
-
if (handle.terminalCleanupRetryTimer) {
|
|
460
|
-
clearTimeout(handle.terminalCleanupRetryTimer);
|
|
461
|
-
delete handle.terminalCleanupRetryTimer;
|
|
462
|
-
}
|
|
463
|
-
handle.terminalCleanupRetryFailures = 0;
|
|
464
454
|
for (const handoff of [...(this.terminalInputHandoffs.get(handle) ?? [])]) {
|
|
465
455
|
this.finishTerminalInputHandoff(handle, handoff);
|
|
466
456
|
}
|
|
@@ -475,22 +465,6 @@ export class RunnerManager {
|
|
|
475
465
|
handoff.reservation = undefined;
|
|
476
466
|
}
|
|
477
467
|
}
|
|
478
|
-
scheduleTerminalInputCleanupRetry(handle) {
|
|
479
|
-
if (handle.terminalCleanupRetryTimer ||
|
|
480
|
-
!this.terminalInputHandoffs.has(handle))
|
|
481
|
-
return;
|
|
482
|
-
const exponent = Math.min(handle.terminalCleanupRetryFailures - 1, 10);
|
|
483
|
-
const delay = Math.min(this.terminalInputCleanupRetryMs * (2 ** Math.max(0, exponent)), MAX_TERMINAL_INPUT_CLEANUP_RETRY_MS);
|
|
484
|
-
handle.terminalCleanupRetryTimer = setTimeout(() => {
|
|
485
|
-
delete handle.terminalCleanupRetryTimer;
|
|
486
|
-
if (!this.terminalInputHandoffs.has(handle))
|
|
487
|
-
return;
|
|
488
|
-
void this.terminateHandle(handle, "retrying unproven terminal input cleanup").catch((error) => {
|
|
489
|
-
logTerminationFailure(handle, error);
|
|
490
|
-
});
|
|
491
|
-
}, delay);
|
|
492
|
-
handle.terminalCleanupRetryTimer.unref?.();
|
|
493
|
-
}
|
|
494
468
|
expireTerminalInputHandoffs(handle) {
|
|
495
469
|
const handoffs = this.terminalInputHandoffs.get(handle);
|
|
496
470
|
if (!handoffs?.length)
|
|
@@ -668,29 +642,29 @@ export class RunnerManager {
|
|
|
668
642
|
const reqId = randomUUID();
|
|
669
643
|
return new Promise((resolve) => {
|
|
670
644
|
handle.live.set(reqId, (res) => {
|
|
671
|
-
const
|
|
645
|
+
const result = res.result ?? { outcome: "failed" };
|
|
672
646
|
const finish = () => {
|
|
673
|
-
if (outcome === "injected" || outcome === "steered") {
|
|
647
|
+
if (result.outcome === "injected" || result.outcome === "steered") {
|
|
674
648
|
this.liveErrors.delete(localThreadId);
|
|
675
649
|
}
|
|
676
650
|
else {
|
|
677
651
|
this.liveErrors.set(localThreadId, res.error ?? {
|
|
678
|
-
message: `live injection ${outcome}`,
|
|
679
|
-
code: outcome === "notReady"
|
|
652
|
+
message: `live injection ${result.outcome}`,
|
|
653
|
+
code: result.outcome === "notReady"
|
|
680
654
|
? "native_thread_not_ready"
|
|
681
|
-
: outcome === "notLive"
|
|
655
|
+
: result.outcome === "notLive"
|
|
682
656
|
? "native_session_not_live"
|
|
683
657
|
: "native_message_injection_failed",
|
|
684
658
|
statusCode: 503,
|
|
685
659
|
});
|
|
686
660
|
}
|
|
687
|
-
return
|
|
661
|
+
return result;
|
|
688
662
|
};
|
|
689
663
|
const reservation = this.forkReservations.get(localThreadId);
|
|
690
664
|
if (reservation) {
|
|
691
665
|
void reservation
|
|
692
666
|
.then(() => finish())
|
|
693
|
-
.then(resolve, () => resolve("failed"));
|
|
667
|
+
.then(resolve, () => resolve({ outcome: "failed" }));
|
|
694
668
|
return;
|
|
695
669
|
}
|
|
696
670
|
resolve(finish());
|
|
@@ -1211,7 +1185,6 @@ export class RunnerManager {
|
|
|
1211
1185
|
activeResponseIds: new Set(),
|
|
1212
1186
|
dead: false,
|
|
1213
1187
|
completion,
|
|
1214
|
-
terminalCleanupRetryFailures: 0,
|
|
1215
1188
|
processGroup,
|
|
1216
1189
|
...(sessionContext ? { sessionContext } : {}),
|
|
1217
1190
|
caps: new Map(),
|
|
@@ -1298,7 +1271,7 @@ export class RunnerManager {
|
|
|
1298
1271
|
const resolve = handle.live.get(msg.reqId);
|
|
1299
1272
|
handle.live.delete(msg.reqId);
|
|
1300
1273
|
resolve?.(msg.t === "injected"
|
|
1301
|
-
? {
|
|
1274
|
+
? { result: msg.result, error: msg.error }
|
|
1302
1275
|
: { ok: msg.ok, error: msg.error });
|
|
1303
1276
|
return;
|
|
1304
1277
|
}
|
|
@@ -1354,10 +1327,6 @@ export class RunnerManager {
|
|
|
1354
1327
|
terminateHandle(handle, reason) {
|
|
1355
1328
|
if (handle.termination)
|
|
1356
1329
|
return handle.termination;
|
|
1357
|
-
if (handle.terminalCleanupRetryTimer) {
|
|
1358
|
-
clearTimeout(handle.terminalCleanupRetryTimer);
|
|
1359
|
-
delete handle.terminalCleanupRetryTimer;
|
|
1360
|
-
}
|
|
1361
1330
|
const attempt = (async () => {
|
|
1362
1331
|
if (!handle.dead) {
|
|
1363
1332
|
try {
|
|
@@ -1374,15 +1343,14 @@ export class RunnerManager {
|
|
|
1374
1343
|
this.signalChild(handle.child, "SIGKILL", handle.processGroup);
|
|
1375
1344
|
childExited = await waitForChildExit(handle.completion, this.shutdownKillGraceMs);
|
|
1376
1345
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
+
}
|
|
1380
1351
|
if (!childExited) {
|
|
1381
1352
|
throw new Error(`runner child ${handle.child.pid ?? handle.key} did not exit after SIGKILL`);
|
|
1382
1353
|
}
|
|
1383
|
-
if (!terminalStopped) {
|
|
1384
|
-
throw new Error(`runner terminal ${handle.key}-main remained live after child exit`);
|
|
1385
|
-
}
|
|
1386
1354
|
this.childHandles.delete(handle);
|
|
1387
1355
|
})();
|
|
1388
1356
|
handle.termination = attempt;
|
|
@@ -1391,12 +1359,9 @@ export class RunnerManager {
|
|
|
1391
1359
|
}, () => {
|
|
1392
1360
|
if (handle.termination === attempt)
|
|
1393
1361
|
delete handle.termination;
|
|
1394
|
-
|
|
1395
|
-
//
|
|
1396
|
-
// submission in the daemon activity snapshot, but release its gate
|
|
1397
|
-
// 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.
|
|
1398
1364
|
this.preserveTerminalInputHandoffsAsActivity(handle);
|
|
1399
|
-
this.scheduleTerminalInputCleanupRetry(handle);
|
|
1400
1365
|
});
|
|
1401
1366
|
return attempt;
|
|
1402
1367
|
}
|
|
@@ -1529,6 +1494,7 @@ export class RunnerManager {
|
|
|
1529
1494
|
return;
|
|
1530
1495
|
case "response.completed":
|
|
1531
1496
|
case "response.failed":
|
|
1497
|
+
case "session.interrupted":
|
|
1532
1498
|
handle.activeResponseIds.delete(event.responseId);
|
|
1533
1499
|
return;
|
|
1534
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 {
|
|
@@ -166,14 +166,14 @@ export type FromChild = {
|
|
|
166
166
|
ok: boolean;
|
|
167
167
|
error?: WireError;
|
|
168
168
|
}
|
|
169
|
-
/** Result of an `inject`: `outcome` distinguishes a new turn, an
|
|
170
|
-
*
|
|
171
|
-
*
|
|
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`. */
|
|
172
172
|
| {
|
|
173
173
|
t: "injected";
|
|
174
174
|
reqId: string;
|
|
175
175
|
localThreadId: string;
|
|
176
|
-
|
|
176
|
+
result: InjectResult;
|
|
177
177
|
error?: WireError;
|
|
178
178
|
}
|
|
179
179
|
/** Result of a `live.interrupt`: `ok` when an active turn was interrupted (false
|
|
@@ -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;
|