@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.
@@ -1,5 +1,5 @@
1
1
  import { codexUserContent } from "../input-resources.js";
2
- import { mapCodexItem, mapCodexNotification } from "./mapping.js";
2
+ import { codexResumeTerminalStatus, mapCodexItem, mapCodexNotification, } from "./mapping.js";
3
3
  const MCP_STARTUP_STATUS_METHOD = "mcpServer/startupStatus/updated";
4
4
  const MCP_TERMINAL_STATES = new Set(["ready", "failed", "cancelled"]);
5
5
  function threadIdFrom(params) {
@@ -45,6 +45,20 @@ function isModelOutput(method, params) {
45
45
  const item = params?.item;
46
46
  return Boolean(item?.type && item.type !== "userMessage");
47
47
  }
48
+ function isTerminalTurnMethod(method) {
49
+ return method === "turn/completed" || method === "turn/failed";
50
+ }
51
+ /** Turn-scoped notifications must never be reassigned to whichever newer turn
52
+ * happens to be current when an app-server frame arrives late. */
53
+ function isTurnScopedMethod(method) {
54
+ return method.startsWith("turn/") || method.startsWith("item/") ||
55
+ method === "queue/status" || method === "error";
56
+ }
57
+ /** Recover a missed turn/started only from visible, fully-scoped assistant/plan
58
+ * deltas. Other deltas require an already matching active turn. */
59
+ function canRecoverMissedTurnFrom(method) {
60
+ return method === "item/agentMessage/delta" || method === "item/plan/delta";
61
+ }
48
62
  export class CodexSessionForwarder {
49
63
  client;
50
64
  sink;
@@ -55,6 +69,9 @@ export class CodexSessionForwarder {
55
69
  currentThreadIdValue = null;
56
70
  activeSignaled = false;
57
71
  completionTimer = null;
72
+ /** Turn id retained only for late-item dedup while a terminal response waits
73
+ * for its bounded output-ordering grace. It is not an active provider turn. */
74
+ pendingCompletionTurnId = null;
58
75
  assistantMessageTimer = null;
59
76
  deferredAssistantMessage = null;
60
77
  pendingCompletion = null;
@@ -97,12 +114,17 @@ export class CodexSessionForwarder {
97
114
  this.flushDeferredAssistantMessage();
98
115
  if (this.turnOpen) {
99
116
  this.turnOpen = false;
117
+ this.currentTurnIdValue = null;
100
118
  this.sink.onTurnEnd();
101
119
  }
120
+ else {
121
+ this.currentTurnIdValue = null;
122
+ }
102
123
  }
103
- /** True while a turn is open — injection uses `turn/steer` then, else `turn/start`. */
124
+ /** True while the provider owns an active turn, including the short interval
125
+ * between injection acceptance and observer confirmation. */
104
126
  isTurnOpen() {
105
- return this.turnOpen;
127
+ return this.turnOpen || this.currentTurnIdValue !== null;
106
128
  }
107
129
  /** The current turn's codex id (only meaningful while {@link isTurnOpen}). */
108
130
  currentTurnId() {
@@ -117,8 +139,9 @@ export class CodexSessionForwarder {
117
139
  noteTurnAccepted(turnId) {
118
140
  if (!turnId)
119
141
  return;
142
+ if (this.pendingCompletion)
143
+ this.flushPendingCompletion();
120
144
  this.currentTurnIdValue = turnId;
121
- this.ensureTurn();
122
145
  this.emitMcpStartupStatus();
123
146
  }
124
147
  hasPendingMcpStartup() {
@@ -131,7 +154,7 @@ export class CodexSessionForwarder {
131
154
  return pending;
132
155
  this.pendingMcpServers.clear();
133
156
  this.clearMcpStartupTimer();
134
- if (this.turnOpen) {
157
+ if (this.isTurnOpen()) {
135
158
  this.emitMcpStatus(`MCP startup cancelled: ${pending.join(", ")}`);
136
159
  }
137
160
  return pending;
@@ -150,6 +173,16 @@ export class CodexSessionForwarder {
150
173
  threadId() {
151
174
  return this.currentThreadIdValue;
152
175
  }
176
+ /** Seed an already-persisted/resumed thread binding. The bridge retains this
177
+ * state even when app-server does not rebroadcast `thread/started`, so
178
+ * terminal-boundary recovery must know it too. */
179
+ noteThreadBound(threadId) {
180
+ if (!threadId || this.currentThreadIdValue === threadId)
181
+ return;
182
+ if (this.currentThreadIdValue !== null || this.turnOpen)
183
+ return;
184
+ this.currentThreadIdValue = threadId;
185
+ }
153
186
  /**
154
187
  * Replay the backlog turns from a `thread/resume` response as if they were live
155
188
  * `item/completed` notifications — the fresh-thread first-turn backfill. Each
@@ -165,34 +198,22 @@ export class CodexSessionForwarder {
165
198
  this.ensureTurn();
166
199
  for (const item of turn.items ?? [])
167
200
  this.processCompletedItem(item);
168
- if (turn.status === "inProgress")
201
+ if (codexResumeTerminalStatus(turn) === undefined)
169
202
  continue;
170
203
  const mapped = mapCodexNotification("turn/completed", { turn });
171
204
  this.turnOpen = false;
205
+ this.currentTurnIdValue = null;
172
206
  if (mapped.fatalError)
173
207
  this.sink.onTurnError(mapped.fatalError);
174
208
  else
175
209
  this.sink.onTurnEnd(mapped.usage);
176
210
  }
177
211
  }
178
- /** Reconcile only the exact active turn after an observer resume. Historical
179
- * items are intentionally not replayed on an existing-thread reconnect. */
180
- reconcileActiveTurn(turn) {
181
- if (!this.turnOpen || !turn || turn.status === "inProgress")
182
- return false;
183
- const resumedTurnId = turn.id ?? turn.turnId;
184
- if (!this.currentTurnIdValue || resumedTurnId !== this.currentTurnIdValue)
185
- return false;
186
- const mapped = mapCodexNotification("turn/completed", { turn });
187
- this.settle(mapped.fatalError
188
- ? { kind: "error", error: mapped.fatalError }
189
- : { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
190
- return true;
191
- }
192
212
  /** Fail an open response exactly once when its observer or terminal exits. */
193
213
  failOpenTurn(error) {
194
- if (!this.turnOpen)
214
+ if (!this.isTurnOpen())
195
215
  return false;
216
+ this.ensureTurn();
196
217
  this.settle({ kind: "error", error });
197
218
  return true;
198
219
  }
@@ -240,17 +261,63 @@ export class CodexSessionForwarder {
240
261
  // behavior unchanged unless the Traex host explicitly opts in.
241
262
  if (method === "queue/status" && !this.options.surfaceQueueStatus)
242
263
  return;
264
+ if (method === "queue/status" && this.currentTurnIdValue === null)
265
+ return;
243
266
  const carriedTurnId = turnIdFrom(params);
244
267
  if (method === "turn/started") {
245
- this.flushPendingCompletion();
246
- this.flushDeferredAssistantMessage();
247
- if (carriedTurnId)
268
+ this.beginTurn(carriedTurnId);
269
+ this.sink.onTurnObserved?.(carriedTurnId);
270
+ return;
271
+ }
272
+ if (isTerminalTurnMethod(method)) {
273
+ if (!this.terminalBoundaryMatchesActiveTurn(carriedTurnId, notificationThreadId)) {
274
+ return;
275
+ }
276
+ const hadActiveTurn = this.currentTurnIdValue !== null;
277
+ const hadOpenResponse = this.turnOpen;
278
+ const mapped = mapCodexNotification(method, params);
279
+ if (!hadActiveTurn && !hadOpenResponse) {
280
+ const turn = params?.turn;
281
+ const recoveredStatus = mapped.fatalError ||
282
+ method === "turn/failed" ||
283
+ codexResumeTerminalStatus(turn) === "failed"
284
+ ? "failed"
285
+ : "idle";
286
+ this.sink.onRecoveredTurnStatus?.(recoveredStatus, carriedTurnId, mapped.fatalError);
287
+ return;
288
+ }
289
+ if (carriedTurnId && this.currentTurnIdValue === null) {
248
290
  this.currentTurnIdValue = carriedTurnId;
291
+ }
249
292
  this.ensureTurn();
293
+ this.pendingCompletionTurnId = carriedTurnId ?? this.currentTurnIdValue;
294
+ // The provider lifecycle closes immediately. A configured grace keeps
295
+ // only the canonical response open long enough to absorb late items.
296
+ this.currentTurnIdValue = null;
297
+ for (const event of mapped.events.filter((event) => event.type !== "runtime_debug")) {
298
+ this.sink.onEvent(event);
299
+ }
300
+ this.scheduleCompletion(mapped.fatalError
301
+ ? { kind: "error", error: mapped.fatalError }
302
+ : { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
250
303
  return;
251
304
  }
252
- if (carriedTurnId)
253
- this.currentTurnIdValue = carriedTurnId;
305
+ // A late event from an older turn must not replace the app-server's active
306
+ // turn id. That was the source of stale completions closing a newer turn.
307
+ if (carriedTurnId &&
308
+ this.currentTurnIdValue &&
309
+ carriedTurnId !== this.currentTurnIdValue &&
310
+ isTurnScopedMethod(method)) {
311
+ return;
312
+ }
313
+ if (carriedTurnId && this.currentTurnIdValue === null) {
314
+ if (canRecoverMissedTurnFrom(method)) {
315
+ if (!this.notificationMatchesCurrentThread(notificationThreadId))
316
+ return;
317
+ this.currentTurnIdValue = carriedTurnId;
318
+ this.ensureTurn();
319
+ }
320
+ }
254
321
  // Completed items (user echo, assistant, tool, reasoning) go through the
255
322
  // deduped path so a resume backfill and the live stream never double them.
256
323
  if (method === "item/completed") {
@@ -330,6 +397,8 @@ export class CodexSessionForwarder {
330
397
  settle(completion) {
331
398
  this.flushDeferredAssistantMessage();
332
399
  this.turnOpen = false;
400
+ this.currentTurnIdValue = null;
401
+ this.pendingCompletionTurnId = null;
333
402
  if (completion.kind === "error")
334
403
  this.sink.onTurnError(completion.error);
335
404
  else
@@ -373,7 +442,7 @@ export class CodexSessionForwarder {
373
442
  this.mcpStartupTimer = null;
374
443
  }
375
444
  emitMcpStartupStatus() {
376
- if (!this.turnOpen)
445
+ if (!this.isTurnOpen())
377
446
  return;
378
447
  const pending = [...this.pendingMcpServers].sort();
379
448
  if (pending.length) {
@@ -464,14 +533,15 @@ export class CodexSessionForwarder {
464
533
  * {@link advanceAnonCounter}). Mirrors reference implementation `_completed_item_key`. */
465
534
  completedItemKey(item) {
466
535
  const threadId = this.currentThreadIdValue ?? "thread";
467
- const turnId = this.currentTurnIdValue ?? "turn";
536
+ const turnId = this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
468
537
  if (item.id)
469
538
  return { key: `${threadId}:${turnId}:${item.id}`, isAnon: false };
470
539
  const n = this.anonCounters.get(`${threadId}:${turnId}`) ?? 0;
471
540
  return { key: `${threadId}:${turnId}:anon:${n}`, isAnon: true };
472
541
  }
473
542
  advanceAnonCounter() {
474
- const k = `${this.currentThreadIdValue ?? "thread"}:${this.currentTurnIdValue ?? "turn"}`;
543
+ const turnId = this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
544
+ const k = `${this.currentThreadIdValue ?? "thread"}:${turnId}`;
475
545
  this.anonCounters.set(k, (this.anonCounters.get(k) ?? 0) + 1);
476
546
  }
477
547
  ensureTurn() {
@@ -480,4 +550,45 @@ export class CodexSessionForwarder {
480
550
  this.sink.onTurnStart(this.currentTurnIdValue ?? undefined);
481
551
  }
482
552
  }
553
+ /** Start (or confirm) the app-server's authoritative active turn. A newer
554
+ * start supersedes an older response whose terminal edge arrived late; a
555
+ * pending Traex completion is flushed first so its final item grace remains
556
+ * intact. */
557
+ beginTurn(turnId) {
558
+ if (this.pendingCompletion)
559
+ this.flushPendingCompletion();
560
+ const nextTurnId = turnId ?? null;
561
+ const changesIdentifiedTurn = this.turnOpen &&
562
+ this.currentTurnIdValue !== null &&
563
+ nextTurnId !== null &&
564
+ this.currentTurnIdValue !== nextTurnId;
565
+ if (changesIdentifiedTurn) {
566
+ this.flushPendingCompletion();
567
+ this.flushDeferredAssistantMessage();
568
+ if (this.turnOpen) {
569
+ this.turnOpen = false;
570
+ this.sink.onTurnEnd(undefined, "superseded");
571
+ }
572
+ }
573
+ this.currentTurnIdValue = nextTurnId;
574
+ this.ensureTurn();
575
+ }
576
+ /** Active-turn clearing contract:
577
+ *
578
+ * - an identified active turn is closed only by the same id;
579
+ * - an id-less boundary cannot close an identified active turn;
580
+ * - with no observed active turn, an identified boundary may recover a
581
+ * missed start only when it carries the currently-bound thread id. */
582
+ terminalBoundaryMatchesActiveTurn(terminalTurnId, notificationThreadId) {
583
+ if (this.currentTurnIdValue !== null) {
584
+ return terminalTurnId === this.currentTurnIdValue;
585
+ }
586
+ if (terminalTurnId === undefined)
587
+ return true;
588
+ return this.notificationMatchesCurrentThread(notificationThreadId);
589
+ }
590
+ notificationMatchesCurrentThread(notificationThreadId) {
591
+ return this.currentThreadIdValue !== null &&
592
+ notificationThreadId === this.currentThreadIdValue;
593
+ }
483
594
  }
@@ -13,6 +13,8 @@ export interface CodexMapResult {
13
13
  turnCompleted?: boolean;
14
14
  fatalError?: Error;
15
15
  }
16
+ export declare function codexTurnStatus(turn: unknown): string | undefined;
17
+ export declare function codexResumeTerminalStatus(turn: unknown): "idle" | "failed" | undefined;
16
18
  /** Map one thread item (`item/started` | `item/completed`) to events. */
17
19
  export declare function mapCodexItem(method: string, item: ThreadItem): CodexMapResult;
18
20
  /** Map one codex app-server notification to events (+ turn/usage/error signals). */
@@ -1,12 +1,98 @@
1
+ function isRecord(value) {
2
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+ const CODEX_AUTH_ERROR_FRAGMENTS = [
5
+ "401",
6
+ "403",
7
+ "unauthorized",
8
+ "authentication",
9
+ "not logged in",
10
+ "not authenticated",
11
+ "log in",
12
+ "login",
13
+ "sign in",
14
+ "re-authenticate",
15
+ "reauthenticate",
16
+ "credentials",
17
+ "access token",
18
+ "token expired",
19
+ "expired token",
20
+ "session expired",
21
+ "api key",
22
+ ];
23
+ const CODEX_REAUTH_HINT = "Codex needs you to re-authenticate. Run `codex login` and retry.";
24
+ function codexErrorMessage(error) {
25
+ for (const key of ["message", "error", "text", "detail"]) {
26
+ const value = error[key];
27
+ if (typeof value === "string" && value.trim())
28
+ return value.trim();
29
+ }
30
+ return "Codex turn ended with an unspecified error.";
31
+ }
32
+ function codexErrorIsAuth(error, message) {
33
+ const info = error.codexErrorInfo;
34
+ let variant;
35
+ let httpStatusCode;
36
+ if (typeof info === "string") {
37
+ variant = info;
38
+ }
39
+ else if (isRecord(info)) {
40
+ variant = info.type ?? info.kind ?? info.variant;
41
+ httpStatusCode = info.httpStatusCode;
42
+ }
43
+ if (typeof variant === "string" && variant.toLowerCase() === "unauthorized")
44
+ return true;
45
+ if (httpStatusCode === 401 || httpStatusCode === 403)
46
+ return true;
47
+ const lowered = message.toLowerCase();
48
+ return CODEX_AUTH_ERROR_FRAGMENTS.some((fragment) => lowered.includes(fragment));
49
+ }
1
50
  function turnError(error, fallback) {
2
- if (typeof error === "string" && error.trim())
3
- return new Error(error);
4
- if (error && typeof error === "object" && "message" in error) {
5
- const message = error.message;
6
- if (typeof message === "string" && message.trim())
7
- return new Error(message);
51
+ if (!error)
52
+ return new Error(fallback);
53
+ const message = codexErrorMessage(error);
54
+ return new Error(codexErrorIsAuth(error, message)
55
+ ? `${message}\n\n${CODEX_REAUTH_HINT}`
56
+ : message);
57
+ }
58
+ export function codexTurnStatus(turn) {
59
+ if (!isRecord(turn))
60
+ return undefined;
61
+ const status = turn.status;
62
+ if (typeof status === "string")
63
+ return status;
64
+ if (!isRecord(status))
65
+ return undefined;
66
+ const value = status.type ?? status.status;
67
+ return typeof value === "string" ? value : undefined;
68
+ }
69
+ export function codexResumeTerminalStatus(turn) {
70
+ if (terminalTurnError(turn, "turn/completed"))
71
+ return "failed";
72
+ const status = codexTurnStatus(turn);
73
+ if (status === "completed" || status === "interrupted" ||
74
+ status === "cancelled" || status === "canceled") {
75
+ return "idle";
8
76
  }
9
- return new Error(fallback);
77
+ if (status === "failed" || status === "errored")
78
+ return "failed";
79
+ return undefined;
80
+ }
81
+ function terminalTurnError(turn, method) {
82
+ if (!isRecord(turn)) {
83
+ return method === "turn/failed" ? new Error("Codex turn failed") : undefined;
84
+ }
85
+ let error = turn.error;
86
+ if (!isRecord(error) && Array.isArray(turn.items)) {
87
+ error = turn.items.find((item) => isRecord(item) && item.type === "error");
88
+ }
89
+ if (isRecord(error))
90
+ return turnError(error, "Codex turn failed");
91
+ const status = codexTurnStatus(turn);
92
+ if (method === "turn/failed" || status === "failed" || status === "errored") {
93
+ return turnError(undefined, "Codex turn failed");
94
+ }
95
+ return undefined;
10
96
  }
11
97
  function webSearchInput(item) {
12
98
  const action = item.action;
@@ -151,23 +237,11 @@ export function mapCodexNotification(method, params) {
151
237
  return { events };
152
238
  case "turn/started":
153
239
  return { events };
154
- case "turn/completed": {
240
+ case "turn/completed":
241
+ case "turn/failed": {
155
242
  const turnPayload = typed.params?.turn;
156
- if (turnPayload?.status === "failed") {
157
- return {
158
- events,
159
- fatalError: turnError(turnPayload.error, "Codex turn failed"),
160
- turnCompleted: true,
161
- };
162
- }
163
- if (turnPayload?.status === "interrupted") {
164
- return {
165
- events,
166
- fatalError: turnError(turnPayload.error, "Codex turn was interrupted"),
167
- turnCompleted: true,
168
- };
169
- }
170
- return { events, turnCompleted: true };
243
+ const fatalError = terminalTurnError(turnPayload, typed.method);
244
+ return { events, ...(fatalError ? { fatalError } : {}), turnCompleted: true };
171
245
  }
172
246
  case "turn/plan/updated": {
173
247
  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;
@@ -172,7 +171,7 @@ export interface TurnSteerParams {
172
171
  expectedTurnId: string;
173
172
  input: UserInput[];
174
173
  }
175
- export type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
174
+ export type TurnStatus = "completed" | "interrupted" | "cancelled" | "canceled" | "failed" | "errored" | "inProgress";
176
175
  export interface TurnPlanStep {
177
176
  step: string;
178
177
  status: "pending" | "inProgress" | "completed";
package/dist/host.d.ts CHANGED
@@ -1,4 +1,4 @@
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";
@@ -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,32 +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's terminal status and never replays
230
- * historical items. Once resume succeeds, 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.
231
245
  */
232
246
  private subscribeUntilReady;
233
- /** Restore the independent observer after an unexpected exit. The active turn
234
- * remains open during the bounded grace so resume can reconcile its exact id. */
235
- private reconnectForwarder;
236
- private armObserverReconnectDeadline;
237
- private clearObserverReconnectDeadline;
238
247
  /** Await a live session's thread binding. `null` leaves the deadline to the
239
248
  * caller; a number keeps the Provider-local bound. Returns false on timeout /
240
249
  * no live session. Injection and the runner's `live.ready` gate on this. */
241
250
  waitLiveReady(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
242
- /** Await the stronger Terminal gate: another app-server connection has
243
- * successfully resumed the thread, so the detached TUI cannot race rollout
244
- * 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. */
245
254
  waitTerminalReady(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
246
255
  /** Diagnostic from the provider adapter when native discovery/resume failed. */
247
256
  liveSessionError(localThreadId: string): string | undefined;
257
+ /** Structured phase + concrete cause used by the runner wire protocol. */
258
+ liveSessionFailure(localThreadId: string): LiveSessionFailure | undefined;
248
259
  /** Publish the background TUI/thread discovery failure so an executor
249
260
  * already waiting in the 60s bridge window exits immediately with the exact
250
261
  * 30s discovery cause. */
@@ -338,7 +349,3 @@ export declare function isUnsupportedMethodError(error: unknown): boolean;
338
349
  * — a fresh TUI thread before its first turn. Retryable (park until active).
339
350
  * Mirrors reference implementation's `_is_thread_not_ready_error`. */
340
351
  export declare function isThreadNotReadyError(error: unknown): boolean;
341
- /** A persisted thread id that a freshly started app-server cannot load yet.
342
- * During startup, both errors can be transient while the rollout index catches
343
- * up. Retry the same id; never use either error as permission to replace it. */
344
- export declare function isRetryableThreadResumeError(error: unknown): boolean;