@rynx-ai/runtime 0.1.11-beta.3 → 0.1.11-beta.30

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 (44) hide show
  1. package/dist/claude/executor.d.ts +19 -5
  2. package/dist/claude/executor.js +56 -12
  3. package/dist/claude/models.d.ts +0 -5
  4. package/dist/claude/models.js +1 -7
  5. package/dist/claude/native-bridge.d.ts +2 -0
  6. package/dist/claude/native-bridge.js +23 -0
  7. package/dist/claude/native-hook-main.js +62 -0
  8. package/dist/claude/native-integration.d.ts +50 -10
  9. package/dist/claude/native-integration.js +262 -37
  10. package/dist/claude/session-status.d.ts +39 -0
  11. package/dist/claude/session-status.js +163 -0
  12. package/dist/claude/transcript.js +27 -17
  13. package/dist/codex-app-server/client.d.ts +10 -6
  14. package/dist/codex-app-server/client.js +67 -15
  15. package/dist/codex-app-server/forwarder.d.ts +92 -3
  16. package/dist/codex-app-server/forwarder.js +509 -56
  17. package/dist/codex-app-server/mapping.d.ts +3 -6
  18. package/dist/codex-app-server/mapping.js +174 -28
  19. package/dist/codex-app-server/mcp-startup.d.ts +13 -0
  20. package/dist/codex-app-server/mcp-startup.js +63 -0
  21. package/dist/codex-app-server/protocol.d.ts +64 -7
  22. package/dist/codex-app-server/ws-channel.js +19 -19
  23. package/dist/codex-home.js +2 -4
  24. package/dist/host.d.ts +64 -21
  25. package/dist/host.js +1330 -441
  26. package/dist/index.d.ts +1 -1
  27. package/dist/input-resources.d.ts +4 -0
  28. package/dist/input-resources.js +21 -5
  29. package/dist/models-catalog.d.ts +2 -1
  30. package/dist/models-catalog.js +94 -6
  31. package/dist/runner/child.d.ts +48 -21
  32. package/dist/runner/child.js +550 -48
  33. package/dist/runner/manager.d.ts +54 -13
  34. package/dist/runner/manager.js +479 -114
  35. package/dist/runner/protocol.d.ts +62 -19
  36. package/dist/runner/protocol.js +5 -0
  37. package/dist/runner/startup-policy.d.ts +7 -0
  38. package/dist/runner/startup-policy.js +10 -0
  39. package/dist/terminal/claude-tui.d.ts +3 -1
  40. package/dist/terminal/claude-tui.js +3 -1
  41. package/dist/terminal/registry.js +3 -2
  42. package/dist/terminal/tmux.d.ts +50 -7
  43. package/dist/terminal/tmux.js +168 -47
  44. package/package.json +4 -3
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export type { CodexCapabilities, CapabilityResult, CodexRuntimeStatus, } from ".
9
9
  export type { ClaudeForkIntent, CodexSessionStore, CodexSessionRecord, } from "./codex-session-store.js";
10
10
  export { RunnerManager, TerminalOpenError } from "./runner/manager.js";
11
11
  export type { RunnerManagerOptions, RunnerSessionContext, RunnerSessionContextProvider, OpenTerminalOptions, ParentTerminal, } from "./runner/manager.js";
12
- export type { InjectOutcome, TerminalOpenErrorCode, TerminalRole } from "./runner/protocol.js";
12
+ export type { InjectOutcome, InjectResult, TerminalOpenErrorCode, TerminalRole } from "./runner/protocol.js";
13
13
  export type { ResolveInteractionResult, RuntimeInteractionEvent, RuntimeInteractionListener, } from "./interactions.js";
14
14
  export { probeRuntimeStatus } from "./runtime-status.js";
15
15
  export { listRuntimeModels } from "./models-catalog.js";
@@ -3,6 +3,10 @@ import type { UserInput } from "./codex-app-server/protocol.js";
3
3
  /** Convert the provider-native user echo back to resource references. Unknown
4
4
  * local paths and remote URLs are deliberately omitted rather than exposed. */
5
5
  export declare function codexUserContent(input: readonly UserInput[]): UserContentPart[];
6
+ /** Match the forwarder's public user echo shape exactly. Codex merges adjacent
7
+ * text inputs (including the internal file marker) into one string before the
8
+ * Host sees the completed userMessage item. */
9
+ export declare function codexUserEchoContent(input: readonly UserInput[]): string | UserContentPart[] | undefined;
6
10
  /** Claude's native TUI has no structured image RPC. The target daemon supplies
7
11
  * only managed local paths and the marker makes the transcript echo reversible. */
8
12
  export declare function claudeInputText(input: RuntimeUserInput, attachmentToken?: string): string;
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { basename, extname } from "node:path";
3
3
  const CLAUDE_ATTACHMENT_TOKEN = "RYNX_ATTACHMENT_SET";
4
- const CLAUDE_ATTACHMENT_TOKEN_RE = /^\[RYNX_ATTACHMENT_SET (att_[0-9a-f-]{36}): inspect each RYNX_IMAGE_RESOURCE path with the Read tool before answering\.\]$/m;
4
+ const CLAUDE_ATTACHMENT_TOKEN_RE = /^\[RYNX_ATTACHMENT_SET (att_[0-9a-f-]{36}): inspect each RYNX_IMAGE_RESOURCE or RYNX_FILE_RESOURCE path with the Read tool before answering\.\]$/m;
5
5
  /** Convert the provider-native user echo back to resource references. Unknown
6
6
  * local paths and remote URLs are deliberately omitted rather than exposed. */
7
7
  export function codexUserContent(input) {
@@ -20,21 +20,37 @@ export function codexUserContent(input) {
20
20
  }
21
21
  return parts;
22
22
  }
23
+ /** Match the forwarder's public user echo shape exactly. Codex merges adjacent
24
+ * text inputs (including the internal file marker) into one string before the
25
+ * Host sees the completed userMessage item. */
26
+ export function codexUserEchoContent(input) {
27
+ const parts = codexUserContent(input);
28
+ if (parts.length === 0)
29
+ return undefined;
30
+ if (parts.every((part) => part.type === "input_text")) {
31
+ const text = parts.map((part) => part.text).join("").trim();
32
+ return text || undefined;
33
+ }
34
+ return parts;
35
+ }
23
36
  /** Claude's native TUI has no structured image RPC. The target daemon supplies
24
37
  * only managed local paths and the marker makes the transcript echo reversible. */
25
38
  export function claudeInputText(input, attachmentToken = `att_${randomUUID()}`) {
26
- const images = input.content.filter((part) => part.type === "local_image");
39
+ const attachments = input.content.filter((part) => part.type !== "text");
27
40
  const text = input.content
28
41
  .filter((part) => part.type === "text")
29
42
  .map((part) => part.text)
30
43
  .join("");
31
- if (images.length === 0)
44
+ if (attachments.length === 0)
32
45
  return text;
33
46
  if (!/^att_[0-9a-f-]{36}$/.test(attachmentToken)) {
34
47
  throw new Error("Claude attachment token is invalid");
35
48
  }
36
- const preamble = `[${CLAUDE_ATTACHMENT_TOKEN} ${attachmentToken}: inspect each RYNX_IMAGE_RESOURCE path with the Read tool before answering.]`;
37
- const markers = images.map((part) => `[[RYNX_IMAGE_RESOURCE ${JSON.stringify({ path: part.path })}]]`);
49
+ const preamble = `[${CLAUDE_ATTACHMENT_TOKEN} ${attachmentToken}: inspect each RYNX_IMAGE_RESOURCE or RYNX_FILE_RESOURCE path with the Read tool before answering.]`;
50
+ const markers = attachments.map((part) => `[[RYNX_${part.type === "local_image" ? "IMAGE" : "FILE"}_RESOURCE ${JSON.stringify({
51
+ path: part.path,
52
+ ...(part.resource.filename ? { filename: part.resource.filename } : {}),
53
+ })}]]`);
38
54
  return [preamble, ...markers, text]
39
55
  .filter((part, index) => index <= markers.length || part.length > 0)
40
56
  .join("\n");
@@ -1,11 +1,12 @@
1
1
  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
+ readCodexModels?: () => Promise<unknown>;
4
5
  readTraexModels?: () => Promise<unknown>;
5
6
  readTraexDebugModels?: () => Promise<unknown>;
6
7
  }
7
8
  /**
8
9
  * The model list for a runtime, without an execution backend.
9
- * Falls back to the configured model if live Traex discovery is unavailable.
10
+ * Falls back to the configured model if local/native discovery is unavailable.
10
11
  */
11
12
  export declare function listRuntimeModels(config: AppConfig, runtime: AgentRuntimeId, deps?: RuntimeModelCatalogDeps): Promise<ModelListResponse | null>;
@@ -3,24 +3,37 @@
3
3
  *
4
4
  * The parent control plane no longer holds an app-server, so `/models` can't be
5
5
  * a live `model/list` RPC anymore. Following reference implementation's static-catalog model, we
6
- * serve a config-derived Codex list and Traex's native `models --json` catalog.
6
+ * serve Codex's local model cache and Traex's native `models --json` catalog.
7
7
  * claude already has its own static list ({@link listClaudeModels}).
8
8
  */
9
9
  import { execFile } from "node:child_process";
10
+ import { readFile } from "node:fs/promises";
11
+ import { join } from "node:path";
10
12
  import { promisify } from "node:util";
11
- import { resolveRuntimeBinary, resolveRuntimeModel, } from "@rynx-ai/core";
13
+ import { getRuntimeProfile, resolveRuntimeBinary, resolveRuntimeHome, resolveRuntimeModel, } from "@rynx-ai/core";
12
14
  import { listClaudeModels } from "./claude/models.js";
13
15
  const execFileAsync = promisify(execFile);
14
16
  const TRAEX_MODELS_TIMEOUT_MS = 8_000;
15
17
  const TRAEX_MODELS_MAX_BYTES = 2 * 1024 * 1024;
16
18
  /**
17
19
  * The model list for a runtime, without an execution backend.
18
- * Falls back to the configured model if live Traex discovery is unavailable.
20
+ * Falls back to the configured model if local/native discovery is unavailable.
19
21
  */
20
22
  export async function listRuntimeModels(config, runtime, deps = {}) {
21
23
  if (runtime === "claude") {
22
24
  return listClaudeModels();
23
25
  }
26
+ if (runtime === "codex") {
27
+ try {
28
+ const configuredDefault = resolveRuntimeModel(config, runtime).trim();
29
+ const models = normalizeCodexModels(await (deps.readCodexModels ?? readCodexModels)(), configuredDefault);
30
+ if (models.length > 0)
31
+ return { data: models };
32
+ }
33
+ catch {
34
+ // Keep the configured fallback usable before Codex has populated its cache.
35
+ }
36
+ }
24
37
  if (runtime === "traex") {
25
38
  try {
26
39
  const value = await (deps.readTraexModels ?? readTraexModels)();
@@ -48,6 +61,72 @@ export async function listRuntimeModels(config, runtime, deps = {}) {
48
61
  }
49
62
  return { data: [{ id: model, model, isDefault: true }] };
50
63
  }
64
+ async function readCodexModels() {
65
+ const cachePath = join(resolveRuntimeHome(getRuntimeProfile("codex")), "models_cache.json");
66
+ return JSON.parse(await readFile(cachePath, "utf8"));
67
+ }
68
+ function normalizeCodexModels(value, configuredDefault) {
69
+ if (!value || typeof value !== "object" || Array.isArray(value))
70
+ return [];
71
+ const entries = value.models;
72
+ if (!Array.isArray(entries))
73
+ return [];
74
+ const models = [];
75
+ const seen = new Set();
76
+ for (const item of entries) {
77
+ if (!item || typeof item !== "object" || Array.isArray(item))
78
+ continue;
79
+ const record = item;
80
+ const id = typeof record.slug === "string" ? record.slug.trim() : "";
81
+ if (!id || seen.has(id) || record.visibility === "hide")
82
+ continue;
83
+ seen.add(id);
84
+ const supportedReasoningEfforts = Array.isArray(record.supported_reasoning_levels)
85
+ ? record.supported_reasoning_levels.flatMap((raw) => {
86
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
87
+ return [];
88
+ const effort = typeof raw.effort === "string"
89
+ ? raw.effort.trim()
90
+ : "";
91
+ if (!effort)
92
+ return [];
93
+ const description = raw.description;
94
+ return [{
95
+ reasoningEffort: effort,
96
+ ...(typeof description === "string" && description.trim()
97
+ ? { description: description.trim() }
98
+ : {}),
99
+ }];
100
+ })
101
+ : [];
102
+ const displayName = typeof record.display_name === "string"
103
+ ? record.display_name.trim()
104
+ : "";
105
+ const description = typeof record.description === "string"
106
+ ? record.description.trim()
107
+ : "";
108
+ const defaultReasoningEffort = typeof record.default_reasoning_level === "string"
109
+ ? record.default_reasoning_level.trim()
110
+ : "";
111
+ models.push({
112
+ id,
113
+ model: id,
114
+ ...(displayName ? { displayName } : {}),
115
+ ...(description ? { description } : {}),
116
+ isDefault: id === configuredDefault,
117
+ ...(supportedReasoningEfforts.length > 0 ? { supportedReasoningEfforts } : {}),
118
+ ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
119
+ });
120
+ }
121
+ if (configuredDefault && !seen.has(configuredDefault)) {
122
+ models.unshift({
123
+ id: configuredDefault,
124
+ model: configuredDefault,
125
+ isDefault: true,
126
+ });
127
+ }
128
+ return models;
129
+ }
51
130
  async function readTraexModels() {
52
131
  const { stdout } = await execFileAsync(resolveRuntimeBinary("traex"), ["models", "--json"], {
53
132
  encoding: "utf8",
@@ -93,19 +172,28 @@ function normalizeTraexModels(value, configuredDefault) {
93
172
  return [];
94
173
  const models = [];
95
174
  const seen = new Set();
175
+ let foundConfiguredDefault = false;
96
176
  for (const item of value) {
97
177
  if (!item || typeof item !== "object" || Array.isArray(item))
98
178
  continue;
99
179
  const record = item;
100
- const id = typeof record.name === "string" ? record.name.trim() : "";
180
+ const name = typeof record.name === "string" ? record.name.trim() : "";
181
+ // Traex 0.201.1 exposes the launchable model slug as real_name; name is its label/config alias.
182
+ const realName = typeof record.real_name === "string" ? record.real_name.trim() : "";
183
+ const id = realName || name;
101
184
  if (!id || seen.has(id))
102
185
  continue;
103
186
  seen.add(id);
187
+ const isDefault = configuredDefault
188
+ ? configuredDefault === id || configuredDefault === name
189
+ : models.length === 0;
190
+ foundConfiguredDefault ||= isDefault;
104
191
  models.push({
105
192
  id,
106
193
  model: id,
194
+ ...(name && name !== id ? { displayName: name } : {}),
107
195
  ...(typeof record.description === "string" ? { description: record.description } : {}),
108
- isDefault: configuredDefault ? id === configuredDefault : models.length === 0,
196
+ isDefault,
109
197
  ...(typeof record.context_window === "number" ? { contextWindow: record.context_window } : {}),
110
198
  ...(Array.isArray(record.supported_mime_types)
111
199
  ? { supportedMimeTypes: record.supported_mime_types }
@@ -113,7 +201,7 @@ function normalizeTraexModels(value, configuredDefault) {
113
201
  ...(record._meta && typeof record._meta === "object" ? { _meta: record._meta } : {}),
114
202
  });
115
203
  }
116
- if (configuredDefault && !seen.has(configuredDefault)) {
204
+ if (configuredDefault && !foundConfiguredDefault) {
117
205
  models.unshift({ id: configuredDefault, model: configuredDefault, isDefault: true });
118
206
  }
119
207
  return models;
@@ -1,17 +1,8 @@
1
- /**
2
- * Child-side runner session. One per runner process (i.e. per session); owns the
3
- * single execution backend via the injected executor (a {@link LocalAgentHost}
4
- * in production, which in a per-session process holds exactly one backend). It
5
- * translates inbound {@link ToChild} control messages into the live co-drive
6
- * surface (bring up the session's forwarder + TUI, inject / interrupt turns) and
7
- * per-thread capabilities, answered against the same backend so the parent never
8
- * needs an app-server of its own.
9
- */
10
- import { type AgentCapabilities, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
1
+ import { type AgentCapabilities, type LiveSessionFailure, type ResolvedExecutionSnapshot, type RuntimeTurnOptions, type RuntimeUserInput, type SessionCollaborationMode, type SessionInteractionResolution, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
11
2
  import type { SessionEvent } from "@rynx-ai/core";
12
3
  import type { TerminalInjector } from "../claude/native-integration.js";
13
4
  import type { ResolveInteractionResult } from "../interactions.js";
14
- import { type InjectOutcome } from "./protocol.js";
5
+ import { type InjectResult } from "./protocol.js";
15
6
  import type { ChildTransport } from "./transport.js";
16
7
  /** Re-target the mirror to a freshly minted rynx session (claude `/clear`·`/fork`)
17
8
  * + record the terminal transfer with the daemon. The child owns the transport,
@@ -31,24 +22,40 @@ interface LiveCodexProvider {
31
22
  args: string[];
32
23
  cwd: string;
33
24
  env?: Record<string, string>;
25
+ skipTraexStartupPrompts?: boolean;
34
26
  } | null>;
35
27
  ensureLiveCodexSession?(localThreadId: string, emit: (event: SessionEvent) => void, opts: {
36
28
  workspace: SessionWorkspaceSnapshot;
37
29
  execution: ResolvedExecutionSnapshot;
38
30
  retargetMirror?: RetargetMirror;
39
31
  }): Promise<boolean>;
40
- waitLiveReady?(localThreadId: string, timeoutMs?: number): Promise<boolean>;
41
- waitTerminalReady?(localThreadId: string, timeoutMs?: number): Promise<boolean>;
32
+ /** Known-thread Codex/Traex resume only: start the independent transcript
33
+ * observer after the replacement TUI has launched. Fresh discovery connects
34
+ * its listener before launch and this method is an idempotent no-op. */
35
+ startLiveCodexObserver?(localThreadId: string): void;
36
+ /** `null` delegates the deadline to RunnerManager. */
37
+ waitLiveReady?(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
38
+ /** `null` delegates the deadline to RunnerManager. */
39
+ waitTerminalReady?(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
42
40
  liveSessionError?(localThreadId: string): string | undefined;
43
- injectMessage?(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectOutcome>;
41
+ liveSessionFailure?(localThreadId: string): LiveSessionFailure | undefined;
42
+ failLiveStartup?(localThreadId: string, error: Error): boolean;
43
+ failLiveSession?(localThreadId: string, error: Error): boolean;
44
+ /** Codex/Traex auxiliary Terminal lifecycle: settle the current Turn, stop
45
+ * observer/forwarder, and evict the owned app-server while preserving the
46
+ * durable native-session binding for a later cold resume. */
47
+ teardownLiveCodexSession?(localThreadId: string, error?: Error): boolean;
48
+ injectMessage?(localThreadId: string, input: RuntimeUserInput | string, options?: RuntimeTurnOptions): Promise<InjectResult>;
49
+ updateCollaborationMode?(localThreadId: string, mode: SessionCollaborationMode): Promise<void>;
44
50
  interruptLive?(localThreadId: string): Promise<boolean>;
45
51
  stopLiveCodexSession?(localThreadId: string, opts?: {
46
52
  deferClaudeInteractionCleanup?: boolean;
47
53
  }): void;
48
54
  finalizeStoppedLiveSessions?(): void;
49
55
  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). */
56
+ /** Hand the session's tmux pane injector to the host (which doesn't own
57
+ * tmux). Claude uses it for messages; Codex uses it only for TUI-local
58
+ * controls that the app-server cannot resolve, such as the final Plan picker. */
52
59
  attachTerminalInjector?(localThreadId: string, injector: TerminalInjector): void;
53
60
  /** Internal Provider operation used only by MachineSessionService through
54
61
  * RunnerManager. It is deliberately absent from public AgentCapabilities. */
@@ -71,11 +78,18 @@ export declare class RunnerSession {
71
78
  /** Live terminals hosted by this session, and per-attach client handles. */
72
79
  private readonly terminals;
73
80
  private readonly attachments;
81
+ private readonly attachmentThreadIds;
82
+ private readonly traexStartupWatchers;
83
+ private readonly terminalWatchers;
74
84
  /** Opens are async; a close received before attach resolves tombstones the id. */
75
85
  private readonly pendingTerminalOpens;
76
86
  private readonly cancelledTerminalOpens;
77
87
  /** Session ids with a live codex forwarder started here (stopped on shutdown). */
78
88
  private readonly liveIds;
89
+ private mirrorQueue;
90
+ private readonly mirrorImageAcks;
91
+ /** Provider name retained for asynchronous Terminal-exit diagnostics. */
92
+ private readonly liveRuntimes;
79
93
  private shuttingDown;
80
94
  constructor({ transport, executor, onShutdown }: RunnerSessionDeps);
81
95
  private handle;
@@ -87,20 +101,33 @@ export declare class RunnerSession {
87
101
  * new session AND tells the daemon to alias the runner (terminal transfer) so
88
102
  * the new session stays injectable. */
89
103
  private mirrorChannel;
104
+ private enqueueMirror;
105
+ private sendMirroredEvent;
106
+ private sendMirrorImageFrame;
107
+ private rejectMirrorImageAcks;
90
108
  /**
91
- * Eagerly bring up a session's codex-native live view: start the persistent
92
- * forwarder connection (which resume-subscribes to mirror every turn) and
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.
109
+ * Eagerly bring up a session's codex-native live view. Fresh sessions connect
110
+ * the discovery listener before launching the detached TUI; known-thread
111
+ * resumes preload first, launch the replacement TUI, then start their
112
+ * persistent observer in the background.
96
113
  */
97
114
  private ensureLive;
115
+ private monitorCodexThreadStartup;
116
+ /** Discovery failure is terminal for this partial native launch. Publish the
117
+ * exact cause first so an in-flight injection wakes, then drop the observer
118
+ * and TUI so a later task retry creates a clean launch instead of reusing an
119
+ * already-rejected readiness promise. */
120
+ private failCodexThreadStartup;
98
121
  private inject;
122
+ private updateCollaborationMode;
99
123
  private interruptLive;
100
124
  /** Launch (idempotently) the session's codex TUI pane from the executor's
101
125
  * `codexTerminalSpec`. Shares the `${id}-main` terminal id with `term.open`,
102
126
  * so the web attach reuses the same detached pane. */
103
127
  private launchCodexPane;
128
+ private watchNativeTerminal;
129
+ private cancelTraexStartupWatcher;
130
+ private skipTraexStartupPrompts;
104
131
  private stopLive;
105
132
  /** Stop event forwarding, kill native terminals/hooks, then synchronously
106
133
  * scrub provider handoff files before the child process is allowed to exit. */