@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.
@@ -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
- /** Setup-pane launch includes the app-server readiness phase but does not wait
38
- * for native thread discovery. Keep enough headroom around the app-server's
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 fresh sessions, resolves after the
513
- * app-server/observer and pane are launched; thread discovery continues in
514
- * parallel with injection. Other providers retain their readiness gate.
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 timeoutMs = !waitForReady
554
- ? this.liveStartTimeoutMs
555
- : opts.execution.provider === "claude"
556
- ? this.nativeLiveStartTimeoutMs
557
- : isManagedNativeProvider(opts.execution.provider)
558
- ? this.liveStartTimeoutMs
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 = setTimeout(() => {
561
- if (!handle.live.delete(reqId))
562
- return;
563
- const finishTimeout = () => {
564
- const provider = opts.execution.provider;
565
- const reason = !waitForReady
566
- ? `runner did not acknowledge terminal start within ${timeoutMs}ms`
567
- : provider === "claude"
568
- ? `Claude SessionStart was not observed within ${Math.round(timeoutMs / 1_000)}s; the runner process was reset before retry`
569
- : provider === "codex" || provider === "traex"
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
- this.liveErrors.set(localThreadId, reason);
573
- this.liveSessionKeys.delete(localThreadId);
574
- // A child that cannot answer a bounded control round-trip is unsafe
575
- // to reuse. Reap it so the next click gets a fresh runner.
576
- void this.terminateHandle(handle, reason).catch((error) => {
577
- logTerminationFailure(handle, error);
578
- });
579
- return false;
580
- };
581
- const reservation = allowReservedForkTarget
582
- ? undefined
583
- : this.forkReservations.get(localThreadId);
584
- if (reservation) {
585
- void reservation
586
- .then(() => finishTimeout())
587
- .then(resolve, () => resolve(false));
588
- return;
589
- }
590
- resolve(finishTimeout());
591
- }, timeoutMs);
592
- timeout.unref?.();
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
- clearTimeout(timeout);
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 ?? "live session did not become ready");
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 outcome = res.outcome ?? "failed";
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 ?? `live injection ${outcome}`);
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 outcome;
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
- ? { outcome: msg.outcome, error: msg.error }
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({ ok: false, error: message });
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
- const terminalStopped = handle.key === CAP_KEY
1352
- ? true
1353
- : await Promise.resolve(this.terminateTerminalServer(`${handle.key}-main`)).catch(() => false);
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
- handle.terminalCleanupRetryFailures += 1;
1369
- // A failed cleanup must not pin closeAndDrain forever. Keep the
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 InjectOutcome, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
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. For Codex-lineage fresh sessions, `live.ready` acknowledges that
68
- * the pane and observer exist; background thread discovery races the
69
- * executor's bridge wait. */
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 the pane
159
- * and observer started; thread discovery continues in parallel with
160
- * injection. Other Providers retain their own readiness gate. */
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?: string;
167
+ error?: WireError;
167
168
  }
168
- /** Result of an `inject`: `outcome` distinguishes a new turn, an active-turn
169
- * steer, and failures so the caller never falls back to a second output path.
170
- * Echoes the request `reqId`. */
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
- outcome: InjectOutcome;
176
- error?: string;
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?: string;
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
- terminal.kill();
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() {
@@ -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
- /** Kill one private tmux server and verify it no longer answers. Missing
66
- * sockets are already stopped; an existing socket with an unusable tmux
67
- * command is unproven and returns false. */
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"). reference implementation's `_tmux_session_alive` uses an async
127
- * subprocess + timeout for exactly this reason. */
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;