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

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 (65) 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 +103 -1
  6. package/dist/claude/native-bridge.js +445 -30
  7. package/dist/claude/native-hook-main.js +81 -1
  8. package/dist/claude/native-hooks.js +7 -0
  9. package/dist/claude/native-integration.d.ts +178 -26
  10. package/dist/claude/native-integration.js +1528 -170
  11. package/dist/claude/session-status.d.ts +39 -0
  12. package/dist/claude/session-status.js +163 -0
  13. package/dist/claude/transcript-clone.d.ts +18 -0
  14. package/dist/claude/transcript-clone.js +497 -0
  15. package/dist/claude/transcript.d.ts +27 -4
  16. package/dist/claude/transcript.js +158 -47
  17. package/dist/codex-app-server/client.d.ts +10 -6
  18. package/dist/codex-app-server/client.js +67 -15
  19. package/dist/codex-app-server/forwarder.d.ts +92 -3
  20. package/dist/codex-app-server/forwarder.js +532 -57
  21. package/dist/codex-app-server/mapping.d.ts +3 -6
  22. package/dist/codex-app-server/mapping.js +206 -36
  23. package/dist/codex-app-server/mcp-startup.d.ts +13 -0
  24. package/dist/codex-app-server/mcp-startup.js +63 -0
  25. package/dist/codex-app-server/process-registry.d.ts +36 -0
  26. package/dist/codex-app-server/process-registry.js +320 -0
  27. package/dist/codex-app-server/protocol.d.ts +64 -7
  28. package/dist/codex-app-server/ws-channel.d.ts +7 -0
  29. package/dist/codex-app-server/ws-channel.js +104 -28
  30. package/dist/codex-home.d.ts +35 -3
  31. package/dist/codex-home.js +323 -18
  32. package/dist/codex-session-store.d.ts +23 -0
  33. package/dist/codex-session-store.js +21 -0
  34. package/dist/host.d.ts +103 -46
  35. package/dist/host.js +1988 -634
  36. package/dist/index.d.ts +3 -3
  37. package/dist/index.js +1 -1
  38. package/dist/input-resources.d.ts +4 -0
  39. package/dist/input-resources.js +21 -5
  40. package/dist/models-catalog.d.ts +2 -1
  41. package/dist/models-catalog.js +94 -6
  42. package/dist/runner/child.d.ts +97 -28
  43. package/dist/runner/child.js +1486 -100
  44. package/dist/runner/manager.d.ts +110 -29
  45. package/dist/runner/manager.js +1481 -246
  46. package/dist/runner/protocol.d.ts +212 -24
  47. package/dist/runner/protocol.js +5 -0
  48. package/dist/runner/startup-policy.d.ts +7 -0
  49. package/dist/runner/startup-policy.js +10 -0
  50. package/dist/runner/transport.d.ts +18 -2
  51. package/dist/runner/transport.js +82 -3
  52. package/dist/runner-main.js +8 -3
  53. package/dist/terminal/claude-tui.d.ts +3 -1
  54. package/dist/terminal/claude-tui.js +3 -1
  55. package/dist/terminal/codex-tui.d.ts +4 -0
  56. package/dist/terminal/codex-tui.js +5 -0
  57. package/dist/terminal/control-parser.d.ts +39 -0
  58. package/dist/terminal/control-parser.js +172 -0
  59. package/dist/terminal/registry.d.ts +18 -15
  60. package/dist/terminal/registry.js +44 -23
  61. package/dist/terminal/spool.d.ts +47 -0
  62. package/dist/terminal/spool.js +231 -0
  63. package/dist/terminal/tmux.d.ts +126 -74
  64. package/dist/terminal/tmux.js +807 -211
  65. package/package.json +4 -4
@@ -6,7 +6,8 @@
6
6
  * (`live.ensure` / `inject` / `live.interrupt`) plus per-thread capabilities and
7
7
  * terminal attachment; the reply channels mirror each request's `reqId`.
8
8
  */
9
- import { AgentRuntimeError, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
9
+ import { AgentRuntimeError, type InjectResult, type ResolvedExecutionSnapshot, type RuntimeTurnOptions, type RuntimeUserInput, type SessionCollaborationMode, type SessionInteractionResolution, type SessionEvent, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
10
+ export type { InjectOutcome, InjectResult } from "@rynx-ai/core";
10
11
  import type { ResolveInteractionResult } from "../interactions.js";
11
12
  /** A runtime error flattened for the wire; rebuilt parent-side as `AgentRuntimeError`. */
12
13
  export interface WireError {
@@ -15,6 +16,10 @@ export interface WireError {
15
16
  code: string;
16
17
  statusCode: number;
17
18
  }
19
+ /** Parent classification for a mirror write. `ambiguous` means persistence
20
+ * crossed the bounded response deadline: the
21
+ * operation may still commit, so retrying it would risk a duplicate. */
22
+ export type MirrorNackClassification = "transient" | "permanent" | "ambiguous";
18
23
  /** Capabilities forwarded to a runner (parent serves status/listModels itself). */
19
24
  export type CapName = "getGoal" | "setGoal" | "clearGoal" | "forkSession";
20
25
  /** A live terminal role: `owner` is read-write, `read-only` a viewer. */
@@ -22,16 +27,50 @@ export type TerminalRole = "owner" | "read-only";
22
27
  /** Stable reason for a terminal attach failure. `terminal_not_live` is an
23
28
  * expected, retryable absence; `terminal_open_failed` is an infrastructure error. */
24
29
  export type TerminalOpenErrorCode = "terminal_not_live" | "terminal_open_failed";
25
- /** Outcome of an `inject`. `injected` = the app-server / tmux accepted the turn.
26
- * `notLive` = no live forwarder for this session (caller may use the run path).
27
- * `notReady` = live but not ready within the park window; `failed` = injection
28
- * threw. For BOTH notReady and failed the caller reports an error and MUST NOT
29
- * re-run the turn (re-running would double-write alongside the forwarder).
30
- * Mirrors reference implementation's "inject failure ⇒ response.failed, never re-run locally". */
31
- export type InjectOutcome = "injected" | "notLive" | "notReady" | "failed";
32
- /** Parent → child. Terminal PTY bytes ride `term.input` base64-encoded so the
30
+ /** Parent → child. Raw terminal bytes ride `term.input` base64-encoded so the
33
31
  * NDJSON line framing stays intact (raw bytes contain newlines). */
34
32
  export type ToChild = {
33
+ t: "mirror.ack";
34
+ deliveryId: string;
35
+ generation: number;
36
+ } | {
37
+ t: "mirror.nack";
38
+ deliveryId: string;
39
+ generation: number;
40
+ classification: MirrorNackClassification;
41
+ message: string;
42
+ } | {
43
+ t: "mirror.image.ack";
44
+ transferId: string;
45
+ seq: number;
46
+ generation: number;
47
+ } | {
48
+ t: "mirror.image.hold";
49
+ transferId: string;
50
+ seq: number;
51
+ generation: number;
52
+ } | {
53
+ t: "mirror.image.nack";
54
+ transferId: string;
55
+ seq: number;
56
+ generation: number;
57
+ classification: MirrorNackClassification;
58
+ message: string;
59
+ } | {
60
+ t: "rotate.ack";
61
+ rotationId: string;
62
+ generation: number;
63
+ } | {
64
+ t: "rotate.nack";
65
+ rotationId: string;
66
+ generation: number;
67
+ classification: MirrorNackClassification;
68
+ message: string;
69
+ } | {
70
+ t: "rotate.applied.ack";
71
+ rotationId: string;
72
+ generation: number;
73
+ } | {
35
74
  t: "cap";
36
75
  capId: string;
37
76
  name: CapName;
@@ -49,13 +88,31 @@ export type ToChild = {
49
88
  rows: number;
50
89
  command?: string;
51
90
  args?: string[];
91
+ } | {
92
+ t: "term.seed.read";
93
+ attachId: string;
94
+ reqId: string;
95
+ offset: number;
96
+ maxBytes: number;
97
+ } | {
98
+ t: "term.start";
99
+ attachId: string;
100
+ reqId: string;
101
+ } | {
102
+ t: "term.read";
103
+ attachId: string;
104
+ reqId: string;
105
+ offset: number;
106
+ maxBytes: number;
52
107
  } | {
53
108
  t: "term.input";
54
109
  attachId: string;
110
+ reqId: string;
55
111
  dataB64: string;
56
112
  } | {
57
113
  t: "term.resize";
58
114
  attachId: string;
115
+ reqId: string;
59
116
  cols: number;
60
117
  rows: number;
61
118
  } | {
@@ -70,8 +127,9 @@ export type ToChild = {
70
127
  }
71
128
  /** Eagerly bring up a session's live codex TUI + forwarder (codex-native), so
72
129
  * 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. */
130
+ * terminal. Fresh sessions connect the discovery listener before TUI launch;
131
+ * known-thread resumes launch the TUI after backend preload and attach their
132
+ * observer in the background. */
75
133
  | {
76
134
  t: "live.ensure";
77
135
  reqId: string;
@@ -93,6 +151,7 @@ export type ToChild = {
93
151
  t: "inject";
94
152
  reqId: string;
95
153
  localThreadId: string;
154
+ options?: RuntimeTurnOptions;
96
155
  } & ({
97
156
  input: RuntimeUserInput;
98
157
  text?: never;
@@ -100,6 +159,14 @@ export type ToChild = {
100
159
  text: string;
101
160
  input?: never;
102
161
  }))
162
+ /** Apply a Codex-lineage collaboration mode to an already-loaded native
163
+ * thread. This control path never starts a runner or waits for a new Turn. */
164
+ | {
165
+ t: "collaboration.update";
166
+ reqId: string;
167
+ localThreadId: string;
168
+ mode: SessionCollaborationMode;
169
+ }
103
170
  /** Interrupt the session's active turn — the web Stop button. codex: app-server
104
171
  * `turn/interrupt`; claude: an Escape keystroke to the pane. The child replies
105
172
  * `interrupted` (echoing `reqId`). */
@@ -107,10 +174,18 @@ export type ToChild = {
107
174
  t: "live.interrupt";
108
175
  reqId: string;
109
176
  localThreadId: string;
177
+ }
178
+ /** Pane-scoped idle cleanup. The child removes the terminal resource and
179
+ * performs the lifecycle adapter's explicit cleanup before acknowledging. */
180
+ | {
181
+ t: "terminal.reap";
182
+ reqId: string;
183
+ localThreadId: string;
184
+ terminalId: string;
110
185
  } | {
111
186
  t: "shutdown";
112
187
  };
113
- /** Child → parent. `term.data` carries base64 PTY output (line-safe NDJSON). */
188
+ /** Child → parent. `term.data` carries raw pane output as base64 (line-safe NDJSON). */
114
189
  export type FromChild = {
115
190
  t: "ready";
116
191
  } | {
@@ -124,20 +199,52 @@ export type FromChild = {
124
199
  ok: false;
125
200
  error: WireError;
126
201
  } | {
127
- t: "term.opened";
202
+ t: "term.prepared";
128
203
  attachId: string;
129
204
  role: TerminalRole;
205
+ seedBytes: number;
206
+ cols?: number;
207
+ rows?: number;
130
208
  } | {
131
- t: "term.data";
209
+ t: "term.seed.chunk";
132
210
  attachId: string;
211
+ reqId: string;
133
212
  dataB64: string;
213
+ nextOffset: number;
214
+ done: boolean;
215
+ finalOffset?: number;
216
+ } | {
217
+ t: "term.started";
218
+ attachId: string;
219
+ reqId: string;
134
220
  } | {
135
- t: "term.exit";
221
+ t: "term.chunk";
136
222
  attachId: string;
223
+ reqId: string;
224
+ dataB64: string;
225
+ nextOffset: number;
226
+ done: boolean;
227
+ finalOffset?: number;
228
+ } | {
229
+ t: "term.reader.done";
230
+ attachId: string;
231
+ finalOffset: number;
232
+ reason: "terminal_exited" | "client_closed" | "backpressure" | "internal";
137
233
  exitCode: number;
234
+ } | {
235
+ t: "term.ack";
236
+ attachId: string;
237
+ reqId: string;
238
+ operation: "input" | "resize";
239
+ } | {
240
+ t: "term.dimensions";
241
+ attachId: string;
242
+ cols: number;
243
+ rows: number;
138
244
  } | {
139
245
  t: "term.error";
140
246
  attachId: string;
247
+ reqId?: string;
141
248
  code: TerminalOpenErrorCode;
142
249
  message: string;
143
250
  }
@@ -147,6 +254,62 @@ export type FromChild = {
147
254
  t: "mirror";
148
255
  sessionId: string;
149
256
  event: SessionEvent;
257
+ /** Correlates the event with the parent's post-persistence ACK. */
258
+ deliveryId: string;
259
+ /** Logical mirror generation. A clear/fork supersedes every pending
260
+ * delivery from an older generation. */
261
+ generation: number;
262
+ /** One-based attempt for the same logical ordinary delivery. Lets the
263
+ * parent settle process-local projections exactly when the permanent
264
+ * retry budget is exhausted. */
265
+ attempt: number;
266
+ }
267
+ /** The child stopped waiting for an ordinary delivery after an ambiguous
268
+ * outcome or an exhausted permanent budget. The parent keeps the exact
269
+ * delivery-to-projection association until the listener settles, so a late
270
+ * persistence failure can retire only the matching optimistic input. */
271
+ | {
272
+ t: "mirror.abandon";
273
+ deliveryId: string;
274
+ generation: number;
275
+ } | {
276
+ t: "mirror.image.begin";
277
+ transferId: string;
278
+ sessionId: string;
279
+ event: SessionEvent;
280
+ totalChars: number;
281
+ generation: number;
282
+ } | {
283
+ t: "mirror.image.chunk";
284
+ transferId: string;
285
+ seq: number;
286
+ data: string;
287
+ generation: number;
288
+ } | {
289
+ t: "mirror.image.commit";
290
+ transferId: string;
291
+ seq: number;
292
+ generation: number;
293
+ }
294
+ /** Child no longer waits for this transfer (ambiguous handled, permanent
295
+ * budget exhausted, or generation superseded); parent may release chunks. */
296
+ | {
297
+ t: "mirror.image.abandon";
298
+ transferId: string;
299
+ generation: number;
300
+ } | {
301
+ t: "terminal.lifecycle.ended";
302
+ localThreadId: string;
303
+ terminalId: string;
304
+ lifecycle: "required" | "auxiliary";
305
+ status: "idle" | "running" | "failed" | "unknown";
306
+ } | {
307
+ t: "terminal.reaped";
308
+ reqId: string;
309
+ localThreadId: string;
310
+ terminalId: string;
311
+ ok: boolean;
312
+ error?: WireError;
150
313
  }
151
314
  /** The session rotated to a fresh machine-session (claude `/clear`·`/fork`): the
152
315
  * child re-pointed its mirror to `to` and asks the daemon to alias the runner
@@ -155,29 +318,43 @@ export type FromChild = {
155
318
  t: "rotate";
156
319
  from: string;
157
320
  to: string;
321
+ rotationId: string;
322
+ generation: number;
158
323
  kind: "clear" | "fork";
159
324
  workspace: SessionWorkspaceSnapshot;
160
325
  execution: ResolvedExecutionSnapshot;
161
326
  parentSessionId?: string;
162
327
  }
163
- /** Result of a `live.ensure`: `ok` once the requested gate is reached (pane
164
- * started for setup, otherwise native thread bound). Echoes `reqId`. */
328
+ /** Child-side terminal/runtime ownership has moved to the durable target.
329
+ * The parent must not expose the target routing alias before this frame. */
330
+ | {
331
+ t: "rotate.applied";
332
+ rotationId: string;
333
+ generation: number;
334
+ from: string;
335
+ to: string;
336
+ }
337
+ /** Result of a `live.ensure`: for Codex-lineage sessions `ok` means backend
338
+ * preload and pane launch completed. A resume observer and fresh-thread
339
+ * discovery may continue in the background. Other Providers retain their
340
+ * own readiness gate. */
165
341
  | {
166
342
  t: "live.ready";
167
343
  reqId: string;
168
344
  localThreadId: string;
169
345
  ok: boolean;
170
- error?: string;
346
+ error?: WireError;
171
347
  }
172
- /** Result of an `inject`: `outcome` distinguishes injected / notLive / notReady /
173
- * failed so the caller never falls back to a second output path on a live
174
- * session. Echoes the request `reqId`. */
348
+ /** Result of an `inject`: `result.outcome` distinguishes a new turn, an
349
+ * active-turn steer, and failures. RPC-backed success carries the canonical
350
+ * Response identity immediately; transcript-backed success may publish it on
351
+ * the mirrored user record shortly afterward. Echoes the request `reqId`. */
175
352
  | {
176
353
  t: "injected";
177
354
  reqId: string;
178
355
  localThreadId: string;
179
- outcome: InjectOutcome;
180
- error?: string;
356
+ result: InjectResult;
357
+ error?: WireError;
181
358
  }
182
359
  /** Result of a `live.interrupt`: `ok` when an active turn was interrupted (false
183
360
  * when there was none). Echoes the request `reqId`. */
@@ -186,13 +363,24 @@ export type FromChild = {
186
363
  reqId: string;
187
364
  localThreadId: string;
188
365
  ok: boolean;
189
- error?: string;
366
+ error?: WireError;
367
+ } | {
368
+ t: "collaboration.updated";
369
+ reqId: string;
370
+ localThreadId: string;
371
+ ok: boolean;
372
+ error?: WireError;
190
373
  } | {
191
374
  t: "interaction.resolved";
192
375
  reqId: string;
193
376
  localThreadId: string;
194
377
  result: ResolveInteractionResult;
195
378
  };
379
+ /** Internal runner wire bounds. Generated images are already subject to the
380
+ * Session image byte limit; chunks keep the NDJSON pipe and parser bounded and
381
+ * each chunk is acknowledged before the next is sent. */
382
+ export declare const RUNNER_IMAGE_CHUNK_CHARS: number;
383
+ export declare const RUNNER_IMAGE_MAX_RESULT_CHARS: number;
196
384
  /** Encode a message as a single NDJSON line (newline included). */
197
385
  export declare function encodeMessage(msg: ToChild | FromChild): string;
198
386
  /** Parse one NDJSON line; returns `null` for blank lines or malformed JSON. */
@@ -7,6 +7,11 @@
7
7
  * terminal attachment; the reply channels mirror each request's `reqId`.
8
8
  */
9
9
  import { AgentRuntimeError, } from "@rynx-ai/core";
10
+ /** Internal runner wire bounds. Generated images are already subject to the
11
+ * Session image byte limit; chunks keep the NDJSON pipe and parser bounded and
12
+ * each chunk is acknowledged before the next is sent. */
13
+ export const RUNNER_IMAGE_CHUNK_CHARS = 32 * 1024;
14
+ export const RUNNER_IMAGE_MAX_RESULT_CHARS = 8 * 1024 * 1024;
10
15
  /** Encode a message as a single NDJSON line (newline included). */
11
16
  export function encodeMessage(msg) {
12
17
  return `${JSON.stringify(msg)}\n`;
@@ -0,0 +1,7 @@
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;
5
+ /** Codex-lineage providers own app-server, thread-discovery, and injection
6
+ * phase errors. Claude instead retains its parent-owned SessionStart gate. */
7
+ export declare function isCodexLineageProvider(provider: AgentRuntimeId | undefined): provider is Exclude<AgentRuntimeId, "claude">;
@@ -0,0 +1,10 @@
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
+ }
6
+ /** Codex-lineage providers own app-server, thread-discovery, and injection
7
+ * phase errors. Claude instead retains its parent-owned SessionStart gate. */
8
+ export function isCodexLineageProvider(provider) {
9
+ return provider === "codex" || provider === "traex";
10
+ }
@@ -3,9 +3,13 @@ import { type FromChild, type ToChild } from "./protocol.js";
3
3
  export interface RunnerTransport<TSend, TRecv> {
4
4
  /** Send one message. Best-effort: a dead peer is surfaced via {@link onClose}. */
5
5
  send(msg: TSend): void;
6
+ /** Send one bounded message and resolve only after the writable accepts and
7
+ * drains it. Terminal pull responses use this to keep stdio end-to-end
8
+ * demand-driven. */
9
+ sendAndDrain(msg: TSend): Promise<void>;
6
10
  /** Register the single message handler. */
7
11
  onMessage(cb: (msg: TRecv) => void): void;
8
- /** Register a close/error handler (peer stream ended). */
12
+ /** Register a close/error observer (peer stream ended). */
9
13
  onClose(cb: (error?: Error) => void): void;
10
14
  /** Stop reading and release the readline interface. */
11
15
  close(): void;
@@ -14,6 +18,15 @@ export interface RunnerTransport<TSend, TRecv> {
14
18
  export type ParentTransport = RunnerTransport<ToChild, FromChild>;
15
19
  /** Child-side transport: sends {@link FromChild}, receives {@link ToChild}. */
16
20
  export type ChildTransport = RunnerTransport<FromChild, ToChild>;
21
+ /** Structured write failure used by delivery code to distinguish a frame that
22
+ * definitely never entered the pipe from one whose post-write result is
23
+ * uncertain. */
24
+ export declare class RunnerTransportWriteError extends Error {
25
+ readonly delivery: "not-sent" | "ambiguous";
26
+ constructor(message: string, delivery: "not-sent" | "ambiguous", options?: {
27
+ cause?: unknown;
28
+ });
29
+ }
17
30
  /**
18
31
  * NDJSON-over-streams transport. `input` is line-read for inbound messages;
19
32
  * `output` receives one JSON line per {@link send}. Used on both ends:
@@ -25,12 +38,15 @@ export declare class StdioRunnerTransport<TSend, TRecv> implements RunnerTranspo
25
38
  private readonly output;
26
39
  private readonly rl;
27
40
  private messageHandler;
28
- private closeHandler;
41
+ private readonly closeHandlers;
29
42
  private closed;
43
+ private writeTail;
30
44
  constructor(input: Readable, output: Writable);
31
45
  send(msg: TSend): void;
46
+ sendAndDrain(msg: TSend): Promise<void>;
32
47
  onMessage(cb: (msg: TRecv) => void): void;
33
48
  onClose(cb: (error?: Error) => void): void;
34
49
  close(): void;
35
50
  private handleClose;
51
+ private writeMessage;
36
52
  }
@@ -11,6 +11,17 @@
11
11
  */
12
12
  import readline from "node:readline";
13
13
  import { decodeMessage, encodeMessage } from "./protocol.js";
14
+ /** Structured write failure used by delivery code to distinguish a frame that
15
+ * definitely never entered the pipe from one whose post-write result is
16
+ * uncertain. */
17
+ export class RunnerTransportWriteError extends Error {
18
+ delivery;
19
+ constructor(message, delivery, options) {
20
+ super(message, options);
21
+ this.delivery = delivery;
22
+ this.name = "RunnerTransportWriteError";
23
+ }
24
+ }
14
25
  /**
15
26
  * NDJSON-over-streams transport. `input` is line-read for inbound messages;
16
27
  * `output` receives one JSON line per {@link send}. Used on both ends:
@@ -22,8 +33,9 @@ export class StdioRunnerTransport {
22
33
  output;
23
34
  rl;
24
35
  messageHandler = null;
25
- closeHandler = null;
36
+ closeHandlers = new Set();
26
37
  closed = false;
38
+ writeTail = Promise.resolve();
27
39
  constructor(input, output) {
28
40
  this.input = input;
29
41
  this.output = output;
@@ -52,11 +64,16 @@ export class StdioRunnerTransport {
52
64
  }
53
65
  this.output.write(encodeMessage(msg));
54
66
  }
67
+ sendAndDrain(msg) {
68
+ const operation = this.writeTail.then(() => this.writeMessage(msg));
69
+ this.writeTail = operation.catch(() => undefined);
70
+ return operation;
71
+ }
55
72
  onMessage(cb) {
56
73
  this.messageHandler = cb;
57
74
  }
58
75
  onClose(cb) {
59
- this.closeHandler = cb;
76
+ this.closeHandlers.add(cb);
60
77
  }
61
78
  close() {
62
79
  this.closed = true;
@@ -67,6 +84,68 @@ export class StdioRunnerTransport {
67
84
  return;
68
85
  }
69
86
  this.closed = true;
70
- this.closeHandler?.(error);
87
+ for (const handler of this.closeHandlers)
88
+ handler(error);
89
+ }
90
+ writeMessage(msg) {
91
+ if (this.closed) {
92
+ return Promise.reject(new RunnerTransportWriteError("runner transport is closed before write", "not-sent"));
93
+ }
94
+ let encoded;
95
+ try {
96
+ encoded = encodeMessage(msg);
97
+ }
98
+ catch (error) {
99
+ return Promise.reject(new RunnerTransportWriteError("runner transport frame could not be encoded", "not-sent", { cause: error }));
100
+ }
101
+ return new Promise((resolve, reject) => {
102
+ let callbackDone = false;
103
+ let drainDone = false;
104
+ let settled = false;
105
+ const cleanup = () => {
106
+ this.output.off("drain", onDrain);
107
+ this.output.off("error", onError);
108
+ };
109
+ const finish = () => {
110
+ if (settled || !callbackDone || !drainDone)
111
+ return;
112
+ settled = true;
113
+ cleanup();
114
+ resolve();
115
+ };
116
+ const onDrain = () => {
117
+ drainDone = true;
118
+ finish();
119
+ };
120
+ const onError = (error) => {
121
+ if (settled)
122
+ return;
123
+ settled = true;
124
+ cleanup();
125
+ reject(new RunnerTransportWriteError("runner transport write result is uncertain", "ambiguous", { cause: error }));
126
+ };
127
+ this.output.once("error", onError);
128
+ let accepted;
129
+ try {
130
+ accepted = this.output.write(encoded, (error) => {
131
+ if (error) {
132
+ onError(error);
133
+ return;
134
+ }
135
+ callbackDone = true;
136
+ finish();
137
+ });
138
+ }
139
+ catch (error) {
140
+ settled = true;
141
+ cleanup();
142
+ reject(new RunnerTransportWriteError("runner transport rejected the frame before write", "not-sent", { cause: error }));
143
+ return;
144
+ }
145
+ drainDone = accepted;
146
+ if (!accepted)
147
+ this.output.once("drain", onDrain);
148
+ finish();
149
+ });
71
150
  }
72
151
  }
@@ -20,6 +20,8 @@ import { loadConfig } from "@rynx-ai/core";
20
20
  import { FileCodexSessionStore, LocalAgentHost, resolveCodexSessionStorePath, } from "./host.js";
21
21
  import { RunnerSession } from "./runner/child.js";
22
22
  import { StdioRunnerTransport } from "./runner/transport.js";
23
+ import { reapOrphanedTerminals } from "./terminal/tmux.js";
24
+ import { reconcileRuntimeProcesses } from "./codex-app-server/process-registry.js";
23
25
  // stdout is the protocol channel — keep all logging on stderr.
24
26
  console.log = console.error.bind(console);
25
27
  console.info = console.error.bind(console);
@@ -34,6 +36,8 @@ for (const stream of [process.stdout, process.stderr]) {
34
36
  });
35
37
  }
36
38
  async function main() {
39
+ reapOrphanedTerminals();
40
+ reconcileRuntimeProcesses();
37
41
  const config = loadConfig();
38
42
  const sessionStore = new FileCodexSessionStore(resolveCodexSessionStorePath(config));
39
43
  const sessionId = process.env.RYNX_RUNNER_SESSION;
@@ -51,8 +55,7 @@ async function main() {
51
55
  const transport = new StdioRunnerTransport(process.stdin, process.stdout);
52
56
  let runner;
53
57
  transport.onClose(() => {
54
- runner?.shutdown();
55
- process.exit(0);
58
+ void runner?.shutdown().finally(() => process.exit(0));
56
59
  });
57
60
  runner = new RunnerSession({
58
61
  transport,
@@ -64,7 +67,9 @@ async function main() {
64
67
  });
65
68
  // A parent SIGTERM (idle reap / shutdown) uses the same ordered cleanup path
66
69
  // as the protocol shutdown before ending the process.
67
- process.on("SIGTERM", () => runner?.shutdown());
70
+ process.on("SIGTERM", () => {
71
+ void runner?.shutdown();
72
+ });
68
73
  }
69
74
  void main().catch((error) => {
70
75
  console.error(error);
@@ -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);
@@ -49,6 +49,10 @@ export interface LaunchCodexTuiOptions extends CodexRemoteArgs {
49
49
  env?: Record<string, string>;
50
50
  cols?: number;
51
51
  rows?: number;
52
+ scrollback?: number;
53
+ tmuxAllowPassthrough?: boolean;
54
+ tmuxStartOnAttach?: boolean;
55
+ keepAliveAfterExit?: boolean;
52
56
  }
53
57
  /**
54
58
  * Create (idempotently) the codex TUI terminal in the registry, running the real
@@ -25,11 +25,16 @@ export function buildCodexRemoteArgs({ remoteUrl, threadId, configOverrides = []
25
25
  */
26
26
  export function launchCodexTui(opts) {
27
27
  opts.registry.getOrCreate(opts.terminalId, {
28
+ lifecycle: "auxiliary",
28
29
  cwd: opts.cwd,
29
30
  command: opts.codexBin,
30
31
  args: buildCodexRemoteArgs(opts),
31
32
  env: opts.env,
32
33
  cols: opts.cols,
33
34
  rows: opts.rows,
35
+ scrollback: opts.scrollback ?? 100_000,
36
+ tmuxAllowPassthrough: opts.tmuxAllowPassthrough ?? true,
37
+ tmuxStartOnAttach: opts.tmuxStartOnAttach ?? false,
38
+ keepAliveAfterExit: opts.keepAliveAfterExit ?? false,
34
39
  });
35
40
  }