@rynx-ai/runtime 0.1.10 → 0.1.11-beta.10

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.
Files changed (40) hide show
  1. package/dist/claude/models.d.ts +0 -5
  2. package/dist/claude/models.js +1 -7
  3. package/dist/claude/native-bridge.js +3 -8
  4. package/dist/claude/native-integration.d.ts +12 -1
  5. package/dist/claude/native-integration.js +16 -2
  6. package/dist/claude/transcript.d.ts +0 -7
  7. package/dist/claude/transcript.js +6 -20
  8. package/dist/codex-app-server/client.d.ts +2 -1
  9. package/dist/codex-app-server/forwarder.d.ts +4 -1
  10. package/dist/codex-app-server/forwarder.js +19 -1
  11. package/dist/codex-app-server/protocol.d.ts +45 -1
  12. package/dist/codex-home.d.ts +9 -26
  13. package/dist/codex-home.js +37 -65
  14. package/dist/codex-session-store.d.ts +22 -10
  15. package/dist/codex-session-store.js +277 -12
  16. package/dist/host.d.ts +48 -47
  17. package/dist/host.js +810 -355
  18. package/dist/index.d.ts +2 -3
  19. package/dist/index.js +1 -2
  20. package/dist/models-catalog.d.ts +3 -1
  21. package/dist/models-catalog.js +125 -4
  22. package/dist/provider-workspace.d.ts +56 -0
  23. package/dist/provider-workspace.js +83 -0
  24. package/dist/runner/child.d.ts +59 -6
  25. package/dist/runner/child.js +138 -19
  26. package/dist/runner/manager.d.ts +104 -19
  27. package/dist/runner/manager.js +922 -89
  28. package/dist/runner/protocol.d.ts +7 -18
  29. package/dist/runner-main.js +12 -4
  30. package/dist/runtime-state-paths.d.ts +10 -0
  31. package/dist/runtime-state-paths.js +53 -0
  32. package/dist/terminal/claude-tui.d.ts +10 -1
  33. package/dist/terminal/claude-tui.js +9 -1
  34. package/dist/terminal/codex-tui.d.ts +5 -1
  35. package/dist/terminal/codex-tui.js +12 -3
  36. package/dist/terminal/tmux.d.ts +11 -1
  37. package/dist/terminal/tmux.js +61 -12
  38. package/package.json +3 -3
  39. package/dist/codex/rollout-synth.d.ts +0 -42
  40. package/dist/codex/rollout-synth.js +0 -245
@@ -1,5 +1,18 @@
1
1
  import { TerminalRegistry } from "../terminal/registry.js";
2
2
  import { toWireError } from "./protocol.js";
3
+ const TRAEX_STARTUP_WATCH_MS = 20_000;
4
+ const TRAEX_STARTUP_POLL_MS = 100;
5
+ const TRAEX_PROMPT_RETRY_MS = 500;
6
+ function normalizeTraexPane(pane) {
7
+ return pane.toLowerCase().replace(/\s+/g, " ").trim();
8
+ }
9
+ function isTerminalProtocolResponse(input) {
10
+ // xterm answers terminal queries through the same onData channel as real
11
+ // keystrokes. CSI carries device/focus/position reports; OSC carries color
12
+ // query replies such as `OSC 10;rgb:... ST` and `OSC 11;rgb:... ST`.
13
+ // Neither is evidence that the user has taken over startup prompt handling.
14
+ return /^(?:\x1b\[(?:(?:\?|>)[0-9;]*c|[0-9;]*(?:n|R|t)|[IO])|\x1b\][0-9]+;[^\x07\x1b]*(?:\x07|\x1b\\))+$/.test(input);
15
+ }
3
16
  export class RunnerSession {
4
17
  transport;
5
18
  executor;
@@ -7,6 +20,8 @@ export class RunnerSession {
7
20
  /** Live terminals hosted by this session, and per-attach client handles. */
8
21
  terminals = new TerminalRegistry();
9
22
  attachments = new Map();
23
+ attachmentThreadIds = new Map();
24
+ traexStartupWatchers = new Map();
10
25
  /** Opens are async; a close received before attach resolves tombstones the id. */
11
26
  pendingTerminalOpens = new Set();
12
27
  cancelledTerminalOpens = new Set();
@@ -46,7 +61,17 @@ export class RunnerSession {
46
61
  });
47
62
  return;
48
63
  case "term.input":
49
- this.attachments.get(msg.attachId)?.write(Buffer.from(msg.dataB64, "base64").toString("utf8"));
64
+ {
65
+ const localThreadId = this.attachmentThreadIds.get(msg.attachId);
66
+ const input = Buffer.from(msg.dataB64, "base64").toString("utf8");
67
+ // xterm sends device/focus reports through the same onData channel as
68
+ // keystrokes. They must reach the TUI without pretending the user has
69
+ // taken over startup prompt handling.
70
+ if (!isTerminalProtocolResponse(input)) {
71
+ this.cancelTraexStartupWatcher(localThreadId);
72
+ }
73
+ this.attachments.get(msg.attachId)?.write(input);
74
+ }
50
75
  return;
51
76
  case "term.resize":
52
77
  this.attachments.get(msg.attachId)?.resize(msg.cols, msg.rows);
@@ -57,6 +82,7 @@ export class RunnerSession {
57
82
  }
58
83
  const attachment = this.attachments.get(msg.attachId);
59
84
  this.attachments.delete(msg.attachId);
85
+ this.attachmentThreadIds.delete(msg.attachId);
60
86
  attachment?.kill();
61
87
  return;
62
88
  }
@@ -114,9 +140,8 @@ export class RunnerSession {
114
140
  from: localThreadId,
115
141
  to: newId,
116
142
  kind: meta.kind,
117
- ...(meta.agent ? { agent: meta.agent } : {}),
118
- ...(meta.model ? { model: meta.model } : {}),
119
- ...(meta.cwd ? { cwd: meta.cwd } : {}),
143
+ workspace: meta.workspace,
144
+ execution: meta.execution,
120
145
  ...(meta.parentSessionId ? { parentSessionId: meta.parentSessionId } : {}),
121
146
  });
122
147
  };
@@ -125,21 +150,17 @@ export class RunnerSession {
125
150
  /**
126
151
  * Eagerly bring up a session's codex-native live view: start the persistent
127
152
  * forwarder connection (which resume-subscribes to mirror every turn) and
128
- * launch the detached `codex --remote` TUI, which CREATES the codex thread. The
129
- * forwarder's connection sees the TUI's broadcast `thread/started`, binds, and
130
- * subscribes — so the TUI is usable immediately (a fresh `--remote` needs no
131
- * rollout) and its turns mirror to chat (reference implementation's model).
153
+ * launch the detached `codex --remote resume` TUI against the thread the
154
+ * structured runtime created. The forwarder subscribes to that same thread,
155
+ * so the TUI is usable immediately and its turns mirror to chat.
132
156
  */
133
157
  async ensureLive(msg) {
134
158
  const provider = this.liveProvider;
135
159
  try {
136
160
  const { emit, retarget } = this.mirrorChannel(msg.localThreadId);
137
161
  const started = await provider.ensureLiveCodexSession?.(msg.localThreadId, emit, {
138
- ...(msg.cwd ? { cwd: msg.cwd } : {}),
139
- ...(msg.runtime ? { runtime: msg.runtime } : {}),
140
- ...(msg.reasoningEffort ? { reasoningEffort: msg.reasoningEffort } : {}),
141
- ...(msg.agentName ? { agentName: msg.agentName } : {}),
142
- ...(msg.agentSpec ? { agentSpec: msg.agentSpec } : {}),
162
+ workspace: msg.workspace,
163
+ execution: msg.execution,
143
164
  retargetMirror: retarget,
144
165
  });
145
166
  if (!started) {
@@ -153,12 +174,24 @@ export class RunnerSession {
153
174
  return;
154
175
  }
155
176
  this.liveIds.add(msg.localThreadId);
156
- // Launch the TUI (fresh `--remote` it creates the thread) so the forwarder
157
- // captures the broadcast `thread/started` and binds. Re-launch when the pane
158
- // is absent OR its process has died (`isAlive` probes `#{pane_dead}`) — so a
177
+ // Launch the TUI attached to the already-bound thread. Re-launch when the
178
+ // pane is absent OR its process has died (`isAlive` probes `#{pane_dead}`) so a
159
179
  // reconnect after the TUI exited restarts it instead of skipping (a
160
180
  // launched-once guard would leave a dead "Pane is dead" husk forever).
161
181
  if (!this.terminals.get(`${msg.localThreadId}-main`)?.isAlive()) {
182
+ const terminalReady = provider.waitTerminalReady
183
+ ? await provider.waitTerminalReady(msg.localThreadId)
184
+ : true;
185
+ if (!terminalReady) {
186
+ this.transport.send({
187
+ t: "live.ready",
188
+ reqId: msg.reqId,
189
+ localThreadId: msg.localThreadId,
190
+ ok: false,
191
+ error: "Provider thread was not ready for Terminal resume",
192
+ });
193
+ return;
194
+ }
162
195
  await this.launchCodexPane(msg.localThreadId, msg.cols, msg.rows);
163
196
  }
164
197
  const ready = msg.waitForReady === false
@@ -166,12 +199,27 @@ export class RunnerSession {
166
199
  : provider.waitLiveReady
167
200
  ? await provider.waitLiveReady(msg.localThreadId)
168
201
  : true;
202
+ const terminal = this.terminals.get(`${msg.localThreadId}-main`);
203
+ const paneFailure = !ready && terminal && !terminal.isAlive()
204
+ ? terminal
205
+ .capturePane()
206
+ .split("\n")
207
+ .map((line) => line.trim())
208
+ .filter(Boolean)
209
+ .slice(-6)
210
+ .join(" ")
211
+ .slice(-1_000)
212
+ : "";
213
+ const readinessError = provider.liveSessionError?.(msg.localThreadId)
214
+ ?? (paneFailure
215
+ ? `Provider terminal exited before session discovery: ${paneFailure}`
216
+ : "live session was not ready before timeout");
169
217
  this.transport.send({
170
218
  t: "live.ready",
171
219
  reqId: msg.reqId,
172
220
  localThreadId: msg.localThreadId,
173
221
  ok: ready,
174
- ...(ready ? {} : { error: "live session was not ready before timeout" }),
222
+ ...(ready ? {} : { error: readinessError }),
175
223
  });
176
224
  }
177
225
  catch (error) {
@@ -189,6 +237,8 @@ export class RunnerSession {
189
237
  try {
190
238
  const input = msg.input ?? msg.text;
191
239
  const outcome = (await provider.injectMessage?.(msg.localThreadId, input)) ?? "notLive";
240
+ // App-server injection is independent of the Terminal TUI startup, so it
241
+ // must not cancel prompt handling for the pane that is still starting.
192
242
  this.transport.send({ t: "injected", reqId: msg.reqId, localThreadId: msg.localThreadId, outcome });
193
243
  }
194
244
  catch (error) {
@@ -226,7 +276,8 @@ export class RunnerSession {
226
276
  const spec = await this.liveProvider.codexTerminalSpec?.(localThreadId);
227
277
  if (!spec)
228
278
  return;
229
- const term = this.terminals.getOrCreate(`${localThreadId}-main`, {
279
+ const terminalId = `${localThreadId}-main`;
280
+ const term = this.terminals.getOrCreate(terminalId, {
230
281
  cwd: spec.cwd,
231
282
  command: spec.command,
232
283
  args: spec.args,
@@ -234,8 +285,70 @@ export class RunnerSession {
234
285
  rows: rows ?? 40,
235
286
  ...(spec.env ? { env: spec.env } : {}),
236
287
  });
288
+ if (spec.skipTraexStartupPrompts) {
289
+ const watcher = Symbol(localThreadId);
290
+ this.traexStartupWatchers.set(localThreadId, watcher);
291
+ void this.skipTraexStartupPrompts(localThreadId, term, watcher);
292
+ }
237
293
  this.liveProvider.attachTerminalInjector?.(localThreadId, term);
238
294
  }
295
+ cancelTraexStartupWatcher(localThreadId) {
296
+ if (localThreadId)
297
+ this.traexStartupWatchers.delete(localThreadId);
298
+ }
299
+ async skipTraexStartupPrompts(localThreadId, terminal, watcher) {
300
+ const prompts = [
301
+ {
302
+ id: "welcome",
303
+ matches: (pane) => pane.includes("welcome to trae cli") && pane.includes("press enter to continue"),
304
+ dismiss: () => terminal.sendEnter(),
305
+ },
306
+ {
307
+ id: "migration",
308
+ matches: (pane) => pane.includes("legacy trae cli data detected") &&
309
+ pane.includes("select what to import") &&
310
+ (pane.includes("skip for now") || pane.includes("don't ask again")),
311
+ dismiss: () => terminal.interrupt(),
312
+ },
313
+ {
314
+ id: "hooks",
315
+ matches: (pane) => pane.includes("hooks need review") &&
316
+ pane.includes("trust all and continue") &&
317
+ pane.includes("continue without trusting"),
318
+ dismiss: () => terminal.interrupt(),
319
+ },
320
+ ];
321
+ let activePromptId;
322
+ let lastDismissedAt = 0;
323
+ const deadline = Date.now() + TRAEX_STARTUP_WATCH_MS;
324
+ const terminalId = `${localThreadId}-main`;
325
+ while (!this.shuttingDown &&
326
+ Date.now() < deadline &&
327
+ this.traexStartupWatchers.get(localThreadId) === watcher &&
328
+ this.terminals.get(terminalId) === terminal) {
329
+ // Do not treat a composer frame as completion: Traex can render it before
330
+ // the startup modals arrive. The bounded deadline stops this watcher.
331
+ const pane = normalizeTraexPane(terminal.capturePane());
332
+ const prompt = prompts.find((candidate) => candidate.matches(pane));
333
+ const now = Date.now();
334
+ if (!prompt) {
335
+ activePromptId = undefined;
336
+ }
337
+ else if (prompt.id !== activePromptId ||
338
+ now - lastDismissedAt >= TRAEX_PROMPT_RETRY_MS) {
339
+ prompt.dismiss();
340
+ activePromptId = prompt.id;
341
+ lastDismissedAt = now;
342
+ }
343
+ await new Promise((resolve) => {
344
+ const timer = setTimeout(resolve, TRAEX_STARTUP_POLL_MS);
345
+ timer.unref();
346
+ });
347
+ }
348
+ if (this.traexStartupWatchers.get(localThreadId) === watcher) {
349
+ this.traexStartupWatchers.delete(localThreadId);
350
+ }
351
+ }
239
352
  stopLive() {
240
353
  for (const id of this.liveIds) {
241
354
  this.liveProvider.stopLiveCodexSession?.(id, {
@@ -314,6 +427,8 @@ export class RunnerSession {
314
427
  return;
315
428
  }
316
429
  this.attachments.set(msg.attachId, attachment);
430
+ if (msg.localThreadId)
431
+ this.attachmentThreadIds.set(msg.attachId, msg.localThreadId);
317
432
  attachment.onData((chunk) => this.transport.send({
318
433
  t: "term.data",
319
434
  attachId: msg.attachId,
@@ -321,6 +436,7 @@ export class RunnerSession {
321
436
  }));
322
437
  attachment.onExit((info) => {
323
438
  this.attachments.delete(msg.attachId);
439
+ this.attachmentThreadIds.delete(msg.attachId);
324
440
  this.transport.send({ t: "term.exit", attachId: msg.attachId, exitCode: info.exitCode });
325
441
  });
326
442
  this.transport.send({ t: "term.opened", attachId: msg.attachId, role });
@@ -343,7 +459,10 @@ export class RunnerSession {
343
459
  case "clearGoal":
344
460
  return this.executor.clearGoal(args[0]);
345
461
  case "forkSession":
346
- return this.executor.forkSession(args[0], args[1]);
462
+ if (!this.executor.forkSession) {
463
+ throw new Error("Provider fork is unavailable");
464
+ }
465
+ return this.executor.forkSession(args[0], args[1], args[2]);
347
466
  }
348
467
  }
349
468
  }
@@ -15,8 +15,8 @@
15
15
  * transport drops in here later without touching `ConversationRuntime` or any
16
16
  * channel.
17
17
  */
18
- import { spawn as nodeSpawn } from "node:child_process";
19
- import { type AgentCapabilities, type AgentRuntimeId, type AgentSpec, type AppConfig, type CapabilityResult, type ModelListResponse, type ReasoningEffort, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type ThreadGoal } from "@rynx-ai/core";
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";
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,6 +43,9 @@ 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
49
  /** Background reap sweep interval (ms); `0` disables the timer (tests). */
47
50
  reapIntervalMs?: number;
48
51
  /** Grace period between SIGTERM and SIGKILL during runner shutdown. */
@@ -53,8 +56,22 @@ export interface RunnerManagerOptions {
53
56
  liveStartTimeoutMs?: number;
54
57
  /** Max wait for a Provider-native thread to become ready. */
55
58
  liveReadyTimeoutMs?: number;
59
+ /** Max wait for a native interrupt acknowledgement before reporting it
60
+ * unproven. Callers may then use the explicit force-stop path. */
61
+ liveInterruptTimeoutMs?: number;
62
+ /** Max wait for an accepted owner TUI submission to become observable as a
63
+ * mirrored response or a published native rotation. */
64
+ terminalInputHandoffTimeoutMs?: number;
65
+ /** Initial exponential backoff for retrying unproven terminal cleanup. */
66
+ terminalInputCleanupRetryMs?: number;
56
67
  /** Injected for tests. */
57
68
  spawn?: typeof nodeSpawn;
69
+ /** Injected for tests. Defaults to signaling the whole POSIX process group
70
+ * created for the runner child, with direct-child fallback. */
71
+ signalChild?: (child: ChildProcess, signal: NodeJS.Signals, processGroup: boolean) => void;
72
+ /** Injected for tests. Kills and verifies the deterministic private tmux
73
+ * server owned by a Session runner. */
74
+ terminateTerminalServer?: (terminalName: string) => boolean | Promise<boolean>;
58
75
  now?: () => number;
59
76
  /** Extra env merged into every spawned runner child — e.g. the control-plane
60
77
  * URL + token so a claude-native PermissionRequest hook can POST back. */
@@ -62,6 +79,13 @@ export interface RunnerManagerOptions {
62
79
  /** Optional lifecycle context for Session runners. The shared `__cap__`
63
80
  * runner never opens one. */
64
81
  sessionContextProvider?: RunnerSessionContextProvider;
82
+ /** Dynamic process-wide admission fence. Existing cancellation and read-only
83
+ * inspection remain available; every path that may spawn or attach execution
84
+ * checks this immediately before admission. */
85
+ admissionOpen?: () => boolean;
86
+ /** Atomically reserves one work-producing admission while it crosses the
87
+ * asynchronous boundary into an observable live runner/turn. */
88
+ admissionReserve?: () => AdmissionReservation | undefined;
65
89
  }
66
90
  /** A session rotation reported by a runner child (claude `/clear`·`/fork`): the
67
91
  * new session `to` is now aliased to `from`'s runner; carries meta to carry over. */
@@ -69,9 +93,8 @@ export interface RotateInfo {
69
93
  from: string;
70
94
  to: string;
71
95
  kind: "clear" | "fork";
72
- agent?: string;
73
- model?: string;
74
- cwd?: string;
96
+ workspace: SessionWorkspaceSnapshot;
97
+ execution: ResolvedExecutionSnapshot;
75
98
  parentSessionId?: string;
76
99
  }
77
100
  /** Options for {@link RunnerManager.openTerminal}. */
@@ -116,9 +139,12 @@ export declare class RunnerManager implements AgentCapabilities {
116
139
  private readonly sessionStore;
117
140
  private readonly runnerEntry;
118
141
  private readonly idleTtlMs;
142
+ private readonly staleActiveTtlMs;
119
143
  private readonly spawn;
120
144
  private readonly childEnv;
121
145
  private readonly sessionContextProvider;
146
+ private readonly admissionOpen;
147
+ private readonly admissionReserve;
122
148
  private readonly now;
123
149
  private readonly defaultRuntime;
124
150
  private readonly handles;
@@ -128,8 +154,13 @@ export declare class RunnerManager implements AgentCapabilities {
128
154
  private readonly reapTimer;
129
155
  private readonly shutdownGraceMs;
130
156
  private readonly shutdownKillGraceMs;
157
+ private readonly signalChild;
158
+ private readonly terminateTerminalServer;
131
159
  private readonly liveStartTimeoutMs;
132
160
  private readonly liveReadyTimeoutMs;
161
+ private readonly liveInterruptTimeoutMs;
162
+ private readonly terminalInputHandoffTimeoutMs;
163
+ private readonly terminalInputCleanupRetryMs;
133
164
  private stopping;
134
165
  private stopPromise;
135
166
  /** Sink for mirrored {@link SessionEvent}s from every session's forwarder. */
@@ -140,6 +171,24 @@ export declare class RunnerManager implements AgentCapabilities {
140
171
  private readonly liveSessionKeys;
141
172
  /** Last live-start error per local session, surfaced by the control API. */
142
173
  private readonly liveErrors;
174
+ /** Last immutable launch snapshots seen for a Session. Used only to restore a
175
+ * target request that crossed a fork reservation boundary. */
176
+ private readonly liveOptions;
177
+ /** A fork reserves its target before the first asynchronous store read. This
178
+ * prevents another entry point from starting the target against an
179
+ * uncommitted Provider binding. */
180
+ private readonly forkReservations;
181
+ /** A native fork temporarily makes the source read-only so its canonical
182
+ * snapshot and Provider context are captured at the same boundary. */
183
+ private readonly sourceForkReservations;
184
+ /** Manager-wide fork de-duplication. LocalAgentHost only sees one source
185
+ * runner, so the fence must live here to cover concurrent source runners. */
186
+ private readonly forkOperations;
187
+ private readonly forkBufferedMessages;
188
+ private readonly forkBufferedTerminalInputs;
189
+ /** Owner TUI submissions accepted by the parent but not yet represented by a
190
+ * mirrored response or a durably published native rotation. */
191
+ private readonly terminalInputHandoffs;
143
192
  constructor(opts: RunnerManagerOptions);
144
193
  /**
145
194
  * Open a live terminal on the session's runner child (spawning it if needed).
@@ -166,33 +215,39 @@ export declare class RunnerManager implements AgentCapabilities {
166
215
  onMirror(listener: (sessionId: string, event: SessionEvent) => void): void;
167
216
  /** Register the sink for session rotations (claude `/clear`·`/fork`): the server
168
217
  * records the new session's meta (carry-over agent/model/title). */
169
- onRotate(listener: (rotation: RotateInfo) => void): void;
218
+ onRotate(listener: (rotation: RotateInfo) => void | Promise<void>): void;
219
+ /** Accepted TUI submissions that have not crossed into an observable runtime
220
+ * state. The daemon folds this into runningTurns after closing admission. */
221
+ pendingTerminalInputCount(): number;
222
+ private beginTerminalInputHandoffs;
223
+ private settleTerminalInputHandoff;
224
+ private settleTerminalRotationHandoff;
225
+ private currentTerminalSessionId;
226
+ private finishTerminalInputHandoff;
227
+ private finishAllTerminalInputHandoffs;
228
+ private preserveTerminalInputHandoffsAsActivity;
229
+ private scheduleTerminalInputCleanupRetry;
230
+ private expireTerminalInputHandoffs;
170
231
  /**
171
232
  * Eagerly bring up a session's codex-native live view (persistent forwarder +
172
233
  * detached `codex --remote` TUI) in its runner child, spawning the runner if
173
234
  * needed. Idempotent. Resolves true once the codex thread is bound; false for a
174
235
  * non-codex / non-live session (the caller then uses the normal run path).
175
236
  */
176
- ensureLiveSession(localThreadId: string, opts?: {
177
- cwd?: string;
237
+ ensureLiveSession(localThreadId: string, opts: {
238
+ workspace: SessionWorkspaceSnapshot;
239
+ execution: ResolvedExecutionSnapshot;
178
240
  cols?: number;
179
241
  rows?: number;
180
- runtime?: AgentRuntimeId;
181
- reasoningEffort?: ReasoningEffort;
182
- agentName?: string;
183
- agentSpec?: AgentSpec;
184
242
  }): Promise<boolean>;
185
243
  /** Start the Provider pane without waiting for login/onboarding to create a
186
244
  * native thread. This is the setup-terminal gate; callers may attach as soon
187
245
  * as it resolves, while normal message delivery still uses ensureLiveSession. */
188
- startLiveSession(localThreadId: string, opts?: {
189
- cwd?: string;
246
+ startLiveSession(localThreadId: string, opts: {
247
+ workspace: SessionWorkspaceSnapshot;
248
+ execution: ResolvedExecutionSnapshot;
190
249
  cols?: number;
191
250
  rows?: number;
192
- runtime?: AgentRuntimeId;
193
- reasoningEffort?: ReasoningEffort;
194
- agentName?: string;
195
- agentSpec?: AgentSpec;
196
251
  }): Promise<boolean>;
197
252
  private requestLiveSession;
198
253
  lastLiveSessionError(localThreadId: string): string | undefined;
@@ -203,6 +258,7 @@ export declare class RunnerManager implements AgentCapabilities {
203
258
  * the session has no live forwarder (caller falls back to the run path).
204
259
  */
205
260
  injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectOutcome>;
261
+ private injectMessageAdmitted;
206
262
  /**
207
263
  * Interrupt a session's active live turn — the web Stop button (codex
208
264
  * `turn/interrupt`, claude Escape). Best-effort: resolves false with NO spawn
@@ -215,7 +271,16 @@ export declare class RunnerManager implements AgentCapabilities {
215
271
  getGoal(localThreadId: string): Promise<CapabilityResult<ThreadGoal | null>>;
216
272
  setGoal(localThreadId: string, objective: string): Promise<CapabilityResult>;
217
273
  clearGoal(localThreadId: string): Promise<CapabilityResult>;
218
- forkSession(currentLocalThreadId: string, newLocalThreadId: string): Promise<CapabilityResult>;
274
+ forkSession(currentLocalThreadId: string, newLocalThreadId: string, options: {
275
+ workspace: SessionWorkspaceSnapshot;
276
+ execution: ResolvedExecutionSnapshot;
277
+ beforeProviderFork: () => Promise<void>;
278
+ }): Promise<{
279
+ ok: true;
280
+ } | {
281
+ ok: false;
282
+ message: string;
283
+ }>;
219
284
  /**
220
285
  * Backend-free runtime readiness (not part of `AgentCapabilities`; surfaced for
221
286
  * the control console). Never spawns a runner.
@@ -226,18 +291,38 @@ export declare class RunnerManager implements AgentCapabilities {
226
291
  }): Promise<import("../host.js").CodexRuntimeStatus>;
227
292
  /** Stop the runner bound to one session (if any), rejecting its in-flight work. */
228
293
  stopRunner(localThreadId: string): void;
294
+ /** Force-stop and join the runner bound to one Session. Unlike stopRunner,
295
+ * completion proves that the child process has exited. */
296
+ terminateLiveSession(localThreadId: string): Promise<boolean>;
229
297
  /** Stop every runner and join all child exits. Idempotent across concurrent calls. */
230
298
  stop(): Promise<void>;
299
+ private handleForCleanup;
231
300
  /** Forward a capability to a runner child. Session-less caps (listModels/status)
232
301
  * use the shared `CAP_KEY` child; per-thread caps pass the session's key so they
233
302
  * run on that session's child (which owns its private CODEX_HOME). */
234
303
  private forwardCap;
235
304
  private getOrSpawn;
305
+ private assertAdmissionOpen;
306
+ private reserveAdmission;
307
+ private performManagedFork;
308
+ private performClaudeManagedFork;
309
+ private bufferForkTargetMessage;
310
+ private reservedForkTarget;
311
+ private deliverForkBufferedMessage;
312
+ private deliverRotateMessage;
236
313
  private spawnHandle;
237
314
  private onChildMessage;
238
315
  /** Mark a handle dead and reject every pending run/cap with the exit reason. */
239
316
  private failHandle;
240
317
  private terminateHandle;
241
318
  private reapIdle;
319
+ /** Close server-side runtime state before fencing a Provider that stayed
320
+ * active past its hard inactivity deadline. The child transport is closed by
321
+ * terminateHandle immediately afterwards, so these are the final events for
322
+ * the abandoned responses. */
323
+ private failStaleActiveResponses;
324
+ /** Track only response lifecycle, not output volume. Output deltas from a
325
+ * runaway TUI/Provider must not refresh the stale-active deadline. */
326
+ private observeHandleRuntimeEvent;
242
327
  private openSessionContext;
243
328
  }