@rynx-ai/runtime 0.1.11-beta.4 → 0.1.11-beta.6

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/host.js CHANGED
@@ -1536,6 +1536,9 @@ export class LocalAgentHost {
1536
1536
  // Explicit rynx hooks and selected-skill plugin dirs remain enabled.
1537
1537
  settingSources: "",
1538
1538
  model: live.execution.model,
1539
+ ...(live.execution.reasoningEffort
1540
+ ? { reasoningEffort: live.execution.reasoningEffort }
1541
+ : {}),
1539
1542
  ...(live.execution.instructions
1540
1543
  ? { appendSystemPrompt: live.execution.instructions }
1541
1544
  : {}),
@@ -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",
@@ -1,5 +1,14 @@
1
1
  import { TerminalRegistry } from "../terminal/registry.js";
2
2
  import { toWireError } from "./protocol.js";
3
+ const TRAEX_STARTUP_WATCH_MS = 20_000;
4
+ const TRAEX_STARTUP_POLL_MS = 100;
5
+ const TRAEX_PROMPT_RETRY_MS = 500;
6
+ function normalizeTraexPane(pane) {
7
+ return pane.toLowerCase().replace(/\s+/g, " ").trim();
8
+ }
9
+ function isTerminalProtocolResponse(input) {
10
+ return /^(?:\x1b\[(?:(?:\?|>)[0-9;]*c|[0-9;]*(?:n|R|t)|[IO]))+$/.test(input);
11
+ }
3
12
  export class RunnerSession {
4
13
  transport;
5
14
  executor;
@@ -48,8 +57,17 @@ export class RunnerSession {
48
57
  });
49
58
  return;
50
59
  case "term.input":
51
- this.cancelTraexStartupWatcher(this.attachmentThreadIds.get(msg.attachId));
52
- this.attachments.get(msg.attachId)?.write(Buffer.from(msg.dataB64, "base64").toString("utf8"));
60
+ {
61
+ const localThreadId = this.attachmentThreadIds.get(msg.attachId);
62
+ const input = Buffer.from(msg.dataB64, "base64").toString("utf8");
63
+ // xterm sends device/focus reports through the same onData channel as
64
+ // keystrokes. They must reach the TUI without pretending the user has
65
+ // taken over startup prompt handling.
66
+ if (!isTerminalProtocolResponse(input)) {
67
+ this.cancelTraexStartupWatcher(localThreadId);
68
+ }
69
+ this.attachments.get(msg.attachId)?.write(input);
70
+ }
53
71
  return;
54
72
  case "term.resize":
55
73
  this.attachments.get(msg.attachId)?.resize(msg.cols, msg.rows);
@@ -215,8 +233,8 @@ export class RunnerSession {
215
233
  try {
216
234
  const input = msg.input ?? msg.text;
217
235
  const outcome = (await provider.injectMessage?.(msg.localThreadId, input)) ?? "notLive";
218
- if (outcome === "injected")
219
- this.cancelTraexStartupWatcher(msg.localThreadId);
236
+ // App-server injection is independent of the Terminal TUI startup, so it
237
+ // must not cancel prompt handling for the pane that is still starting.
220
238
  this.transport.send({ t: "injected", reqId: msg.reqId, localThreadId: msg.localThreadId, outcome });
221
239
  }
222
240
  catch (error) {
@@ -255,7 +273,6 @@ export class RunnerSession {
255
273
  if (!spec)
256
274
  return;
257
275
  const terminalId = `${localThreadId}-main`;
258
- const coldStart = !this.terminals.has(terminalId);
259
276
  const term = this.terminals.getOrCreate(terminalId, {
260
277
  cwd: spec.cwd,
261
278
  command: spec.command,
@@ -264,7 +281,7 @@ export class RunnerSession {
264
281
  rows: rows ?? 40,
265
282
  ...(spec.env ? { env: spec.env } : {}),
266
283
  });
267
- if (coldStart && spec.skipTraexStartupPrompts) {
284
+ if (spec.skipTraexStartupPrompts) {
268
285
  const watcher = Symbol(localThreadId);
269
286
  this.traexStartupWatchers.set(localThreadId, watcher);
270
287
  void this.skipTraexStartupPrompts(localThreadId, term, watcher);
@@ -279,46 +296,48 @@ export class RunnerSession {
279
296
  const prompts = [
280
297
  {
281
298
  id: "welcome",
282
- matches: (pane) => pane.includes("Welcome to TRAE CLI") && pane.includes("Press enter to continue"),
299
+ matches: (pane) => pane.includes("welcome to trae cli") && pane.includes("press enter to continue"),
283
300
  dismiss: () => terminal.sendEnter(),
284
301
  },
285
302
  {
286
303
  id: "migration",
287
- matches: (pane) => pane.includes("Legacy TRAE CLI data detected") &&
288
- pane.includes("Would you like to migrate detected data to the new TRAE CLI storage?") &&
289
- pane.includes("Select what to import:"),
304
+ matches: (pane) => pane.includes("legacy trae cli data detected") &&
305
+ pane.includes("select what to import") &&
306
+ (pane.includes("skip for now") || pane.includes("don't ask again")),
290
307
  dismiss: () => terminal.interrupt(),
291
308
  },
292
309
  {
293
310
  id: "hooks",
294
- matches: (pane) => pane.includes("Hooks need review") &&
295
- pane.includes("hooks are new or changed.") &&
296
- pane.includes("Trust all and continue") &&
297
- pane.includes("Continue without trusting"),
311
+ matches: (pane) => pane.includes("hooks need review") &&
312
+ pane.includes("trust all and continue") &&
313
+ pane.includes("continue without trusting"),
298
314
  dismiss: () => terminal.interrupt(),
299
315
  },
300
316
  ];
301
- const dismissed = new Set();
302
- const deadline = Date.now() + 20_000;
317
+ let activePromptId;
318
+ let lastDismissedAt = 0;
319
+ const deadline = Date.now() + TRAEX_STARTUP_WATCH_MS;
303
320
  const terminalId = `${localThreadId}-main`;
304
321
  while (!this.shuttingDown &&
305
322
  Date.now() < deadline &&
306
323
  this.traexStartupWatchers.get(localThreadId) === watcher &&
307
324
  this.terminals.get(terminalId) === terminal) {
308
- const pane = terminal.capturePane();
309
- // Once the normal composer is visible, startup is complete and there is
310
- // no reason to keep polling the tmux pane for the remainder of the window.
311
- if (pane.includes("TRAE CLI Next"))
312
- break;
325
+ // Do not treat a composer frame as completion: Traex can render it before
326
+ // the startup modals arrive. The bounded deadline stops this watcher.
327
+ const pane = normalizeTraexPane(terminal.capturePane());
313
328
  const prompt = prompts.find((candidate) => candidate.matches(pane));
314
- if (prompt && !dismissed.has(prompt.id)) {
329
+ const now = Date.now();
330
+ if (!prompt) {
331
+ activePromptId = undefined;
332
+ }
333
+ else if (prompt.id !== activePromptId ||
334
+ now - lastDismissedAt >= TRAEX_PROMPT_RETRY_MS) {
315
335
  prompt.dismiss();
316
- dismissed.add(prompt.id);
317
- if (dismissed.size === prompts.length)
318
- break;
336
+ activePromptId = prompt.id;
337
+ lastDismissedAt = now;
319
338
  }
320
339
  await new Promise((resolve) => {
321
- const timer = setTimeout(resolve, 100);
340
+ const timer = setTimeout(resolve, TRAEX_STARTUP_POLL_MS);
322
341
  timer.unref();
323
342
  });
324
343
  }
@@ -18,6 +18,8 @@ export interface ClaudeTuiArgs {
18
18
  settingSources?: string;
19
19
  /** Launch model (`--model`); omit to use claude's default. */
20
20
  model?: string;
21
+ /** Native Claude Code effort (`--effort`); omit to use its default. */
22
+ reasoningEffort?: string;
21
23
  /** Agent instructions appended to Claude Code's native system prompt. */
22
24
  appendSystemPrompt?: string;
23
25
  /** Resume a specific prior claude session (`--resume <id>`). */
@@ -36,4 +38,4 @@ export interface ClaudeTuiArgs {
36
38
  * Build the `claude` argv for an interactive, co-drivable TUI:
37
39
  * `[..extra] [--resume <id>] [--model <m>] [--append-system-prompt <text>] --settings <json>`.
38
40
  */
39
- export declare function buildClaudeTuiArgs({ settingsJson, settingSources, model, appendSystemPrompt, resume, forkSession, sessionId, additionalDirs, extraArgs, }: ClaudeTuiArgs): string[];
41
+ export declare function buildClaudeTuiArgs({ settingsJson, settingSources, model, reasoningEffort, appendSystemPrompt, resume, forkSession, sessionId, additionalDirs, extraArgs, }: ClaudeTuiArgs): string[];
@@ -2,7 +2,7 @@
2
2
  * Build the `claude` argv for an interactive, co-drivable TUI:
3
3
  * `[..extra] [--resume <id>] [--model <m>] [--append-system-prompt <text>] --settings <json>`.
4
4
  */
5
- export function buildClaudeTuiArgs({ settingsJson, settingSources, model, appendSystemPrompt, resume, forkSession = false, sessionId, additionalDirs = [], extraArgs = [], }) {
5
+ export function buildClaudeTuiArgs({ settingsJson, settingSources, model, reasoningEffort, appendSystemPrompt, resume, forkSession = false, sessionId, additionalDirs = [], extraArgs = [], }) {
6
6
  const args = [...extraArgs];
7
7
  if (additionalDirs.length > 0)
8
8
  args.push("--add-dir", ...additionalDirs);
@@ -16,6 +16,8 @@ export function buildClaudeTuiArgs({ settingsJson, settingSources, model, append
16
16
  args.push("--session-id", sessionId);
17
17
  if (model)
18
18
  args.push("--model", model);
19
+ if (reasoningEffort)
20
+ args.push("--effort", reasoningEffort);
19
21
  if (appendSystemPrompt)
20
22
  args.push("--append-system-prompt", appendSystemPrompt);
21
23
  args.push("--settings", settingsJson);
@@ -146,7 +146,9 @@ export declare class TmuxTerminal {
146
146
  paste(text: string, bufferName?: string): void;
147
147
  /**
148
148
  * Attach a client. `role: "read-only"` passes tmux `-r` so the viewer cannot
149
- * type (defense-in-depth on top of the WS bridge dropping input frames).
149
+ * type and `ignore-size` so even its initial PTY dimensions cannot resize the
150
+ * owner's pane (defense-in-depth on top of the WS bridge dropping input and
151
+ * resize frames).
150
152
  */
151
153
  attach(role: "owner" | "read-only", dims?: {
152
154
  cols?: number;
@@ -319,14 +319,17 @@ export class TmuxTerminal {
319
319
  }
320
320
  /**
321
321
  * Attach a client. `role: "read-only"` passes tmux `-r` so the viewer cannot
322
- * type (defense-in-depth on top of the WS bridge dropping input frames).
322
+ * type and `ignore-size` so even its initial PTY dimensions cannot resize the
323
+ * owner's pane (defense-in-depth on top of the WS bridge dropping input and
324
+ * resize frames).
323
325
  */
324
326
  async attach(role, dims) {
325
327
  this.start();
326
328
  const spawn = this.injectedSpawn ?? (await loadPtySpawn());
327
- const args = [...this.base(), "attach-session", "-t", TMUX_TARGET];
329
+ const args = [...this.base(), "attach-session"];
328
330
  if (role === "read-only")
329
- args.push("-r");
331
+ args.push("-r", "-f", "ignore-size");
332
+ args.push("-t", TMUX_TARGET);
330
333
  const proc = spawn(this.tmuxBin, args, {
331
334
  name: "xterm-256color",
332
335
  cols: dims?.cols ?? this.cols,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/runtime",
3
- "version": "0.1.11-beta.4",
3
+ "version": "0.1.11-beta.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "node-pty": "^1.0.0",
28
28
  "ws": "^8.21.0",
29
- "@rynx-ai/core": "0.1.11-beta.4"
29
+ "@rynx-ai/core": "0.1.11-beta.6"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/ws": "^8.18.1"