@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
package/dist/index.d.ts CHANGED
@@ -6,8 +6,7 @@
6
6
  */
7
7
  export { LocalAgentHost, CodexRuntimeError, SpawnCodexCommandRunner, FileCodexSessionStore, resolveCodexSessionStorePath, } from "./host.js";
8
8
  export type { CodexCapabilities, CapabilityResult, CodexRuntimeStatus, } from "./host.js";
9
- export type { CodexSessionStore, CodexSessionRecord } from "./codex-session-store.js";
10
- export { ensureCodexResumeRollout } from "./codex/rollout-synth.js";
9
+ export type { ClaudeForkIntent, CodexSessionStore, CodexSessionRecord, } from "./codex-session-store.js";
11
10
  export { RunnerManager, TerminalOpenError } from "./runner/manager.js";
12
11
  export type { RunnerManagerOptions, RunnerSessionContext, RunnerSessionContextProvider, OpenTerminalOptions, ParentTerminal, } from "./runner/manager.js";
13
12
  export type { InjectOutcome, TerminalOpenErrorCode, TerminalRole } from "./runner/protocol.js";
package/dist/index.js CHANGED
@@ -5,7 +5,6 @@
5
5
  * nothing about any channel.
6
6
  */
7
7
  export { LocalAgentHost, CodexRuntimeError, SpawnCodexCommandRunner, FileCodexSessionStore, resolveCodexSessionStorePath, } from "./host.js";
8
- export { ensureCodexResumeRollout } from "./codex/rollout-synth.js";
9
8
  // Runner subprocess layer: the parent-side manager (an `AgentExecutor` +
10
9
  // `AgentCapabilities` that spawns per-session runner children) plus the wire
11
10
  // types. The composition root uses `RunnerManager` in place of `LocalAgentHost`.
@@ -2,6 +2,7 @@ import { type AgentRuntimeId, type AppConfig } from "@rynx-ai/core";
2
2
  import type { ModelListResponse } from "./codex-app-server/protocol.js";
3
3
  export interface RuntimeModelCatalogDeps {
4
4
  readTraexModels?: () => Promise<unknown>;
5
+ readTraexDebugModels?: () => Promise<unknown>;
5
6
  }
6
7
  /**
7
8
  * The model list for a runtime, without an execution backend.
@@ -24,7 +24,17 @@ export async function listRuntimeModels(config, runtime, deps = {}) {
24
24
  if (runtime === "traex") {
25
25
  try {
26
26
  const value = await (deps.readTraexModels ?? readTraexModels)();
27
- const models = normalizeTraexModels(value, resolveRuntimeModel(config, runtime).trim());
27
+ const configuredDefault = resolveRuntimeModel(config, runtime).trim();
28
+ const defaultModel = configuredDefault || resolveTraexNativeDefault(await (deps.readTraexDebugModels ?? readTraexDebugModels)());
29
+ if (!configuredDefault &&
30
+ (!Array.isArray(value) ||
31
+ !value.some((item) => item &&
32
+ typeof item === "object" &&
33
+ !Array.isArray(item) &&
34
+ item.name === defaultModel))) {
35
+ throw new Error(`Traex native default is missing from the public catalog: ${defaultModel}`);
36
+ }
37
+ const models = normalizeTraexModels(value, defaultModel);
28
38
  if (models.length > 0)
29
39
  return { data: models };
30
40
  }
@@ -46,6 +56,38 @@ async function readTraexModels() {
46
56
  });
47
57
  return JSON.parse(stdout);
48
58
  }
59
+ async function readTraexDebugModels() {
60
+ const { stdout } = await execFileAsync(resolveRuntimeBinary("traex"), ["debug", "models"], {
61
+ encoding: "utf8",
62
+ timeout: TRAEX_MODELS_TIMEOUT_MS,
63
+ maxBuffer: TRAEX_MODELS_MAX_BYTES,
64
+ });
65
+ return JSON.parse(stdout);
66
+ }
67
+ function resolveTraexNativeDefault(value) {
68
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
69
+ throw new Error("Traex debug model catalog is invalid");
70
+ }
71
+ const models = value.models;
72
+ if (!Array.isArray(models)) {
73
+ throw new Error("Traex debug model catalog is invalid");
74
+ }
75
+ const defaults = models.filter((item) => item &&
76
+ typeof item === "object" &&
77
+ !Array.isArray(item) &&
78
+ item.business_metadata &&
79
+ typeof item.business_metadata === "object" &&
80
+ item.business_metadata
81
+ .backend_is_default === true);
82
+ if (defaults.length !== 1) {
83
+ throw new Error(`Traex debug model catalog has ${defaults.length} native defaults`);
84
+ }
85
+ const name = defaults[0].config_name;
86
+ if (typeof name !== "string" || !name.trim()) {
87
+ throw new Error("Traex native default has no config_name");
88
+ }
89
+ return name.trim();
90
+ }
49
91
  function normalizeTraexModels(value, configuredDefault) {
50
92
  if (!Array.isArray(value))
51
93
  return [];
@@ -0,0 +1,56 @@
1
+ import type { AgentRuntimeId, SandboxMode } from "@rynx-ai/core";
2
+ import type { PermissionProfileSelectionParams, SandboxPolicy } from "./codex-app-server/protocol.js";
3
+ /** Structural view of the immutable Session workspace snapshot. The canonical
4
+ * type lives in core; keeping the adapter input structural avoids inventing a
5
+ * second domain model in the Provider layer. */
6
+ export interface ProviderWorkspace {
7
+ readonly cwd: string;
8
+ readonly allowedDirs: readonly string[];
9
+ }
10
+ /** Canonical roots projected without mutating the Session snapshot, with the
11
+ * primary working directory first for every Provider. */
12
+ export declare function providerWorkspaceRoots(workspace: ProviderWorkspace): string[];
13
+ export declare function providerAdditionalDirs(workspace: ProviderWorkspace): string[];
14
+ export declare function sandboxPolicyForWorkspace(mode: SandboxMode, workspace: ProviderWorkspace): SandboxPolicy;
15
+ export declare function codexThreadWorkspaceParams(workspace: ProviderWorkspace): {
16
+ cwd: string;
17
+ runtimeWorkspaceRoots: string[];
18
+ };
19
+ export declare function codexTurnWorkspaceParams(workspace: ProviderWorkspace, mode: SandboxMode): {
20
+ cwd: string;
21
+ runtimeWorkspaceRoots: string[];
22
+ sandboxPolicy: SandboxPolicy;
23
+ };
24
+ /** Traex has no runtimeWorkspaceRoots field. For a multi-root writable
25
+ * workspace, use the bounded named-profile modification supported by Traex
26
+ * 0.200; never combine it with the legacy sandbox field. */
27
+ export declare function traexThreadWorkspaceParams(workspace: ProviderWorkspace, mode: SandboxMode): {
28
+ cwd: string;
29
+ permissions: PermissionProfileSelectionParams;
30
+ } | {
31
+ cwd: string;
32
+ sandbox: SandboxMode;
33
+ };
34
+ /** Turn equivalent of {@link traexThreadWorkspaceParams}; `permissions` and
35
+ * `sandboxPolicy` are intentionally mutually exclusive. */
36
+ export declare function traexTurnWorkspaceParams(workspace: ProviderWorkspace, mode: SandboxMode): {
37
+ cwd: string;
38
+ permissions: PermissionProfileSelectionParams;
39
+ } | {
40
+ cwd: string;
41
+ sandboxPolicy: SandboxPolicy;
42
+ };
43
+ export declare function threadWorkspaceParams(runtime: Exclude<AgentRuntimeId, "claude">, workspace: ProviderWorkspace, mode: SandboxMode): {
44
+ cwd: string;
45
+ permissions: PermissionProfileSelectionParams;
46
+ } | {
47
+ cwd: string;
48
+ sandbox: SandboxMode;
49
+ };
50
+ export declare function turnWorkspaceParams(runtime: Exclude<AgentRuntimeId, "claude">, workspace: ProviderWorkspace, mode: SandboxMode): {
51
+ cwd: string;
52
+ permissions: PermissionProfileSelectionParams;
53
+ } | {
54
+ cwd: string;
55
+ sandboxPolicy: SandboxPolicy;
56
+ };
@@ -0,0 +1,83 @@
1
+ /** Canonical roots projected without mutating the Session snapshot, with the
2
+ * primary working directory first for every Provider. */
3
+ export function providerWorkspaceRoots(workspace) {
4
+ return [...new Set([workspace.cwd, ...workspace.allowedDirs])];
5
+ }
6
+ export function providerAdditionalDirs(workspace) {
7
+ return providerWorkspaceRoots(workspace).filter((dir) => dir !== workspace.cwd);
8
+ }
9
+ export function sandboxPolicyForWorkspace(mode, workspace) {
10
+ switch (mode) {
11
+ case "read-only":
12
+ return { type: "readOnly", networkAccess: false };
13
+ case "workspace-write":
14
+ return {
15
+ type: "workspaceWrite",
16
+ writableRoots: providerWorkspaceRoots(workspace),
17
+ networkAccess: true,
18
+ excludeTmpdirEnvVar: false,
19
+ excludeSlashTmp: false,
20
+ };
21
+ case "danger-full-access":
22
+ return { type: "dangerFullAccess" };
23
+ }
24
+ }
25
+ export function codexThreadWorkspaceParams(workspace) {
26
+ return {
27
+ cwd: workspace.cwd,
28
+ runtimeWorkspaceRoots: providerWorkspaceRoots(workspace),
29
+ };
30
+ }
31
+ export function codexTurnWorkspaceParams(workspace, mode) {
32
+ return {
33
+ ...codexThreadWorkspaceParams(workspace),
34
+ sandboxPolicy: sandboxPolicyForWorkspace(mode, workspace),
35
+ };
36
+ }
37
+ function traexWorkspacePermissions(workspace) {
38
+ const additionalDirs = providerAdditionalDirs(workspace);
39
+ if (additionalDirs.length === 0)
40
+ return undefined;
41
+ return {
42
+ type: "profile",
43
+ id: ":workspace",
44
+ modifications: additionalDirs.map((path) => ({
45
+ type: "additionalWritableRoot",
46
+ path,
47
+ })),
48
+ };
49
+ }
50
+ /** Traex has no runtimeWorkspaceRoots field. For a multi-root writable
51
+ * workspace, use the bounded named-profile modification supported by Traex
52
+ * 0.200; never combine it with the legacy sandbox field. */
53
+ export function traexThreadWorkspaceParams(workspace, mode) {
54
+ const permissions = mode === "workspace-write"
55
+ ? traexWorkspacePermissions(workspace)
56
+ : undefined;
57
+ return permissions
58
+ ? { cwd: workspace.cwd, permissions }
59
+ : { cwd: workspace.cwd, sandbox: mode };
60
+ }
61
+ /** Turn equivalent of {@link traexThreadWorkspaceParams}; `permissions` and
62
+ * `sandboxPolicy` are intentionally mutually exclusive. */
63
+ export function traexTurnWorkspaceParams(workspace, mode) {
64
+ const permissions = mode === "workspace-write"
65
+ ? traexWorkspacePermissions(workspace)
66
+ : undefined;
67
+ return permissions
68
+ ? { cwd: workspace.cwd, permissions }
69
+ : {
70
+ cwd: workspace.cwd,
71
+ sandboxPolicy: sandboxPolicyForWorkspace(mode, workspace),
72
+ };
73
+ }
74
+ export function threadWorkspaceParams(runtime, workspace, mode) {
75
+ return runtime === "traex"
76
+ ? traexThreadWorkspaceParams(workspace, mode)
77
+ : { ...codexThreadWorkspaceParams(workspace), sandbox: mode };
78
+ }
79
+ export function turnWorkspaceParams(runtime, workspace, mode) {
80
+ return runtime === "traex"
81
+ ? traexTurnWorkspaceParams(workspace, mode)
82
+ : codexTurnWorkspaceParams(workspace, mode);
83
+ }
@@ -7,12 +7,60 @@
7
7
  * per-thread capabilities, answered against the same backend so the parent never
8
8
  * needs an app-server of its own.
9
9
  */
10
- import { type AgentCapabilities } from "@rynx-ai/core";
10
+ import { type AgentCapabilities, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
11
+ import type { SessionEvent } from "@rynx-ai/core";
12
+ import type { TerminalInjector } from "../claude/native-integration.js";
13
+ import type { ResolveInteractionResult } from "../interactions.js";
14
+ import { type InjectOutcome } from "./protocol.js";
11
15
  import type { ChildTransport } from "./transport.js";
16
+ /** Re-target the mirror to a freshly minted rynx session (claude `/clear`·`/fork`)
17
+ * + record the terminal transfer with the daemon. The child owns the transport,
18
+ * so it supplies this to the host. */
19
+ type RetargetMirror = (newSessionId: string, meta: {
20
+ kind: "clear" | "fork";
21
+ workspace: SessionWorkspaceSnapshot;
22
+ execution: ResolvedExecutionSnapshot;
23
+ parentSessionId?: string;
24
+ }) => void;
25
+ /** Codex-native live-session methods the executor (a `LocalAgentHost`) exposes
26
+ * beyond the `AgentExecutor`/`AgentCapabilities` contract. Duck-typed so the
27
+ * runner works with any executor (a claude-only host simply lacks them). */
28
+ interface LiveCodexProvider {
29
+ codexTerminalSpec?(localThreadId: string): Promise<{
30
+ command: string;
31
+ args: string[];
32
+ cwd: string;
33
+ env?: Record<string, string>;
34
+ } | null>;
35
+ ensureLiveCodexSession?(localThreadId: string, emit: (event: SessionEvent) => void, opts: {
36
+ workspace: SessionWorkspaceSnapshot;
37
+ execution: ResolvedExecutionSnapshot;
38
+ retargetMirror?: RetargetMirror;
39
+ }): Promise<boolean>;
40
+ waitLiveReady?(localThreadId: string, timeoutMs?: number): Promise<boolean>;
41
+ waitTerminalReady?(localThreadId: string, timeoutMs?: number): Promise<boolean>;
42
+ liveSessionError?(localThreadId: string): string | undefined;
43
+ injectMessage?(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectOutcome>;
44
+ interruptLive?(localThreadId: string): Promise<boolean>;
45
+ stopLiveCodexSession?(localThreadId: string, opts?: {
46
+ deferClaudeInteractionCleanup?: boolean;
47
+ }): void;
48
+ finalizeStoppedLiveSessions?(): void;
49
+ resolveInteraction?(localThreadId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<ResolveInteractionResult>;
50
+ /** claude-native: hand the session's tmux pane injector to the host (which
51
+ * doesn't own tmux). No-op for codex sessions (they inject via app-server). */
52
+ attachTerminalInjector?(localThreadId: string, injector: TerminalInjector): void;
53
+ /** Internal Provider operation used only by MachineSessionService through
54
+ * RunnerManager. It is deliberately absent from public AgentCapabilities. */
55
+ forkSession?(currentLocalThreadId: string, newLocalThreadId: string, options: {
56
+ workspace: SessionWorkspaceSnapshot;
57
+ execution: ResolvedExecutionSnapshot;
58
+ }): Promise<import("@rynx-ai/core").CapabilityResult>;
59
+ }
12
60
  export interface RunnerSessionDeps {
13
61
  transport: ChildTransport;
14
62
  /** The single-backend capability + live co-drive surface (a `LocalAgentHost`). */
15
- executor: AgentCapabilities;
63
+ executor: AgentCapabilities & LiveCodexProvider;
16
64
  /** Invoked on a `shutdown` message (default: close transport). */
17
65
  onShutdown?: () => void;
18
66
  }
@@ -42,10 +90,9 @@ export declare class RunnerSession {
42
90
  /**
43
91
  * Eagerly bring up a session's codex-native live view: start the persistent
44
92
  * forwarder connection (which resume-subscribes to mirror every turn) and
45
- * launch the detached `codex --remote` TUI, which CREATES the codex thread. The
46
- * forwarder's connection sees the TUI's broadcast `thread/started`, binds, and
47
- * subscribes — so the TUI is usable immediately (a fresh `--remote` needs no
48
- * rollout) and its turns mirror to chat (reference implementation's model).
93
+ * launch the detached `codex --remote resume` TUI against the thread the
94
+ * structured runtime created. The forwarder subscribes to that same thread,
95
+ * so the TUI is usable immediately and its turns mirror to chat.
49
96
  */
50
97
  private ensureLive;
51
98
  private inject;
@@ -64,3 +111,4 @@ export declare class RunnerSession {
64
111
  private runCap;
65
112
  private dispatchCap;
66
113
  }
114
+ export {};
@@ -114,9 +114,8 @@ export class RunnerSession {
114
114
  from: localThreadId,
115
115
  to: newId,
116
116
  kind: meta.kind,
117
- ...(meta.agent ? { agent: meta.agent } : {}),
118
- ...(meta.model ? { model: meta.model } : {}),
119
- ...(meta.cwd ? { cwd: meta.cwd } : {}),
117
+ workspace: meta.workspace,
118
+ execution: meta.execution,
120
119
  ...(meta.parentSessionId ? { parentSessionId: meta.parentSessionId } : {}),
121
120
  });
122
121
  };
@@ -125,21 +124,17 @@ export class RunnerSession {
125
124
  /**
126
125
  * Eagerly bring up a session's codex-native live view: start the persistent
127
126
  * forwarder connection (which resume-subscribes to mirror every turn) and
128
- * launch the detached `codex --remote` TUI, which CREATES the codex thread. The
129
- * forwarder's connection sees the TUI's broadcast `thread/started`, binds, and
130
- * subscribes — so the TUI is usable immediately (a fresh `--remote` needs no
131
- * rollout) and its turns mirror to chat (reference implementation's model).
127
+ * launch the detached `codex --remote resume` TUI against the thread the
128
+ * structured runtime created. The forwarder subscribes to that same thread,
129
+ * so the TUI is usable immediately and its turns mirror to chat.
132
130
  */
133
131
  async ensureLive(msg) {
134
132
  const provider = this.liveProvider;
135
133
  try {
136
134
  const { emit, retarget } = this.mirrorChannel(msg.localThreadId);
137
135
  const started = await provider.ensureLiveCodexSession?.(msg.localThreadId, emit, {
138
- ...(msg.cwd ? { cwd: msg.cwd } : {}),
139
- ...(msg.runtime ? { runtime: msg.runtime } : {}),
140
- ...(msg.reasoningEffort ? { reasoningEffort: msg.reasoningEffort } : {}),
141
- ...(msg.agentName ? { agentName: msg.agentName } : {}),
142
- ...(msg.agentSpec ? { agentSpec: msg.agentSpec } : {}),
136
+ workspace: msg.workspace,
137
+ execution: msg.execution,
143
138
  retargetMirror: retarget,
144
139
  });
145
140
  if (!started) {
@@ -153,12 +148,24 @@ export class RunnerSession {
153
148
  return;
154
149
  }
155
150
  this.liveIds.add(msg.localThreadId);
156
- // Launch the TUI (fresh `--remote` it creates the thread) so the forwarder
157
- // captures the broadcast `thread/started` and binds. Re-launch when the pane
158
- // is absent OR its process has died (`isAlive` probes `#{pane_dead}`) — so a
151
+ // Launch the TUI attached to the already-bound thread. Re-launch when the
152
+ // pane is absent OR its process has died (`isAlive` probes `#{pane_dead}`) so a
159
153
  // reconnect after the TUI exited restarts it instead of skipping (a
160
154
  // launched-once guard would leave a dead "Pane is dead" husk forever).
161
155
  if (!this.terminals.get(`${msg.localThreadId}-main`)?.isAlive()) {
156
+ const terminalReady = provider.waitTerminalReady
157
+ ? await provider.waitTerminalReady(msg.localThreadId)
158
+ : true;
159
+ if (!terminalReady) {
160
+ this.transport.send({
161
+ t: "live.ready",
162
+ reqId: msg.reqId,
163
+ localThreadId: msg.localThreadId,
164
+ ok: false,
165
+ error: "Provider thread was not ready for Terminal resume",
166
+ });
167
+ return;
168
+ }
162
169
  await this.launchCodexPane(msg.localThreadId, msg.cols, msg.rows);
163
170
  }
164
171
  const ready = msg.waitForReady === false
@@ -166,12 +173,27 @@ export class RunnerSession {
166
173
  : provider.waitLiveReady
167
174
  ? await provider.waitLiveReady(msg.localThreadId)
168
175
  : true;
176
+ const terminal = this.terminals.get(`${msg.localThreadId}-main`);
177
+ const paneFailure = !ready && terminal && !terminal.isAlive()
178
+ ? terminal
179
+ .capturePane()
180
+ .split("\n")
181
+ .map((line) => line.trim())
182
+ .filter(Boolean)
183
+ .slice(-6)
184
+ .join(" ")
185
+ .slice(-1_000)
186
+ : "";
187
+ const readinessError = provider.liveSessionError?.(msg.localThreadId)
188
+ ?? (paneFailure
189
+ ? `Provider terminal exited before session discovery: ${paneFailure}`
190
+ : "live session was not ready before timeout");
169
191
  this.transport.send({
170
192
  t: "live.ready",
171
193
  reqId: msg.reqId,
172
194
  localThreadId: msg.localThreadId,
173
195
  ok: ready,
174
- ...(ready ? {} : { error: "live session was not ready before timeout" }),
196
+ ...(ready ? {} : { error: readinessError }),
175
197
  });
176
198
  }
177
199
  catch (error) {
@@ -343,7 +365,10 @@ export class RunnerSession {
343
365
  case "clearGoal":
344
366
  return this.executor.clearGoal(args[0]);
345
367
  case "forkSession":
346
- return this.executor.forkSession(args[0], args[1]);
368
+ if (!this.executor.forkSession) {
369
+ throw new Error("Provider fork is unavailable");
370
+ }
371
+ return this.executor.forkSession(args[0], args[1], args[2]);
347
372
  }
348
373
  }
349
374
  }
@@ -16,7 +16,7 @@
16
16
  * channel.
17
17
  */
18
18
  import { spawn as nodeSpawn } from "node:child_process";
19
- import { type AgentCapabilities, type AgentRuntimeId, type AgentSpec, type AppConfig, type CapabilityResult, type ModelListResponse, type ReasoningEffort, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type ThreadGoal } from "@rynx-ai/core";
19
+ import { type AgentCapabilities, type AgentRuntimeId, type AppConfig, type CapabilityResult, type ModelListResponse, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type SessionWorkspaceSnapshot, type ThreadGoal } from "@rynx-ai/core";
20
20
  import { type CodexSessionStore } from "../host.js";
21
21
  import type { ResolveInteractionResult } from "../interactions.js";
22
22
  import { type InjectOutcome, type TerminalOpenErrorCode, type TerminalRole } from "./protocol.js";
@@ -69,9 +69,8 @@ export interface RotateInfo {
69
69
  from: string;
70
70
  to: string;
71
71
  kind: "clear" | "fork";
72
- agent?: string;
73
- model?: string;
74
- cwd?: string;
72
+ workspace: SessionWorkspaceSnapshot;
73
+ execution: ResolvedExecutionSnapshot;
75
74
  parentSessionId?: string;
76
75
  }
77
76
  /** Options for {@link RunnerManager.openTerminal}. */
@@ -140,6 +139,21 @@ export declare class RunnerManager implements AgentCapabilities {
140
139
  private readonly liveSessionKeys;
141
140
  /** Last live-start error per local session, surfaced by the control API. */
142
141
  private readonly liveErrors;
142
+ /** Last immutable launch snapshots seen for a Session. Used only to restore a
143
+ * target request that crossed a fork reservation boundary. */
144
+ private readonly liveOptions;
145
+ /** A fork reserves its target before the first asynchronous store read. This
146
+ * prevents another entry point from starting the target against an
147
+ * uncommitted Provider binding. */
148
+ private readonly forkReservations;
149
+ /** A native fork temporarily makes the source read-only so its canonical
150
+ * snapshot and Provider context are captured at the same boundary. */
151
+ private readonly sourceForkReservations;
152
+ /** Manager-wide fork de-duplication. LocalAgentHost only sees one source
153
+ * runner, so the fence must live here to cover concurrent source runners. */
154
+ private readonly forkOperations;
155
+ private readonly forkBufferedMessages;
156
+ private readonly forkBufferedTerminalInputs;
143
157
  constructor(opts: RunnerManagerOptions);
144
158
  /**
145
159
  * Open a live terminal on the session's runner child (spawning it if needed).
@@ -166,33 +180,27 @@ export declare class RunnerManager implements AgentCapabilities {
166
180
  onMirror(listener: (sessionId: string, event: SessionEvent) => void): void;
167
181
  /** Register the sink for session rotations (claude `/clear`·`/fork`): the server
168
182
  * records the new session's meta (carry-over agent/model/title). */
169
- onRotate(listener: (rotation: RotateInfo) => void): void;
183
+ onRotate(listener: (rotation: RotateInfo) => void | Promise<void>): void;
170
184
  /**
171
185
  * Eagerly bring up a session's codex-native live view (persistent forwarder +
172
186
  * detached `codex --remote` TUI) in its runner child, spawning the runner if
173
187
  * needed. Idempotent. Resolves true once the codex thread is bound; false for a
174
188
  * non-codex / non-live session (the caller then uses the normal run path).
175
189
  */
176
- ensureLiveSession(localThreadId: string, opts?: {
177
- cwd?: string;
190
+ ensureLiveSession(localThreadId: string, opts: {
191
+ workspace: SessionWorkspaceSnapshot;
192
+ execution: ResolvedExecutionSnapshot;
178
193
  cols?: number;
179
194
  rows?: number;
180
- runtime?: AgentRuntimeId;
181
- reasoningEffort?: ReasoningEffort;
182
- agentName?: string;
183
- agentSpec?: AgentSpec;
184
195
  }): Promise<boolean>;
185
196
  /** Start the Provider pane without waiting for login/onboarding to create a
186
197
  * native thread. This is the setup-terminal gate; callers may attach as soon
187
198
  * as it resolves, while normal message delivery still uses ensureLiveSession. */
188
- startLiveSession(localThreadId: string, opts?: {
189
- cwd?: string;
199
+ startLiveSession(localThreadId: string, opts: {
200
+ workspace: SessionWorkspaceSnapshot;
201
+ execution: ResolvedExecutionSnapshot;
190
202
  cols?: number;
191
203
  rows?: number;
192
- runtime?: AgentRuntimeId;
193
- reasoningEffort?: ReasoningEffort;
194
- agentName?: string;
195
- agentSpec?: AgentSpec;
196
204
  }): Promise<boolean>;
197
205
  private requestLiveSession;
198
206
  lastLiveSessionError(localThreadId: string): string | undefined;
@@ -215,7 +223,16 @@ export declare class RunnerManager implements AgentCapabilities {
215
223
  getGoal(localThreadId: string): Promise<CapabilityResult<ThreadGoal | null>>;
216
224
  setGoal(localThreadId: string, objective: string): Promise<CapabilityResult>;
217
225
  clearGoal(localThreadId: string): Promise<CapabilityResult>;
218
- forkSession(currentLocalThreadId: string, newLocalThreadId: string): Promise<CapabilityResult>;
226
+ forkSession(currentLocalThreadId: string, newLocalThreadId: string, options: {
227
+ workspace: SessionWorkspaceSnapshot;
228
+ execution: ResolvedExecutionSnapshot;
229
+ beforeProviderFork: () => Promise<void>;
230
+ }): Promise<{
231
+ ok: true;
232
+ } | {
233
+ ok: false;
234
+ message: string;
235
+ }>;
219
236
  /**
220
237
  * Backend-free runtime readiness (not part of `AgentCapabilities`; surfaced for
221
238
  * the control console). Never spawns a runner.
@@ -233,6 +250,12 @@ export declare class RunnerManager implements AgentCapabilities {
233
250
  * run on that session's child (which owns its private CODEX_HOME). */
234
251
  private forwardCap;
235
252
  private getOrSpawn;
253
+ private performManagedFork;
254
+ private performClaudeManagedFork;
255
+ private bufferForkTargetMessage;
256
+ private reservedForkTarget;
257
+ private deliverForkBufferedMessage;
258
+ private deliverRotateMessage;
236
259
  private spawnHandle;
237
260
  private onChildMessage;
238
261
  /** Mark a handle dead and reject every pending run/cap with the exit reason. */