@rynx-ai/runtime 0.1.0 → 0.1.10-beta.2

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 (56) hide show
  1. package/dist/claude/executor.d.ts +3 -5
  2. package/dist/claude/executor.js +3 -5
  3. package/dist/claude/native-bridge.d.ts +74 -17
  4. package/dist/claude/native-bridge.js +225 -30
  5. package/dist/claude/native-hook-main.js +327 -38
  6. package/dist/claude/native-hooks.d.ts +3 -2
  7. package/dist/claude/native-hooks.js +15 -6
  8. package/dist/claude/native-integration.d.ts +123 -16
  9. package/dist/claude/native-integration.js +624 -81
  10. package/dist/claude/settings.d.ts +8 -0
  11. package/dist/claude/settings.js +50 -0
  12. package/dist/claude/transcript.d.ts +2 -2
  13. package/dist/claude/transcript.js +14 -3
  14. package/dist/codex/rollout-synth.d.ts +8 -3
  15. package/dist/codex/rollout-synth.js +65 -32
  16. package/dist/codex-app-server/client.d.ts +27 -40
  17. package/dist/codex-app-server/client.js +1134 -99
  18. package/dist/codex-app-server/forwarder.d.ts +36 -10
  19. package/dist/codex-app-server/forwarder.js +146 -28
  20. package/dist/codex-app-server/mapping.d.ts +1 -1
  21. package/dist/codex-app-server/mapping.js +64 -5
  22. package/dist/codex-app-server/protocol.d.ts +269 -4
  23. package/dist/codex-app-server/transport.d.ts +20 -5
  24. package/dist/codex-app-server/transport.js +93 -40
  25. package/dist/codex-app-server/ws-channel.d.ts +3 -3
  26. package/dist/codex-app-server/ws-channel.js +23 -7
  27. package/dist/codex-child-env.js +33 -0
  28. package/dist/codex-home.d.ts +16 -6
  29. package/dist/codex-home.js +46 -15
  30. package/dist/codex-session-store.d.ts +2 -1
  31. package/dist/host.d.ts +38 -38
  32. package/dist/host.js +626 -121
  33. package/dist/index.d.ts +4 -3
  34. package/dist/index.js +1 -1
  35. package/dist/input-resources.d.ts +13 -0
  36. package/dist/input-resources.js +67 -0
  37. package/dist/interactions.d.ts +61 -0
  38. package/dist/interactions.js +236 -0
  39. package/dist/models-catalog.d.ts +5 -13
  40. package/dist/models-catalog.js +60 -9
  41. package/dist/runner/child.d.ts +9 -1
  42. package/dist/runner/child.js +100 -19
  43. package/dist/runner/manager.d.ts +79 -11
  44. package/dist/runner/manager.js +423 -43
  45. package/dist/runner/protocol.d.ts +30 -11
  46. package/dist/runner-main.js +9 -6
  47. package/dist/runtime-status.js +1 -1
  48. package/dist/terminal/claude-tui.d.ts +8 -3
  49. package/dist/terminal/claude-tui.js +6 -2
  50. package/dist/terminal/codex-tui.d.ts +3 -3
  51. package/dist/terminal/codex-tui.js +1 -1
  52. package/dist/terminal/registry.d.ts +1 -1
  53. package/dist/terminal/registry.js +1 -1
  54. package/dist/terminal/tmux.d.ts +6 -6
  55. package/dist/terminal/tmux.js +10 -10
  56. package/package.json +8 -3
@@ -7,15 +7,18 @@ export class RunnerSession {
7
7
  /** Live terminals hosted by this session, and per-attach client handles. */
8
8
  terminals = new TerminalRegistry();
9
9
  attachments = new Map();
10
+ /** Opens are async; a close received before attach resolves tombstones the id. */
11
+ pendingTerminalOpens = new Set();
12
+ cancelledTerminalOpens = new Set();
10
13
  /** Session ids with a live codex forwarder started here (stopped on shutdown). */
11
14
  liveIds = new Set();
15
+ shuttingDown = false;
12
16
  constructor({ transport, executor, onShutdown }) {
13
17
  this.transport = transport;
14
18
  this.executor = executor;
15
19
  this.onShutdown =
16
20
  onShutdown ??
17
21
  (() => {
18
- this.terminals.closeAll();
19
22
  transport.close();
20
23
  });
21
24
  this.transport.onMessage((msg) => this.handle(msg));
@@ -27,7 +30,20 @@ export class RunnerSession {
27
30
  void this.runCap(msg.capId, msg.name, msg.args);
28
31
  return;
29
32
  case "term.open":
30
- void this.openTerminal(msg);
33
+ if (this.pendingTerminalOpens.has(msg.attachId) || this.attachments.has(msg.attachId)) {
34
+ this.transport.send({
35
+ t: "term.error",
36
+ attachId: msg.attachId,
37
+ code: "terminal_open_failed",
38
+ message: "terminal attachment id is already active",
39
+ });
40
+ return;
41
+ }
42
+ this.pendingTerminalOpens.add(msg.attachId);
43
+ void this.openTerminal(msg).finally(() => {
44
+ this.pendingTerminalOpens.delete(msg.attachId);
45
+ this.cancelledTerminalOpens.delete(msg.attachId);
46
+ });
31
47
  return;
32
48
  case "term.input":
33
49
  this.attachments.get(msg.attachId)?.write(Buffer.from(msg.dataB64, "base64").toString("utf8"));
@@ -36,14 +52,16 @@ export class RunnerSession {
36
52
  this.attachments.get(msg.attachId)?.resize(msg.cols, msg.rows);
37
53
  return;
38
54
  case "term.close": {
55
+ if (this.pendingTerminalOpens.has(msg.attachId)) {
56
+ this.cancelledTerminalOpens.add(msg.attachId);
57
+ }
39
58
  const attachment = this.attachments.get(msg.attachId);
40
59
  this.attachments.delete(msg.attachId);
41
60
  attachment?.kill();
42
61
  return;
43
62
  }
44
- case "approval.resolve": {
45
- const provider = this.executor;
46
- void provider.resolveApproval?.(msg.localThreadId, msg.approvalId, msg.decision);
63
+ case "interaction.resolve": {
64
+ void this.resolveInteraction(msg);
47
65
  return;
48
66
  }
49
67
  case "live.ensure":
@@ -56,15 +74,31 @@ export class RunnerSession {
56
74
  void this.interruptLive(msg);
57
75
  return;
58
76
  case "shutdown":
59
- this.stopLive();
60
- this.terminals.closeAll();
61
- this.onShutdown();
77
+ this.shutdown();
62
78
  return;
63
79
  }
64
80
  }
65
81
  get liveProvider() {
66
82
  return this.executor;
67
83
  }
84
+ async resolveInteraction(msg) {
85
+ let result;
86
+ try {
87
+ result = await this.liveProvider.resolveInteraction?.(msg.localThreadId, msg.interactionId, msg.resolution) ?? { disposition: "not_found" };
88
+ }
89
+ catch (error) {
90
+ result = {
91
+ disposition: "invalid",
92
+ message: error instanceof Error ? error.message : String(error),
93
+ };
94
+ }
95
+ this.transport.send({
96
+ t: "interaction.resolved",
97
+ reqId: msg.reqId,
98
+ localThreadId: msg.localThreadId,
99
+ result,
100
+ });
101
+ }
68
102
  /** The mirror channel for a session: an `emit` that forwards every canonical
69
103
  * event to the parent under the CURRENT session id (mutable), plus a `retarget`
70
104
  * the host calls on a `/clear`·`/fork` rotation — it re-points the mirror to the
@@ -94,20 +128,28 @@ export class RunnerSession {
94
128
  * launch the detached `codex --remote` TUI, which CREATES the codex thread. The
95
129
  * forwarder's connection sees the TUI's broadcast `thread/started`, binds, and
96
130
  * subscribes — so the TUI is usable immediately (a fresh `--remote` needs no
97
- * rollout) and its turns mirror to chat (omnigent's model).
131
+ * rollout) and its turns mirror to chat (reference implementation's model).
98
132
  */
99
133
  async ensureLive(msg) {
100
134
  const provider = this.liveProvider;
101
135
  try {
102
136
  const { emit, retarget } = this.mirrorChannel(msg.localThreadId);
103
137
  const started = await provider.ensureLiveCodexSession?.(msg.localThreadId, emit, {
138
+ ...(msg.cwd ? { cwd: msg.cwd } : {}),
104
139
  ...(msg.runtime ? { runtime: msg.runtime } : {}),
140
+ ...(msg.reasoningEffort ? { reasoningEffort: msg.reasoningEffort } : {}),
105
141
  ...(msg.agentName ? { agentName: msg.agentName } : {}),
106
142
  ...(msg.agentSpec ? { agentSpec: msg.agentSpec } : {}),
107
143
  retargetMirror: retarget,
108
144
  });
109
145
  if (!started) {
110
- this.transport.send({ t: "live.ready", reqId: msg.reqId, localThreadId: msg.localThreadId, ok: false });
146
+ this.transport.send({
147
+ t: "live.ready",
148
+ reqId: msg.reqId,
149
+ localThreadId: msg.localThreadId,
150
+ ok: false,
151
+ error: "live provider did not start",
152
+ });
111
153
  return;
112
154
  }
113
155
  this.liveIds.add(msg.localThreadId);
@@ -119,10 +161,18 @@ export class RunnerSession {
119
161
  if (!this.terminals.get(`${msg.localThreadId}-main`)?.isAlive()) {
120
162
  await this.launchCodexPane(msg.localThreadId, msg.cols, msg.rows);
121
163
  }
122
- const ready = provider.waitLiveReady
123
- ? await provider.waitLiveReady(msg.localThreadId)
124
- : true;
125
- this.transport.send({ t: "live.ready", reqId: msg.reqId, localThreadId: msg.localThreadId, ok: ready });
164
+ const ready = msg.waitForReady === false
165
+ ? true
166
+ : provider.waitLiveReady
167
+ ? await provider.waitLiveReady(msg.localThreadId)
168
+ : true;
169
+ this.transport.send({
170
+ t: "live.ready",
171
+ reqId: msg.reqId,
172
+ localThreadId: msg.localThreadId,
173
+ ok: ready,
174
+ ...(ready ? {} : { error: "live session was not ready before timeout" }),
175
+ });
126
176
  }
127
177
  catch (error) {
128
178
  this.transport.send({
@@ -137,7 +187,8 @@ export class RunnerSession {
137
187
  async inject(msg) {
138
188
  const provider = this.liveProvider;
139
189
  try {
140
- const outcome = (await provider.injectMessage?.(msg.localThreadId, msg.text)) ?? "notLive";
190
+ const input = msg.input ?? msg.text;
191
+ const outcome = (await provider.injectMessage?.(msg.localThreadId, input)) ?? "notLive";
141
192
  this.transport.send({ t: "injected", reqId: msg.reqId, localThreadId: msg.localThreadId, outcome });
142
193
  }
143
194
  catch (error) {
@@ -186,13 +237,27 @@ export class RunnerSession {
186
237
  this.liveProvider.attachTerminalInjector?.(localThreadId, term);
187
238
  }
188
239
  stopLive() {
189
- for (const id of this.liveIds)
190
- this.liveProvider.stopLiveCodexSession?.(id);
240
+ for (const id of this.liveIds) {
241
+ this.liveProvider.stopLiveCodexSession?.(id, {
242
+ deferClaudeInteractionCleanup: true,
243
+ });
244
+ }
191
245
  this.liveIds.clear();
192
246
  }
247
+ /** Stop event forwarding, kill native terminals/hooks, then synchronously
248
+ * scrub provider handoff files before the child process is allowed to exit. */
249
+ shutdown() {
250
+ if (this.shuttingDown)
251
+ return;
252
+ this.shuttingDown = true;
253
+ this.stopLive();
254
+ this.terminals.closeAll();
255
+ this.liveProvider.finalizeStoppedLiveSessions?.();
256
+ this.onShutdown();
257
+ }
193
258
  async openTerminal(msg) {
194
259
  try {
195
- // codex/claude-native session (no explicit command): DUMB ATTACH — omnigent's
260
+ // codex/claude-native session (no explicit command): DUMB ATTACH — reference implementation's
196
261
  // reattach (codex_native.py:905-942, `app_server=None`). A tab switch ONLY
197
262
  // attaches an already-live pane; it NEVER ensures the forwarder or relaunches a
198
263
  // dead pane. Creation/relaunch happens on message-send (`live.ensure`) or an
@@ -202,7 +267,12 @@ export class RunnerSession {
202
267
  if (!msg.command && msg.localThreadId) {
203
268
  const existing = this.terminals.get(msg.terminalId);
204
269
  if (!existing || !existing.isAlive()) {
205
- this.transport.send({ t: "term.error", attachId: msg.attachId, message: "terminal not live" });
270
+ this.transport.send({
271
+ t: "term.error",
272
+ attachId: msg.attachId,
273
+ code: "terminal_not_live",
274
+ message: "terminal not live",
275
+ });
206
276
  return;
207
277
  }
208
278
  await this.attachExisting(msg);
@@ -222,6 +292,7 @@ export class RunnerSession {
222
292
  this.transport.send({
223
293
  t: "term.error",
224
294
  attachId: msg.attachId,
295
+ code: "terminal_open_failed",
225
296
  message: error instanceof Error ? error.message : String(error),
226
297
  });
227
298
  }
@@ -232,6 +303,16 @@ export class RunnerSession {
232
303
  cols: msg.cols,
233
304
  rows: msg.rows,
234
305
  });
306
+ if (this.cancelledTerminalOpens.has(msg.attachId)) {
307
+ attachment.kill();
308
+ this.transport.send({
309
+ t: "term.error",
310
+ attachId: msg.attachId,
311
+ code: "terminal_open_failed",
312
+ message: "terminal attachment was cancelled while opening",
313
+ });
314
+ return;
315
+ }
235
316
  this.attachments.set(msg.attachId, attachment);
236
317
  attachment.onData((chunk) => this.transport.send({
237
318
  t: "term.data",
@@ -16,9 +16,25 @@
16
16
  * channel.
17
17
  */
18
18
  import { spawn as nodeSpawn } from "node:child_process";
19
- import { type AgentCapabilities, type AgentRuntimeId, type AgentSpec, type AppConfig, type CapabilityResult, type ModelListResponse, type SessionEvent, type ThreadGoal } from "@rynx-ai/core";
19
+ import { type AgentCapabilities, type AgentRuntimeId, type AgentSpec, type AppConfig, type CapabilityResult, type ModelListResponse, type ReasoningEffort, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type ThreadGoal } from "@rynx-ai/core";
20
20
  import { type CodexSessionStore } from "../host.js";
21
- import { type InjectOutcome, type TerminalRole } from "./protocol.js";
21
+ import type { ResolveInteractionResult } from "../interactions.js";
22
+ import { type InjectOutcome, type TerminalOpenErrorCode, type TerminalRole } from "./protocol.js";
23
+ /** Runtime-local context attached to one Session runner process. The provider is
24
+ * intentionally generic: package-specific resources stay in the composition
25
+ * root that implements this port. */
26
+ export interface RunnerSessionContext {
27
+ /** Extra environment for the runner child. Validated and copied at spawn. */
28
+ readonly childEnv: Readonly<Record<string, string>>;
29
+ /** Rebind resources before a child-published Session rotation becomes visible. */
30
+ rotate(newSessionId: string): void;
31
+ /** Release the context. RunnerManager calls this exactly once. */
32
+ close(): void;
33
+ }
34
+ /** Optional synchronous factory for per-Session runner context. */
35
+ export interface RunnerSessionContextProvider {
36
+ open(sessionId: string): RunnerSessionContext;
37
+ }
22
38
  export interface RunnerManagerOptions {
23
39
  config: AppConfig;
24
40
  /** Shared session store (used by the backend-free status probe). */
@@ -29,12 +45,23 @@ export interface RunnerManagerOptions {
29
45
  idleTtlMs?: number;
30
46
  /** Background reap sweep interval (ms); `0` disables the timer (tests). */
31
47
  reapIntervalMs?: number;
48
+ /** Grace period between SIGTERM and SIGKILL during runner shutdown. */
49
+ shutdownGraceMs?: number;
50
+ /** Final bounded wait for child exit after SIGKILL. */
51
+ shutdownKillGraceMs?: number;
52
+ /** Max wait for a setup-pane launch acknowledgement. */
53
+ liveStartTimeoutMs?: number;
54
+ /** Max wait for a Provider-native thread to become ready. */
55
+ liveReadyTimeoutMs?: number;
32
56
  /** Injected for tests. */
33
57
  spawn?: typeof nodeSpawn;
34
58
  now?: () => number;
35
59
  /** Extra env merged into every spawned runner child — e.g. the control-plane
36
60
  * URL + token so a claude-native PermissionRequest hook can POST back. */
37
61
  childEnv?: Record<string, string>;
62
+ /** Optional lifecycle context for Session runners. The shared `__cap__`
63
+ * runner never opens one. */
64
+ sessionContextProvider?: RunnerSessionContextProvider;
38
65
  }
39
66
  /** A session rotation reported by a runner child (claude `/clear`·`/fork`): the
40
67
  * new session `to` is now aliased to `from`'s runner; carries meta to carry over. */
@@ -77,6 +104,13 @@ export interface ParentTerminal {
77
104
  resize(cols: number, rows: number): void;
78
105
  close(): void;
79
106
  }
107
+ /** A typed attach failure so transport adapters can distinguish an expected
108
+ * absent pane from an infrastructure failure without matching error strings. */
109
+ export declare class TerminalOpenError extends Error {
110
+ readonly code: TerminalOpenErrorCode;
111
+ readonly name = "TerminalOpenError";
112
+ constructor(message: string, code: TerminalOpenErrorCode);
113
+ }
80
114
  export declare class RunnerManager implements AgentCapabilities {
81
115
  private readonly config;
82
116
  private readonly sessionStore;
@@ -84,16 +118,28 @@ export declare class RunnerManager implements AgentCapabilities {
84
118
  private readonly idleTtlMs;
85
119
  private readonly spawn;
86
120
  private readonly childEnv;
121
+ private readonly sessionContextProvider;
87
122
  private readonly now;
88
123
  private readonly defaultRuntime;
89
124
  private readonly handles;
125
+ /** Every spawned child that has not exited (or failed to spawn), including
126
+ * handles already removed from routing by stopRunner/idle reap. */
127
+ private readonly childHandles;
90
128
  private readonly reapTimer;
129
+ private readonly shutdownGraceMs;
130
+ private readonly shutdownKillGraceMs;
131
+ private readonly liveStartTimeoutMs;
132
+ private readonly liveReadyTimeoutMs;
133
+ private stopping;
134
+ private stopPromise;
91
135
  /** Sink for mirrored {@link SessionEvent}s from every session's forwarder. */
92
136
  private mirrorListener;
93
137
  /** Sink for session rotations (claude `/clear`·`/fork`) — server records meta. */
94
138
  private rotateListener;
95
139
  /** Session keys with a live codex forwarder — never reaped while present. */
96
140
  private readonly liveSessionKeys;
141
+ /** Last live-start error per local session, surfaced by the control API. */
142
+ private readonly liveErrors;
97
143
  constructor(opts: RunnerManagerOptions);
98
144
  /**
99
145
  * Open a live terminal on the session's runner child (spawning it if needed).
@@ -103,6 +149,15 @@ export declare class RunnerManager implements AgentCapabilities {
103
149
  * bridge calls it directly.
104
150
  */
105
151
  openTerminal(localThreadId: string, opts: OpenTerminalOptions): ParentTerminal;
152
+ /**
153
+ * Attach to a Session runner that is already live. Unlike {@link openTerminal},
154
+ * this never creates a child, including when liveness changes between lookup
155
+ * and attach.
156
+ */
157
+ openLiveTerminal(localThreadId: string, opts: OpenTerminalOptions): ParentTerminal;
158
+ private openTerminalOnHandle;
159
+ /** Whether this process already owns a live runner for the Session. Read-only; never spawns. */
160
+ hasLiveSession(localThreadId: string): boolean;
106
161
  /**
107
162
  * Register the sink for mirrored {@link SessionEvent}s produced by every
108
163
  * session's persistent codex forwarder (web- AND TUI-initiated turns). The
@@ -123,28 +178,39 @@ export declare class RunnerManager implements AgentCapabilities {
123
178
  cols?: number;
124
179
  rows?: number;
125
180
  runtime?: AgentRuntimeId;
181
+ reasoningEffort?: ReasoningEffort;
126
182
  agentName?: string;
127
183
  agentSpec?: AgentSpec;
128
184
  }): Promise<boolean>;
185
+ /** Start the Provider pane without waiting for login/onboarding to create a
186
+ * native thread. This is the setup-terminal gate; callers may attach as soon
187
+ * as it resolves, while normal message delivery still uses ensureLiveSession. */
188
+ startLiveSession(localThreadId: string, opts?: {
189
+ cwd?: string;
190
+ cols?: number;
191
+ rows?: number;
192
+ runtime?: AgentRuntimeId;
193
+ reasoningEffort?: ReasoningEffort;
194
+ agentName?: string;
195
+ agentSpec?: AgentSpec;
196
+ }): Promise<boolean>;
197
+ private requestLiveSession;
198
+ lastLiveSessionError(localThreadId: string): string | undefined;
129
199
  /**
130
- * Inject a user turn into a session's live codex thread — omnigent's
200
+ * Inject a user turn into a session's live codex thread — reference implementation's
131
201
  * single-writer web send (`turn/start` / `turn/steer`); the forwarder mirrors
132
202
  * all output. Resolves true when the app-server accepted the turn, false when
133
203
  * the session has no live forwarder (caller falls back to the run path).
134
204
  */
135
- injectMessage(localThreadId: string, text: string): Promise<InjectOutcome>;
205
+ injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectOutcome>;
136
206
  /**
137
207
  * Interrupt a session's active live turn — the web Stop button (codex
138
208
  * `turn/interrupt`, claude Escape). Best-effort: resolves false with NO spawn
139
209
  * when the session has no live runner (nothing to interrupt).
140
210
  */
141
211
  interruptLiveSession(localThreadId: string): Promise<boolean>;
142
- /**
143
- * Deliver a user's interactive-approval decision to the session's runner
144
- * child (Phase D). Best-effort: no-op if the runner isn't live (the pending
145
- * approval would have died with it). The child routes it to its codex client.
146
- */
147
- resolveApproval(localThreadId: string, approvalId: string, decision: "acceptForSession" | "accept" | "decline" | "cancel"): void;
212
+ /** Resolve a native question/approval without ever spawning a new runner. */
213
+ resolveInteraction(localThreadId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<ResolveInteractionResult>;
148
214
  listModels(runtime?: AgentRuntimeId): Promise<ModelListResponse | null>;
149
215
  getGoal(localThreadId: string): Promise<CapabilityResult<ThreadGoal | null>>;
150
216
  setGoal(localThreadId: string, objective: string): Promise<CapabilityResult>;
@@ -160,7 +226,7 @@ export declare class RunnerManager implements AgentCapabilities {
160
226
  }): Promise<import("../host.js").CodexRuntimeStatus>;
161
227
  /** Stop the runner bound to one session (if any), rejecting its in-flight work. */
162
228
  stopRunner(localThreadId: string): void;
163
- /** Kill every runner. Call on server shutdown. */
229
+ /** Stop every runner and join all child exits. Idempotent across concurrent calls. */
164
230
  stop(): Promise<void>;
165
231
  /** Forward a capability to a runner child. Session-less caps (listModels/status)
166
232
  * use the shared `CAP_KEY` child; per-thread caps pass the session's key so they
@@ -171,5 +237,7 @@ export declare class RunnerManager implements AgentCapabilities {
171
237
  private onChildMessage;
172
238
  /** Mark a handle dead and reject every pending run/cap with the exit reason. */
173
239
  private failHandle;
240
+ private terminateHandle;
174
241
  private reapIdle;
242
+ private openSessionContext;
175
243
  }