@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.
@@ -22,6 +22,11 @@ export interface ClaudeForwarderSink {
22
22
  /** The current turn finished (a new user prompt, or the inactivity backstop).
23
23
  * `usage` carries the latest statusLine context/cost snapshot, when captured. */
24
24
  onTurnEnd(usage?: Record<string, unknown>): void;
25
+ /** The current turn ended because the user explicitly interrupted it. */
26
+ onTurnInterrupted?(usage?: Record<string, unknown>): void;
27
+ /** Escape was sent for the open Turn. Publish cancelled UI state immediately;
28
+ * the final close remains delayed so a late transcript record can join it. */
29
+ onTurnInterruptRequested?(): void;
25
30
  /** The runtime status changed according to Claude's session metadata. */
26
31
  onStatus?(status: ClaudeRunnerStatus, blockedOn?: string): void;
27
32
  /** The runtime went idle on the legacy hook fallback — surface idle WITHOUT finalizing
@@ -102,6 +107,8 @@ export declare class ClaudeLiveSession {
102
107
  private readonly seenClaudeSessionIds;
103
108
  private turnOpen;
104
109
  private currentTurnId?;
110
+ /** The open turn received an explicit Escape/Stop and must close cancelled. */
111
+ private turnInterrupted;
105
112
  /** The Turn opened from a pre-transcript interaction. Its unique provisional
106
113
  * id keeps responses distinct until the real prompt uuid can be adopted. */
107
114
  private syntheticTurn;
@@ -27,6 +27,10 @@ const MAX_SETTLED_INTERACTIONS = 512;
27
27
  const MAX_MESSAGE_CORRELATION_BACKLOG = 64;
28
28
  const MAX_SUBMISSION_OBSERVATIONS = 64;
29
29
  const INTERACTION_ACK_TIMEOUT_MS = 5_000;
30
+ // Claude writes this synthetic user record after Escape (including a tool-use
31
+ // interruption). It is lifecycle, not a new prompt. Kept aligned with
32
+ // Omnigent's `_CLAUDE_INTERRUPT_RECORD_RE`.
33
+ const CLAUDE_INTERRUPT_RECORD_RE = /^\[Request interrupted by user(?: for tool use)?\]$/;
30
34
  const INTERACTION_LEASE_TIMEOUT_MS = 30_000;
31
35
  function processIsAlive(pid) {
32
36
  try {
@@ -165,6 +169,8 @@ export class ClaudeLiveSession {
165
169
  seenClaudeSessionIds = new Set();
166
170
  turnOpen = false;
167
171
  currentTurnId;
172
+ /** The open turn received an explicit Escape/Stop and must close cancelled. */
173
+ turnInterrupted = false;
168
174
  /** The Turn opened from a pre-transcript interaction. Its unique provisional
169
175
  * id keeps responses distinct until the real prompt uuid can be adopted. */
170
176
  syntheticTurn = false;
@@ -846,6 +852,12 @@ export class ClaudeLiveSession {
846
852
  }
847
853
  const content = userStringContent(rec);
848
854
  if (content !== undefined) {
855
+ const firstLine = content.trim().split("\n", 1)[0] ?? "";
856
+ if (CLAUDE_INTERRUPT_RECORD_RE.test(firstLine)) {
857
+ this.noteInterrupted();
858
+ this.lastActivityAt = this.now();
859
+ return;
860
+ }
849
861
  const candidates = submissionCandidates(content);
850
862
  const queuedPromotion = rec.promptSource !== "typed" &&
851
863
  candidates.some((candidate) => this.consumeQueuedPromotion(candidate));
@@ -890,6 +902,7 @@ export class ClaudeLiveSession {
890
902
  this.closeTurn();
891
903
  this.currentTurnId = turnId;
892
904
  this.turnOpen = true;
905
+ this.turnInterrupted = false;
893
906
  this.syntheticTurn = false;
894
907
  this.providerIdleAt = null;
895
908
  this.openToolIds.clear();
@@ -1100,6 +1113,7 @@ export class ClaudeLiveSession {
1100
1113
  }
1101
1114
  this.providerIdleAt = null;
1102
1115
  this.turnOpen = true;
1116
+ this.turnInterrupted = false;
1103
1117
  this.sink.onTurnStart(this.currentTurnId);
1104
1118
  }
1105
1119
  /** Mirror a local `!` command as its own mini-turn: close any open turn, then
@@ -1120,8 +1134,10 @@ export class ClaudeLiveSession {
1120
1134
  * turn's close via the same short grace. The Escape has stopped claude, so this
1121
1135
  * does not race a still-running response. */
1122
1136
  noteInterrupted() {
1123
- if (!this.turnOpen)
1137
+ if (!this.turnOpen || this.turnInterrupted)
1124
1138
  return;
1139
+ this.turnInterrupted = true;
1140
+ this.sink.onTurnInterruptRequested?.();
1125
1141
  this.cancelPendingInteractions("turn_interrupted");
1126
1142
  this.stopSignalPending = false;
1127
1143
  if (this.statusPoller?.active)
@@ -1160,7 +1176,13 @@ export class ClaudeLiveSession {
1160
1176
  this.stopSignalPending = false;
1161
1177
  this.stopPendingAt = null;
1162
1178
  this.resetMessageCorrelation();
1163
- this.sink.onTurnEnd(this.statusUsage());
1179
+ const interrupted = this.turnInterrupted;
1180
+ this.turnInterrupted = false;
1181
+ const usage = this.statusUsage();
1182
+ if (interrupted && this.sink.onTurnInterrupted)
1183
+ this.sink.onTurnInterrupted(usage);
1184
+ else
1185
+ this.sink.onTurnEnd(usage);
1164
1186
  }
1165
1187
  closeTurnError(error) {
1166
1188
  if (!this.turnOpen)
@@ -1171,6 +1193,7 @@ export class ClaudeLiveSession {
1171
1193
  this.currentTurnId = undefined;
1172
1194
  this.syntheticTurn = false;
1173
1195
  this.openToolIds.clear();
1196
+ this.turnInterrupted = false;
1174
1197
  this.stopSignalPending = false;
1175
1198
  this.stopPendingAt = null;
1176
1199
  this.resetMessageCorrelation();
@@ -29,9 +29,11 @@ export declare class CodexAppServerClient {
29
29
  private interactionListener;
30
30
  private connectionListener;
31
31
  private connectionState;
32
+ private connectionEstablished;
32
33
  private readonly pendingInteractions;
33
34
  private readonly settledInteractions;
34
35
  private initializeResponse;
36
+ private initializePromise;
35
37
  constructor({ spawner, channel, logger, clientInfo, approvalDecisionPolicy, }: CodexAppServerClientOptions);
36
38
  /**
37
39
  * The multi-client endpoint a `codex --remote` TUI can attach to, when this
@@ -94,9 +96,8 @@ export declare class CodexAppServerClient {
94
96
  * resolver still wins correctly.
95
97
  */
96
98
  setInteractionListener(listener: RuntimeInteractionListener | null): void;
97
- /** Observe the underlying app-server connection independently from any one
98
- * interaction. A disconnected client that never received the duplicate
99
- * native request still has to count as unavailable during host failover. */
99
+ /** Observe the initialized connection lifecycle. Registration never reports
100
+ * disconnected for a client that has not connected yet. */
100
101
  setConnectionListener(listener: ((state: "connected" | "disconnected") => void) | null): void;
101
102
  private setConnectionState;
102
103
  resolveInteraction(interactionId: string, resolution: SessionInteractionResolution): ResolveInteractionResult;
@@ -953,9 +953,11 @@ export class CodexAppServerClient {
953
953
  interactionListener = null;
954
954
  connectionListener = null;
955
955
  connectionState = "disconnected";
956
+ connectionEstablished = false;
956
957
  pendingInteractions = new Map();
957
958
  settledInteractions = new Set();
958
959
  initializeResponse = null;
960
+ initializePromise = null;
959
961
  constructor({ spawner, channel, logger = defaultLogger, clientInfo = DEFAULT_CLIENT_INFO, approvalDecisionPolicy = "auto-approve-session", }) {
960
962
  this.logger = logger;
961
963
  this.clientInfo = clientInfo;
@@ -988,14 +990,31 @@ export class CodexAppServerClient {
988
990
  if (this.initializeResponse) {
989
991
  return this.initializeResponse;
990
992
  }
991
- await this.transport.ensureStarted();
992
- const response = await this.transport.sendRequest("initialize", {
993
- clientInfo: this.clientInfo,
994
- capabilities: { experimentalApi: true },
995
- });
996
- this.initializeResponse = response;
997
- this.setConnectionState("connected");
998
- return response;
993
+ if (this.initializePromise)
994
+ return this.initializePromise;
995
+ const initializing = (async () => {
996
+ await this.transport.ensureStarted();
997
+ const response = await this.transport.sendRequest("initialize", {
998
+ clientInfo: this.clientInfo,
999
+ capabilities: { experimentalApi: true },
1000
+ });
1001
+ // Codex app-server uses the full initialize handshake: it does not accept
1002
+ // capability requests after merely replying to `initialize`. The client
1003
+ // must acknowledge that response with the `initialized` notification
1004
+ // before `thread/resume`, `turn/start`, and the other APIs are legal.
1005
+ await this.transport.sendNotification("initialized");
1006
+ this.initializeResponse = response;
1007
+ this.setConnectionState("connected");
1008
+ return response;
1009
+ })();
1010
+ this.initializePromise = initializing;
1011
+ try {
1012
+ return await initializing;
1013
+ }
1014
+ finally {
1015
+ if (this.initializePromise === initializing)
1016
+ this.initializePromise = null;
1017
+ }
999
1018
  }
1000
1019
  async getAuthStatus(params = {}) {
1001
1020
  await this.ensureInitialized();
@@ -1146,14 +1165,18 @@ export class CodexAppServerClient {
1146
1165
  setInteractionListener(listener) {
1147
1166
  this.interactionListener = listener;
1148
1167
  }
1149
- /** Observe the underlying app-server connection independently from any one
1150
- * interaction. A disconnected client that never received the duplicate
1151
- * native request still has to count as unavailable during host failover. */
1168
+ /** Observe the initialized connection lifecycle. Registration never reports
1169
+ * disconnected for a client that has not connected yet. */
1152
1170
  setConnectionListener(listener) {
1153
1171
  this.connectionListener = listener;
1154
- listener?.(this.connectionState);
1172
+ if (listener && this.connectionEstablished)
1173
+ listener(this.connectionState);
1155
1174
  }
1156
1175
  setConnectionState(state) {
1176
+ if (state === "connected")
1177
+ this.connectionEstablished = true;
1178
+ if (state === "disconnected" && !this.connectionEstablished)
1179
+ return;
1157
1180
  if (this.connectionState === state)
1158
1181
  return;
1159
1182
  this.connectionState = state;
@@ -39,6 +39,8 @@ export interface CodexForwarderSink {
39
39
  onStatus?(note: string | undefined, statusKind?: "startup"): void;
40
40
  /** The current turn finished; `usage` is the runtime's raw snapshot if any. */
41
41
  onTurnEnd(usage?: Record<string, unknown>, reason?: "superseded"): void;
42
+ /** The provider confirmed that the active turn was explicitly interrupted. */
43
+ onTurnInterrupted?(usage?: Record<string, unknown>): void;
42
44
  /** Resume proved that the newest turn is terminal even though its live edge
43
45
  * was missed. This updates session state without replaying historical items. */
44
46
  onRecoveredTurnStatus?(status: "idle" | "failed", turnId: string | undefined, error?: Error): void;
@@ -134,13 +136,6 @@ export declare class CodexSessionForwarder {
134
136
  * not doubled.
135
137
  */
136
138
  replayBackfill(turns: ResumedTurn[]): void;
137
- /** Reconcile only the exact active turn after an observer resume. Historical
138
- * items are intentionally not replayed on an existing-thread reconnect. */
139
- reconcileActiveTurn(turn: ResumedTurn | undefined): boolean;
140
- /** Reconcile an observer reconnect from the explicit resume snapshot. An
141
- * identified active turn must match exactly; with no active turn, only the
142
- * newest explicit terminal status is published and no history is replayed. */
143
- reconcileResumeTurns(turns: ResumedTurn[]): boolean;
144
139
  /** Fail an open response exactly once when its observer or terminal exits. */
145
140
  failOpenTurn(error: Error): boolean;
146
141
  private handle;
@@ -209,54 +209,6 @@ export class CodexSessionForwarder {
209
209
  this.sink.onTurnEnd(mapped.usage);
210
210
  }
211
211
  }
212
- /** Reconcile only the exact active turn after an observer resume. Historical
213
- * items are intentionally not replayed on an existing-thread reconnect. */
214
- reconcileActiveTurn(turn) {
215
- if (!this.isTurnOpen() || !turn || codexResumeTerminalStatus(turn) === undefined)
216
- return false;
217
- const resumedTurnId = turn.id ?? turn.turnId;
218
- if (!this.currentTurnIdValue || resumedTurnId !== this.currentTurnIdValue)
219
- return false;
220
- const mapped = mapCodexNotification("turn/completed", { turn });
221
- this.ensureTurn();
222
- this.settle(mapped.fatalError
223
- ? { kind: "error", error: mapped.fatalError }
224
- : { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
225
- return true;
226
- }
227
- /** Reconcile an observer reconnect from the explicit resume snapshot. An
228
- * identified active turn must match exactly; with no active turn, only the
229
- * newest explicit terminal status is published and no history is replayed. */
230
- reconcileResumeTurns(turns) {
231
- if (this.currentThreadIdValue === null)
232
- return false;
233
- for (let index = turns.length - 1; index >= 0; index -= 1) {
234
- const turn = turns[index];
235
- if (!turn)
236
- continue;
237
- const turnId = turn.id ?? turn.turnId;
238
- if (!turnId)
239
- return false;
240
- if (this.currentTurnIdValue !== null && this.currentTurnIdValue !== turnId) {
241
- return false;
242
- }
243
- const status = codexResumeTerminalStatus(turn);
244
- if (!status)
245
- return false;
246
- const mapped = mapCodexNotification("turn/completed", { turn });
247
- if (this.currentTurnIdValue !== null || this.turnOpen) {
248
- this.ensureTurn();
249
- this.settle(mapped.fatalError
250
- ? { kind: "error", error: mapped.fatalError }
251
- : { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
252
- }
253
- else {
254
- this.sink.onRecoveredTurnStatus?.(status, turnId, mapped.fatalError);
255
- }
256
- return true;
257
- }
258
- return false;
259
- }
260
212
  /** Fail an open response exactly once when its observer or terminal exits. */
261
213
  failOpenTurn(error) {
262
214
  if (!this.isTurnOpen())
@@ -347,7 +299,9 @@ export class CodexSessionForwarder {
347
299
  }
348
300
  this.scheduleCompletion(mapped.fatalError
349
301
  ? { kind: "error", error: mapped.fatalError }
350
- : { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
302
+ : mapped.turnInterrupted
303
+ ? { kind: "interrupted", ...(mapped.usage ? { usage: mapped.usage } : {}) }
304
+ : { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
351
305
  return;
352
306
  }
353
307
  // A late event from an older turn must not replace the app-server's active
@@ -407,7 +361,9 @@ export class CodexSessionForwarder {
407
361
  return;
408
362
  }
409
363
  if (mapped.turnCompleted) {
410
- this.scheduleCompletion({ kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
364
+ this.scheduleCompletion(mapped.turnInterrupted
365
+ ? { kind: "interrupted", ...(mapped.usage ? { usage: mapped.usage } : {}) }
366
+ : { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
411
367
  }
412
368
  else {
413
369
  this.refreshCompletionGrace();
@@ -449,6 +405,12 @@ export class CodexSessionForwarder {
449
405
  this.pendingCompletionTurnId = null;
450
406
  if (completion.kind === "error")
451
407
  this.sink.onTurnError(completion.error);
408
+ else if (completion.kind === "interrupted") {
409
+ if (this.sink.onTurnInterrupted)
410
+ this.sink.onTurnInterrupted(completion.usage);
411
+ else
412
+ this.sink.onTurnEnd(completion.usage);
413
+ }
452
414
  else
453
415
  this.sink.onTurnEnd(completion.usage);
454
416
  }
@@ -11,6 +11,7 @@ export interface CodexMapResult {
11
11
  finalText?: string;
12
12
  usage?: Record<string, unknown>;
13
13
  turnCompleted?: boolean;
14
+ turnInterrupted?: boolean;
14
15
  fatalError?: Error;
15
16
  }
16
17
  export declare function codexTurnStatus(turn: unknown): string | undefined;
@@ -241,7 +241,14 @@ export function mapCodexNotification(method, params) {
241
241
  case "turn/failed": {
242
242
  const turnPayload = typed.params?.turn;
243
243
  const fatalError = terminalTurnError(turnPayload, typed.method);
244
- return { events, ...(fatalError ? { fatalError } : {}), turnCompleted: true };
244
+ const turnInterrupted = typed.method === "turn/completed" &&
245
+ ["interrupted", "cancelled", "canceled"].includes(codexTurnStatus(turnPayload) ?? "");
246
+ return {
247
+ events,
248
+ ...(fatalError ? { fatalError } : {}),
249
+ turnCompleted: true,
250
+ ...(turnInterrupted ? { turnInterrupted: true } : {}),
251
+ };
245
252
  }
246
253
  case "turn/plan/updated": {
247
254
  const planParams = typed.params;
@@ -105,8 +105,7 @@ export interface ThreadStartParams {
105
105
  }
106
106
  export interface ThreadResumeParams extends ThreadStartParams {
107
107
  threadId: string;
108
- /** Legacy whole-backlog suppression. New callers should prefer
109
- * `initialTurnsPage` so recovery can fetch one summarized terminal status. */
108
+ /** Suppress rollout history when the caller only needs to load/subscribe. */
110
109
  excludeTurns?: boolean;
111
110
  initialTurnsPage?: {
112
111
  limit?: number | null;
package/dist/host.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { type ResolvedExecutionSnapshot, type ResolvedExecutionBudget, type RuntimeUserInput, type SessionWorkspaceSnapshot, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
1
+ import { type ResolvedExecutionSnapshot, type ResolvedExecutionBudget, type RuntimeUserInput, type LiveSessionFailure, type SessionWorkspaceSnapshot, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
2
2
  import { type AgentRuntimeId } from "@rynx-ai/core";
3
3
  import { type AppConfig } from "@rynx-ai/core";
4
4
  import { createCodexChildEnv } from "./codex-child-env.js";
5
- import type { InjectOutcome } from "./runner/protocol.js";
5
+ import type { InjectResult } from "./runner/protocol.js";
6
6
  import type { ResolveInteractionResult } from "./interactions.js";
7
7
  import { CodexAppServerClient } from "./codex-app-server/client.js";
8
8
  import type { ModelListResponse, ThreadGoal } from "./codex-app-server/protocol.js";
@@ -125,7 +125,9 @@ export declare class LocalAgentHost implements CodexCapabilities {
125
125
  private readonly clock;
126
126
  private readonly backendIdleTtlMs;
127
127
  private readonly forwarderClientFactory;
128
- private readonly observerReconnectTimeoutMs;
128
+ private readonly preloadClientFactory;
129
+ /** Builds one short-lived message/interrupt client. */
130
+ private readonly injectionClientFactory;
129
131
  private readonly backends;
130
132
  private readonly sessionId;
131
133
  private readonly runtimeHomeSessionId;
@@ -146,7 +148,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
146
148
  /** Short-lived dedupe for managed fork notifications delivered after the
147
149
  * `thread/fork` response. Values are expected source Provider thread ids. */
148
150
  private readonly managedForkThreadStarts;
149
- constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory, observerReconnectTimeoutMs, sessionId, runtimeHomeSessionId, }: {
151
+ constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory, preloadClientFactory, injectionClientFactory, sessionId, runtimeHomeSessionId, }: {
150
152
  config: AppConfig;
151
153
  commandRunner?: CodexCommandRunner;
152
154
  sessionStore?: CodexSessionStore;
@@ -158,8 +160,10 @@ export declare class LocalAgentHost implements CodexCapabilities {
158
160
  backendIdleTtlMs?: number;
159
161
  /** Override the live-session forwarder connection factory (tests inject a fake). */
160
162
  forwarderClientFactory?: (appServerUrl: string) => CodexAppServerClient;
161
- /** Grace window for observer reconnect before an active response is failed. */
162
- observerReconnectTimeoutMs?: number;
163
+ /** Override the short-lived known-thread preload connection factory. */
164
+ preloadClientFactory?: (appServerUrl: string) => CodexAppServerClient;
165
+ /** Override the short-lived message/interrupt connection factory. */
166
+ injectionClientFactory?: (appServerUrl: string) => CodexAppServerClient;
163
167
  /** The rynx session (localThreadId) this host serves — a per-session runner
164
168
  * child sets it from `RYNX_RUNNER_SESSION` so the private CODEX_HOME is
165
169
  * session-scoped. The shared `__cap__` child / tests fall back to a sentinel
@@ -219,33 +223,39 @@ export declare class LocalAgentHost implements CodexCapabilities {
219
223
  /** Bind a session's codex thread id once known (TUI broadcast or store): persist
220
224
  * it, unblock injection, and kick off the resume-subscribe loop (once). */
221
225
  private onLiveThreadStarted;
226
+ /** Start the known-thread observer after the replacement TUI has launched.
227
+ * Fresh sessions already connected their discovery listener before launch;
228
+ * this is therefore an idempotent no-op for them and for a healthy observer. */
229
+ startLiveCodexObserver(localThreadId: string): void;
230
+ /** Omnigent treats the forwarder as a required component of one native
231
+ * lifecycle: if its transport dies, it closes the app-server instead of
232
+ * accepting turns that can no longer reach the canonical mirror. */
233
+ private failObserverLifecycle;
222
234
  private shouldIgnoreManagedForkThreadStarted;
223
235
  private rememberManagedForkThreadStart;
224
236
  /**
225
237
  * Subscribe the forwarder connection to a thread (reference implementation's
226
238
  * `_subscribe_until_ready`). A fresh TUI thread has no rollout until its first
227
239
  * turn, so `thread/resume` is retried: park until the forwarder observes the
228
- * thread active, then retry. Resume only fetches the newest summarized Turn:
229
- * recovery reconciles the exact active Turn or publishes the newest explicit
230
- * terminal status without replaying historical items. Once resume succeeds,
231
- * subsequent turns arrive live.
240
+ * thread active, then retry. Like Omnigent, the first attempt always uses
241
+ * `excludeTurns`: a known-session cold resume therefore never reconstructs
242
+ * historical running/completed state. Only a fresh thread whose first attempt
243
+ * failed as not-ready retries without `excludeTurns`, backfilling the newly
244
+ * materialized first turn and de-duplicating it against live notifications.
232
245
  */
233
246
  private subscribeUntilReady;
234
- /** Restore the independent observer after an unexpected exit. The active turn
235
- * remains open during the bounded grace so resume can reconcile its exact id. */
236
- private reconnectForwarder;
237
- private armObserverReconnectDeadline;
238
- private clearObserverReconnectDeadline;
239
247
  /** Await a live session's thread binding. `null` leaves the deadline to the
240
248
  * caller; a number keeps the Provider-local bound. Returns false on timeout /
241
249
  * no live session. Injection and the runner's `live.ready` gate on this. */
242
250
  waitLiveReady(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
243
- /** Await the stronger Terminal gate: another app-server connection has
244
- * successfully resumed the thread, so the detached TUI cannot race rollout
245
- * discovery or indexing. */
251
+ /** Await the observer subscription diagnostic. Codex/Traex Terminal startup
252
+ * deliberately does not gate on this promise: the dedicated preload owns resume,
253
+ * while the independent observer attaches in the background. */
246
254
  waitTerminalReady(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
247
255
  /** Diagnostic from the provider adapter when native discovery/resume failed. */
248
256
  liveSessionError(localThreadId: string): string | undefined;
257
+ /** Structured phase + concrete cause used by the runner wire protocol. */
258
+ liveSessionFailure(localThreadId: string): LiveSessionFailure | undefined;
249
259
  /** Publish the background TUI/thread discovery failure so an executor
250
260
  * already waiting in the 60s bridge window exits immediately with the exact
251
261
  * 30s discovery cause. */
@@ -257,13 +267,13 @@ export declare class LocalAgentHost implements CodexCapabilities {
257
267
  * resumed from the persisted native id. Serialized per session so two
258
268
  * injects can't double-open a turn.
259
269
  *
260
- * Returns an {@link InjectOutcome}: `notLive` when this session has no live
270
+ * Returns an {@link InjectResult}: `notLive` when this session has no live
261
271
  * forwarder (caller may use the run path); `notReady`/`failed` are hard errors
262
272
  * the caller reports WITHOUT re-running (re-running double-writes alongside the
263
273
  * forwarder). Park-until-ready (~60s), aligning reference implementation's executor waiting for
264
274
  * the bridge instead of a short race that falls back to a second output path.
265
275
  */
266
- injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectOutcome>;
276
+ injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectResult>;
267
277
  /**
268
278
  * Interrupt the session's active turn — the web Stop button. codex: the
269
279
  * app-server `turn/interrupt` on the active `{threadId, turnId}` (exactly what
@@ -277,6 +287,11 @@ export declare class LocalAgentHost implements CodexCapabilities {
277
287
  stopLiveCodexSession(localThreadId: string, opts?: {
278
288
  deferClaudeInteractionCleanup?: boolean;
279
289
  }): void;
290
+ /** Tear down one codex-lineage native runtime without deleting its durable
291
+ * session-store binding. Omnigent couples its auxiliary Terminal, observer,
292
+ * forwarder and per-session app-server as one disposable runtime envelope;
293
+ * the next message recreates that envelope and cold-resumes the native id. */
294
+ teardownLiveCodexSession(localThreadId: string, error?: Error): boolean;
280
295
  /** Complete the second shutdown phase after the runner has killed all native
281
296
  * terminals and hook subprocesses. Must run before the runner process exits. */
282
297
  finalizeStoppedLiveSessions(): void;
@@ -298,7 +313,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
298
313
  * serialized per session. Parks until the thread is ready AND the tmux injector
299
314
  * is (re)attached, then pastes; `injectViaTerminal` RAISES if the prompt never
300
315
  * appears (reference implementation RAISE), so a not-ready pane is a hard error — NOT a
301
- * fall-through-to-run signal. Returns {@link InjectOutcome}. */
316
+ * fall-through-to-run signal. Returns {@link InjectResult}. */
302
317
  private injectClaude;
303
318
  /** Park until the claude session's tmux injector is (re)attached by the
304
319
  * runner-child, or the deadline passes. Pane relaunch re-attaches it via
@@ -339,7 +354,3 @@ export declare function isUnsupportedMethodError(error: unknown): boolean;
339
354
  * — a fresh TUI thread before its first turn. Retryable (park until active).
340
355
  * Mirrors reference implementation's `_is_thread_not_ready_error`. */
341
356
  export declare function isThreadNotReadyError(error: unknown): boolean;
342
- /** A persisted thread id that a freshly started app-server cannot load yet.
343
- * During startup, both errors can be transient while the rollout index catches
344
- * up. Retry the same id; never use either error as permission to replace it. */
345
- export declare function isRetryableThreadResumeError(error: unknown): boolean;