@rynx-ai/runtime 0.1.11-beta.32 → 0.1.11-beta.34
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/native-bridge.d.ts +16 -1
- package/dist/claude/native-bridge.js +91 -17
- package/dist/claude/native-integration.d.ts +10 -0
- package/dist/claude/native-integration.js +100 -6
- package/dist/codex-app-server/forwarder.js +17 -1
- package/dist/codex-app-server/process-registry.d.ts +36 -0
- package/dist/codex-app-server/process-registry.js +320 -0
- package/dist/codex-app-server/ws-channel.d.ts +7 -0
- package/dist/codex-app-server/ws-channel.js +85 -9
- package/dist/host.d.ts +9 -1
- package/dist/host.js +48 -14
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/runner/child.d.ts +24 -3
- package/dist/runner/child.js +327 -58
- package/dist/runner/manager.d.ts +33 -18
- package/dist/runner/manager.js +229 -49
- package/dist/runner/protocol.d.ts +79 -7
- package/dist/runner/transport.d.ts +9 -2
- package/dist/runner/transport.js +55 -3
- package/dist/runner-main.js +8 -3
- package/dist/terminal/codex-tui.d.ts +4 -0
- package/dist/terminal/codex-tui.js +5 -0
- package/dist/terminal/control-parser.d.ts +39 -0
- package/dist/terminal/control-parser.js +172 -0
- package/dist/terminal/registry.d.ts +15 -13
- package/dist/terminal/registry.js +38 -20
- package/dist/terminal/spool.d.ts +47 -0
- package/dist/terminal/spool.js +231 -0
- package/dist/terminal/tmux.d.ts +93 -84
- package/dist/terminal/tmux.js +666 -199
- package/package.json +3 -4
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { closeSync, mkdirSync, mkdtempSync, openSync, readSync, rmSync, unlinkSync, writeSync, } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const DEFAULT_SEGMENT_BYTES = 1024 * 1024;
|
|
5
|
+
const MAX_READ_BYTES = 1024 * 1024;
|
|
6
|
+
export class TerminalSpoolCapacityError extends Error {
|
|
7
|
+
name = "TerminalSpoolCapacityError";
|
|
8
|
+
}
|
|
9
|
+
export class TerminalSpoolProtocolError extends Error {
|
|
10
|
+
name = "TerminalSpoolProtocolError";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A single-reader byte log backed by disposable fixed-size files. Producers
|
|
14
|
+
* append while the consumer pulls with an exact monotonic offset. Fully
|
|
15
|
+
* consumed segments are unlinked immediately, so retained disk is bounded by
|
|
16
|
+
* viewer lag rather than the lifetime output volume.
|
|
17
|
+
*/
|
|
18
|
+
export class SegmentedTerminalSpool {
|
|
19
|
+
directory;
|
|
20
|
+
segmentBytes;
|
|
21
|
+
quotaBytes;
|
|
22
|
+
segments = [];
|
|
23
|
+
writeOffset = 0;
|
|
24
|
+
readOffset = 0;
|
|
25
|
+
ended = false;
|
|
26
|
+
closed = false;
|
|
27
|
+
failure;
|
|
28
|
+
pending;
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
this.segmentBytes = positiveInteger(options.segmentBytes ?? DEFAULT_SEGMENT_BYTES, "segmentBytes");
|
|
31
|
+
this.quotaBytes = options.quotaBytes === undefined
|
|
32
|
+
? undefined
|
|
33
|
+
: positiveInteger(options.quotaBytes, "quotaBytes");
|
|
34
|
+
const parent = options.directory ?? tmpdir();
|
|
35
|
+
mkdirSync(parent, { recursive: true });
|
|
36
|
+
this.directory = mkdtempSync(join(parent, "rynx-terminal-spool-"));
|
|
37
|
+
}
|
|
38
|
+
get retainedBytes() {
|
|
39
|
+
return this.writeOffset - this.readOffset;
|
|
40
|
+
}
|
|
41
|
+
get finalOffset() {
|
|
42
|
+
return this.ended ? this.writeOffset : undefined;
|
|
43
|
+
}
|
|
44
|
+
append(input) {
|
|
45
|
+
this.assertWritable();
|
|
46
|
+
if (input.byteLength === 0)
|
|
47
|
+
return;
|
|
48
|
+
if (this.quotaBytes !== undefined &&
|
|
49
|
+
this.retainedBytes + input.byteLength > this.quotaBytes) {
|
|
50
|
+
const error = new TerminalSpoolCapacityError(`Terminal viewer lag exceeded ${this.quotaBytes} bytes`);
|
|
51
|
+
this.fail(error);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
const source = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
55
|
+
let sourceOffset = 0;
|
|
56
|
+
while (sourceOffset < source.byteLength) {
|
|
57
|
+
let segment = this.segments.at(-1);
|
|
58
|
+
if (!segment || segment.sealed)
|
|
59
|
+
segment = this.createSegment();
|
|
60
|
+
const writable = Math.min(this.segmentBytes - segment.length, source.byteLength - sourceOffset);
|
|
61
|
+
writeSync(segment.fd, source, sourceOffset, writable, segment.length);
|
|
62
|
+
segment.length += writable;
|
|
63
|
+
sourceOffset += writable;
|
|
64
|
+
this.writeOffset += writable;
|
|
65
|
+
if (segment.length === this.segmentBytes) {
|
|
66
|
+
segment.sealed = true;
|
|
67
|
+
closeSync(segment.fd);
|
|
68
|
+
segment.fd = -1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.settlePending();
|
|
72
|
+
}
|
|
73
|
+
end() {
|
|
74
|
+
if (this.closed || this.ended)
|
|
75
|
+
return;
|
|
76
|
+
this.ended = true;
|
|
77
|
+
const segment = this.segments.at(-1);
|
|
78
|
+
if (segment && !segment.sealed) {
|
|
79
|
+
segment.sealed = true;
|
|
80
|
+
closeSync(segment.fd);
|
|
81
|
+
segment.fd = -1;
|
|
82
|
+
}
|
|
83
|
+
this.settlePending();
|
|
84
|
+
}
|
|
85
|
+
fail(error) {
|
|
86
|
+
if (this.closed || this.failure)
|
|
87
|
+
return;
|
|
88
|
+
this.failure = error;
|
|
89
|
+
this.ended = true;
|
|
90
|
+
const pending = this.pending;
|
|
91
|
+
this.pending = undefined;
|
|
92
|
+
pending?.reject(error);
|
|
93
|
+
}
|
|
94
|
+
read(offset, maxBytes) {
|
|
95
|
+
if (this.closed)
|
|
96
|
+
return Promise.reject(new TerminalSpoolProtocolError("Terminal spool is closed"));
|
|
97
|
+
if (this.failure)
|
|
98
|
+
return Promise.reject(this.failure);
|
|
99
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset !== this.readOffset) {
|
|
100
|
+
return Promise.reject(new TerminalSpoolProtocolError(`Terminal spool expected offset ${this.readOffset}, received ${String(offset)}`));
|
|
101
|
+
}
|
|
102
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_READ_BYTES) {
|
|
103
|
+
return Promise.reject(new TerminalSpoolProtocolError(`Terminal spool maxBytes must be within 1..${MAX_READ_BYTES}`));
|
|
104
|
+
}
|
|
105
|
+
if (this.pending) {
|
|
106
|
+
return Promise.reject(new TerminalSpoolProtocolError("Concurrent reads from one terminal spool are not allowed"));
|
|
107
|
+
}
|
|
108
|
+
const available = this.writeOffset - this.readOffset;
|
|
109
|
+
if (available > 0 || this.ended)
|
|
110
|
+
return Promise.resolve(this.readNow(maxBytes));
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
this.pending = { offset, maxBytes, resolve, reject };
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
close() {
|
|
116
|
+
if (this.closed)
|
|
117
|
+
return;
|
|
118
|
+
this.closed = true;
|
|
119
|
+
const error = new TerminalSpoolProtocolError("Terminal spool is closed");
|
|
120
|
+
const pending = this.pending;
|
|
121
|
+
this.pending = undefined;
|
|
122
|
+
pending?.reject(error);
|
|
123
|
+
for (const segment of this.segments) {
|
|
124
|
+
if (segment.fd !== -1) {
|
|
125
|
+
try {
|
|
126
|
+
closeSync(segment.fd);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Best-effort cleanup continues through every segment.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this.segments.length = 0;
|
|
134
|
+
rmSync(this.directory, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
assertWritable() {
|
|
137
|
+
if (this.closed)
|
|
138
|
+
throw new TerminalSpoolProtocolError("Terminal spool is closed");
|
|
139
|
+
if (this.failure)
|
|
140
|
+
throw this.failure;
|
|
141
|
+
if (this.ended)
|
|
142
|
+
throw new TerminalSpoolProtocolError("Terminal spool has ended");
|
|
143
|
+
}
|
|
144
|
+
createSegment() {
|
|
145
|
+
const start = this.writeOffset;
|
|
146
|
+
const path = join(this.directory, `${String(start).padStart(20, "0")}.bin`);
|
|
147
|
+
const segment = {
|
|
148
|
+
path,
|
|
149
|
+
start,
|
|
150
|
+
fd: openSync(path, "wx+", 0o600),
|
|
151
|
+
length: 0,
|
|
152
|
+
sealed: false,
|
|
153
|
+
};
|
|
154
|
+
this.segments.push(segment);
|
|
155
|
+
return segment;
|
|
156
|
+
}
|
|
157
|
+
settlePending() {
|
|
158
|
+
const pending = this.pending;
|
|
159
|
+
if (!pending || (this.writeOffset === pending.offset && !this.ended))
|
|
160
|
+
return;
|
|
161
|
+
this.pending = undefined;
|
|
162
|
+
if (this.failure)
|
|
163
|
+
pending.reject(this.failure);
|
|
164
|
+
else
|
|
165
|
+
pending.resolve(this.readNow(pending.maxBytes));
|
|
166
|
+
}
|
|
167
|
+
readNow(maxBytes) {
|
|
168
|
+
if (this.failure)
|
|
169
|
+
throw this.failure;
|
|
170
|
+
const available = this.writeOffset - this.readOffset;
|
|
171
|
+
const length = Math.min(maxBytes, available);
|
|
172
|
+
const data = Buffer.allocUnsafe(length);
|
|
173
|
+
let copied = 0;
|
|
174
|
+
let cursor = this.readOffset;
|
|
175
|
+
for (const segment of this.segments) {
|
|
176
|
+
const segmentEnd = segment.start + segment.length;
|
|
177
|
+
if (cursor >= segmentEnd)
|
|
178
|
+
continue;
|
|
179
|
+
if (cursor < segment.start) {
|
|
180
|
+
throw new TerminalSpoolProtocolError("Terminal spool contains a byte gap");
|
|
181
|
+
}
|
|
182
|
+
const within = cursor - segment.start;
|
|
183
|
+
const count = Math.min(segment.length - within, length - copied);
|
|
184
|
+
if (count <= 0)
|
|
185
|
+
break;
|
|
186
|
+
const fd = segment.fd === -1 ? openSync(segment.path, "r") : segment.fd;
|
|
187
|
+
try {
|
|
188
|
+
const actual = readSync(fd, data, copied, count, within);
|
|
189
|
+
if (actual !== count)
|
|
190
|
+
throw new TerminalSpoolProtocolError("Terminal spool segment was truncated");
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
if (segment.fd === -1)
|
|
194
|
+
closeSync(fd);
|
|
195
|
+
}
|
|
196
|
+
copied += count;
|
|
197
|
+
cursor += count;
|
|
198
|
+
if (copied === length)
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
this.readOffset += copied;
|
|
202
|
+
this.discardConsumedSegments();
|
|
203
|
+
const done = this.ended && this.readOffset === this.writeOffset;
|
|
204
|
+
return {
|
|
205
|
+
data: Uint8Array.from(data.subarray(0, copied)),
|
|
206
|
+
nextOffset: this.readOffset,
|
|
207
|
+
done,
|
|
208
|
+
...(done ? { finalOffset: this.writeOffset } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
discardConsumedSegments() {
|
|
212
|
+
while (this.segments.length > 0) {
|
|
213
|
+
const segment = this.segments[0];
|
|
214
|
+
if (!segment.sealed || segment.start + segment.length > this.readOffset)
|
|
215
|
+
return;
|
|
216
|
+
this.segments.shift();
|
|
217
|
+
try {
|
|
218
|
+
unlinkSync(segment.path);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// The directory-level close remains the final cleanup fence.
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function positiveInteger(value, field) {
|
|
227
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
228
|
+
throw new TypeError(`${field} must be a positive integer`);
|
|
229
|
+
}
|
|
230
|
+
return value;
|
|
231
|
+
}
|
package/dist/terminal/tmux.d.ts
CHANGED
|
@@ -1,38 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { type TerminalSpoolRead } from "./spool.js";
|
|
2
|
+
export type TerminalReaderDoneReason = "terminal_exited" | "client_closed" | "backpressure" | "internal";
|
|
3
|
+
export interface TerminalReaderDone {
|
|
4
|
+
reason: TerminalReaderDoneReason;
|
|
5
|
+
finalOffset: number;
|
|
6
|
+
exitCode: number;
|
|
7
|
+
}
|
|
8
|
+
/** Live half of a prepared control attachment. Output is pulled by exact
|
|
9
|
+
* offset; reader completion is published independently of pull activity. */
|
|
10
|
+
export interface PullTerminalAttachment {
|
|
11
|
+
readonly dimensions?: {
|
|
12
|
+
cols: number;
|
|
13
|
+
rows: number;
|
|
14
|
+
};
|
|
15
|
+
readonly readerDone: Promise<TerminalReaderDone>;
|
|
16
|
+
read(offset: number, maxBytes: number): Promise<TerminalSpoolRead>;
|
|
17
|
+
onResize(listener: (dimensions: {
|
|
18
|
+
cols: number;
|
|
19
|
+
rows: number;
|
|
8
20
|
}) => void): void;
|
|
9
|
-
write(data:
|
|
10
|
-
resize(cols: number, rows: number): void
|
|
11
|
-
|
|
21
|
+
write(data: Uint8Array): Promise<void>;
|
|
22
|
+
resize(cols: number, rows: number): Promise<void>;
|
|
23
|
+
kill(): void;
|
|
24
|
+
}
|
|
25
|
+
/** Capture phase. No tmux control client exists until `start` succeeds. */
|
|
26
|
+
export interface PreparedTerminalAttachment {
|
|
27
|
+
readonly seedBytes: number;
|
|
28
|
+
readonly dimensions?: {
|
|
29
|
+
cols: number;
|
|
30
|
+
rows: number;
|
|
31
|
+
};
|
|
32
|
+
readSeed(offset: number, maxBytes: number): Promise<TerminalSpoolRead>;
|
|
33
|
+
start(): Promise<PullTerminalAttachment>;
|
|
12
34
|
kill(): void;
|
|
13
35
|
}
|
|
14
36
|
/** Result of probing the inner tmux pane. `unknown` is deliberately distinct
|
|
15
37
|
* from `dead`: a timed-out/unexecutable control command is inconclusive;
|
|
16
|
-
* `dead` requires an explicit dead pane. The lifecycle watcher separately
|
|
17
|
-
*
|
|
38
|
+
* `dead` requires an explicit dead pane. The lifecycle watcher separately uses
|
|
39
|
+
* capture failure to identify a vanished server/session. */
|
|
18
40
|
export type TerminalLiveness = "alive" | "dead" | "unknown";
|
|
19
|
-
/** Minimal node-pty surface (kept local so this module has no type dep on it). */
|
|
20
|
-
interface PtyProcess {
|
|
21
|
-
onData(cb: (data: string) => void): void;
|
|
22
|
-
onExit(cb: (e: {
|
|
23
|
-
exitCode: number;
|
|
24
|
-
}) => void): void;
|
|
25
|
-
write(data: string): void;
|
|
26
|
-
resize(cols: number, rows: number): void;
|
|
27
|
-
kill(signal?: string): void;
|
|
28
|
-
}
|
|
29
|
-
type PtySpawn = (file: string, args: string[], opts: {
|
|
30
|
-
name: string;
|
|
31
|
-
cols: number;
|
|
32
|
-
rows: number;
|
|
33
|
-
cwd: string;
|
|
34
|
-
env: Record<string, string>;
|
|
35
|
-
}) => PtyProcess;
|
|
36
41
|
export interface TmuxTerminalOptions {
|
|
37
42
|
/** Unique name for the tmux session + socket, e.g. `rynx-<sessionId>-<termId>`. */
|
|
38
43
|
name: string;
|
|
@@ -43,17 +48,42 @@ export interface TmuxTerminalOptions {
|
|
|
43
48
|
env?: Record<string, string>;
|
|
44
49
|
cols?: number;
|
|
45
50
|
rows?: number;
|
|
46
|
-
/**
|
|
47
|
-
|
|
51
|
+
/** Rows retained by tmux for reconnect-time history. */
|
|
52
|
+
scrollback?: number;
|
|
53
|
+
/** Allow pane escape sequences to pass through tmux. */
|
|
54
|
+
tmuxAllowPassthrough?: boolean;
|
|
55
|
+
/** Delay the inner command until the first client attaches. */
|
|
56
|
+
tmuxStartOnAttach?: boolean;
|
|
57
|
+
/** Preserve a dead pane for diagnostics until lifecycle cleanup runs. */
|
|
58
|
+
keepAliveAfterExit?: boolean;
|
|
48
59
|
/** Injectable for tests; defaults to the real `tmux` binary path. */
|
|
49
60
|
tmuxBin?: string;
|
|
50
61
|
}
|
|
62
|
+
/** Decode tmux's three-digit octal escapes in a `%output` payload. */
|
|
63
|
+
export declare function unescapeControlOutput(value: Uint8Array): Uint8Array;
|
|
64
|
+
/** Byte-exact control commands for one browser input frame. */
|
|
65
|
+
export declare function hexSendKeysCommands(target: string, data: Uint8Array): Buffer[];
|
|
66
|
+
/** Global tmux argv for a control-mode attach. Exported to pin role semantics. */
|
|
67
|
+
export declare function controlAttachArgs(socketPath: string, role: "owner" | "read-only"): string[];
|
|
68
|
+
export interface TmuxManagedOptionSpec {
|
|
69
|
+
scrollback: number;
|
|
70
|
+
allowPassthrough: boolean;
|
|
71
|
+
keepAliveAfterExit: boolean;
|
|
72
|
+
}
|
|
73
|
+
/** Options applied before pane creation so per-pane values take effect. */
|
|
74
|
+
export declare function tmuxManagedOptionCommands(spec: TmuxManagedOptionSpec): string[][];
|
|
75
|
+
/** Extract the pane size from tmux's `%layout-change` notification. */
|
|
76
|
+
export declare function controlLayoutDimensions(line: Uint8Array): {
|
|
77
|
+
cols: number;
|
|
78
|
+
rows: number;
|
|
79
|
+
} | undefined;
|
|
51
80
|
/** Resolve the exact tmux executable selected by the current Rynx distribution. */
|
|
52
81
|
export declare function resolveTmuxBin(explicit?: string, env?: NodeJS.ProcessEnv): string;
|
|
53
82
|
/** Deterministic private socket owned by one terminal name. Parent-side
|
|
54
83
|
* shutdown uses the same mapping when the runner child is too wedged to clean
|
|
55
84
|
* up its own tmux server. */
|
|
56
|
-
export declare function tmuxSocketPath(name: string): string;
|
|
85
|
+
export declare function tmuxSocketPath(name: string, ownerPid?: number): string;
|
|
86
|
+
export declare function tmuxInstanceDir(name: string, ownerPid?: number): string;
|
|
57
87
|
/** Read tmux's own output-activity clock for a private terminal window.
|
|
58
88
|
*
|
|
59
89
|
* `#{window_activity}` is an epoch timestamp updated by tmux whenever the pane
|
|
@@ -62,17 +92,23 @@ export declare function tmuxSocketPath(name: string): string;
|
|
|
62
92
|
* may have stalled when the idle reaper needs an independent liveness signal.
|
|
63
93
|
* Missing servers, command failures, timeouts, and unparseable output are all
|
|
64
94
|
* treated as unknown (`null`). */
|
|
65
|
-
export declare function tmuxWindowActivityAt(name: string, tmuxBin?: string): Promise<number | null>;
|
|
95
|
+
export declare function tmuxWindowActivityAt(name: string, tmuxBin?: string, ownerPid?: number): Promise<number | null>;
|
|
66
96
|
/** Whether tmux itself reports an attached client for this private terminal.
|
|
67
97
|
* This remains authoritative if a parent-side attachment bookkeeping edge was
|
|
68
98
|
* missed during a transport failure. */
|
|
69
|
-
export declare function tmuxHasAttachedClient(name: string, tmuxBin?: string): Promise<boolean>;
|
|
70
|
-
/** Best-effort close of one private tmux server.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
|
|
75
|
-
|
|
99
|
+
export declare function tmuxHasAttachedClient(name: string, tmuxBin?: string, ownerPid?: number): Promise<boolean>;
|
|
100
|
+
/** Best-effort close of one private tmux server. Attempt `kill-server`, then
|
|
101
|
+
* retire the private socket regardless of command outcome. The registry has
|
|
102
|
+
* already forgotten the resource, so cleanup failure is diagnostic rather than
|
|
103
|
+
* a second lifecycle state. */
|
|
104
|
+
export declare function terminateTmuxServer(name: string, tmuxBin?: string, ownerPid?: number): boolean;
|
|
105
|
+
/** Reap private terminal servers whose owning runner process is gone. Dirs
|
|
106
|
+
* without an owner marker are deliberately ignored. */
|
|
107
|
+
export declare function reapOrphanedTerminals(opts?: {
|
|
108
|
+
root?: string;
|
|
109
|
+
tmuxBin?: string;
|
|
110
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
111
|
+
}): number;
|
|
76
112
|
/** Is a usable `tmux` on PATH? Cheap probe for the capability gate. */
|
|
77
113
|
export declare function isTmuxAvailable(tmuxBin?: string): boolean;
|
|
78
114
|
export declare class TmuxTerminal {
|
|
@@ -84,8 +120,11 @@ export declare class TmuxTerminal {
|
|
|
84
120
|
private readonly env;
|
|
85
121
|
private readonly cols;
|
|
86
122
|
private readonly rows;
|
|
123
|
+
private readonly scrollback;
|
|
124
|
+
private readonly tmuxAllowPassthrough;
|
|
125
|
+
private readonly tmuxStartOnAttach;
|
|
126
|
+
private readonly keepAliveAfterExit;
|
|
87
127
|
private readonly tmuxBin;
|
|
88
|
-
private readonly injectedSpawn?;
|
|
89
128
|
private started;
|
|
90
129
|
private lastPaneSnapshot;
|
|
91
130
|
/** Shared by every attachment watcher and by the lifecycle watcher's
|
|
@@ -93,7 +132,7 @@ export declare class TmuxTerminal {
|
|
|
93
132
|
* out once per attached client. */
|
|
94
133
|
private paneLivenessFlight?;
|
|
95
134
|
/** Shared by lifecycle callers so capture + pane-dead remains one ordered
|
|
96
|
-
*
|
|
135
|
+
* observation per Terminal. */
|
|
97
136
|
private lifecycleLivenessFlight?;
|
|
98
137
|
constructor(opts: TmuxTerminalOptions);
|
|
99
138
|
/** tmux argv prefix targeting this terminal's private server. */
|
|
@@ -101,47 +140,20 @@ export declare class TmuxTerminal {
|
|
|
101
140
|
/** Create the private tmux server + detached session running the inner
|
|
102
141
|
* command. Idempotent: a second call is a no-op once the session exists. */
|
|
103
142
|
start(): void;
|
|
104
|
-
/**
|
|
105
|
-
* Apply reference implementation's tmux option suite to the private server (inner/terminal.py).
|
|
106
|
-
* These are NOT cosmetic — several fix real co-drive behavior that the tmux
|
|
107
|
-
* defaults break:
|
|
108
|
-
* - `mouse on`: the web terminal's wheel scrolls the pane's scrollback (and
|
|
109
|
-
* mouse events reach a TUI that requests them). Without it, no scrolling.
|
|
110
|
-
* - `extended-keys on` + `csi-u`: tmux forwards Kitty Keyboard Protocol / CSI-u
|
|
111
|
-
* keys (Ctrl+C, Shift+Enter, modified keys) that codex/claude TUIs request —
|
|
112
|
-
* without it tmux downgrades them and the vendor TUI mis-reads modifiers.
|
|
113
|
-
* - `escape-time 0`: kills tmux's default 500 ms wait after ESC, which otherwise
|
|
114
|
-
* makes arrow keys / Alt-combos / pasted CSI feel laggy or mis-parse.
|
|
115
|
-
* - `prefix None` + `prefix2 None` + unbind the prefix table: the user's
|
|
116
|
-
* keystrokes (notably C-b) go to the pane, never tmux — a co-drive terminal
|
|
117
|
-
* must not intercept a prefix.
|
|
118
|
-
* - `focus-events on`, `allow-passthrough on`, `history-limit`: focus reporting,
|
|
119
|
-
* passthrough sequences, scrollback depth.
|
|
120
|
-
* - `remain-on-exit on` + `exit-empty off`: keep the dead pane + server after the
|
|
121
|
-
* inner CLI exits so its last output stays capturable and `#{pane_dead}` reads
|
|
122
|
-
* the exit (liveness probe), instead of the server vanishing.
|
|
123
|
-
* - `MouseDown3*` unbinds: no right-click menu to spawn extra panes/windows.
|
|
124
|
-
* - `status off`: hide tmux chrome. reference implementation keeps the status line only to show
|
|
125
|
-
* a conversation link; rynx has none, so the whole line (and its
|
|
126
|
-
* `[main] 0:node*` window list) is hidden.
|
|
127
|
-
* `-q`/`-gq`/`-sq` keep an older tmux that lacks an option from failing launch.
|
|
128
|
-
* Batched into one invocation with `;` command separators (one spawn).
|
|
129
|
-
*/
|
|
130
|
-
private configureSession;
|
|
131
143
|
/** Whether the terminal's INNER PROCESS is still running. Probes the pane's
|
|
132
144
|
* `#{pane_dead}` flag rather than mere session existence: with
|
|
133
145
|
* `remain-on-exit on` the session/server deliberately outlive the inner CLI's
|
|
134
146
|
* exit (a dead pane shows tmux's "Pane is dead"), so `has-session` succeeding
|
|
135
147
|
* no longer implies a live process. Alive only when the session exists AND its
|
|
136
|
-
* pane process has not exited.
|
|
148
|
+
* pane process has not exited. */
|
|
137
149
|
isAlive(): boolean;
|
|
138
150
|
/** Async pane-liveness probe — MUST NOT block the event loop. The attach
|
|
139
151
|
* pane-death watcher polls this on an interval; a synchronous `execFileSync`
|
|
140
152
|
* there stalls the runner child's event loop (freezing the PTY stream → the
|
|
141
|
-
* terminal appears "stuck").
|
|
142
|
-
*
|
|
153
|
+
* terminal appears "stuck"). Every command error is `unknown`; only
|
|
154
|
+
* `#{pane_dead}=1` is definitive evidence of `dead`. */
|
|
143
155
|
livenessAsync(): Promise<TerminalLiveness>;
|
|
144
|
-
/**
|
|
156
|
+
/** The always-on terminal lifecycle watcher first captures the pane:
|
|
145
157
|
* a control command that ran and reports the target missing is terminal exit;
|
|
146
158
|
* a probe that cannot spawn is inconclusive. If capture succeeds, the normal
|
|
147
159
|
* definitive `pane_dead` probe distinguishes live from exited. */
|
|
@@ -168,11 +180,11 @@ export declare class TmuxTerminal {
|
|
|
168
180
|
sendEnter(): void;
|
|
169
181
|
/** Interrupt the pane's running TUI turn with an Escape key — codex/claude both
|
|
170
182
|
* cancel an in-flight response on a single Esc ("esc to interrupt"). A key NAME
|
|
171
|
-
* (no `-l`) so tmux interprets it.
|
|
183
|
+
* (no `-l`) so tmux interprets it. */
|
|
172
184
|
interrupt(): void;
|
|
173
185
|
/** Clear the current input line before an injection so leftover keystrokes
|
|
174
186
|
* can't prepend to the pasted draft. `C-a` (Home) + `C-k` (kill-to-end) is
|
|
175
|
-
* the safe pair
|
|
187
|
+
* the safe pair because `C-u` only clears backwards from the cursor. */
|
|
176
188
|
clearInputLine(): void;
|
|
177
189
|
/** Send one or more tmux key NAMES (e.g. `Enter`, `C-a`) to the pane. */
|
|
178
190
|
private sendKeyNames;
|
|
@@ -185,17 +197,14 @@ export declare class TmuxTerminal {
|
|
|
185
197
|
* that would overflow a `send-keys` argv.
|
|
186
198
|
*/
|
|
187
199
|
paste(text: string, bufferName?: string): void;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
* owner's pane (defense-in-depth on top of the WS bridge dropping input and
|
|
192
|
-
* resize frames).
|
|
193
|
-
*/
|
|
194
|
-
attach(role: "owner" | "read-only", dims?: {
|
|
200
|
+
private execTmuxBuffer;
|
|
201
|
+
/** Prepare connect-time history without creating a live tmux client. */
|
|
202
|
+
prepare(role: "owner" | "read-only", dims?: {
|
|
195
203
|
cols?: number;
|
|
196
204
|
rows?: number;
|
|
197
|
-
}): Promise<
|
|
205
|
+
}): Promise<PreparedTerminalAttachment>;
|
|
206
|
+
private controlModeSeedSpool;
|
|
207
|
+
private startPullAttachment;
|
|
198
208
|
/** Kill the tmux server (ends the session and all attaches). */
|
|
199
209
|
kill(): void;
|
|
200
210
|
}
|
|
201
|
-
export {};
|