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

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.
Files changed (36) hide show
  1. package/dist/claude/native-bridge.js +3 -8
  2. package/dist/claude/native-integration.d.ts +12 -1
  3. package/dist/claude/native-integration.js +16 -2
  4. package/dist/claude/transcript.d.ts +0 -7
  5. package/dist/claude/transcript.js +6 -20
  6. package/dist/codex-app-server/client.d.ts +2 -1
  7. package/dist/codex-app-server/forwarder.d.ts +4 -1
  8. package/dist/codex-app-server/forwarder.js +19 -1
  9. package/dist/codex-app-server/protocol.d.ts +45 -1
  10. package/dist/codex-home.d.ts +9 -26
  11. package/dist/codex-home.js +37 -65
  12. package/dist/codex-session-store.d.ts +22 -10
  13. package/dist/codex-session-store.js +277 -12
  14. package/dist/host.d.ts +47 -47
  15. package/dist/host.js +790 -350
  16. package/dist/index.d.ts +1 -2
  17. package/dist/index.js +0 -1
  18. package/dist/models-catalog.d.ts +1 -0
  19. package/dist/models-catalog.js +43 -1
  20. package/dist/provider-workspace.d.ts +56 -0
  21. package/dist/provider-workspace.js +83 -0
  22. package/dist/runner/child.d.ts +54 -6
  23. package/dist/runner/child.js +42 -17
  24. package/dist/runner/manager.d.ts +41 -18
  25. package/dist/runner/manager.js +432 -55
  26. package/dist/runner/protocol.d.ts +7 -18
  27. package/dist/runner-main.js +12 -4
  28. package/dist/runtime-state-paths.d.ts +10 -0
  29. package/dist/runtime-state-paths.js +53 -0
  30. package/dist/terminal/claude-tui.d.ts +8 -1
  31. package/dist/terminal/claude-tui.js +7 -1
  32. package/dist/terminal/codex-tui.d.ts +5 -1
  33. package/dist/terminal/codex-tui.js +12 -3
  34. package/package.json +2 -2
  35. package/dist/codex/rollout-synth.d.ts +0 -42
  36. package/dist/codex/rollout-synth.js +0 -245
@@ -1,8 +1,14 @@
1
+ import { execFile } from "node:child_process";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { constants as fsConstants } from "node:fs";
3
- import { access, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
4
+ import { access, mkdir, readFile, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
4
5
  import path from "node:path";
5
- import { normalizeRuntimeId } from "@rynx-ai/core";
6
+ import { promisify } from "node:util";
7
+ import { rynxHome, } from "@rynx-ai/core";
8
+ const FILE_LOCK_LEASE_MS = 5_000;
9
+ const FILE_LOCK_HEARTBEAT_MS = 1_000;
10
+ const execFileAsync = promisify(execFile);
11
+ let ownProcessIdentity;
6
12
  export class FileCodexSessionStore {
7
13
  filePath;
8
14
  tail = Promise.resolve();
@@ -44,6 +50,27 @@ export class FileCodexSessionStore {
44
50
  }
45
51
  return null;
46
52
  }
53
+ async getClaudeForkIntent(targetSessionId) {
54
+ const data = await this.readAll();
55
+ return data.claudeForkIntents?.[targetSessionId] ?? null;
56
+ }
57
+ async setClaudeForkIntent(intent) {
58
+ await this.runExclusive(async () => {
59
+ const data = await this.readAll();
60
+ data.claudeForkIntents ??= {};
61
+ data.claudeForkIntents[intent.targetSessionId] = intent;
62
+ await this.writeAll(data);
63
+ });
64
+ }
65
+ async deleteClaudeForkIntent(targetSessionId) {
66
+ await this.runExclusive(async () => {
67
+ const data = await this.readAll();
68
+ if (!data.claudeForkIntents?.[targetSessionId])
69
+ return;
70
+ delete data.claudeForkIntents[targetSessionId];
71
+ await this.writeAll(data);
72
+ });
73
+ }
47
74
  async isWritable() {
48
75
  try {
49
76
  await mkdir(path.dirname(this.filePath), { recursive: true });
@@ -66,14 +93,22 @@ export class FileCodexSessionStore {
66
93
  if (!parsed || typeof parsed !== "object") {
67
94
  return {};
68
95
  }
69
- // Backfill the runtime on legacy records written before multi-runtime
70
- // support (default to codex), and normalize the renamed `traecli` runtime
71
- // to `traex` so routing and thread-binding checks stay consistent.
96
+ // The provider binding no longer owns workspace/execution state. Keep
97
+ // accepting legacy JSON fields, but strip them from the in-memory record
98
+ // so callers cannot accidentally treat them as a second snapshot.
72
99
  if (parsed.sessions) {
73
- for (const record of Object.values(parsed.sessions)) {
74
- if (record) {
75
- record.runtime = normalizeRuntimeId(record.runtime) ?? "codex";
76
- }
100
+ for (const [id, record] of Object.entries(parsed.sessions)) {
101
+ if (!record)
102
+ continue;
103
+ parsed.sessions[id] = {
104
+ localThreadId: record.localThreadId,
105
+ codexSessionId: record.codexSessionId,
106
+ ...(record.parentSessionId ? { parentSessionId: record.parentSessionId } : {}),
107
+ ...(record.runtimeHomeOwnerSessionId
108
+ ? { runtimeHomeOwnerSessionId: record.runtimeHomeOwnerSessionId }
109
+ : {}),
110
+ updatedAt: record.updatedAt,
111
+ };
77
112
  }
78
113
  }
79
114
  return parsed;
@@ -98,16 +133,113 @@ export class FileCodexSessionStore {
98
133
  release = resolve;
99
134
  });
100
135
  await previous.catch(() => undefined);
136
+ let releaseFileLock;
101
137
  try {
138
+ releaseFileLock = await this.acquireFileLock();
102
139
  return await task();
103
140
  }
104
141
  finally {
105
- release();
142
+ try {
143
+ await releaseFileLock?.();
144
+ }
145
+ finally {
146
+ release();
147
+ }
148
+ }
149
+ }
150
+ async acquireFileLock() {
151
+ await mkdir(path.dirname(this.filePath), { recursive: true });
152
+ const lockPath = `${this.filePath}.lock`;
153
+ const recoveryPath = `${lockPath}.recovery`;
154
+ const token = randomUUID();
155
+ const ownerPath = path.join(lockPath, "owner");
156
+ ownProcessIdentity ??= processIdentity(process.pid);
157
+ const processIdentityValue = await ownProcessIdentity;
158
+ if (!processIdentityValue) {
159
+ throw new Error("could not resolve Session binding lock process identity");
106
160
  }
161
+ for (let attempt = 0; attempt < 1_000; attempt += 1) {
162
+ if (await pathExists(recoveryPath)) {
163
+ await recoverExpiredLockDirectory(recoveryPath);
164
+ await lockRetryDelay();
165
+ continue;
166
+ }
167
+ if (await publishOwnedLockDirectory(lockPath, token, processIdentityValue)) {
168
+ const heartbeat = startLockHeartbeat(lockPath);
169
+ let recoveryWaits = 0;
170
+ while (await pathExists(recoveryPath)) {
171
+ await recoverExpiredLockDirectory(recoveryPath);
172
+ recoveryWaits += 1;
173
+ if (recoveryWaits >= 1_000) {
174
+ clearInterval(heartbeat);
175
+ await releaseOwnedLockDirectory(lockPath, token, "release");
176
+ throw new Error(`timed out waiting for Session binding store recovery: ${this.filePath}`);
177
+ }
178
+ await lockRetryDelay();
179
+ }
180
+ if (await lockOwnerToken(ownerPath) !== token) {
181
+ clearInterval(heartbeat);
182
+ continue;
183
+ }
184
+ return async () => {
185
+ clearInterval(heartbeat);
186
+ await releaseOwnedLockDirectory(lockPath, token, "release");
187
+ };
188
+ }
189
+ try {
190
+ const lockStat = await stat(lockPath);
191
+ if (Date.now() - lockStat.mtimeMs > FILE_LOCK_LEASE_MS) {
192
+ const recoveryToken = randomUUID();
193
+ const recoveryOwnerPath = path.join(recoveryPath, "owner");
194
+ if (!await publishOwnedLockDirectory(recoveryPath, recoveryToken, processIdentityValue)) {
195
+ await lockRetryDelay();
196
+ continue;
197
+ }
198
+ const recoveryHeartbeat = startLockHeartbeat(recoveryPath);
199
+ try {
200
+ const currentStat = await stat(lockPath);
201
+ if (Date.now() - currentStat.mtimeMs <= FILE_LOCK_LEASE_MS) {
202
+ continue;
203
+ }
204
+ const owner = await readLockOwner(ownerPath);
205
+ if (owner && await lockOwnerProcessIsCurrent(owner)) {
206
+ continue;
207
+ }
208
+ if (await lockOwnerToken(recoveryOwnerPath) !== recoveryToken) {
209
+ continue;
210
+ }
211
+ const stalePath = `${lockPath}.stale-${randomUUID()}`;
212
+ try {
213
+ await rename(lockPath, stalePath);
214
+ await rm(stalePath, { recursive: true, force: true });
215
+ }
216
+ catch (error) {
217
+ if (!isMissingFileError(error))
218
+ throw error;
219
+ }
220
+ }
221
+ finally {
222
+ clearInterval(recoveryHeartbeat);
223
+ await releaseOwnedLockDirectory(recoveryPath, recoveryToken, "recovery-release");
224
+ }
225
+ continue;
226
+ }
227
+ }
228
+ catch (error) {
229
+ if (!isMissingFileError(error))
230
+ throw error;
231
+ }
232
+ await lockRetryDelay();
233
+ }
234
+ throw new Error(`timed out acquiring Session binding store lock: ${this.filePath}`);
107
235
  }
108
236
  }
109
- export function resolveCodexSessionStorePath(config) {
110
- return path.resolve(config.AGENT_SESSION_STORE_PATH ?? path.join(process.cwd(), ".codex-proxy", "sessions.json"));
237
+ export function resolveCodexSessionStorePath(config, env = process.env) {
238
+ const home = rynxHome(env);
239
+ const configured = config.AGENT_SESSION_STORE_PATH;
240
+ return configured
241
+ ? path.resolve(home, configured)
242
+ : path.join(home, ".codex-proxy", "sessions.json");
111
243
  }
112
244
  async function pathExists(targetPath) {
113
245
  try {
@@ -124,3 +256,136 @@ async function pathExists(targetPath) {
124
256
  function isMissingFileError(error) {
125
257
  return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
126
258
  }
259
+ async function readLockOwner(ownerPath) {
260
+ try {
261
+ const owner = JSON.parse(await readFile(ownerPath, "utf8"));
262
+ if (!Number.isInteger(owner.pid)
263
+ || owner.pid <= 0
264
+ || typeof owner.token !== "string"
265
+ || (owner.processIdentity !== null && typeof owner.processIdentity !== "string")) {
266
+ return null;
267
+ }
268
+ return owner;
269
+ }
270
+ catch {
271
+ return null;
272
+ }
273
+ }
274
+ async function lockOwnerToken(ownerPath) {
275
+ return (await readLockOwner(ownerPath))?.token ?? null;
276
+ }
277
+ async function lockOwnerProcessIsCurrent(owner) {
278
+ if (!owner.processIdentity)
279
+ return false;
280
+ return await processIdentity(owner.pid) === owner.processIdentity;
281
+ }
282
+ async function processIdentity(pid) {
283
+ try {
284
+ if (process.platform === "linux") {
285
+ const [statLine, bootId] = await Promise.all([
286
+ readFile(`/proc/${pid}/stat`, "utf8"),
287
+ readFile("/proc/sys/kernel/random/boot_id", "utf8"),
288
+ ]);
289
+ const closeParen = statLine.lastIndexOf(")");
290
+ const fields = statLine.slice(closeParen + 2).trim().split(/\s+/);
291
+ const startTicks = fields[19];
292
+ return startTicks ? `linux:${bootId.trim()}:${startTicks}` : null;
293
+ }
294
+ if (process.platform === "darwin") {
295
+ const { stdout } = await execFileAsync("/bin/ps", ["-p", String(pid), "-o", "lstart=", "-o", "command="], {
296
+ encoding: "utf8",
297
+ env: { ...process.env, LANG: "C", LC_ALL: "C" },
298
+ });
299
+ const startedAt = stdout.trim();
300
+ return startedAt ? `darwin:${startedAt}` : null;
301
+ }
302
+ if (process.platform === "win32") {
303
+ const { stdout } = await execFileAsync("powershell.exe", [
304
+ "-NoProfile",
305
+ "-Command",
306
+ `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
307
+ ], { encoding: "utf8" });
308
+ const startedAt = stdout.trim();
309
+ return startedAt ? `win32:${startedAt}` : null;
310
+ }
311
+ }
312
+ catch {
313
+ return null;
314
+ }
315
+ return null;
316
+ }
317
+ function lockRetryDelay() {
318
+ return new Promise((resolve) => setTimeout(resolve, 10));
319
+ }
320
+ function startLockHeartbeat(lockPath) {
321
+ const heartbeat = setInterval(() => {
322
+ const now = new Date();
323
+ void utimes(lockPath, now, now).catch(() => undefined);
324
+ }, FILE_LOCK_HEARTBEAT_MS);
325
+ heartbeat.unref?.();
326
+ return heartbeat;
327
+ }
328
+ async function publishOwnedLockDirectory(lockPath, token, processIdentityValue) {
329
+ const candidatePath = `${lockPath}.claim-${process.pid}-${token}`;
330
+ await mkdir(candidatePath);
331
+ try {
332
+ await writeFile(path.join(candidatePath, "owner"), JSON.stringify({
333
+ pid: process.pid,
334
+ processIdentity: processIdentityValue,
335
+ token,
336
+ }), "utf8");
337
+ try {
338
+ await rename(candidatePath, lockPath);
339
+ return true;
340
+ }
341
+ catch (error) {
342
+ if (isLockContentionError(error))
343
+ return false;
344
+ throw error;
345
+ }
346
+ }
347
+ finally {
348
+ await rm(candidatePath, { recursive: true, force: true });
349
+ }
350
+ }
351
+ async function recoverExpiredLockDirectory(lockPath) {
352
+ try {
353
+ const lockStat = await stat(lockPath);
354
+ if (Date.now() - lockStat.mtimeMs <= FILE_LOCK_LEASE_MS)
355
+ return false;
356
+ const owner = await readLockOwner(path.join(lockPath, "owner"));
357
+ if (owner && await lockOwnerProcessIsCurrent(owner))
358
+ return false;
359
+ const stalePath = `${lockPath}.stale-${randomUUID()}`;
360
+ await rename(lockPath, stalePath);
361
+ await rm(stalePath, { recursive: true, force: true });
362
+ return true;
363
+ }
364
+ catch (error) {
365
+ if (isMissingFileError(error))
366
+ return false;
367
+ throw error;
368
+ }
369
+ }
370
+ async function releaseOwnedLockDirectory(lockPath, token, suffix) {
371
+ try {
372
+ if (await lockOwnerToken(path.join(lockPath, "owner")) !== token)
373
+ return;
374
+ const releasePath = `${lockPath}.${suffix}-${token}`;
375
+ await rename(lockPath, releasePath);
376
+ await rm(releasePath, { recursive: true, force: true });
377
+ }
378
+ catch (error) {
379
+ if (!isMissingFileError(error))
380
+ throw error;
381
+ }
382
+ }
383
+ function isLockContentionError(error) {
384
+ return Boolean(error
385
+ && typeof error === "object"
386
+ && "code" in error
387
+ && (error.code === "EEXIST"
388
+ || error.code === "ENOTEMPTY"
389
+ || error.code === "EACCES"
390
+ || error.code === "EPERM"));
391
+ }
package/dist/host.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type AgentSpec, type ReasoningEffort, type ResolvedExecutionBudget, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
1
+ import { type ResolvedExecutionSnapshot, type ResolvedExecutionBudget, type RuntimeUserInput, 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";
@@ -7,9 +7,9 @@ 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";
9
9
  import { type TerminalInjector } from "./claude/native-integration.js";
10
- import { FileCodexSessionStore, resolveCodexSessionStorePath, type CodexSessionRecord, type CodexSessionStore } from "./codex-session-store.js";
10
+ import { FileCodexSessionStore, resolveCodexSessionStorePath, type ClaudeForkIntent, type CodexSessionRecord, type CodexSessionStore } from "./codex-session-store.js";
11
11
  import { AgentRuntimeError as CodexRuntimeError } from "@rynx-ai/core";
12
- export { FileCodexSessionStore, resolveCodexSessionStorePath, type CodexSessionRecord, type CodexSessionStore, };
12
+ export { FileCodexSessionStore, resolveCodexSessionStorePath, type CodexSessionRecord, type CodexSessionStore, type ClaudeForkIntent, };
13
13
  export interface CodexRuntimeStatus {
14
14
  codex_available: boolean;
15
15
  logged_in: boolean;
@@ -84,12 +84,6 @@ export interface CodexCapabilities {
84
84
  getGoal(localThreadId: string): Promise<CapabilityResult<ThreadGoal | null>>;
85
85
  setGoal(localThreadId: string, objective: string): Promise<CapabilityResult>;
86
86
  clearGoal(localThreadId: string): Promise<CapabilityResult>;
87
- /**
88
- * Fork the Codex thread bound to `currentLocalThreadId` and bind the forked
89
- * thread (carrying full context) to `newLocalThreadId`, so a new Lark
90
- * conversation resumes from it. Used by `/fork`.
91
- */
92
- forkSession(currentLocalThreadId: string, newLocalThreadId: string): Promise<CapabilityResult>;
93
87
  }
94
88
  /**
95
89
  * Per-session claude-native live handle. claude has no app-server, so the
@@ -102,20 +96,15 @@ export interface CodexCapabilities {
102
96
  * The runner-child owns the mirror emitter + transport, so it supplies this. */
103
97
  export type RetargetMirror = (newSessionId: string, meta: {
104
98
  kind: "clear" | "fork";
105
- agent?: string;
106
- model?: string;
107
- cwd?: string;
99
+ workspace: SessionWorkspaceSnapshot;
100
+ execution: ResolvedExecutionSnapshot;
108
101
  parentSessionId?: string;
109
102
  }) => void;
110
103
  export interface LiveSessionOpts {
111
- cwd?: string;
112
- runtime?: AgentRuntimeId;
113
- reasoningEffort?: ReasoningEffort;
114
- /** Preset agent id (or inline {@link LiveSessionOpts.agentSpec}), so the live
115
- * launch applies the agent's model / skills / instructions — not just the
116
- * runtime. Absent ⇒ falls back to the config defaults (prior behavior). */
117
- agentName?: string;
118
- agentSpec?: AgentSpec;
104
+ /** Immutable Session-owned snapshots. Provider launch/resume never re-opens a
105
+ * Project or Agent template. */
106
+ workspace: SessionWorkspaceSnapshot;
107
+ execution: ResolvedExecutionSnapshot;
119
108
  retargetMirror?: RetargetMirror;
120
109
  }
121
110
  /**
@@ -138,6 +127,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
138
127
  private readonly forwarderClientFactory;
139
128
  private readonly backends;
140
129
  private readonly sessionId;
130
+ private readonly runtimeHomeSessionId;
141
131
  private sessionSandbox?;
142
132
  private sessionApprovalPolicy?;
143
133
  private readonly runtimeHomes;
@@ -147,7 +137,11 @@ export declare class LocalAgentHost implements CodexCapabilities {
147
137
  * finalizes these synchronously afterwards to scrub raw interaction answers. */
148
138
  private readonly pendingClaudeFinalizers;
149
139
  private readonly liveEnsuring;
150
- constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory, sessionId, }: {
140
+ private readonly forkingTargets;
141
+ /** Short-lived dedupe for managed fork notifications delivered after the
142
+ * `thread/fork` response. Values are expected source Provider thread ids. */
143
+ private readonly managedForkThreadStarts;
144
+ constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory, sessionId, runtimeHomeSessionId, }: {
151
145
  config: AppConfig;
152
146
  commandRunner?: CodexCommandRunner;
153
147
  sessionStore?: CodexSessionStore;
@@ -164,6 +158,10 @@ export declare class LocalAgentHost implements CodexCapabilities {
164
158
  * session-scoped. The shared `__cap__` child / tests fall back to a sentinel
165
159
  * (its home only backs home-agnostic caps like listModels). */
166
160
  sessionId?: string;
161
+ /** Provider-internal home owner loaded from the durable native binding.
162
+ * Forked Sessions keep independent runners/contexts while sharing the
163
+ * Provider store in which thread/fork atomically persisted their rollout. */
164
+ runtimeHomeSessionId?: string;
167
165
  });
168
166
  private getBackend;
169
167
  /**
@@ -203,19 +201,18 @@ export declare class LocalAgentHost implements CodexCapabilities {
203
201
  * DETERMINISTIC (`resp_codex_<turnId>`) so items converge across snapshot/live.
204
202
  *
205
203
  * Returns false for claude / no app-server (the caller falls back to the
206
- * streaming run path). This client CREATES the codex thread (`threadStart`) so
207
- * it owns the thread's item/turn notification stream the app-server delivers
208
- * items to the thread-creating connection, for turns started by ANY client (web
209
- * inject AND the co-driving `codex --remote` TUI). The TUI pane (launched by the
210
- * runner child, which owns the terminal registry) merely `resume`s this thread
211
- * to display it. The thread id is persisted so `codexTerminalSpec` resumes it.
204
+ * streaming run path). For a fresh session the native remote TUI creates the
205
+ * Codex thread and its `thread/started` notification supplies the id. For an
206
+ * existing session, the app-server resumes the persisted id. Rynx never writes
207
+ * or repairs Codex's private rollout files.
212
208
  */
213
- ensureLiveCodexSession(localThreadId: string, emit: (event: SessionEvent) => void, opts?: LiveSessionOpts): Promise<boolean>;
209
+ ensureLiveCodexSession(localThreadId: string, emit: (event: SessionEvent) => void, opts: LiveSessionOpts): Promise<boolean>;
214
210
  private startLiveCodexSession;
215
- private refreshLiveCodexSession;
216
211
  /** Bind a session's codex thread id once known (TUI broadcast or store): persist
217
212
  * it, unblock injection, and kick off the resume-subscribe loop (once). */
218
213
  private onLiveThreadStarted;
214
+ private shouldIgnoreManagedForkThreadStarted;
215
+ private rememberManagedForkThreadStart;
219
216
  /**
220
217
  * Subscribe the forwarder connection to a thread (reference implementation's
221
218
  * `_subscribe_until_ready`). A fresh TUI thread has no rollout until its first
@@ -228,11 +225,17 @@ export declare class LocalAgentHost implements CodexCapabilities {
228
225
  /** Await a live session's thread binding (bounded). Returns false on timeout /
229
226
  * no live session. Injection and the runner's `live.ready` gate on this. */
230
227
  waitLiveReady(localThreadId: string, timeoutMs?: number): Promise<boolean>;
228
+ /** Await the stronger Terminal gate: another app-server connection has
229
+ * successfully resumed the thread, so the detached TUI cannot race rollout
230
+ * discovery or indexing. */
231
+ waitTerminalReady(localThreadId: string, timeoutMs?: number): Promise<boolean>;
232
+ /** Diagnostic from the provider adapter when native discovery/resume failed. */
233
+ liveSessionError(localThreadId: string): string | undefined;
231
234
  /**
232
235
  * Inject a user turn into a session's live codex thread — reference implementation's
233
236
  * single-writer web send. `turn/steer` when a turn is open (mid-turn
234
- * supplement), else `turn/start`. NEVER creates a thread (the TUI owns creation;
235
- * this targets the id the forwarder captured). Serialized per session so two
237
+ * supplement), else `turn/start`. The thread was created by the native TUI or
238
+ * resumed from the persisted native id. Serialized per session so two
236
239
  * injects can't double-open a turn.
237
240
  *
238
241
  * Returns an {@link InjectOutcome}: `notLive` when this session has no live
@@ -264,21 +267,10 @@ export declare class LocalAgentHost implements CodexCapabilities {
264
267
  * prompt. Questions and PermissionRequest approvals use the runtime-local
265
268
  * generic interaction bridge and remain inside the active Turn. */
266
269
  private claudeTerminalSpec;
267
- /** Bring up a claude-native live forwarder: prepare the bridge dir (the TUI's
268
- * hooks write into it), then tail bridge + transcript and drive a per-turn
269
- * {@link SessionNormalizer} → `emit` (the SAME sink shape the codex path uses). */
270
- /** Resolve an agent spec's launch config — model, instructions, and the session skill env —
271
- * for a LIVE native session, reusing the same core resolvers the non-live
272
- * run path uses ({@link resolveAgentExecution} for the model,
273
- * {@link resolveAgent} for instructions and skills).
274
- *
275
- * Skills are spec-rooted: each declared ref is replayed into a
276
- * SESSION-scoped temp dir (cache-accelerated); the owner's catalog plays no
277
- * role, and no spec / no `skills` means ZERO skills. Every declared skill
278
- * must resolve at its declared content hash; missing, failed, or drifted
279
- * materialization aborts launch before a native session starts.
280
- * `skillsCleanup` removes the session dir (call on session stop). */
281
- private resolveLiveAgentConfig;
270
+ /** Materialize only the immutable skill declarations stored on the Session.
271
+ * Provider startup never re-opens an Agent template or process-level plugin
272
+ * snapshot. */
273
+ private prepareExecutionSkills;
282
274
  private startLiveClaudeSession;
283
275
  /** Persist claude's discovered session id (reusing the `codexSessionId` store
284
276
  * field, as the claude executor already does) and release the readiness gate. */
@@ -309,7 +301,11 @@ export declare class LocalAgentHost implements CodexCapabilities {
309
301
  getGoal(localThreadId: string): Promise<CapabilityResult<ThreadGoal | null>>;
310
302
  setGoal(localThreadId: string, objective: string): Promise<CapabilityResult>;
311
303
  clearGoal(localThreadId: string): Promise<CapabilityResult>;
312
- forkSession(currentLocalThreadId: string, newLocalThreadId: string): Promise<CapabilityResult>;
304
+ forkSession(currentLocalThreadId: string, newLocalThreadId: string, options?: {
305
+ workspace: SessionWorkspaceSnapshot;
306
+ execution: ResolvedExecutionSnapshot;
307
+ }): Promise<CapabilityResult>;
308
+ private performForkSession;
313
309
  }
314
310
  export declare function parseCodexLoginStatus(exitCode: number, output: string): {
315
311
  loggedIn: boolean;
@@ -322,3 +318,7 @@ export declare function isUnsupportedMethodError(error: unknown): boolean;
322
318
  * — a fresh TUI thread before its first turn. Retryable (park until active).
323
319
  * Mirrors reference implementation's `_is_thread_not_ready_error`. */
324
320
  export declare function isThreadNotReadyError(error: unknown): boolean;
321
+ /** A persisted thread id that a freshly started app-server cannot load yet.
322
+ * During startup, both errors can be transient while the rollout index catches
323
+ * up. Retry the same id; never use either error as permission to replace it. */
324
+ export declare function isRetryableThreadResumeError(error: unknown): boolean;