@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/host.js CHANGED
@@ -15,8 +15,10 @@ import { validateInteractionResolution } from "./interactions.js";
15
15
  import { CodexAppServerClient, buildRuntimeUserInput, } from "./codex-app-server/client.js";
16
16
  import { buildAppServerBaseArgs, CodexTransportError, } from "./codex-app-server/transport.js";
17
17
  import { WsRpcChannel, ExternalWsChannel } from "./codex-app-server/ws-channel.js";
18
+ import { reapRuntimeProcessesForStateDir } from "./codex-app-server/process-registry.js";
18
19
  import { CodexSessionForwarder } from "./codex-app-server/forwarder.js";
19
20
  import { readMcpStartupPlan } from "./codex-app-server/mcp-startup.js";
21
+ import { runtimeSessionStateDir } from "./runtime-state-paths.js";
20
22
  import { buildCodexRemoteArgs } from "./terminal/codex-tui.js";
21
23
  import { buildClaudeTuiArgs } from "./terminal/claude-tui.js";
22
24
  import { providerAdditionalDirs, threadWorkspaceParams, turnWorkspaceParams, } from "./provider-workspace.js";
@@ -361,6 +363,7 @@ export class LocalAgentHost {
361
363
  /** Claude forwarders stopped before their terminal is killed. Runner shutdown
362
364
  * finalizes these synchronously afterwards to scrub raw interaction answers. */
363
365
  pendingClaudeFinalizers = new Set();
366
+ pendingCleanupTasks = new Set();
364
367
  // In-flight `ensureLiveCodexSession` calls, so concurrent triggers (a web
365
368
  // `live.ensure` racing a `term.open`) share ONE forwarder — never two.
366
369
  liveEnsuring = new Map();
@@ -477,6 +480,7 @@ export class LocalAgentHost {
477
480
  const channel = new WsRpcChannel({
478
481
  cliPath,
479
482
  baseArgs: buildAppServerBaseArgs(sandbox),
483
+ stateDir: runtimeSessionStateDir(this.runtimeHomeSessionId),
480
484
  extraEnv: {
481
485
  ...(extraEnv ?? {}),
482
486
  [profile.homeEnvVar]: this.runtimeHome(runtime),
@@ -639,6 +643,7 @@ export class LocalAgentHost {
639
643
  const profile = getRuntimeProfile(runtime);
640
644
  const activeThreadId = live?.threadId ?? record?.codexSessionId;
641
645
  return {
646
+ lifecycle: "auxiliary",
642
647
  command: resolveRuntimeBinary(runtime),
643
648
  args: buildCodexRemoteArgs({
644
649
  remoteUrl,
@@ -648,6 +653,10 @@ export class LocalAgentHost {
648
653
  additionalDirs: providerAdditionalDirs(live.workspace),
649
654
  }),
650
655
  cwd: live.workspace.cwd,
656
+ scrollback: 100_000,
657
+ tmuxAllowPassthrough: true,
658
+ tmuxStartOnAttach: false,
659
+ keepAliveAfterExit: false,
651
660
  ...(runtime === "traex" ? { skipTraexStartupPrompts: true } : {}),
652
661
  // Share the app-server's private CODEX_HOME so the TUI inherits the same
653
662
  // login/settings and skips the real home's update/NUX prompt. RYNX_SESSION_ID
@@ -765,6 +774,7 @@ export class LocalAgentHost {
765
774
  void snapshotSkills.skillsCleanup();
766
775
  return false;
767
776
  };
777
+ reapRuntimeProcessesForStateDir(runtimeSessionStateDir(this.runtimeHomeSessionId));
768
778
  const appServerOwner = this.getBackend(runtime, execution.budget ?? undefined).appServerClient;
769
779
  if (!appServerOwner) {
770
780
  this.liveStartupErrors.set(localThreadId, nativeLiveFailure(runtime, "native_app_server_unavailable", "app-server is unavailable for this Session"));
@@ -1418,6 +1428,7 @@ export class LocalAgentHost {
1418
1428
  kind,
1419
1429
  });
1420
1430
  this.liveSessions.set(newSessionId, live);
1431
+ this.liveSessions.delete(previousSessionId);
1421
1432
  retargetMirror?.(newSessionId, {
1422
1433
  kind,
1423
1434
  workspace: structuredClone(live.workspace),
@@ -1843,7 +1854,8 @@ export class LocalAgentHost {
1843
1854
  claude.pendingImageInputs.delete(token);
1844
1855
  }
1845
1856
  else {
1846
- pendingInput.responseId = result.responseId;
1857
+ if (result.responseId)
1858
+ pendingInput.responseId = result.responseId;
1847
1859
  if (pendingInput.observed)
1848
1860
  forgetPendingInput();
1849
1861
  else
@@ -2088,6 +2100,7 @@ export class LocalAgentHost {
2088
2100
  this.liveClaudeSessions.delete(localThreadId);
2089
2101
  claude.stopped = true;
2090
2102
  claude.forwarder.stop();
2103
+ this.trackCleanup(claude.forwarder.waitForStop());
2091
2104
  if (opts.deferClaudeInteractionCleanup) {
2092
2105
  this.pendingClaudeFinalizers.add(claude.forwarder);
2093
2106
  }
@@ -2118,9 +2131,10 @@ export class LocalAgentHost {
2118
2131
  live.canonicalInteractions.clear();
2119
2132
  live.settledInteractions.clear();
2120
2133
  live.syntheticInteractions.clear();
2121
- void live.forwarderClient.stop().catch(() => undefined);
2134
+ this.trackCleanup(live.forwarderClient.stop().catch(() => undefined));
2122
2135
  // Remove the session-scoped skills dir — the machine keeps zero task residue.
2123
- void live.skillsCleanup?.();
2136
+ if (live.skillsCleanup)
2137
+ this.trackCleanup(live.skillsCleanup());
2124
2138
  }
2125
2139
  /** Tear down one codex-lineage native runtime without deleting its durable
2126
2140
  * session-store binding. reference implementation couples its auxiliary Terminal, observer,
@@ -2138,14 +2152,19 @@ export class LocalAgentHost {
2138
2152
  if (backend?.appServerClient === appServerOwner) {
2139
2153
  this.backends.delete(backendKey);
2140
2154
  }
2141
- void appServerOwner.stop().catch((stopError) => {
2155
+ this.trackCleanup(appServerOwner.stop().catch((stopError) => {
2142
2156
  console.warn(`[codex-live] session=${localThreadId} runtime=${live.runtime} app-server teardown failed: ${stopError instanceof Error ? stopError.message : String(stopError)}`);
2143
- });
2157
+ }));
2144
2158
  return turnFailed;
2145
2159
  }
2160
+ async teardownAuxiliaryTerminalRuntime(localThreadId) {
2161
+ const owner = this.liveSessions.get(localThreadId)?.appServerOwner;
2162
+ this.teardownLiveCodexSession(localThreadId);
2163
+ await owner?.stop().catch(() => undefined);
2164
+ }
2146
2165
  /** Complete the second shutdown phase after the runner has killed all native
2147
2166
  * terminals and hook subprocesses. Must run before the runner process exits. */
2148
- finalizeStoppedLiveSessions() {
2167
+ async finalizeStoppedLiveSessions() {
2149
2168
  for (const forwarder of this.pendingClaudeFinalizers)
2150
2169
  forwarder.finalizeStop();
2151
2170
  this.pendingClaudeFinalizers.clear();
@@ -2154,10 +2173,20 @@ export class LocalAgentHost {
2154
2173
  // that can only reconnect to a dead session. `stop()` sends SIGTERM before
2155
2174
  // its returned promise yields, so this remains safe in the synchronous
2156
2175
  // runner shutdown path.
2157
- for (const backend of this.backends.values()) {
2158
- void backend.appServerClient?.stop().catch(() => undefined);
2159
- }
2176
+ const backendStops = [...this.backends.values()].map((backend) => backend.appServerClient?.stop().catch(() => undefined));
2160
2177
  this.backends.clear();
2178
+ await Promise.all([
2179
+ ...backendStops,
2180
+ ...this.pendingCleanupTasks,
2181
+ ]);
2182
+ this.pendingCleanupTasks.clear();
2183
+ }
2184
+ trackCleanup(task) {
2185
+ const guarded = task.catch((error) => {
2186
+ console.warn(`[native-runtime] cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
2187
+ });
2188
+ this.pendingCleanupTasks.add(guarded);
2189
+ void guarded.then(() => this.pendingCleanupTasks.delete(guarded));
2161
2190
  }
2162
2191
  // --- claude-native live session (parallel to the codex methods above) -----
2163
2192
  /** The interactive `claude` TUI spec for a claude-native live session: the real
@@ -2216,9 +2245,14 @@ export class LocalAgentHost {
2216
2245
  ...(live.launchExtraArgs.length ? { extraArgs: live.launchExtraArgs } : {}),
2217
2246
  });
2218
2247
  return {
2248
+ lifecycle: "required",
2219
2249
  command: resolveRuntimeBinary("claude"),
2220
2250
  args,
2221
2251
  cwd,
2252
+ scrollback: 50_000,
2253
+ tmuxAllowPassthrough: false,
2254
+ tmuxStartOnAttach: false,
2255
+ keepAliveAfterExit: true,
2222
2256
  // Same env the pane would inherit by default, plus the session scope for
2223
2257
  // agent-run CLIs (rynx-emulator resolves device-less commands via it).
2224
2258
  env: { ...process.env, RYNX_SESSION_ID: localThreadId },
@@ -2598,6 +2632,7 @@ export class LocalAgentHost {
2598
2632
  live.contextWarned = false;
2599
2633
  pendingRotationEvents = [];
2600
2634
  this.liveClaudeSessions.set(newSessionId, live);
2635
+ this.liveClaudeSessions.delete(previousSessionId);
2601
2636
  void this.sessionStore
2602
2637
  .set({
2603
2638
  localThreadId: newSessionId,
@@ -2703,11 +2738,10 @@ export class LocalAgentHost {
2703
2738
  if (!ok)
2704
2739
  return { outcome: "failed" };
2705
2740
  const responseId = pendingInput.responseId ?? live.currentResponseId();
2706
- if (!responseId) {
2707
- live.error = "Claude accepted the message but did not publish its native Turn identity";
2708
- return { outcome: "failed" };
2709
- }
2710
- return { outcome: steered ? "steered" : "injected", responseId };
2741
+ return {
2742
+ outcome: steered ? "steered" : "injected",
2743
+ ...(responseId ? { responseId } : {}),
2744
+ };
2711
2745
  }
2712
2746
  catch (error) {
2713
2747
  live.error = error instanceof Error ? error.message : String(error);
package/dist/index.d.ts CHANGED
@@ -13,6 +13,6 @@ export type { InjectOutcome, InjectResult, TerminalOpenErrorCode, TerminalRole }
13
13
  export type { ResolveInteractionResult, RuntimeInteractionEvent, RuntimeInteractionListener, } from "./interactions.js";
14
14
  export { probeRuntimeStatus } from "./runtime-status.js";
15
15
  export { listRuntimeModels } from "./models-catalog.js";
16
- export { TmuxTerminal, isTmuxAvailable, terminateTmuxServer, tmuxSocketPath, } from "./terminal/tmux.js";
17
- export type { TerminalAttachment, TmuxTerminalOptions } from "./terminal/tmux.js";
16
+ export { TmuxTerminal, isTmuxAvailable, resolveTmuxBin, terminateTmuxServer, tmuxSocketPath, } from "./terminal/tmux.js";
17
+ export type { PreparedTerminalAttachment, PullTerminalAttachment, TmuxTerminalOptions, } from "./terminal/tmux.js";
18
18
  export { TerminalRegistry } from "./terminal/registry.js";
package/dist/index.js CHANGED
@@ -12,5 +12,5 @@ export { RunnerManager, TerminalOpenError } from "./runner/manager.js";
12
12
  export { probeRuntimeStatus } from "./runtime-status.js";
13
13
  export { listRuntimeModels } from "./models-catalog.js";
14
14
  // Live-terminal subsystem (Phase C): tmux-backed terminals + per-runner registry.
15
- export { TmuxTerminal, isTmuxAvailable, terminateTmuxServer, tmuxSocketPath, } from "./terminal/tmux.js";
15
+ export { TmuxTerminal, isTmuxAvailable, resolveTmuxBin, terminateTmuxServer, tmuxSocketPath, } from "./terminal/tmux.js";
16
16
  export { TerminalRegistry } from "./terminal/registry.js";
@@ -1,5 +1,6 @@
1
1
  import { type AgentCapabilities, type LiveSessionFailure, type ResolvedExecutionSnapshot, type RuntimeTurnOptions, type RuntimeUserInput, type SessionCollaborationMode, type SessionInteractionResolution, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
2
2
  import type { SessionEvent } from "@rynx-ai/core";
3
+ import { type TerminalLifecycle } from "../terminal/registry.js";
3
4
  import type { TerminalInjector } from "../claude/native-integration.js";
4
5
  import type { ResolveInteractionResult } from "../interactions.js";
5
6
  import { type InjectResult } from "./protocol.js";
@@ -23,6 +24,11 @@ interface LiveCodexProvider {
23
24
  cwd: string;
24
25
  env?: Record<string, string>;
25
26
  skipTraexStartupPrompts?: boolean;
27
+ scrollback?: number;
28
+ tmuxAllowPassthrough?: boolean;
29
+ tmuxStartOnAttach?: boolean;
30
+ keepAliveAfterExit?: boolean;
31
+ lifecycle: TerminalLifecycle;
26
32
  } | null>;
27
33
  ensureLiveCodexSession?(localThreadId: string, emit: (event: SessionEvent) => void, opts: {
28
34
  workspace: SessionWorkspaceSnapshot;
@@ -45,13 +51,16 @@ interface LiveCodexProvider {
45
51
  * observer/forwarder, and evict the owned app-server while preserving the
46
52
  * durable native-session binding for a later cold resume. */
47
53
  teardownLiveCodexSession?(localThreadId: string, error?: Error): boolean;
54
+ /** Cleanup coupled runtime resources after an auxiliary terminal is
55
+ * intentionally idle-reaped. Unexpected auxiliary exit does not call this. */
56
+ teardownAuxiliaryTerminalRuntime?(localThreadId: string): Promise<void>;
48
57
  injectMessage?(localThreadId: string, input: RuntimeUserInput | string, options?: RuntimeTurnOptions): Promise<InjectResult>;
49
58
  updateCollaborationMode?(localThreadId: string, mode: SessionCollaborationMode): Promise<void>;
50
59
  interruptLive?(localThreadId: string): Promise<boolean>;
51
60
  stopLiveCodexSession?(localThreadId: string, opts?: {
52
61
  deferClaudeInteractionCleanup?: boolean;
53
62
  }): void;
54
- finalizeStoppedLiveSessions?(): void;
63
+ finalizeStoppedLiveSessions?(): void | Promise<void>;
55
64
  resolveInteraction?(localThreadId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<ResolveInteractionResult>;
56
65
  /** Hand the session's tmux pane injector to the host (which doesn't own
57
66
  * tmux). Claude uses it for messages; Codex uses it only for TUI-local
@@ -77,8 +86,10 @@ export declare class RunnerSession {
77
86
  private readonly onShutdown;
78
87
  /** Live terminals hosted by this session, and per-attach client handles. */
79
88
  private readonly terminals;
89
+ private readonly preparations;
80
90
  private readonly attachments;
81
91
  private readonly attachmentThreadIds;
92
+ private readonly attachmentRoles;
82
93
  private readonly traexStartupWatchers;
83
94
  private readonly traexStartupGates;
84
95
  private readonly terminalWatchers;
@@ -91,7 +102,9 @@ export declare class RunnerSession {
91
102
  private readonly mirrorImageAcks;
92
103
  /** Provider name retained for asynchronous Terminal-exit diagnostics. */
93
104
  private readonly liveRuntimes;
105
+ private readonly terminalStatuses;
94
106
  private shuttingDown;
107
+ private shutdownPromise;
95
108
  constructor({ transport, executor, onShutdown }: RunnerSessionDeps);
96
109
  private handle;
97
110
  private get liveProvider();
@@ -102,6 +115,7 @@ export declare class RunnerSession {
102
115
  * new session AND tells the daemon to alias the runner (terminal transfer) so
103
116
  * the new session stays injectable. */
104
117
  private mirrorChannel;
118
+ private transferNativeSessionResources;
105
119
  private enqueueMirror;
106
120
  private sendMirroredEvent;
107
121
  private sendMirrorImageFrame;
@@ -122,6 +136,7 @@ export declare class RunnerSession {
122
136
  private inject;
123
137
  private updateCollaborationMode;
124
138
  private interruptLive;
139
+ private reapTerminal;
125
140
  /** Launch (idempotently) the session's codex TUI pane from the executor's
126
141
  * `codexTerminalSpec`. Shares the `${id}-main` terminal id with `term.open`,
127
142
  * so the web attach reuses the same detached pane. */
@@ -132,10 +147,16 @@ export declare class RunnerSession {
132
147
  private stopLive;
133
148
  /** Stop event forwarding, kill native terminals/hooks, then synchronously
134
149
  * scrub provider handoff files before the child process is allowed to exit. */
135
- shutdown(): void;
150
+ shutdown(): Promise<void>;
136
151
  private openTerminal;
137
- /** Attach an already-created terminal and forward its data/exit to the parent. */
152
+ /** Prepare captured history without starting the live control client. */
138
153
  private attachExisting;
154
+ private readTerminalSeed;
155
+ private startTerminal;
156
+ private readTerminalOutput;
157
+ private writeTerminalInput;
158
+ private resizeTerminal;
159
+ private sendTerminalOperationError;
139
160
  private runCap;
140
161
  private dispatchCap;
141
162
  }