@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.
- package/dist/codex-app-server/client.d.ts +4 -3
- package/dist/codex-app-server/client.js +35 -12
- package/dist/codex-app-server/forwarder.d.ts +0 -7
- package/dist/codex-app-server/forwarder.js +0 -48
- package/dist/codex-app-server/protocol.d.ts +1 -2
- package/dist/host.d.ts +27 -21
- package/dist/host.js +237 -376
- package/dist/runner/child.d.ts +12 -5
- package/dist/runner/child.js +115 -36
- package/dist/runner/manager.d.ts +10 -7
- package/dist/runner/manager.js +77 -51
- package/dist/runner/protocol.d.ts +10 -9
- package/dist/runner/startup-policy.d.ts +3 -0
- package/dist/runner/startup-policy.js +5 -0
- package/package.json +2 -2
|
@@ -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
|
|
98
|
-
*
|
|
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
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
|
1150
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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,
|
|
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
|
-
/**
|
|
162
|
-
|
|
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.
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
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
|
|
244
|
-
*
|
|
245
|
-
*
|
|
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;
|