@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
@@ -2,13 +2,13 @@
2
2
  * WebSocket {@link RpcChannel} for a codex `app-server --listen ws://IP:PORT`.
3
3
  *
4
4
  * Why a WebSocket and not the default stdio: stdio admits exactly one client
5
- * (the process that spawned it), so it cannot be co-driven. omnigent's working
5
+ * (the process that spawned it), so it cannot be co-driven. reference implementation's working
6
6
  * codex-native uses a multi-client transport (ws / uds) so a separate `codex
7
7
  * --remote` TUI can attach to the SAME app-server and resume the SAME thread.
8
8
  * This channel owns that app-server child on a loopback ws port and connects a
9
9
  * client to it; the TUI attaches to {@link WsRpcChannel.url}.
10
10
  *
11
- * Framing matches codex (verified from omnigent): one JSON-RPC object per
11
+ * Framing matches codex (verified from reference implementation): one JSON-RPC object per
12
12
  * WebSocket text frame — no newline delimiting. So `send` writes one frame per
13
13
  * message and every inbound frame is one complete JSON object.
14
14
  */
@@ -69,12 +69,20 @@ export class WsRpcChannel {
69
69
  this.ws.on("close", (code) => this.emitClose(code ?? null, null, null));
70
70
  this.ws.on("error", (error) => this.emitClose(null, null, error));
71
71
  }
72
- send(line) {
72
+ async send(line) {
73
73
  if (this.ws?.readyState !== WebSocket.OPEN) {
74
74
  throw new Error("codex ws channel is not open");
75
75
  }
76
76
  // Codex expects one JSON object per frame; drop the NDJSON newline.
77
- this.ws.send(line.endsWith("\n") ? line.slice(0, -1) : line);
77
+ const payload = line.endsWith("\n") ? line.slice(0, -1) : line;
78
+ await new Promise((resolve, reject) => {
79
+ this.ws.send(payload, (error) => {
80
+ if (error)
81
+ reject(error);
82
+ else
83
+ resolve();
84
+ });
85
+ });
78
86
  }
79
87
  async stop(signal = "SIGTERM") {
80
88
  try {
@@ -140,7 +148,7 @@ export class WsRpcChannel {
140
148
  /**
141
149
  * Connect-only {@link RpcChannel}: attaches an ADDITIONAL client to an app-server
142
150
  * someone else already started (a {@link WsRpcChannel}'s `url`) — no spawn. This
143
- * is how rynx runs omnigent's multi-connection codex-native model: the backend
151
+ * is how rynx runs reference implementation's multi-connection codex-native model: the backend
144
152
  * client owns the app-server + drives injection, while a SEPARATE forwarder
145
153
  * connection `thread/resume`s the same thread to subscribe to its item/turn
146
154
  * notifications (verified: codex delivers a thread's items to every connection
@@ -189,11 +197,19 @@ export class ExternalWsChannel {
189
197
  this.ws.on("close", (code) => this.emitClose(code ?? null, null, null));
190
198
  this.ws.on("error", (error) => this.emitClose(null, null, error));
191
199
  }
192
- send(line) {
200
+ async send(line) {
193
201
  if (this.ws?.readyState !== WebSocket.OPEN) {
194
202
  throw new Error("external codex ws channel is not open");
195
203
  }
196
- this.ws.send(line.endsWith("\n") ? line.slice(0, -1) : line);
204
+ const payload = line.endsWith("\n") ? line.slice(0, -1) : line;
205
+ await new Promise((resolve, reject) => {
206
+ this.ws.send(payload, (error) => {
207
+ if (error)
208
+ reject(error);
209
+ else
210
+ resolve();
211
+ });
212
+ });
197
213
  }
198
214
  async stop() {
199
215
  try {
@@ -6,6 +6,14 @@ const CODEX_CHILD_ENV_ALLOWLIST = [
6
6
  "TMPDIR",
7
7
  "CODEX_HOME",
8
8
  "TRAE_HOME",
9
+ // Keep Runtime-local CLIs on the same resident daemon when its state root is
10
+ // configured outside the default ~/.rynx location.
11
+ "RYNX_HOME",
12
+ // RunnerManager supplies this rotation-safe handle for the exact managed
13
+ // Session. Codex command executions need it to call their own Runtime
14
+ // Browser; the raw Browser capability and internal runner key stay outside
15
+ // the allowlist.
16
+ "RYNX_BROWSER_CONTEXT_FILE",
9
17
  "HTTP_PROXY",
10
18
  "HTTPS_PROXY",
11
19
  "ALL_PROXY",
@@ -15,6 +23,24 @@ const CODEX_CHILD_ENV_ALLOWLIST = [
15
23
  "all_proxy",
16
24
  "no_proxy",
17
25
  ];
26
+ const PROXY_ENV_KEYS = [
27
+ "HTTP_PROXY",
28
+ "HTTPS_PROXY",
29
+ "ALL_PROXY",
30
+ "http_proxy",
31
+ "https_proxy",
32
+ "all_proxy",
33
+ ];
34
+ const LOOPBACK_NO_PROXY_HOSTS = ["localhost", "127.0.0.1", "::1"];
35
+ function withLoopbackNoProxy(value) {
36
+ const entries = value?.split(",").map((entry) => entry.trim()).filter(Boolean) ?? [];
37
+ const seen = new Set(entries.map((entry) => entry.toLowerCase()));
38
+ for (const host of LOOPBACK_NO_PROXY_HOSTS) {
39
+ if (!seen.has(host))
40
+ entries.push(host);
41
+ }
42
+ return entries.join(",");
43
+ }
18
44
  export function createCodexChildEnv(env) {
19
45
  const childEnv = {};
20
46
  for (const key of CODEX_CHILD_ENV_ALLOWLIST) {
@@ -23,5 +49,12 @@ export function createCodexChildEnv(env) {
23
49
  childEnv[key] = value;
24
50
  }
25
51
  }
52
+ if (childEnv.NO_PROXY || childEnv.no_proxy || PROXY_ENV_KEYS.some((key) => childEnv[key])) {
53
+ const upper = childEnv.NO_PROXY ?? childEnv.no_proxy;
54
+ const lower = childEnv.no_proxy ?? childEnv.NO_PROXY;
55
+ // CIDR matching in NO_PROXY varies; Codex's WebSocket client needs exact loopback hosts.
56
+ childEnv.NO_PROXY = withLoopbackNoProxy(upper);
57
+ childEnv.no_proxy = withLoopbackNoProxy(lower);
58
+ }
26
59
  return childEnv;
27
60
  }
@@ -1,11 +1,15 @@
1
+ import { type AgentRuntimeId } from "@rynx-ai/core";
2
+ export type CodexLineageRuntime = Exclude<AgentRuntimeId, "claude">;
1
3
  /**
2
4
  * The deterministic private CODEX_HOME path for a rynx session. PER-SESSION
3
- * (uid-scoped + `sha256(sessionId)[:32]`), mirroring omnigent's per-session
5
+ * (uid-scoped + `sha256(sessionId)[:32]`), mirroring reference implementation's per-session
4
6
  * `bridge_dir/codex-home` and rynx's own `claudeBridgeDir`. Pure — computes the
5
7
  * path without touching disk, so the daemon (rollout-synth) and the runner child
6
8
  * (app-server) both locate the SAME session's home from `sessionId` alone.
7
9
  */
8
10
  export declare function codexHomePath(sessionId: string): string;
11
+ /** Deterministic private home for a Codex-lineage runtime. */
12
+ export declare function runtimeHomePath(sessionId: string, runtime: CodexLineageRuntime): string;
9
13
  /** The OLD uid-scoped shared home (pre per-session). Kept ONLY for back-compat
10
14
  * resume fallback — a session's rollout may still live under here. */
11
15
  export declare function legacyCodexHomePath(): string;
@@ -14,17 +18,23 @@ export declare function legacyCodexHomePath(): string;
14
18
  * `auth.json`) and settings (copied `config.toml`) but NOT the real home's pending
15
19
  * update / first-run (NUX) state — so a co-driven `codex` app-server + `--remote`
16
20
  * TUI never block on an "Update now / Press enter to continue" prompt (which would
17
- * wedge terminal injection). Ports omnigent's `_CODEX_HOME_SYMLINK_FILES` /
21
+ * wedge terminal injection). Ports reference implementation's `_CODEX_HOME_SYMLINK_FILES` /
18
22
  * `_CODEX_HOME_COPY_FILES`.
19
23
  *
20
- * PER-SESSION: one private home per rynx session (matching omnigent), so concurrent
24
+ * PER-SESSION: one private home per rynx session (matching reference implementation), so concurrent
21
25
  * agents never share a `skills/` dir. Idempotent: the symlink/copy are refreshed each
22
26
  * call so a re-login/config change propagates. Returns the private home dir.
23
27
  */
24
28
  export declare function prepareCodexHome(sessionId: string, realHome?: string): string;
29
+ /**
30
+ * Prepare a private runtime home while inheriting only the login and settings
31
+ * files required by the selected CLI. Mutable update/NUX state remains in the
32
+ * real home and cannot wedge a managed app-server/TUI pair.
33
+ */
34
+ export declare function prepareRuntimeHome(sessionId: string, runtime: CodexLineageRuntime, realHome?: string): string;
25
35
  /**
26
36
  * Link a resolved skill set into `<codexHome>/skills/<name>/` so the native Codex
27
- * discovers them at `$CODEX_HOME/skills/` — the SAME filesystem mechanism omnigent
37
+ * discovers them at `$CODEX_HOME/skills/` — the SAME filesystem mechanism reference implementation
28
38
  * uses (`populate_codex_skills_from_bundle` → `_populate_codex_skills`), NOT a
29
39
  * `<skills_instructions>` block injected into the prompt. Codex's app-server
30
40
  * watches this dir and re-scans on change, so a live `codex --remote` TUI — whose
@@ -32,9 +42,9 @@ export declare function prepareCodexHome(sessionId: string, realHome?: string):
32
42
  * — still sees the agent's skills.
33
43
  *
34
44
  * Each skill is a symlink to its source dir; a filesystem without symlink support
35
- * falls back to a recursive copy (matches omnigent's fallback).
45
+ * falls back to a recursive copy (matches reference implementation's fallback).
36
46
  *
37
- * omnigent boots a fresh per-session CODEX_HOME, so it only ever links into an
47
+ * reference implementation boots a fresh per-session CODEX_HOME, so it only ever links into an
38
48
  * empty dir. rynx shares ONE private home per runtime (see {@link prepareCodexHome}),
39
49
  * so this CONVERGES the dir to `skills`: it links the missing ones and removes
40
50
  * entries no longer selected, honouring the agent spec's gating. (Concurrent
@@ -1,11 +1,22 @@
1
1
  import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, rmSync, symlinkSync } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
3
  import { homedir, tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
+ import { getRuntimeProfile, resolveRuntimeHome, } from "@rynx-ai/core";
5
6
  /** Inherit the user's LIVE login by symlink (stays in sync). */
6
7
  const SYMLINK_FILES = ["auth.json"];
7
8
  /** Inherit the user's settings by snapshot copy (not the mutable NUX/update state). */
8
9
  const COPY_FILES = ["config.toml"];
10
+ const RUNTIME_HOME_FILES = {
11
+ codex: { symlink: SYMLINK_FILES, copy: COPY_FILES },
12
+ traex: {
13
+ // Traex keeps credentials below `cli/`, while its user configuration lives
14
+ // at the TRAE_HOME root. Keep both the current TOML name and the legacy YAML
15
+ // name so existing installations remain usable without an implicit migrate.
16
+ symlink: ["cli/auth.json"],
17
+ copy: ["traecli.toml", "traecli.yaml"],
18
+ },
19
+ };
9
20
  /** The user's real CODEX_HOME (env override, else `~/.codex`). */
10
21
  function realCodexHome() {
11
22
  return process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
@@ -17,7 +28,7 @@ function rynxUidRoot() {
17
28
  }
18
29
  /**
19
30
  * The deterministic private CODEX_HOME path for a rynx session. PER-SESSION
20
- * (uid-scoped + `sha256(sessionId)[:32]`), mirroring omnigent's per-session
31
+ * (uid-scoped + `sha256(sessionId)[:32]`), mirroring reference implementation's per-session
21
32
  * `bridge_dir/codex-home` and rynx's own `claudeBridgeDir`. Pure — computes the
22
33
  * path without touching disk, so the daemon (rollout-synth) and the runner child
23
34
  * (app-server) both locate the SAME session's home from `sessionId` alone.
@@ -26,6 +37,13 @@ export function codexHomePath(sessionId) {
26
37
  const digest = createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
27
38
  return join(rynxUidRoot(), "codex-native", digest, "codex-home");
28
39
  }
40
+ /** Deterministic private home for a Codex-lineage runtime. */
41
+ export function runtimeHomePath(sessionId, runtime) {
42
+ if (runtime === "codex")
43
+ return codexHomePath(sessionId);
44
+ const digest = createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
45
+ return join(rynxUidRoot(), "traex-native", digest, "trae-home");
46
+ }
29
47
  /** The OLD uid-scoped shared home (pre per-session). Kept ONLY for back-compat
30
48
  * resume fallback — a session's rollout may still live under here. */
31
49
  export function legacyCodexHomePath() {
@@ -36,21 +54,33 @@ export function legacyCodexHomePath() {
36
54
  * `auth.json`) and settings (copied `config.toml`) but NOT the real home's pending
37
55
  * update / first-run (NUX) state — so a co-driven `codex` app-server + `--remote`
38
56
  * TUI never block on an "Update now / Press enter to continue" prompt (which would
39
- * wedge terminal injection). Ports omnigent's `_CODEX_HOME_SYMLINK_FILES` /
57
+ * wedge terminal injection). Ports reference implementation's `_CODEX_HOME_SYMLINK_FILES` /
40
58
  * `_CODEX_HOME_COPY_FILES`.
41
59
  *
42
- * PER-SESSION: one private home per rynx session (matching omnigent), so concurrent
60
+ * PER-SESSION: one private home per rynx session (matching reference implementation), so concurrent
43
61
  * agents never share a `skills/` dir. Idempotent: the symlink/copy are refreshed each
44
62
  * call so a re-login/config change propagates. Returns the private home dir.
45
63
  */
46
64
  export function prepareCodexHome(sessionId, realHome = realCodexHome()) {
47
- const dir = codexHomePath(sessionId);
65
+ return prepareRuntimeHome(sessionId, "codex", realHome);
66
+ }
67
+ /**
68
+ * Prepare a private runtime home while inheriting only the login and settings
69
+ * files required by the selected CLI. Mutable update/NUX state remains in the
70
+ * real home and cannot wedge a managed app-server/TUI pair.
71
+ */
72
+ export function prepareRuntimeHome(sessionId, runtime, realHome = runtime === "codex"
73
+ ? realCodexHome()
74
+ : resolveRuntimeHome(getRuntimeProfile(runtime))) {
75
+ const dir = runtimeHomePath(sessionId, runtime);
48
76
  mkdirSync(dir, { recursive: true, mode: 0o700 });
49
- for (const name of SYMLINK_FILES) {
77
+ const files = RUNTIME_HOME_FILES[runtime];
78
+ for (const name of files.symlink) {
50
79
  const src = join(realHome, name);
51
80
  const dst = join(dir, name);
81
+ mkdirSync(dirname(dst), { recursive: true, mode: 0o700 });
52
82
  try {
53
- rmSync(dst, { force: true });
83
+ rmSync(dst, { recursive: true, force: true });
54
84
  }
55
85
  catch {
56
86
  // absent — fine
@@ -64,11 +94,13 @@ export function prepareCodexHome(sessionId, realHome = realCodexHome()) {
64
94
  }
65
95
  }
66
96
  }
67
- for (const name of COPY_FILES) {
97
+ for (const name of files.copy) {
68
98
  const src = join(realHome, name);
69
99
  if (existsSync(src)) {
70
100
  try {
71
- copyFileSync(src, join(dir, name));
101
+ const dst = join(dir, name);
102
+ mkdirSync(dirname(dst), { recursive: true, mode: 0o700 });
103
+ copyFileSync(src, dst);
72
104
  }
73
105
  catch {
74
106
  // best-effort
@@ -79,7 +111,7 @@ export function prepareCodexHome(sessionId, realHome = realCodexHome()) {
79
111
  }
80
112
  /**
81
113
  * Link a resolved skill set into `<codexHome>/skills/<name>/` so the native Codex
82
- * discovers them at `$CODEX_HOME/skills/` — the SAME filesystem mechanism omnigent
114
+ * discovers them at `$CODEX_HOME/skills/` — the SAME filesystem mechanism reference implementation
83
115
  * uses (`populate_codex_skills_from_bundle` → `_populate_codex_skills`), NOT a
84
116
  * `<skills_instructions>` block injected into the prompt. Codex's app-server
85
117
  * watches this dir and re-scans on change, so a live `codex --remote` TUI — whose
@@ -87,9 +119,9 @@ export function prepareCodexHome(sessionId, realHome = realCodexHome()) {
87
119
  * — still sees the agent's skills.
88
120
  *
89
121
  * Each skill is a symlink to its source dir; a filesystem without symlink support
90
- * falls back to a recursive copy (matches omnigent's fallback).
122
+ * falls back to a recursive copy (matches reference implementation's fallback).
91
123
  *
92
- * omnigent boots a fresh per-session CODEX_HOME, so it only ever links into an
124
+ * reference implementation boots a fresh per-session CODEX_HOME, so it only ever links into an
93
125
  * empty dir. rynx shares ONE private home per runtime (see {@link prepareCodexHome}),
94
126
  * so this CONVERGES the dir to `skills`: it links the missing ones and removes
95
127
  * entries no longer selected, honouring the agent spec's gating. (Concurrent
@@ -99,7 +131,7 @@ export function prepareCodexHome(sessionId, realHome = realCodexHome()) {
99
131
  export function populateCodexSkills(codexHome, skills) {
100
132
  const skillsDir = join(codexHome, "skills");
101
133
  const want = new Map(skills.map((s) => [s.name, s.dir]));
102
- // Converge: drop entries no longer selected (shared-home adaptation; omnigent's
134
+ // Converge: drop entries no longer selected (shared-home adaptation; reference implementation's
103
135
  // per-session home never needs this). `.system` holds codex's own embedded system
104
136
  // skills (the app-server installs them into `$CODEX_HOME/skills/.system`) — never
105
137
  // rynx-managed, so leave it untouched; only converge the entries we linked.
@@ -116,8 +148,7 @@ export function populateCodexSkills(codexHome, skills) {
116
148
  mkdirSync(skillsDir, { recursive: true });
117
149
  for (const [name, src] of want) {
118
150
  const link = join(skillsDir, name);
119
- if (existsSync(link))
120
- continue; // already linked (a skill's dir for a name is stable)
151
+ rmSync(link, { recursive: true, force: true });
121
152
  try {
122
153
  symlinkSync(src, link);
123
154
  }
@@ -1,10 +1,11 @@
1
- import { type AgentRuntimeId } from "@rynx-ai/core";
1
+ import { type AgentRuntimeId, type ReasoningEffort } from "@rynx-ai/core";
2
2
  import type { AppConfig } from "@rynx-ai/core";
3
3
  export interface CodexSessionRecord {
4
4
  localThreadId: string;
5
5
  codexSessionId: string;
6
6
  cwd: string;
7
7
  model: string;
8
+ reasoningEffort?: ReasoningEffort;
8
9
  /** Runtime this thread is bound to. Legacy records backfill to `codex`. */
9
10
  runtime: AgentRuntimeId;
10
11
  /** Declarative agent spec name bound to this thread, if any. */
package/dist/host.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- import { type AgentSpec, type ResolvedExecutionBudget, type SessionEvent } from "@rynx-ai/core";
1
+ import { type AgentSpec, type ReasoningEffort, type ResolvedExecutionBudget, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
2
2
  import { type AgentRuntimeId } from "@rynx-ai/core";
3
3
  import { type AppConfig } from "@rynx-ai/core";
4
4
  import { createCodexChildEnv } from "./codex-child-env.js";
5
5
  import type { InjectOutcome } from "./runner/protocol.js";
6
+ import type { ResolveInteractionResult } from "./interactions.js";
6
7
  import { CodexAppServerClient } from "./codex-app-server/client.js";
7
8
  import type { ModelListResponse, ThreadGoal } from "./codex-app-server/protocol.js";
8
9
  import { type TerminalInjector } from "./claude/native-integration.js";
@@ -107,7 +108,9 @@ export type RetargetMirror = (newSessionId: string, meta: {
107
108
  parentSessionId?: string;
108
109
  }) => void;
109
110
  export interface LiveSessionOpts {
111
+ cwd?: string;
110
112
  runtime?: AgentRuntimeId;
113
+ reasoningEffort?: ReasoningEffort;
111
114
  /** Preset agent id (or inline {@link LiveSessionOpts.agentSpec}), so the live
112
115
  * launch applies the agent's model / skills / instructions — not just the
113
116
  * runtime. Absent ⇒ falls back to the config defaults (prior behavior). */
@@ -137,9 +140,12 @@ export declare class LocalAgentHost implements CodexCapabilities {
137
140
  private readonly sessionId;
138
141
  private sessionSandbox?;
139
142
  private sessionApprovalPolicy?;
140
- private cachedCodexHome?;
143
+ private readonly runtimeHomes;
141
144
  private readonly liveSessions;
142
145
  private readonly liveClaudeSessions;
146
+ /** Claude forwarders stopped before their terminal is killed. Runner shutdown
147
+ * finalizes these synchronously afterwards to scrub raw interaction answers. */
148
+ private readonly pendingClaudeFinalizers;
143
149
  private readonly liveEnsuring;
144
150
  constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory, sessionId, }: {
145
151
  config: AppConfig;
@@ -169,16 +175,11 @@ export declare class LocalAgentHost implements CodexCapabilities {
169
175
  */
170
176
  private reapIdleBackends;
171
177
  private createBackend;
172
- /** This session's private CODEX_HOME (prepared once), shared by its app-server + TUI. */
173
- private codexHome;
178
+ /** This session's private native home, shared by its app-server + TUI. */
179
+ private runtimeHome;
174
180
  private createAppServerClient;
175
- /**
176
- * Deliver a user's decision for a pending interactive approval (Phase D).
177
- * Routes to the codex app-server client for the thread's runtime. Returns
178
- * false when no matching pending approval exists (unknown id / already
179
- * resolved / claude runtime).
180
- */
181
- resolveApproval(localThreadId: string, approvalId: string, decision: "acceptForSession" | "accept" | "decline" | "cancel"): Promise<boolean>;
181
+ /** Resolve a pending native question/approval without opening a new Turn. */
182
+ resolveInteraction(localThreadId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<ResolveInteractionResult>;
182
183
  /**
183
184
  * The command to run in a session's live terminal so it co-drives the codex
184
185
  * app-server thread (Phase D). Returns `null` when live-terminal is off, the
@@ -195,7 +196,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
195
196
  } | null>;
196
197
  /**
197
198
  * Bring up (idempotently) a session's persistent codex forwarder and emit its
198
- * mirrored {@link SessionEvent}s. omnigent's codex-native single-writer model:
199
+ * mirrored {@link SessionEvent}s. reference implementation's codex-native single-writer model:
199
200
  * this forwarder is the SOLE producer of the session's canonical events — every
200
201
  * turn on the thread, web-composer-injected AND co-driving-`codex --remote`-TUI
201
202
  * initiated. Each turn gets a {@link SessionNormalizer} whose `responseId` is
@@ -211,11 +212,12 @@ export declare class LocalAgentHost implements CodexCapabilities {
211
212
  */
212
213
  ensureLiveCodexSession(localThreadId: string, emit: (event: SessionEvent) => void, opts?: LiveSessionOpts): Promise<boolean>;
213
214
  private startLiveCodexSession;
215
+ private refreshLiveCodexSession;
214
216
  /** Bind a session's codex thread id once known (TUI broadcast or store): persist
215
217
  * it, unblock injection, and kick off the resume-subscribe loop (once). */
216
218
  private onLiveThreadStarted;
217
219
  /**
218
- * Subscribe the forwarder connection to a thread (omnigent's
220
+ * Subscribe the forwarder connection to a thread (reference implementation's
219
221
  * `_subscribe_until_ready`). A fresh TUI thread has no rollout until its first
220
222
  * turn, so `thread/resume` is retried: park until the forwarder observes the
221
223
  * thread active, then retry WITHOUT `excludeTurns` so the response backfills the
@@ -227,7 +229,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
227
229
  * no live session. Injection and the runner's `live.ready` gate on this. */
228
230
  waitLiveReady(localThreadId: string, timeoutMs?: number): Promise<boolean>;
229
231
  /**
230
- * Inject a user turn into a session's live codex thread — omnigent's
232
+ * Inject a user turn into a session's live codex thread — reference implementation's
231
233
  * single-writer web send. `turn/steer` when a turn is open (mid-turn
232
234
  * supplement), else `turn/start`. NEVER creates a thread (the TUI owns creation;
233
235
  * this targets the id the forwarder captured). Serialized per session so two
@@ -236,67 +238,66 @@ export declare class LocalAgentHost implements CodexCapabilities {
236
238
  * Returns an {@link InjectOutcome}: `notLive` when this session has no live
237
239
  * forwarder (caller may use the run path); `notReady`/`failed` are hard errors
238
240
  * the caller reports WITHOUT re-running (re-running double-writes alongside the
239
- * forwarder). Park-until-ready (~60s), aligning omnigent's executor waiting for
241
+ * forwarder). Park-until-ready (~60s), aligning reference implementation's executor waiting for
240
242
  * the bridge instead of a short race that falls back to a second output path.
241
243
  */
242
- injectMessage(localThreadId: string, text: string): Promise<InjectOutcome>;
244
+ injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectOutcome>;
243
245
  /**
244
246
  * Interrupt the session's active turn — the web Stop button. codex: the
245
247
  * app-server `turn/interrupt` on the active `{threadId, turnId}` (exactly what
246
- * codex's own TUI interrupt key sends — omnigent `_handle_codex_native_interrupt`).
247
- * claude: an Escape keystroke into the pane (no app-server — omnigent
248
+ * codex's own TUI interrupt key sends — reference implementation `_handle_codex_native_interrupt`).
249
+ * claude: an Escape keystroke into the pane (no app-server — reference implementation
248
250
  * `inject_interrupt`). Returns false as a no-op when there is no active turn.
249
251
  */
250
252
  interruptLive(localThreadId: string): Promise<boolean>;
251
253
  /** Stop + drop a session's live forwarder and its dedicated connection (session
252
254
  * close / runner shutdown). The backend inject client is shared — left running. */
253
- stopLiveCodexSession(localThreadId: string): void;
255
+ stopLiveCodexSession(localThreadId: string, opts?: {
256
+ deferClaudeInteractionCleanup?: boolean;
257
+ }): void;
258
+ /** Complete the second shutdown phase after the runner has killed all native
259
+ * terminals and hook subprocesses. Must run before the runner process exits. */
260
+ finalizeStoppedLiveSessions(): void;
254
261
  /** The interactive `claude` TUI spec for a claude-native live session: the real
255
262
  * `claude` binary with rynx's hooks (`--settings`), resuming the bound session.
256
263
  * Trust is pre-seeded so the daemon-spawned TUI never blocks on a first-run
257
- * prompt. (PermissionRequest approvals land in Phase 6; until then the TUI
258
- * surfaces its own permission prompt in the co-driven pane.) */
264
+ * prompt. Questions and PermissionRequest approvals use the runtime-local
265
+ * generic interaction bridge and remain inside the active Turn. */
259
266
  private claudeTerminalSpec;
260
267
  /** Bring up a claude-native live forwarder: prepare the bridge dir (the TUI's
261
268
  * hooks write into it), then tail bridge + transcript and drive a per-turn
262
269
  * {@link SessionNormalizer} → `emit` (the SAME sink shape the codex path uses). */
263
- /** Resolve an agent spec's launch config — model + the session skill env —
270
+ /** Resolve an agent spec's launch config — model, instructions, and the session skill env —
264
271
  * for a LIVE native session, reusing the same core resolvers the non-live
265
272
  * run path uses ({@link resolveAgentExecution} for the model,
266
- * {@link resolveAgent} for skills). Instructions are intentionally NOT
267
- * returned: the co-driven TUI runs on vendor defaults (see
268
- * startLiveClaudeSession).
273
+ * {@link resolveAgent} for instructions and skills).
269
274
  *
270
275
  * Skills are spec-rooted: each declared ref is replayed into a
271
276
  * SESSION-scoped temp dir (cache-accelerated); the owner's catalog plays no
272
- * role, and no spec / no `skills` means ZERO skills. The env-build report
273
- * (installs, failures, content drift) is logged a declared skill that
274
- * can't be provisioned is loudly absent, never silent; the session still
275
- * starts. `skillsCleanup` removes the session dir (call on session stop). */
277
+ * role, and no spec / no `skills` means ZERO skills. Every declared skill
278
+ * must resolve at its declared content hash; missing, failed, or drifted
279
+ * materialization aborts launch before a native session starts.
280
+ * `skillsCleanup` removes the session dir (call on session stop). */
276
281
  private resolveLiveAgentConfig;
277
282
  private startLiveClaudeSession;
278
283
  /** Persist claude's discovered session id (reusing the `codexSessionId` store
279
284
  * field, as the claude executor already does) and release the readiness gate. */
280
285
  private onClaudeDiscovered;
281
- /** Inject a web message into a claude-native pane via tmux (omnigent recipe),
286
+ /** Inject a web message into a claude-native pane via tmux (reference implementation recipe),
282
287
  * serialized per session. Parks until the thread is ready AND the tmux injector
283
288
  * is (re)attached, then pastes; `injectViaTerminal` RAISES if the prompt never
284
- * appears (omnigent RAISE), so a not-ready pane is a hard error — NOT a
289
+ * appears (reference implementation RAISE), so a not-ready pane is a hard error — NOT a
285
290
  * fall-through-to-run signal. Returns {@link InjectOutcome}. */
286
291
  private injectClaude;
287
292
  /** Park until the claude session's tmux injector is (re)attached by the
288
293
  * runner-child, or the deadline passes. Pane relaunch re-attaches it via
289
294
  * {@link attachTerminalInjector}; without this a message during that window
290
- * would fail and (pre-omnigent) fall through to a second output path. */
295
+ * would fail and (pre-reference implementation) fall through to a second output path. */
291
296
  private waitInjector;
292
297
  /** Attach a session's tmux pane injector (from the runner-child, which owns the
293
298
  * terminal registry — the host does not hold tmux). Ignored for codex sessions
294
299
  * (they inject via the app-server). Idempotent. */
295
300
  attachTerminalInjector(localThreadId: string, injector: TerminalInjector): void;
296
- /** The PermissionRequest hook's server target from the runner-child env (the
297
- * daemon control URL + token), or undefined when unset — then no permission
298
- * hook is registered and claude surfaces its own TUI prompt in the pane. */
299
- private claudePermissionConfig;
300
301
  /** List the models a runtime exposes (App Server for codex/traex; static for claude). */
301
302
  listModels(runtime?: AgentRuntimeId): Promise<ModelListResponse | null>;
302
303
  /**
@@ -315,10 +316,9 @@ export declare function parseCodexLoginStatus(exitCode: number, output: string):
315
316
  authMode: string | null;
316
317
  issues: string[];
317
318
  };
318
- export declare function getCodexModel(config: AppConfig, override?: string): string;
319
319
  export { createCodexChildEnv };
320
320
  export declare function isUnsupportedMethodError(error: unknown): boolean;
321
321
  /** A `thread/resume` failure meaning the thread has no (or an empty) rollout yet
322
322
  * — a fresh TUI thread before its first turn. Retryable (park until active).
323
- * Mirrors omnigent's `_is_thread_not_ready_error`. */
323
+ * Mirrors reference implementation's `_is_thread_not_ready_error`. */
324
324
  export declare function isThreadNotReadyError(error: unknown): boolean;