@rynx-ai/runtime 0.1.11-beta.20 → 0.1.11-beta.22

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.
@@ -7,7 +7,7 @@
7
7
  * per-thread capabilities, answered against the same backend so the parent never
8
8
  * needs an app-server of its own.
9
9
  */
10
- import { type AgentCapabilities, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
10
+ import { type AgentCapabilities, type LiveSessionFailure, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
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";
@@ -38,11 +38,16 @@ interface LiveCodexProvider {
38
38
  execution: ResolvedExecutionSnapshot;
39
39
  retargetMirror?: RetargetMirror;
40
40
  }): Promise<boolean>;
41
+ /** Known-thread Codex/Traex resume only: start the independent transcript
42
+ * observer after the replacement TUI has launched. Fresh discovery connects
43
+ * its listener before launch and this method is an idempotent no-op. */
44
+ startLiveCodexObserver?(localThreadId: string): void;
41
45
  /** `null` delegates the deadline to RunnerManager. */
42
46
  waitLiveReady?(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
43
47
  /** `null` delegates the deadline to RunnerManager. */
44
48
  waitTerminalReady?(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
45
49
  liveSessionError?(localThreadId: string): string | undefined;
50
+ liveSessionFailure?(localThreadId: string): LiveSessionFailure | undefined;
46
51
  failLiveStartup?(localThreadId: string, error: Error): boolean;
47
52
  failLiveSession?(localThreadId: string, error: Error): boolean;
48
53
  injectMessage?(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectOutcome>;
@@ -84,6 +89,8 @@ export declare class RunnerSession {
84
89
  private readonly cancelledTerminalOpens;
85
90
  /** Session ids with a live codex forwarder started here (stopped on shutdown). */
86
91
  private readonly liveIds;
92
+ /** Provider name retained for asynchronous Terminal-exit diagnostics. */
93
+ private readonly liveRuntimes;
87
94
  private shuttingDown;
88
95
  constructor({ transport, executor, onShutdown }: RunnerSessionDeps);
89
96
  private handle;
@@ -96,10 +103,10 @@ export declare class RunnerSession {
96
103
  * the new session stays injectable. */
97
104
  private mirrorChannel;
98
105
  /**
99
- * Eagerly bring up a session's codex-native live view: start the persistent
100
- * observer connection before launching the detached Codex TUI. Fresh-session
101
- * thread discovery then continues in the background while the first
102
- * injection waits for the same bridge state.
106
+ * Eagerly bring up a session's codex-native live view. Fresh sessions connect
107
+ * the discovery listener before launching the detached TUI; known-thread
108
+ * resumes preload first, launch the replacement TUI, then start their
109
+ * persistent observer in the background.
103
110
  */
104
111
  private ensureLive;
105
112
  private monitorCodexThreadStartup;
@@ -1,8 +1,43 @@
1
+ /**
2
+ * Child-side runner session. One per runner process (i.e. per session); owns the
3
+ * single execution backend via the injected executor (a {@link LocalAgentHost}
4
+ * in production, which in a per-session process holds exactly one backend). It
5
+ * translates inbound {@link ToChild} control messages into the live co-drive
6
+ * surface (bring up the session's forwarder + TUI, inject / interrupt turns) and
7
+ * per-thread capabilities, answered against the same backend so the parent never
8
+ * needs an app-server of its own.
9
+ */
10
+ import { AgentRuntimeError, } from "@rynx-ai/core";
1
11
  import { TerminalRegistry } from "../terminal/registry.js";
2
- import { toWireError } from "./protocol.js";
3
- import { isManagedNativeProvider } from "./startup-policy.js";
12
+ import { toWireError, } from "./protocol.js";
13
+ import { isCodexLineageProvider } from "./startup-policy.js";
4
14
  /** Maximum time for a fresh Codex-lineage TUI to publish its native thread. */
5
15
  const CODEX_THREAD_START_TIMEOUT_MS = 30_000;
16
+ function providerDisplayName(runtime) {
17
+ return runtime === "traex" ? "Traex" : runtime === "claude" ? "Claude" : "Codex";
18
+ }
19
+ function providerFailure(provider, localThreadId, fallback) {
20
+ return provider.liveSessionFailure?.(localThreadId) ?? {
21
+ message: provider.liveSessionError?.(localThreadId) ?? fallback.message,
22
+ code: fallback.code,
23
+ statusCode: 503,
24
+ };
25
+ }
26
+ function nativePhaseError(runtime, code, phase, cause) {
27
+ const detail = cause === undefined
28
+ ? ""
29
+ : `: ${cause instanceof Error ? cause.message : String(cause)}`;
30
+ return new AgentRuntimeError(`${providerDisplayName(runtime)} ${phase}${detail}`, 503, code);
31
+ }
32
+ function terminalFailureDetail(pane) {
33
+ return pane
34
+ .split("\n")
35
+ .map((line) => line.trim())
36
+ .filter(Boolean)
37
+ .slice(-6)
38
+ .join(" ")
39
+ .slice(-1_000);
40
+ }
6
41
  const TRAEX_STARTUP_WATCH_MS = 20_000;
7
42
  const TRAEX_STARTUP_POLL_MS = 100;
8
43
  const TRAEX_AUTHORIZATION_POLL_MS = 500;
@@ -38,6 +73,8 @@ export class RunnerSession {
38
73
  cancelledTerminalOpens = new Set();
39
74
  /** Session ids with a live codex forwarder started here (stopped on shutdown). */
40
75
  liveIds = new Set();
76
+ /** Provider name retained for asynchronous Terminal-exit diagnostics. */
77
+ liveRuntimes = new Map();
41
78
  shuttingDown = false;
42
79
  constructor({ transport, executor, onShutdown }) {
43
80
  this.transport = transport;
@@ -159,10 +196,10 @@ export class RunnerSession {
159
196
  return { emit, retarget };
160
197
  }
161
198
  /**
162
- * Eagerly bring up a session's codex-native live view: start the persistent
163
- * observer connection before launching the detached Codex TUI. Fresh-session
164
- * thread discovery then continues in the background while the first
165
- * injection waits for the same bridge state.
199
+ * Eagerly bring up a session's codex-native live view. Fresh sessions connect
200
+ * the discovery listener before launching the detached TUI; known-thread
201
+ * resumes preload first, launch the replacement TUI, then start their
202
+ * persistent observer in the background.
166
203
  */
167
204
  async ensureLive(msg) {
168
205
  const provider = this.liveProvider;
@@ -174,34 +211,38 @@ export class RunnerSession {
174
211
  retargetMirror: retarget,
175
212
  });
176
213
  if (!started) {
214
+ const runtime = msg.execution?.provider;
177
215
  this.transport.send({
178
216
  t: "live.ready",
179
217
  reqId: msg.reqId,
180
218
  localThreadId: msg.localThreadId,
181
219
  ok: false,
182
- error: provider.liveSessionError?.(msg.localThreadId)
183
- ?? "live provider did not start",
220
+ error: providerFailure(provider, msg.localThreadId, {
221
+ runtime,
222
+ code: "native_start_failed",
223
+ message: `${providerDisplayName(runtime)} native session did not start`,
224
+ }),
184
225
  });
185
226
  return;
186
227
  }
187
228
  this.liveIds.add(msg.localThreadId);
188
- // Codex-lineage startup is phase-bounded: app-server readiness is
189
- // handled by its channel, then the already-connected
190
- // observer gets a full window for the TUI's thread/started event.
191
- // Claude retains the parent-owned SessionStart deadline.
192
229
  const runtime = msg.execution?.provider;
193
- // Do not add another provider-local deadline around resume/preload. The
194
- // parent control request remains bounded for process safety; fresh-thread
195
- // discovery owns the separate 30s phase below.
196
- const terminalReadyTimeoutMs = isManagedNativeProvider(runtime) ? null : undefined;
230
+ if (runtime)
231
+ this.liveRuntimes.set(msg.localThreadId, runtime);
197
232
  // Launch the TUI attached to the already-bound thread. Re-launch when the
198
233
  // pane is absent OR its process has died (`isAlive` probes `#{pane_dead}`) — so a
199
234
  // reconnect after the TUI exited restarts it instead of skipping (a
200
235
  // launched-once guard would leave a dead "Pane is dead" husk forever).
201
236
  if (!this.terminals.get(`${msg.localThreadId}-main`)?.isAlive()) {
202
- const terminalReady = provider.waitTerminalReady
203
- ? await provider.waitTerminalReady(msg.localThreadId, terminalReadyTimeoutMs)
204
- : true;
237
+ // Codex/Traex `ensureLiveCodexSession` already performs the backend
238
+ // preload for an existing thread. Match the native lifecycle: launch
239
+ // the TUI immediately after that preload and let the observer subscribe
240
+ // in the background. Fresh sessions likewise launch before discovery.
241
+ const terminalReady = isCodexLineageProvider(runtime)
242
+ ? true
243
+ : provider.waitTerminalReady
244
+ ? await provider.waitTerminalReady(msg.localThreadId)
245
+ : true;
205
246
  if (!terminalReady) {
206
247
  const providerName = runtime === "traex" ? "Traex" : runtime === "codex" ? "Codex" : "Provider";
207
248
  this.transport.send({
@@ -209,14 +250,31 @@ export class RunnerSession {
209
250
  reqId: msg.reqId,
210
251
  localThreadId: msg.localThreadId,
211
252
  ok: false,
212
- error: runtime === "codex" || runtime === "traex"
213
- ? provider.liveSessionError?.(msg.localThreadId)
214
- ?? `${providerName} app-server was ready, but its existing thread could not be resumed for the Terminal`
215
- : "Provider thread was not ready for Terminal resume",
253
+ error: providerFailure(provider, msg.localThreadId, {
254
+ runtime,
255
+ code: "native_resume_failed",
256
+ message: runtime === "codex" || runtime === "traex"
257
+ ? `${providerName} app-server was ready, but its existing session could not be resumed for the Terminal`
258
+ : "Provider thread was not ready for Terminal resume",
259
+ }),
216
260
  });
217
261
  return;
218
262
  }
219
- await this.launchCodexPane(msg.localThreadId, msg.cols, msg.rows);
263
+ try {
264
+ await this.launchCodexPane(msg.localThreadId, msg.cols, msg.rows);
265
+ }
266
+ catch (error) {
267
+ throw error instanceof AgentRuntimeError
268
+ ? error
269
+ : nativePhaseError(runtime, "native_terminal_launch_failed", "Terminal failed to launch", error);
270
+ }
271
+ }
272
+ // Omnigent's known-thread path launches `codex --remote resume` first,
273
+ // then creates `_codex_forward_known_thread` as background work. Keep the
274
+ // ordering explicit here: observer connection failure cannot delay or
275
+ // reject cold-resume admission.
276
+ if (isCodexLineageProvider(runtime)) {
277
+ provider.startLiveCodexObserver?.(msg.localThreadId);
220
278
  }
221
279
  let ready = true;
222
280
  if (runtime === "codex" || runtime === "traex") {
@@ -226,12 +284,15 @@ export class RunnerSession {
226
284
  this.monitorCodexThreadStartup(msg.localThreadId, runtime, provider);
227
285
  }
228
286
  else if (msg.waitForReady !== false && provider.waitLiveReady) {
229
- ready = await provider.waitLiveReady(msg.localThreadId, terminalReadyTimeoutMs);
287
+ ready = await provider.waitLiveReady(msg.localThreadId);
230
288
  }
231
289
  const readinessError = ready
232
290
  ? undefined
233
- : provider.liveSessionError?.(msg.localThreadId)
234
- ?? "live session was not ready before timeout";
291
+ : providerFailure(provider, msg.localThreadId, {
292
+ runtime,
293
+ code: "native_readiness_failed",
294
+ message: `${providerDisplayName(runtime)} native session was not ready`,
295
+ });
235
296
  this.transport.send({
236
297
  t: "live.ready",
237
298
  reqId: msg.reqId,
@@ -246,7 +307,7 @@ export class RunnerSession {
246
307
  reqId: msg.reqId,
247
308
  localThreadId: msg.localThreadId,
248
309
  ok: false,
249
- error: error instanceof Error ? error.message : String(error),
310
+ error: toWireError(error),
250
311
  });
251
312
  }
252
313
  }
@@ -269,11 +330,15 @@ export class RunnerSession {
269
330
  : "";
270
331
  const detail = provider.liveSessionError?.(localThreadId)
271
332
  ?? (paneFailure
272
- ? `Provider terminal exited before session discovery: ${paneFailure}`
333
+ ? `${providerDisplayName(runtime)} Terminal exited before native session discovery: ${paneFailure}`
273
334
  : `${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`);
274
- this.failCodexThreadStartup(localThreadId, provider, new Error(detail));
335
+ this.failCodexThreadStartup(localThreadId, provider, new AgentRuntimeError(detail, 503, paneFailure
336
+ ? "native_terminal_exited_before_session"
337
+ : "native_thread_discovery_timeout"));
275
338
  }).catch((error) => {
276
- this.failCodexThreadStartup(localThreadId, provider, error instanceof Error ? error : new Error(String(error)));
339
+ this.failCodexThreadStartup(localThreadId, provider, error instanceof AgentRuntimeError
340
+ ? error
341
+ : nativePhaseError(runtime, "native_thread_discovery_failed", "native session discovery failed", error));
277
342
  });
278
343
  }
279
344
  /** Discovery failure is terminal for this partial native launch. Publish the
@@ -304,9 +369,20 @@ export class RunnerSession {
304
369
  const outcome = (await provider.injectMessage?.(msg.localThreadId, input)) ?? "notLive";
305
370
  // App-server injection is independent of the Terminal TUI startup, so it
306
371
  // must not cancel prompt handling for the pane that is still starting.
307
- const error = outcome === "injected"
372
+ const error = outcome === "injected" || outcome === "steered"
308
373
  ? undefined
309
- : provider.liveSessionError?.(msg.localThreadId);
374
+ : providerFailure(provider, msg.localThreadId, {
375
+ code: outcome === "notReady"
376
+ ? "native_thread_not_ready"
377
+ : outcome === "notLive"
378
+ ? "native_session_not_live"
379
+ : "native_message_injection_failed",
380
+ message: outcome === "notReady"
381
+ ? "Native session was not ready for message injection"
382
+ : outcome === "notLive"
383
+ ? "Native session was not live for message injection"
384
+ : "Native message injection failed",
385
+ });
310
386
  this.transport.send({
311
387
  t: "injected",
312
388
  reqId: msg.reqId,
@@ -321,7 +397,7 @@ export class RunnerSession {
321
397
  reqId: msg.reqId,
322
398
  localThreadId: msg.localThreadId,
323
399
  outcome: "failed",
324
- error: error instanceof Error ? error.message : String(error),
400
+ error: toWireError(error),
325
401
  });
326
402
  }
327
403
  }
@@ -333,7 +409,7 @@ export class RunnerSession {
333
409
  ok = (await provider.interruptLive?.(msg.localThreadId)) ?? false;
334
410
  }
335
411
  catch (e) {
336
- error = e instanceof Error ? e.message : String(e);
412
+ error = toWireError(e);
337
413
  }
338
414
  this.transport.send({
339
415
  t: "interrupted",
@@ -383,8 +459,10 @@ export class RunnerSession {
383
459
  clearInterval(timer);
384
460
  this.terminalWatchers.delete(localThreadId);
385
461
  this.cancelTraexStartupWatcher(localThreadId);
386
- this.liveProvider.failLiveSession?.(localThreadId, new Error("Provider terminal exited unexpectedly"));
387
- this.failCodexThreadStartup(localThreadId, this.liveProvider, new Error("Provider terminal exited before native thread discovery completed"));
462
+ const runtime = this.liveRuntimes.get(localThreadId);
463
+ const paneFailure = terminalFailureDetail(terminal.capturePane?.() ?? "");
464
+ this.liveProvider.failLiveSession?.(localThreadId, nativePhaseError(runtime, "native_terminal_exited", "Terminal exited unexpectedly", paneFailure || undefined));
465
+ this.failCodexThreadStartup(localThreadId, this.liveProvider, nativePhaseError(runtime, "native_terminal_exited_before_session", "Terminal exited before native session discovery completed", paneFailure || undefined));
388
466
  }).finally(() => {
389
467
  checking = false;
390
468
  });
@@ -469,6 +547,7 @@ export class RunnerSession {
469
547
  });
470
548
  }
471
549
  this.liveIds.clear();
550
+ this.liveRuntimes.clear();
472
551
  }
473
552
  /** Stop event forwarding, kill native terminals/hooks, then synchronously
474
553
  * scrub provider handoff files before the child process is allowed to exit. */
@@ -16,7 +16,7 @@
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 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 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
22
  import { type InjectOutcome, type TerminalOpenErrorCode, type TerminalRole } from "./protocol.js";
@@ -43,21 +43,23 @@ export interface RunnerManagerOptions {
43
43
  runnerEntry?: string;
44
44
  /** Idle TTL (ms) after which an unused runner is reaped. */
45
45
  idleTtlMs?: number;
46
- /** Maximum time without user/control activity that an active response may
47
- * protect a detached runner from idle reaping. */
48
- staleActiveTtlMs?: number;
46
+ /** Native pane idle window. `<= 0` disables native-pane idle reaping. */
47
+ nativePaneIdleTtlMs?: number;
48
+ /** Recent tmux-output window used as independent native-pane busy evidence. */
49
+ nativePaneOutputBusyWindowMs?: number;
49
50
  /** Background reap sweep interval (ms); `0` disables the timer (tests). */
50
51
  reapIntervalMs?: number;
51
52
  /** Grace period between SIGTERM and SIGKILL during runner shutdown. */
52
53
  shutdownGraceMs?: number;
53
54
  /** Final bounded wait for child exit after SIGKILL. */
54
55
  shutdownKillGraceMs?: number;
55
- /** Max wait for a setup-pane launch acknowledgement. */
56
+ /** Max wait for a legacy setup-pane launch acknowledgement. Codex/Traex use
57
+ * their internal phase deadlines and are not wrapped by this timer. */
56
58
  liveStartTimeoutMs?: number;
57
59
  /** Max wait for a Provider-native thread to become ready. */
58
60
  liveReadyTimeoutMs?: number;
59
- /** Claude SessionStart deadline. Codex/Traex use `liveStartTimeoutMs` for the
60
- * pane/observer acknowledgement; their thread discovery is asynchronous. */
61
+ /** Claude SessionStart deadline. Codex/Traex have no parent-owned startup
62
+ * deadline; their app-server/discovery/injection phases report directly. */
61
63
  nativeLiveStartTimeoutMs?: number;
62
64
  /** Max wait for a native interrupt acknowledgement before reporting it
63
65
  * unproven. Callers may then use the explicit force-stop path. */
@@ -75,7 +77,13 @@ export interface RunnerManagerOptions {
75
77
  /** Injected for tests. Kills and verifies the deterministic private tmux
76
78
  * server owned by a Session runner. */
77
79
  terminateTerminalServer?: (terminalName: string) => boolean | Promise<boolean>;
80
+ /** Injected tmux `#{window_activity}` reader. Returns epoch seconds. */
81
+ terminalWindowActivityAt?: (terminalName: string) => number | null | Promise<number | null>;
82
+ /** Injected tmux attached-client probe. */
83
+ terminalHasAttachedClient?: (terminalName: string) => boolean | Promise<boolean>;
78
84
  now?: () => number;
85
+ /** Wall clock used to compare tmux's epoch timestamp. */
86
+ wallNow?: () => number;
79
87
  /** Extra env merged into every spawned runner child — e.g. the control-plane
80
88
  * URL + token so a claude-native PermissionRequest hook can POST back. */
81
89
  childEnv?: Record<string, string>;
@@ -142,7 +150,8 @@ export declare class RunnerManager implements AgentCapabilities {
142
150
  private readonly sessionStore;
143
151
  private readonly runnerEntry;
144
152
  private readonly idleTtlMs;
145
- private readonly staleActiveTtlMs;
153
+ private readonly nativePaneIdleTtlMs;
154
+ private readonly nativePaneOutputBusyWindowMs;
146
155
  private readonly spawn;
147
156
  private readonly childEnv;
148
157
  private readonly sessionContextProvider;
@@ -159,12 +168,16 @@ export declare class RunnerManager implements AgentCapabilities {
159
168
  private readonly shutdownKillGraceMs;
160
169
  private readonly signalChild;
161
170
  private readonly terminateTerminalServer;
171
+ private readonly terminalWindowActivityAt;
172
+ private readonly terminalHasAttachedClient;
173
+ private readonly wallNow;
162
174
  private readonly liveStartTimeoutMs;
163
175
  private readonly liveReadyTimeoutMs;
164
176
  private readonly nativeLiveStartTimeoutMs;
165
177
  private readonly liveInterruptTimeoutMs;
166
178
  private readonly terminalInputHandoffTimeoutMs;
167
179
  private readonly terminalInputCleanupRetryMs;
180
+ private reapPromise;
168
181
  private stopping;
169
182
  private stopPromise;
170
183
  /** Sink for mirrored {@link SessionEvent}s from every session's forwarder. */
@@ -235,9 +248,10 @@ export declare class RunnerManager implements AgentCapabilities {
235
248
  /**
236
249
  * Eagerly bring up a session's codex-native live view (persistent forwarder +
237
250
  * detached `codex --remote` TUI) in its runner child, spawning the runner if
238
- * needed. Idempotent. For Codex-lineage fresh sessions, resolves after the
239
- * app-server/observer and pane are launched; thread discovery continues in
240
- * parallel with injection. Other providers retain their readiness gate.
251
+ * needed. Idempotent. For Codex-lineage sessions, resolves after backend
252
+ * preload and pane launch; known-thread observer attach and fresh-thread
253
+ * discovery continue in the background. Other providers retain their
254
+ * readiness gate.
241
255
  */
242
256
  ensureLiveSession(localThreadId: string, opts: {
243
257
  workspace: SessionWorkspaceSnapshot;
@@ -256,6 +270,7 @@ export declare class RunnerManager implements AgentCapabilities {
256
270
  }): Promise<boolean>;
257
271
  private requestLiveSession;
258
272
  lastLiveSessionError(localThreadId: string): string | undefined;
273
+ lastLiveSessionFailure(localThreadId: string): LiveSessionFailure | undefined;
259
274
  /**
260
275
  * Inject a user turn into a session's live codex thread — reference implementation's
261
276
  * single-writer web send (`turn/start` / `turn/steer`); the forwarder mirrors
@@ -321,14 +336,17 @@ export declare class RunnerManager implements AgentCapabilities {
321
336
  private failHandle;
322
337
  private terminateHandle;
323
338
  private reapIdle;
324
- /** Close server-side runtime state before fencing a Provider that stayed
325
- * active past its hard inactivity deadline. The child transport is closed by
326
- * terminateHandle immediately afterwards, so these are the final events for
327
- * the abandoned responses. */
328
- private failStaleActiveResponses;
339
+ private reapIdleOnce;
340
+ /** Provider-authoritative turn state is the lifecycle truth; attached clients
341
+ * and tmux's own activity clock are independent busy evidence. There is no
342
+ * "user was inactive for an hour" override for an active turn. */
343
+ private reapNativePane;
344
+ private isNativePaneBusy;
345
+ private isManagedNativeHandle;
329
346
  private failActiveResponses;
330
- /** Track only response lifecycle, not output volume. Output deltas from a
331
- * runaway TUI/Provider must not refresh the stale-active deadline. */
347
+ /** Track the provider-authoritative response lifecycle. Native-pane busy
348
+ * classification consumes this level directly; output volume is separately
349
+ * grounded in tmux's own activity clock. */
332
350
  private observeHandleRuntimeEvent;
333
351
  private openSessionContext;
334
352
  }