@rynx-ai/runtime 0.1.11-beta.2 → 0.1.11-beta.21

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.
@@ -105,11 +105,14 @@ export interface ThreadStartParams {
105
105
  }
106
106
  export interface ThreadResumeParams extends ThreadStartParams {
107
107
  threadId: string;
108
- /** When true, the resume response omits the thread's `turns` backlog (used to
109
- * SUBSCRIBE without re-replaying history). When false/absent, the response
110
- * carries `thread.turns[].items[]` — the backfill the forwarder replays for a
111
- * fresh thread's first turn (reference implementation's `_replay_resume_response`). */
108
+ /** Legacy whole-backlog suppression. New callers should prefer
109
+ * `initialTurnsPage` so recovery can fetch one summarized terminal status. */
112
110
  excludeTurns?: boolean;
111
+ initialTurnsPage?: {
112
+ limit?: number | null;
113
+ sortDirection?: "asc" | "desc" | null;
114
+ itemsView?: "notLoaded" | "summary" | "full" | null;
115
+ } | null;
113
116
  }
114
117
  /** One turn in a resumed thread's backlog (`thread/resume` response). */
115
118
  export interface ResumedTurn {
@@ -169,7 +172,7 @@ export interface TurnSteerParams {
169
172
  expectedTurnId: string;
170
173
  input: UserInput[];
171
174
  }
172
- export type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
175
+ export type TurnStatus = "completed" | "interrupted" | "cancelled" | "canceled" | "failed" | "errored" | "inProgress";
173
176
  export interface TurnPlanStep {
174
177
  step: string;
175
178
  status: "pending" | "inProgress" | "completed";
@@ -28,6 +28,7 @@ async function freeLoopbackPort() {
28
28
  });
29
29
  });
30
30
  }
31
+ const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
31
32
  export class WsRpcChannel {
32
33
  opts;
33
34
  child = null;
@@ -40,7 +41,7 @@ export class WsRpcChannel {
40
41
  url = "";
41
42
  constructor(opts) {
42
43
  this.opts = opts;
43
- this.readyTimeoutMs = opts.readyTimeoutMs ?? 15_000;
44
+ this.readyTimeoutMs = opts.readyTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
44
45
  }
45
46
  onLine(cb) {
46
47
  this.lineCb = cb;
@@ -114,7 +115,7 @@ export class WsRpcChannel {
114
115
  // eslint-disable-next-line no-constant-condition
115
116
  while (Date.now() < deadline) {
116
117
  try {
117
- return await this.tryConnect(url);
118
+ return await this.tryConnect(url, Math.max(1, deadline - Date.now()));
118
119
  }
119
120
  catch (error) {
120
121
  lastError = error;
@@ -123,9 +124,9 @@ export class WsRpcChannel {
123
124
  }
124
125
  throw new Error(`codex app-server ws did not become ready at ${url}: ${lastError?.message ?? "timeout"}`);
125
126
  }
126
- tryConnect(url) {
127
+ tryConnect(url, handshakeTimeout) {
127
128
  return new Promise((resolve, reject) => {
128
- const ws = new WebSocket(url);
129
+ const ws = new WebSocket(url, { handshakeTimeout });
129
130
  const onOpen = () => {
130
131
  ws.off("error", onError);
131
132
  resolve(ws);
@@ -166,7 +167,7 @@ export class ExternalWsChannel {
166
167
  /** The `ws://IP:PORT` of the already-running app-server to attach to. */
167
168
  url, opts = {}) {
168
169
  this.url = url;
169
- this.readyTimeoutMs = opts.readyTimeoutMs ?? 15_000;
170
+ this.readyTimeoutMs = opts.readyTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
170
171
  }
171
172
  onLine(cb) {
172
173
  this.lineCb = cb;
@@ -178,20 +179,16 @@ export class ExternalWsChannel {
178
179
  return this.ws?.readyState === WebSocket.OPEN;
179
180
  }
180
181
  async start() {
181
- const deadline = Date.now() + this.readyTimeoutMs;
182
- let lastError;
183
- while (Date.now() < deadline) {
184
- try {
185
- this.ws = await this.connect(this.url);
186
- break;
187
- }
188
- catch (error) {
189
- lastError = error;
190
- await new Promise((r) => setTimeout(r, 150));
191
- }
182
+ // A channel instance is reusable after an observer disconnect. Do not let
183
+ // the prior closed socket make a failed reconnect look successful, and arm
184
+ // close delivery for the newly connected socket.
185
+ this.ws = null;
186
+ this.closedEmitted = false;
187
+ try {
188
+ this.ws = await this.connect(this.url);
192
189
  }
193
- if (!this.ws) {
194
- throw new Error(`could not attach to app-server ws ${this.url}: ${lastError?.message ?? "timeout"}`);
190
+ catch (error) {
191
+ throw new Error(`could not attach to app-server ws ${this.url}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
195
192
  }
196
193
  this.ws.on("message", (data) => this.lineCb?.(data.toString()));
197
194
  this.ws.on("close", (code) => this.emitClose(code ?? null, null, null));
@@ -227,7 +224,10 @@ export class ExternalWsChannel {
227
224
  }
228
225
  connect(url) {
229
226
  return new Promise((resolve, reject) => {
230
- const ws = new WebSocket(url);
227
+ // The app-server owner has already completed its readiness probe. Use one
228
+ // bounded attach, then reuse this exact connection as the forwarder
229
+ // instead of running a second startup retry stage.
230
+ const ws = new WebSocket(url, { handshakeTimeout: this.readyTimeoutMs });
231
231
  const onOpen = () => {
232
232
  ws.off("error", onError);
233
233
  resolve(ws);
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { copyFileSync, cpSync, existsSync, mkdirSync, renameSync, rmSync, symlinkSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
- import { getRuntimeProfile, resolveRuntimeHome, SAFE_SKILL_NAME, } from "@rynx-ai/core";
5
+ import { assertSkillPathComponent, getRuntimeProfile, resolveRuntimeHome, } from "@rynx-ai/core";
6
6
  import { adoptLegacyRuntimeDirectory, legacyRuntimeStateRoot, runtimeSessionDigest, runtimeSessionStateDir, } from "./runtime-state-paths.js";
7
7
  /** Inherit the user's LIVE login by symlink (stays in sync). */
8
8
  const SYMLINK_FILES = ["auth.json"];
@@ -113,9 +113,7 @@ export function prepareRuntimeHome(sessionId, runtime, realHome = runtime === "c
113
113
  export function populateCodexSkills(codexHome, skills) {
114
114
  const skillsDir = join(codexHome, "skills");
115
115
  for (const skill of skills) {
116
- if (!SAFE_SKILL_NAME.test(skill.name)) {
117
- throw new Error(`unsafe skill name: ${skill.name}`);
118
- }
116
+ assertSkillPathComponent(skill.name);
119
117
  }
120
118
  if (skills.length === 0)
121
119
  return;
package/dist/host.d.ts CHANGED
@@ -125,6 +125,7 @@ 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
129
  private readonly backends;
129
130
  private readonly sessionId;
130
131
  private readonly runtimeHomeSessionId;
@@ -133,6 +134,10 @@ export declare class LocalAgentHost implements CodexCapabilities {
133
134
  private readonly runtimeHomes;
134
135
  private readonly liveSessions;
135
136
  private readonly liveClaudeSessions;
137
+ /** Exact pre-live startup failure, retained after the partial handle has been
138
+ * cleaned up so Core/plugin callers receive the failed phase, not a generic
139
+ * `live session unavailable` wrapper. */
140
+ private readonly liveStartupErrors;
136
141
  /** Claude forwarders stopped before their terminal is killed. Runner shutdown
137
142
  * finalizes these synchronously afterwards to scrub raw interaction answers. */
138
143
  private readonly pendingClaudeFinalizers;
@@ -141,7 +146,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
141
146
  /** Short-lived dedupe for managed fork notifications delivered after the
142
147
  * `thread/fork` response. Values are expected source Provider thread ids. */
143
148
  private readonly managedForkThreadStarts;
144
- constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory, sessionId, runtimeHomeSessionId, }: {
149
+ constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory, observerReconnectTimeoutMs, sessionId, runtimeHomeSessionId, }: {
145
150
  config: AppConfig;
146
151
  commandRunner?: CodexCommandRunner;
147
152
  sessionStore?: CodexSessionStore;
@@ -153,6 +158,8 @@ export declare class LocalAgentHost implements CodexCapabilities {
153
158
  backendIdleTtlMs?: number;
154
159
  /** Override the live-session forwarder connection factory (tests inject a fake). */
155
160
  forwarderClientFactory?: (appServerUrl: string) => CodexAppServerClient;
161
+ /** Grace window for observer reconnect before an active response is failed. */
162
+ observerReconnectTimeoutMs?: number;
156
163
  /** The rynx session (localThreadId) this host serves — a per-session runner
157
164
  * child sets it from `RYNX_RUNNER_SESSION` so the private CODEX_HOME is
158
165
  * session-scoped. The shared `__cap__` child / tests fall back to a sentinel
@@ -191,6 +198,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
191
198
  args: string[];
192
199
  cwd: string;
193
200
  env?: Record<string, string>;
201
+ skipTraexStartupPrompts?: boolean;
194
202
  } | null>;
195
203
  /**
196
204
  * Bring up (idempotently) a session's persistent codex forwarder and emit its
@@ -217,20 +225,31 @@ export declare class LocalAgentHost implements CodexCapabilities {
217
225
  * Subscribe the forwarder connection to a thread (reference implementation's
218
226
  * `_subscribe_until_ready`). A fresh TUI thread has no rollout until its first
219
227
  * turn, so `thread/resume` is retried: park until the forwarder observes the
220
- * thread active, then retry WITHOUT `excludeTurns` so the response backfills the
221
- * first turn's items (replayed, deduped against the live stream). Once resume
222
- * succeeds, subsequent turns arrive as live notifications on this connection.
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.
223
232
  */
224
233
  private subscribeUntilReady;
225
- /** Await a live session's thread binding (bounded). Returns false on timeout /
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
+ /** Await a live session's thread binding. `null` leaves the deadline to the
240
+ * caller; a number keeps the Provider-local bound. Returns false on timeout /
226
241
  * no live session. Injection and the runner's `live.ready` gate on this. */
227
- waitLiveReady(localThreadId: string, timeoutMs?: number): Promise<boolean>;
242
+ waitLiveReady(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
228
243
  /** Await the stronger Terminal gate: another app-server connection has
229
244
  * successfully resumed the thread, so the detached TUI cannot race rollout
230
245
  * discovery or indexing. */
231
- waitTerminalReady(localThreadId: string, timeoutMs?: number): Promise<boolean>;
246
+ waitTerminalReady(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
232
247
  /** Diagnostic from the provider adapter when native discovery/resume failed. */
233
248
  liveSessionError(localThreadId: string): string | undefined;
249
+ /** Publish the background TUI/thread discovery failure so an executor
250
+ * already waiting in the 60s bridge window exits immediately with the exact
251
+ * 30s discovery cause. */
252
+ failLiveStartup(localThreadId: string, error: Error): boolean;
234
253
  /**
235
254
  * Inject a user turn into a session's live codex thread — reference implementation's
236
255
  * single-writer web send. `turn/steer` when a turn is open (mid-turn
@@ -290,6 +309,8 @@ export declare class LocalAgentHost implements CodexCapabilities {
290
309
  * terminal registry — the host does not hold tmux). Ignored for codex sessions
291
310
  * (they inject via the app-server). Idempotent. */
292
311
  attachTerminalInjector(localThreadId: string, injector: TerminalInjector): void;
312
+ /** Close an active native response when its terminal or runner disappears. */
313
+ failLiveSession(localThreadId: string, error: Error): boolean;
293
314
  /** List the models a runtime exposes (App Server for codex/traex; static for claude). */
294
315
  listModels(runtime?: AgentRuntimeId): Promise<ModelListResponse | null>;
295
316
  /**