@rynx-ai/runtime 0.1.11-beta.21 → 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.
@@ -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;
@@ -134,13 +134,6 @@ export declare class CodexSessionForwarder {
134
134
  * not doubled.
135
135
  */
136
136
  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
137
  /** Fail an open response exactly once when its observer or terminal exits. */
145
138
  failOpenTurn(error: Error): boolean;
146
139
  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())
@@ -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,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,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. */
@@ -339,7 +349,3 @@ export declare function isUnsupportedMethodError(error: unknown): boolean;
339
349
  * — a fresh TUI thread before its first turn. Retryable (park until active).
340
350
  * Mirrors reference implementation's `_is_thread_not_ready_error`. */
341
351
  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;