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

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.
@@ -70,8 +70,9 @@ export type ToChild = {
70
70
  }
71
71
  /** Eagerly bring up a session's live codex TUI + forwarder (codex-native), so
72
72
  * its turns mirror to the bus regardless of whether the web has attached the
73
- * terminal. Idempotent; the child replies `live.ready` (echoing `reqId`) once
74
- * the thread is bound. */
73
+ * terminal. For Codex-lineage fresh sessions, `live.ready` acknowledges that
74
+ * the pane and observer exist; background thread discovery races the
75
+ * executor's bridge wait. */
75
76
  | {
76
77
  t: "live.ensure";
77
78
  reqId: string;
@@ -160,8 +161,9 @@ export type FromChild = {
160
161
  execution: ResolvedExecutionSnapshot;
161
162
  parentSessionId?: string;
162
163
  }
163
- /** Result of a `live.ensure`: `ok` once the requested gate is reached (pane
164
- * started for setup, otherwise native thread bound). Echoes `reqId`. */
164
+ /** Result of a `live.ensure`: for Codex-lineage sessions `ok` means the pane
165
+ * and observer started; thread discovery continues in parallel with
166
+ * injection. Other Providers retain their own readiness gate. */
165
167
  | {
166
168
  t: "live.ready";
167
169
  reqId: string;
@@ -0,0 +1,4 @@
1
+ import type { AgentRuntimeId } from "@rynx-ai/core";
2
+ /** Current live providers are native CLI sessions. Keep this explicit so a
3
+ * future non-native provider does not silently inherit their startup budget. */
4
+ export declare function isManagedNativeProvider(provider: AgentRuntimeId | undefined): provider is AgentRuntimeId;
@@ -0,0 +1,5 @@
1
+ /** Current live providers are native CLI sessions. Keep this explicit so a
2
+ * future non-native provider does not silently inherit their startup budget. */
3
+ export function isManagedNativeProvider(provider) {
4
+ return provider === "codex" || provider === "traex" || provider === "claude";
5
+ }
@@ -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);
@@ -43,6 +43,16 @@ export interface TmuxTerminalOptions {
43
43
  /** Injectable for tests; defaults to the real `tmux` binary path. */
44
44
  tmuxBin?: string;
45
45
  }
46
+ /** Resolve the exact tmux executable selected by the current Rynx distribution. */
47
+ export declare function resolveTmuxBin(explicit?: string, env?: NodeJS.ProcessEnv): string;
48
+ /** Deterministic private socket owned by one terminal name. Parent-side
49
+ * shutdown uses the same mapping when the runner child is too wedged to clean
50
+ * up its own tmux server. */
51
+ export declare function tmuxSocketPath(name: string): string;
52
+ /** Kill one private tmux server and verify it no longer answers. Missing
53
+ * sockets are already stopped; an existing socket with an unusable tmux
54
+ * command is unproven and returns false. */
55
+ export declare function terminateTmuxServer(name: string, tmuxBin?: string): boolean;
46
56
  /** Is a usable `tmux` on PATH? Cheap probe for the capability gate. */
47
57
  export declare function isTmuxAvailable(tmuxBin?: string): boolean;
48
58
  export declare class TmuxTerminal {
@@ -102,7 +112,7 @@ export declare class TmuxTerminal {
102
112
  * there stalls the runner child's event loop (freezing the PTY stream → the
103
113
  * terminal appears "stuck"). reference implementation's `_tmux_session_alive` uses an async
104
114
  * subprocess + timeout for exactly this reason. */
105
- private isAliveAsync;
115
+ isAliveAsync(): Promise<boolean>;
106
116
  /** Type literal text into the pane (agent injection / co-drive from a
107
117
  * non-PTY caller). `-l` sends the text literally rather than as key names. */
108
118
  sendKeys(text: string): void;
@@ -138,7 +148,9 @@ export declare class TmuxTerminal {
138
148
  paste(text: string, bufferName?: string): void;
139
149
  /**
140
150
  * Attach a client. `role: "read-only"` passes tmux `-r` so the viewer cannot
141
- * type (defense-in-depth on top of the WS bridge dropping input frames).
151
+ * type and `ignore-size` so even its initial PTY dimensions cannot resize the
152
+ * owner's pane (defense-in-depth on top of the WS bridge dropping input and
153
+ * resize frames).
142
154
  */
143
155
  attach(role: "owner" | "read-only", dims?: {
144
156
  cols?: number;
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { execFile, execFileSync } from "node:child_process";
18
18
  import { createHash } from "node:crypto";
19
- import { mkdirSync } from "node:fs";
19
+ import { existsSync, mkdirSync, unlinkSync } from "node:fs";
20
20
  import { tmpdir } from "node:os";
21
21
  import { join } from "node:path";
22
22
  const TMUX_TARGET = "main";
@@ -26,8 +26,69 @@ const DEFAULT_ROWS = 24;
26
26
  * wheel-scroll (tmux copy-mode, enabled by `mouse on`) reaches as far back as
27
27
  * the terminal shows. Mirrors reference implementation's `TerminalEnvSpec.scrollback`. */
28
28
  const DEFAULT_SCROLLBACK = 20000;
29
+ /** Resolve the exact tmux executable selected by the current Rynx distribution. */
30
+ export function resolveTmuxBin(explicit, env = process.env) {
31
+ if (explicit !== undefined)
32
+ return explicit;
33
+ if (Object.hasOwn(env, "RYNX_TMUX_BIN")) {
34
+ return env.RYNX_TMUX_BIN?.trim() ?? "";
35
+ }
36
+ return "tmux";
37
+ }
38
+ /** Deterministic private socket owned by one terminal name. Parent-side
39
+ * shutdown uses the same mapping when the runner child is too wedged to clean
40
+ * up its own tmux server. */
41
+ export function tmuxSocketPath(name) {
42
+ const key = createHash("sha256").update(name).digest("hex").slice(0, 16);
43
+ return join(tmpdir(), "rynx-term", `${key}.sock`);
44
+ }
45
+ /** Kill one private tmux server and verify it no longer answers. Missing
46
+ * sockets are already stopped; an existing socket with an unusable tmux
47
+ * command is unproven and returns false. */
48
+ export function terminateTmuxServer(name, tmuxBin = resolveTmuxBin()) {
49
+ const socketPath = tmuxSocketPath(name);
50
+ if (!existsSync(socketPath))
51
+ return true;
52
+ const base = ["-u", "-S", socketPath];
53
+ try {
54
+ execFileSync(tmuxBin, [...base, "kill-server"], { stdio: "ignore" });
55
+ }
56
+ catch {
57
+ // A server may have exited between existsSync and kill-server. Verify below.
58
+ }
59
+ try {
60
+ execFileSync(tmuxBin, [...base, "has-session", "-t", TMUX_TARGET], {
61
+ stdio: "ignore",
62
+ });
63
+ return false;
64
+ }
65
+ catch (error) {
66
+ // Only a real tmux exit status proves the server/session is absent. Spawn
67
+ // failures (missing/unexecutable tmux) leave liveness unproven and must not
68
+ // unlink a socket that could still belong to a live server.
69
+ if (!error ||
70
+ typeof error !== "object" ||
71
+ !("status" in error) ||
72
+ typeof error.status !== "number") {
73
+ return false;
74
+ }
75
+ try {
76
+ // tmux custom-socket servers can exit while leaving the filesystem entry
77
+ // behind. Remove that verified-stale socket so process/socket inventories
78
+ // do not accumulate dead terminals forever.
79
+ unlinkSync(socketPath);
80
+ return true;
81
+ }
82
+ catch (unlinkError) {
83
+ return Boolean(unlinkError &&
84
+ typeof unlinkError === "object" &&
85
+ "code" in unlinkError &&
86
+ unlinkError.code === "ENOENT");
87
+ }
88
+ }
89
+ }
29
90
  /** Is a usable `tmux` on PATH? Cheap probe for the capability gate. */
30
- export function isTmuxAvailable(tmuxBin = "tmux") {
91
+ export function isTmuxAvailable(tmuxBin = resolveTmuxBin()) {
31
92
  try {
32
93
  execFileSync(tmuxBin, ["-V"], { stdio: "ignore" });
33
94
  return true;
@@ -68,7 +129,7 @@ export class TmuxTerminal {
68
129
  this.env = withUtf8Locale(opts.env ?? process.env);
69
130
  this.cols = opts.cols ?? DEFAULT_COLS;
70
131
  this.rows = opts.rows ?? DEFAULT_ROWS;
71
- this.tmuxBin = opts.tmuxBin ?? "tmux";
132
+ this.tmuxBin = resolveTmuxBin(opts.tmuxBin);
72
133
  this.injectedSpawn = opts.ptySpawn;
73
134
  const dir = join(tmpdir(), "rynx-term");
74
135
  mkdirSync(dir, { recursive: true });
@@ -76,8 +137,7 @@ export class TmuxTerminal {
76
137
  // under the unix-domain path limit (~104 chars on macOS) — a full session id
77
138
  // like `sess_<uuid>-main` under $TMPDIR would overflow it and fail with
78
139
  // "File name too long". The hash stays unique per terminal.
79
- const key = createHash("sha256").update(opts.name).digest("hex").slice(0, 16);
80
- this.socketPath = join(dir, `${key}.sock`);
140
+ this.socketPath = tmuxSocketPath(opts.name);
81
141
  }
82
142
  /** tmux argv prefix targeting this terminal's private server. */
83
143
  base() {
@@ -286,14 +346,17 @@ export class TmuxTerminal {
286
346
  }
287
347
  /**
288
348
  * Attach a client. `role: "read-only"` passes tmux `-r` so the viewer cannot
289
- * type (defense-in-depth on top of the WS bridge dropping input frames).
349
+ * type and `ignore-size` so even its initial PTY dimensions cannot resize the
350
+ * owner's pane (defense-in-depth on top of the WS bridge dropping input and
351
+ * resize frames).
290
352
  */
291
353
  async attach(role, dims) {
292
354
  this.start();
293
355
  const spawn = this.injectedSpawn ?? (await loadPtySpawn());
294
- const args = [...this.base(), "attach-session", "-t", TMUX_TARGET];
356
+ const args = [...this.base(), "attach-session"];
295
357
  if (role === "read-only")
296
- args.push("-r");
358
+ args.push("-r", "-f", "ignore-size");
359
+ args.push("-t", TMUX_TARGET);
297
360
  const proc = spawn(this.tmuxBin, args, {
298
361
  name: "xterm-256color",
299
362
  cols: dims?.cols ?? this.cols,
@@ -353,12 +416,7 @@ export class TmuxTerminal {
353
416
  }
354
417
  /** Kill the tmux server (ends the session and all attaches). */
355
418
  kill() {
356
- try {
357
- execFileSync(this.tmuxBin, [...this.base(), "kill-server"], { stdio: "ignore" });
358
- }
359
- catch {
360
- // Already gone — nothing to clean up.
361
- }
419
+ terminateTmuxServer(this.name, this.tmuxBin);
362
420
  this.started = false;
363
421
  }
364
422
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/runtime",
3
- "version": "0.1.11-beta.2",
3
+ "version": "0.1.11-beta.20",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -24,9 +24,10 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "node-pty": "^1.0.0",
27
+ "node-pty": "1.2.0-beta.15",
28
+ "smol-toml": "1.7.1",
28
29
  "ws": "^8.21.0",
29
- "@rynx-ai/core": "0.1.11-beta.2"
30
+ "@rynx-ai/core": "0.1.11-beta.20"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/ws": "^8.18.1"