@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.
- package/dist/claude/executor.d.ts +19 -5
- package/dist/claude/executor.js +56 -12
- package/dist/claude/models.d.ts +0 -5
- package/dist/claude/models.js +1 -7
- package/dist/claude/native-bridge.d.ts +2 -0
- package/dist/claude/native-bridge.js +23 -0
- package/dist/claude/native-hook-main.js +62 -0
- package/dist/claude/native-integration.d.ts +50 -10
- package/dist/claude/native-integration.js +262 -37
- package/dist/claude/session-status.d.ts +39 -0
- package/dist/claude/session-status.js +163 -0
- package/dist/claude/transcript.js +27 -17
- package/dist/codex-app-server/client.d.ts +10 -6
- package/dist/codex-app-server/client.js +67 -15
- package/dist/codex-app-server/forwarder.d.ts +92 -3
- package/dist/codex-app-server/forwarder.js +509 -56
- package/dist/codex-app-server/mapping.d.ts +3 -6
- package/dist/codex-app-server/mapping.js +174 -28
- package/dist/codex-app-server/mcp-startup.d.ts +13 -0
- package/dist/codex-app-server/mcp-startup.js +63 -0
- package/dist/codex-app-server/protocol.d.ts +64 -7
- package/dist/codex-app-server/ws-channel.js +19 -19
- package/dist/codex-home.js +2 -4
- package/dist/host.d.ts +64 -21
- package/dist/host.js +1330 -441
- package/dist/index.d.ts +1 -1
- package/dist/input-resources.d.ts +4 -0
- package/dist/input-resources.js +21 -5
- package/dist/models-catalog.d.ts +2 -1
- package/dist/models-catalog.js +94 -6
- package/dist/runner/child.d.ts +48 -21
- package/dist/runner/child.js +550 -48
- package/dist/runner/manager.d.ts +54 -13
- package/dist/runner/manager.js +479 -114
- package/dist/runner/protocol.d.ts +62 -19
- package/dist/runner/protocol.js +5 -0
- package/dist/runner/startup-policy.d.ts +7 -0
- package/dist/runner/startup-policy.js +10 -0
- package/dist/terminal/claude-tui.d.ts +3 -1
- package/dist/terminal/claude-tui.js +3 -1
- package/dist/terminal/registry.js +3 -2
- package/dist/terminal/tmux.d.ts +50 -7
- package/dist/terminal/tmux.js +168 -47
- package/package.json +4 -3
package/dist/terminal/tmux.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { execFile, execFileSync } from "node:child_process";
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
|
-
import { existsSync, 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,6 +26,15 @@ 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
|
+
}
|
|
29
38
|
/** Deterministic private socket owned by one terminal name. Parent-side
|
|
30
39
|
* shutdown uses the same mapping when the runner child is too wedged to clean
|
|
31
40
|
* up its own tmux server. */
|
|
@@ -33,35 +42,72 @@ export function tmuxSocketPath(name) {
|
|
|
33
42
|
const key = createHash("sha256").update(name).digest("hex").slice(0, 16);
|
|
34
43
|
return join(tmpdir(), "rynx-term", `${key}.sock`);
|
|
35
44
|
}
|
|
36
|
-
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
45
|
+
/** Read tmux's own output-activity clock for a private terminal window.
|
|
46
|
+
*
|
|
47
|
+
* `#{window_activity}` is an epoch timestamp updated by tmux whenever the pane
|
|
48
|
+
* emits bytes. It is deliberately queried out-of-process rather than inferred
|
|
49
|
+
* from Rynx's forwarded PTY stream: the forwarder/status path is exactly what
|
|
50
|
+
* may have stalled when the idle reaper needs an independent liveness signal.
|
|
51
|
+
* Missing servers, command failures, timeouts, and unparseable output are all
|
|
52
|
+
* treated as unknown (`null`). */
|
|
53
|
+
export function tmuxWindowActivityAt(name, tmuxBin = resolveTmuxBin()) {
|
|
54
|
+
const socketPath = tmuxSocketPath(name);
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
execFile(tmuxBin, ["-S", socketPath, "display-message", "-p", "-t", TMUX_TARGET, "#{window_activity}"], { timeout: 2_000 }, (error, stdout) => {
|
|
57
|
+
if (error) {
|
|
58
|
+
resolve(null);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const raw = stdout.toString().trim();
|
|
62
|
+
if (!raw) {
|
|
63
|
+
resolve(null);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const value = Number(raw);
|
|
67
|
+
resolve(Number.isFinite(value) ? value : null);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Whether tmux itself reports an attached client for this private terminal.
|
|
72
|
+
* This remains authoritative if a parent-side attachment bookkeeping edge was
|
|
73
|
+
* missed during a transport failure. */
|
|
74
|
+
export function tmuxHasAttachedClient(name, tmuxBin = resolveTmuxBin()) {
|
|
75
|
+
const socketPath = tmuxSocketPath(name);
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
execFile(tmuxBin, ["-S", socketPath, "list-clients", "-t", TMUX_TARGET, "-F", "#{client_name}"], { timeout: 2_000 }, (error, stdout) => {
|
|
78
|
+
resolve(!error && stdout.toString().split("\n").some((line) => line.trim().length > 0));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Best-effort close of one private tmux server. Mirrors reference implementation's bounded
|
|
83
|
+
* `TerminalInstance.close`: attempt `kill-server`, then retire the private
|
|
84
|
+
* socket regardless of command outcome. The registry has already forgotten
|
|
85
|
+
* the resource, so cleanup failure is diagnostic rather than a second
|
|
86
|
+
* lifecycle state. */
|
|
87
|
+
export function terminateTmuxServer(name, tmuxBin = resolveTmuxBin()) {
|
|
40
88
|
const socketPath = tmuxSocketPath(name);
|
|
41
89
|
if (!existsSync(socketPath))
|
|
42
90
|
return true;
|
|
43
91
|
const base = ["-u", "-S", socketPath];
|
|
44
92
|
try {
|
|
45
|
-
execFileSync(tmuxBin, [...base, "kill-server"], {
|
|
93
|
+
execFileSync(tmuxBin, [...base, "kill-server"], {
|
|
94
|
+
stdio: "ignore",
|
|
95
|
+
timeout: 5_000,
|
|
96
|
+
});
|
|
46
97
|
}
|
|
47
98
|
catch {
|
|
48
|
-
//
|
|
99
|
+
// reference implementation treats close as best-effort and still retires the resource.
|
|
49
100
|
}
|
|
50
101
|
try {
|
|
51
|
-
|
|
52
|
-
stdio: "ignore",
|
|
53
|
-
});
|
|
54
|
-
return false;
|
|
102
|
+
unlinkSync(socketPath);
|
|
55
103
|
}
|
|
56
|
-
catch
|
|
57
|
-
|
|
58
|
-
typeof error === "object" &&
|
|
59
|
-
"code" in error &&
|
|
60
|
-
error.code === "ENOENT");
|
|
104
|
+
catch {
|
|
105
|
+
// Gone already, or best-effort filesystem cleanup failed.
|
|
61
106
|
}
|
|
107
|
+
return true;
|
|
62
108
|
}
|
|
63
109
|
/** Is a usable `tmux` on PATH? Cheap probe for the capability gate. */
|
|
64
|
-
export function isTmuxAvailable(tmuxBin =
|
|
110
|
+
export function isTmuxAvailable(tmuxBin = resolveTmuxBin()) {
|
|
65
111
|
try {
|
|
66
112
|
execFileSync(tmuxBin, ["-V"], { stdio: "ignore" });
|
|
67
113
|
return true;
|
|
@@ -94,6 +140,14 @@ export class TmuxTerminal {
|
|
|
94
140
|
tmuxBin;
|
|
95
141
|
injectedSpawn;
|
|
96
142
|
started = false;
|
|
143
|
+
lastPaneSnapshot = "";
|
|
144
|
+
/** Shared by every attachment watcher and by the lifecycle watcher's
|
|
145
|
+
* pane-dead stage. One Terminal must never fan the same tmux control probe
|
|
146
|
+
* out once per attached client. */
|
|
147
|
+
paneLivenessFlight;
|
|
148
|
+
/** Shared by lifecycle callers so capture + pane-dead remains one ordered
|
|
149
|
+
* reference implementation-style observation per Terminal. */
|
|
150
|
+
lifecycleLivenessFlight;
|
|
97
151
|
constructor(opts) {
|
|
98
152
|
this.name = opts.name;
|
|
99
153
|
this.cwd = opts.cwd;
|
|
@@ -102,7 +156,7 @@ export class TmuxTerminal {
|
|
|
102
156
|
this.env = withUtf8Locale(opts.env ?? process.env);
|
|
103
157
|
this.cols = opts.cols ?? DEFAULT_COLS;
|
|
104
158
|
this.rows = opts.rows ?? DEFAULT_ROWS;
|
|
105
|
-
this.tmuxBin = opts.tmuxBin
|
|
159
|
+
this.tmuxBin = resolveTmuxBin(opts.tmuxBin);
|
|
106
160
|
this.injectedSpawn = opts.ptySpawn;
|
|
107
161
|
const dir = join(tmpdir(), "rynx-term");
|
|
108
162
|
mkdirSync(dir, { recursive: true });
|
|
@@ -124,15 +178,8 @@ export class TmuxTerminal {
|
|
|
124
178
|
start() {
|
|
125
179
|
if (this.started)
|
|
126
180
|
return;
|
|
127
|
-
// Clear any stale server on this
|
|
128
|
-
|
|
129
|
-
// reaps our own leftover, never a live sibling.
|
|
130
|
-
try {
|
|
131
|
-
execFileSync(this.tmuxBin, [...this.base(), "kill-server"], { stdio: "ignore" });
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
// No stale server — expected on a fresh start.
|
|
135
|
-
}
|
|
181
|
+
// Clear any stale server on this deterministic private socket before launch.
|
|
182
|
+
terminateTmuxServer(this.name, this.tmuxBin);
|
|
136
183
|
// `new-session -d` starts detached; `-x/-y` seed the pane size so output
|
|
137
184
|
// wraps sanely before the first client attaches and resizes it.
|
|
138
185
|
execFileSync(this.tmuxBin, [
|
|
@@ -238,21 +285,95 @@ export class TmuxTerminal {
|
|
|
238
285
|
/** Async pane-liveness probe — MUST NOT block the event loop. The attach
|
|
239
286
|
* pane-death watcher polls this on an interval; a synchronous `execFileSync`
|
|
240
287
|
* there stalls the runner child's event loop (freezing the PTY stream → the
|
|
241
|
-
* terminal appears "stuck"). reference implementation's
|
|
242
|
-
*
|
|
243
|
-
|
|
288
|
+
* terminal appears "stuck"). Exactly like reference implementation's definitive pane probe,
|
|
289
|
+
* every command error is `unknown`; only `#{pane_dead}=1` is `dead`. */
|
|
290
|
+
livenessAsync() {
|
|
244
291
|
if (!this.started)
|
|
245
|
-
return Promise.resolve(
|
|
246
|
-
|
|
292
|
+
return Promise.resolve("dead");
|
|
293
|
+
if (this.paneLivenessFlight)
|
|
294
|
+
return this.paneLivenessFlight;
|
|
295
|
+
const flight = new Promise((resolve) => {
|
|
247
296
|
execFile(this.tmuxBin, [...this.base(), "list-panes", "-t", TMUX_TARGET, "-F", "#{pane_dead}"], { timeout: 2000 }, (err, stdout) => {
|
|
248
297
|
if (err) {
|
|
249
|
-
resolve(
|
|
298
|
+
resolve("unknown");
|
|
250
299
|
return;
|
|
251
300
|
}
|
|
252
301
|
const panes = stdout.toString().split(/\s+/).filter(Boolean);
|
|
253
|
-
|
|
302
|
+
if (panes.includes("1")) {
|
|
303
|
+
resolve("dead");
|
|
304
|
+
}
|
|
305
|
+
else if (panes.length > 0) {
|
|
306
|
+
resolve("alive");
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
resolve("unknown");
|
|
310
|
+
}
|
|
254
311
|
});
|
|
255
312
|
});
|
|
313
|
+
this.paneLivenessFlight = flight;
|
|
314
|
+
void flight.then(() => {
|
|
315
|
+
if (this.paneLivenessFlight === flight)
|
|
316
|
+
delete this.paneLivenessFlight;
|
|
317
|
+
}, () => {
|
|
318
|
+
if (this.paneLivenessFlight === flight)
|
|
319
|
+
delete this.paneLivenessFlight;
|
|
320
|
+
});
|
|
321
|
+
return flight;
|
|
322
|
+
}
|
|
323
|
+
/** reference implementation's always-on terminal lifecycle watcher first captures the pane:
|
|
324
|
+
* a control command that ran and reports the target missing is terminal exit;
|
|
325
|
+
* a probe that cannot spawn is inconclusive. If capture succeeds, the normal
|
|
326
|
+
* definitive `pane_dead` probe distinguishes live from exited. */
|
|
327
|
+
lifecycleLivenessAsync() {
|
|
328
|
+
if (!this.started)
|
|
329
|
+
return Promise.resolve("dead");
|
|
330
|
+
if (this.lifecycleLivenessFlight)
|
|
331
|
+
return this.lifecycleLivenessFlight;
|
|
332
|
+
const flight = new Promise((resolve) => {
|
|
333
|
+
execFile(this.tmuxBin, [...this.base(), "capture-pane", "-t", TMUX_TARGET, "-p", "-e"], (error, stdout) => {
|
|
334
|
+
if (error) {
|
|
335
|
+
const code = typeof error === "object" && error && "code" in error
|
|
336
|
+
? error.code
|
|
337
|
+
: undefined;
|
|
338
|
+
resolve(typeof code === "number" ? "dead" : "unknown");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
this.lastPaneSnapshot = stdout.toString();
|
|
342
|
+
void this.livenessAsync().then((liveness) => {
|
|
343
|
+
// A successful capture proves the target exists. reference implementation treats a
|
|
344
|
+
// subsequent pane-dead probe failure as non-dead for this tick.
|
|
345
|
+
resolve(liveness === "dead" ? "dead" : "alive");
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
this.lifecycleLivenessFlight = flight;
|
|
350
|
+
void flight.then(() => {
|
|
351
|
+
if (this.lifecycleLivenessFlight === flight)
|
|
352
|
+
delete this.lifecycleLivenessFlight;
|
|
353
|
+
}, () => {
|
|
354
|
+
if (this.lifecycleLivenessFlight === flight)
|
|
355
|
+
delete this.lifecycleLivenessFlight;
|
|
356
|
+
});
|
|
357
|
+
return flight;
|
|
358
|
+
}
|
|
359
|
+
/** Compatibility boolean for callers that cannot represent an inconclusive
|
|
360
|
+
* probe. Unknown must remain live so a transient tmux failure cannot tear down
|
|
361
|
+
* a healthy native Session. */
|
|
362
|
+
async isAliveAsync() {
|
|
363
|
+
return await this.livenessAsync() !== "dead";
|
|
364
|
+
}
|
|
365
|
+
/** PID of the process currently owning the pane. */
|
|
366
|
+
panePid() {
|
|
367
|
+
if (!this.started)
|
|
368
|
+
return undefined;
|
|
369
|
+
try {
|
|
370
|
+
const value = execFileSync(this.tmuxBin, [...this.base(), "list-panes", "-t", TMUX_TARGET, "-F", "#{pane_pid}"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim().split(/\s+/)[0];
|
|
371
|
+
const pid = Number(value);
|
|
372
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : undefined;
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
256
377
|
}
|
|
257
378
|
/** Type literal text into the pane (agent injection / co-drive from a
|
|
258
379
|
* non-PTY caller). `-l` sends the text literally rather than as key names. */
|
|
@@ -267,13 +388,15 @@ export class TmuxTerminal {
|
|
|
267
388
|
*/
|
|
268
389
|
capturePane() {
|
|
269
390
|
try {
|
|
270
|
-
|
|
391
|
+
const snapshot = execFileSync(this.tmuxBin, [...this.base(), "capture-pane", "-t", TMUX_TARGET, "-p"], {
|
|
271
392
|
env: this.env,
|
|
272
393
|
encoding: "utf8",
|
|
273
394
|
});
|
|
395
|
+
this.lastPaneSnapshot = snapshot;
|
|
396
|
+
return snapshot;
|
|
274
397
|
}
|
|
275
398
|
catch {
|
|
276
|
-
return
|
|
399
|
+
return this.lastPaneSnapshot;
|
|
277
400
|
}
|
|
278
401
|
}
|
|
279
402
|
/** Send a submit Enter as a KEY NAME (no `-l`), committing the input line.
|
|
@@ -319,14 +442,17 @@ export class TmuxTerminal {
|
|
|
319
442
|
}
|
|
320
443
|
/**
|
|
321
444
|
* Attach a client. `role: "read-only"` passes tmux `-r` so the viewer cannot
|
|
322
|
-
* type
|
|
445
|
+
* type and `ignore-size` so even its initial PTY dimensions cannot resize the
|
|
446
|
+
* owner's pane (defense-in-depth on top of the WS bridge dropping input and
|
|
447
|
+
* resize frames).
|
|
323
448
|
*/
|
|
324
449
|
async attach(role, dims) {
|
|
325
450
|
this.start();
|
|
326
451
|
const spawn = this.injectedSpawn ?? (await loadPtySpawn());
|
|
327
|
-
const args = [...this.base(), "attach-session"
|
|
452
|
+
const args = [...this.base(), "attach-session"];
|
|
328
453
|
if (role === "read-only")
|
|
329
|
-
args.push("-r");
|
|
454
|
+
args.push("-r", "-f", "ignore-size");
|
|
455
|
+
args.push("-t", TMUX_TARGET);
|
|
330
456
|
const proc = spawn(this.tmuxBin, args, {
|
|
331
457
|
name: "xterm-256color",
|
|
332
458
|
cols: dims?.cols ?? this.cols,
|
|
@@ -363,8 +489,8 @@ export class TmuxTerminal {
|
|
|
363
489
|
return;
|
|
364
490
|
// ASYNC probe (never execFileSync here — that would block the event loop
|
|
365
491
|
// and freeze the PTY stream, making the terminal unresponsive).
|
|
366
|
-
void this.
|
|
367
|
-
if (
|
|
492
|
+
void this.livenessAsync().then((liveness) => {
|
|
493
|
+
if (liveness === "dead" && !exited)
|
|
368
494
|
fireExit(0);
|
|
369
495
|
});
|
|
370
496
|
}, 1000);
|
|
@@ -386,12 +512,7 @@ export class TmuxTerminal {
|
|
|
386
512
|
}
|
|
387
513
|
/** Kill the tmux server (ends the session and all attaches). */
|
|
388
514
|
kill() {
|
|
389
|
-
|
|
390
|
-
execFileSync(this.tmuxBin, [...this.base(), "kill-server"], { stdio: "ignore" });
|
|
391
|
-
}
|
|
392
|
-
catch {
|
|
393
|
-
// Already gone — nothing to clean up.
|
|
394
|
-
}
|
|
515
|
+
terminateTmuxServer(this.name, this.tmuxBin);
|
|
395
516
|
this.started = false;
|
|
396
517
|
}
|
|
397
518
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynx-ai/runtime",
|
|
3
|
-
"version": "0.1.11-beta.
|
|
3
|
+
"version": "0.1.11-beta.30",
|
|
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": "
|
|
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.
|
|
30
|
+
"@rynx-ai/core": "0.1.11-beta.30"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/ws": "^8.18.1"
|