@rynx-ai/runtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude/executor.d.ts +17 -0
- package/dist/claude/executor.js +28 -0
- package/dist/claude/models.d.ts +10 -0
- package/dist/claude/models.js +33 -0
- package/dist/claude/native-bridge.d.ts +133 -0
- package/dist/claude/native-bridge.js +299 -0
- package/dist/claude/native-hook-main.d.ts +2 -0
- package/dist/claude/native-hook-main.js +74 -0
- package/dist/claude/native-hooks.d.ts +41 -0
- package/dist/claude/native-hooks.js +73 -0
- package/dist/claude/native-integration.d.ts +213 -0
- package/dist/claude/native-integration.js +665 -0
- package/dist/claude/native-message-display-main.d.ts +2 -0
- package/dist/claude/native-message-display-main.js +51 -0
- package/dist/claude/native-status-main.d.ts +2 -0
- package/dist/claude/native-status-main.js +105 -0
- package/dist/claude/status.d.ts +23 -0
- package/dist/claude/status.js +118 -0
- package/dist/claude/transcript.d.ts +79 -0
- package/dist/claude/transcript.js +272 -0
- package/dist/claude/trust.d.ts +6 -0
- package/dist/claude/trust.js +85 -0
- package/dist/codex/rollout-synth.d.ts +37 -0
- package/dist/codex/rollout-synth.js +212 -0
- package/dist/codex-app-server/client.d.ts +138 -0
- package/dist/codex-app-server/client.js +341 -0
- package/dist/codex-app-server/forwarder.d.ts +92 -0
- package/dist/codex-app-server/forwarder.js +188 -0
- package/dist/codex-app-server/mapping.d.ts +19 -0
- package/dist/codex-app-server/mapping.js +189 -0
- package/dist/codex-app-server/protocol.d.ts +472 -0
- package/dist/codex-app-server/protocol.js +12 -0
- package/dist/codex-app-server/transport.d.ts +139 -0
- package/dist/codex-app-server/transport.js +422 -0
- package/dist/codex-app-server/ws-channel.d.ts +72 -0
- package/dist/codex-app-server/ws-channel.js +233 -0
- package/dist/codex-child-env.d.ts +1 -0
- package/dist/codex-child-env.js +27 -0
- package/dist/codex-home.d.ts +47 -0
- package/dist/codex-home.js +135 -0
- package/dist/codex-session-store.d.ts +42 -0
- package/dist/codex-session-store.js +126 -0
- package/dist/host.d.ts +324 -0
- package/dist/host.js +1323 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +17 -0
- package/dist/models-catalog.d.ts +18 -0
- package/dist/models-catalog.js +27 -0
- package/dist/runner/child.d.ts +58 -0
- package/dist/runner/child.js +268 -0
- package/dist/runner/manager.d.ts +175 -0
- package/dist/runner/manager.js +458 -0
- package/dist/runner/protocol.d.ts +195 -0
- package/dist/runner/protocol.js +41 -0
- package/dist/runner/transport.d.ts +36 -0
- package/dist/runner/transport.js +72 -0
- package/dist/runner-main.d.ts +2 -0
- package/dist/runner-main.js +61 -0
- package/dist/runtime-status.d.ts +16 -0
- package/dist/runtime-status.js +80 -0
- package/dist/terminal/claude-tui.d.ts +27 -0
- package/dist/terminal/claude-tui.js +13 -0
- package/dist/terminal/codex-tui.d.ts +54 -0
- package/dist/terminal/codex-tui.js +26 -0
- package/dist/terminal/registry.d.ts +42 -0
- package/dist/terminal/registry.js +70 -0
- package/dist/terminal/tmux.d.ts +150 -0
- package/dist/terminal/tmux.js +364 -0
- package/package.json +32 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rynx-ai/runtime — runtime host that drives local coding-agent CLIs (codex / traex
|
|
3
|
+
* / claude) as live co-drive sessions behind the `@rynx-ai/core` contracts
|
|
4
|
+
* (`AgentCapabilities` + `LiveExecutor`). Depends only on `@rynx-ai/core`; knows
|
|
5
|
+
* nothing about any channel.
|
|
6
|
+
*/
|
|
7
|
+
export { LocalAgentHost, CodexRuntimeError, SpawnCodexCommandRunner, FileCodexSessionStore, resolveCodexSessionStorePath, } from "./host.js";
|
|
8
|
+
export type { CodexCapabilities, CapabilityResult, CodexRuntimeStatus, } from "./host.js";
|
|
9
|
+
export type { CodexSessionStore, CodexSessionRecord } from "./codex-session-store.js";
|
|
10
|
+
export { ensureCodexResumeRollout } from "./codex/rollout-synth.js";
|
|
11
|
+
export { RunnerManager } from "./runner/manager.js";
|
|
12
|
+
export type { RunnerManagerOptions, OpenTerminalOptions, ParentTerminal, } from "./runner/manager.js";
|
|
13
|
+
export type { InjectOutcome, TerminalRole } from "./runner/protocol.js";
|
|
14
|
+
export { probeRuntimeStatus } from "./runtime-status.js";
|
|
15
|
+
export { listRuntimeModels } from "./models-catalog.js";
|
|
16
|
+
export { TmuxTerminal, isTmuxAvailable } from "./terminal/tmux.js";
|
|
17
|
+
export type { TerminalAttachment, TmuxTerminalOptions } from "./terminal/tmux.js";
|
|
18
|
+
export { TerminalRegistry } from "./terminal/registry.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rynx-ai/runtime — runtime host that drives local coding-agent CLIs (codex / traex
|
|
3
|
+
* / claude) as live co-drive sessions behind the `@rynx-ai/core` contracts
|
|
4
|
+
* (`AgentCapabilities` + `LiveExecutor`). Depends only on `@rynx-ai/core`; knows
|
|
5
|
+
* nothing about any channel.
|
|
6
|
+
*/
|
|
7
|
+
export { LocalAgentHost, CodexRuntimeError, SpawnCodexCommandRunner, FileCodexSessionStore, resolveCodexSessionStorePath, } from "./host.js";
|
|
8
|
+
export { ensureCodexResumeRollout } from "./codex/rollout-synth.js";
|
|
9
|
+
// Runner subprocess layer: the parent-side manager (an `AgentExecutor` +
|
|
10
|
+
// `AgentCapabilities` that spawns per-session runner children) plus the wire
|
|
11
|
+
// types. The composition root uses `RunnerManager` in place of `LocalAgentHost`.
|
|
12
|
+
export { RunnerManager } from "./runner/manager.js";
|
|
13
|
+
export { probeRuntimeStatus } from "./runtime-status.js";
|
|
14
|
+
export { listRuntimeModels } from "./models-catalog.js";
|
|
15
|
+
// Live-terminal subsystem (Phase C): tmux-backed terminals + per-runner registry.
|
|
16
|
+
export { TmuxTerminal, isTmuxAvailable } from "./terminal/tmux.js";
|
|
17
|
+
export { TerminalRegistry } from "./terminal/registry.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-free model listing for codex/traex.
|
|
3
|
+
*
|
|
4
|
+
* The parent control plane no longer holds an app-server, so `/models` can't be
|
|
5
|
+
* a live `model/list` RPC anymore. Following omnigent's static-catalog model, we
|
|
6
|
+
* serve a config-derived list: the runtime's configured default model (the one
|
|
7
|
+
* `resolveRuntimeModel` would pick), marked `isDefault`. This is intentionally
|
|
8
|
+
* minimal — a fuller curated catalogue can be added here later without touching
|
|
9
|
+
* any caller. claude already has its own static list ({@link listClaudeModels}).
|
|
10
|
+
*/
|
|
11
|
+
import { type AgentRuntimeId, type AppConfig } from "@rynx-ai/core";
|
|
12
|
+
import type { ModelListResponse } from "./codex-app-server/protocol.js";
|
|
13
|
+
/**
|
|
14
|
+
* The model list for a runtime, without an execution backend.
|
|
15
|
+
* Returns `null` when the runtime exposes no resolvable model (e.g. traex with
|
|
16
|
+
* no `TRAEX_MODEL`), matching the prior "unsupported" semantics of `listModels`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function listRuntimeModels(config: AppConfig, runtime: AgentRuntimeId): ModelListResponse | null;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-free model listing for codex/traex.
|
|
3
|
+
*
|
|
4
|
+
* The parent control plane no longer holds an app-server, so `/models` can't be
|
|
5
|
+
* a live `model/list` RPC anymore. Following omnigent's static-catalog model, we
|
|
6
|
+
* serve a config-derived list: the runtime's configured default model (the one
|
|
7
|
+
* `resolveRuntimeModel` would pick), marked `isDefault`. This is intentionally
|
|
8
|
+
* minimal — a fuller curated catalogue can be added here later without touching
|
|
9
|
+
* any caller. claude already has its own static list ({@link listClaudeModels}).
|
|
10
|
+
*/
|
|
11
|
+
import { resolveRuntimeModel, } from "@rynx-ai/core";
|
|
12
|
+
import { listClaudeModels } from "./claude/models.js";
|
|
13
|
+
/**
|
|
14
|
+
* The model list for a runtime, without an execution backend.
|
|
15
|
+
* Returns `null` when the runtime exposes no resolvable model (e.g. traex with
|
|
16
|
+
* no `TRAEX_MODEL`), matching the prior "unsupported" semantics of `listModels`.
|
|
17
|
+
*/
|
|
18
|
+
export function listRuntimeModels(config, runtime) {
|
|
19
|
+
if (runtime === "claude") {
|
|
20
|
+
return listClaudeModels();
|
|
21
|
+
}
|
|
22
|
+
const model = resolveRuntimeModel(config, runtime).trim();
|
|
23
|
+
if (!model) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return { data: [{ id: model, model, isDefault: true }] };
|
|
27
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-side runner session. One per runner process (i.e. per session); owns the
|
|
3
|
+
* single execution backend via the injected executor (a {@link LocalAgentHost}
|
|
4
|
+
* in production, which in a per-session process holds exactly one backend). It
|
|
5
|
+
* translates inbound {@link ToChild} control messages into the live co-drive
|
|
6
|
+
* surface (bring up the session's forwarder + TUI, inject / interrupt turns) and
|
|
7
|
+
* per-thread capabilities, answered against the same backend so the parent never
|
|
8
|
+
* needs an app-server of its own.
|
|
9
|
+
*/
|
|
10
|
+
import { type AgentCapabilities } from "@rynx-ai/core";
|
|
11
|
+
import type { ChildTransport } from "./transport.js";
|
|
12
|
+
export interface RunnerSessionDeps {
|
|
13
|
+
transport: ChildTransport;
|
|
14
|
+
/** The single-backend capability + live co-drive surface (a `LocalAgentHost`). */
|
|
15
|
+
executor: AgentCapabilities;
|
|
16
|
+
/** Invoked on a `shutdown` message (default: close transport). */
|
|
17
|
+
onShutdown?: () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare class RunnerSession {
|
|
20
|
+
private readonly transport;
|
|
21
|
+
private readonly executor;
|
|
22
|
+
private readonly onShutdown;
|
|
23
|
+
/** Live terminals hosted by this session, and per-attach client handles. */
|
|
24
|
+
private readonly terminals;
|
|
25
|
+
private readonly attachments;
|
|
26
|
+
/** Session ids with a live codex forwarder started here (stopped on shutdown). */
|
|
27
|
+
private readonly liveIds;
|
|
28
|
+
constructor({ transport, executor, onShutdown }: RunnerSessionDeps);
|
|
29
|
+
private handle;
|
|
30
|
+
private get liveProvider();
|
|
31
|
+
/** The mirror channel for a session: an `emit` that forwards every canonical
|
|
32
|
+
* event to the parent under the CURRENT session id (mutable), plus a `retarget`
|
|
33
|
+
* the host calls on a `/clear`·`/fork` rotation — it re-points the mirror to the
|
|
34
|
+
* new session AND tells the daemon to alias the runner (terminal transfer) so
|
|
35
|
+
* the new session stays injectable. */
|
|
36
|
+
private mirrorChannel;
|
|
37
|
+
/**
|
|
38
|
+
* Eagerly bring up a session's codex-native live view: start the persistent
|
|
39
|
+
* forwarder connection (which resume-subscribes to mirror every turn) and
|
|
40
|
+
* launch the detached `codex --remote` TUI, which CREATES the codex thread. The
|
|
41
|
+
* forwarder's connection sees the TUI's broadcast `thread/started`, binds, and
|
|
42
|
+
* subscribes — so the TUI is usable immediately (a fresh `--remote` needs no
|
|
43
|
+
* rollout) and its turns mirror to chat (omnigent's model).
|
|
44
|
+
*/
|
|
45
|
+
private ensureLive;
|
|
46
|
+
private inject;
|
|
47
|
+
private interruptLive;
|
|
48
|
+
/** Launch (idempotently) the session's codex TUI pane from the executor's
|
|
49
|
+
* `codexTerminalSpec`. Shares the `${id}-main` terminal id with `term.open`,
|
|
50
|
+
* so the web attach reuses the same detached pane. */
|
|
51
|
+
private launchCodexPane;
|
|
52
|
+
private stopLive;
|
|
53
|
+
private openTerminal;
|
|
54
|
+
/** Attach an already-created terminal and forward its data/exit to the parent. */
|
|
55
|
+
private attachExisting;
|
|
56
|
+
private runCap;
|
|
57
|
+
private dispatchCap;
|
|
58
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { TerminalRegistry } from "../terminal/registry.js";
|
|
2
|
+
import { toWireError } from "./protocol.js";
|
|
3
|
+
export class RunnerSession {
|
|
4
|
+
transport;
|
|
5
|
+
executor;
|
|
6
|
+
onShutdown;
|
|
7
|
+
/** Live terminals hosted by this session, and per-attach client handles. */
|
|
8
|
+
terminals = new TerminalRegistry();
|
|
9
|
+
attachments = new Map();
|
|
10
|
+
/** Session ids with a live codex forwarder started here (stopped on shutdown). */
|
|
11
|
+
liveIds = new Set();
|
|
12
|
+
constructor({ transport, executor, onShutdown }) {
|
|
13
|
+
this.transport = transport;
|
|
14
|
+
this.executor = executor;
|
|
15
|
+
this.onShutdown =
|
|
16
|
+
onShutdown ??
|
|
17
|
+
(() => {
|
|
18
|
+
this.terminals.closeAll();
|
|
19
|
+
transport.close();
|
|
20
|
+
});
|
|
21
|
+
this.transport.onMessage((msg) => this.handle(msg));
|
|
22
|
+
this.transport.send({ t: "ready" });
|
|
23
|
+
}
|
|
24
|
+
handle(msg) {
|
|
25
|
+
switch (msg.t) {
|
|
26
|
+
case "cap":
|
|
27
|
+
void this.runCap(msg.capId, msg.name, msg.args);
|
|
28
|
+
return;
|
|
29
|
+
case "term.open":
|
|
30
|
+
void this.openTerminal(msg);
|
|
31
|
+
return;
|
|
32
|
+
case "term.input":
|
|
33
|
+
this.attachments.get(msg.attachId)?.write(Buffer.from(msg.dataB64, "base64").toString("utf8"));
|
|
34
|
+
return;
|
|
35
|
+
case "term.resize":
|
|
36
|
+
this.attachments.get(msg.attachId)?.resize(msg.cols, msg.rows);
|
|
37
|
+
return;
|
|
38
|
+
case "term.close": {
|
|
39
|
+
const attachment = this.attachments.get(msg.attachId);
|
|
40
|
+
this.attachments.delete(msg.attachId);
|
|
41
|
+
attachment?.kill();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
case "approval.resolve": {
|
|
45
|
+
const provider = this.executor;
|
|
46
|
+
void provider.resolveApproval?.(msg.localThreadId, msg.approvalId, msg.decision);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
case "live.ensure":
|
|
50
|
+
void this.ensureLive(msg);
|
|
51
|
+
return;
|
|
52
|
+
case "inject":
|
|
53
|
+
void this.inject(msg);
|
|
54
|
+
return;
|
|
55
|
+
case "live.interrupt":
|
|
56
|
+
void this.interruptLive(msg);
|
|
57
|
+
return;
|
|
58
|
+
case "shutdown":
|
|
59
|
+
this.stopLive();
|
|
60
|
+
this.terminals.closeAll();
|
|
61
|
+
this.onShutdown();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
get liveProvider() {
|
|
66
|
+
return this.executor;
|
|
67
|
+
}
|
|
68
|
+
/** The mirror channel for a session: an `emit` that forwards every canonical
|
|
69
|
+
* event to the parent under the CURRENT session id (mutable), plus a `retarget`
|
|
70
|
+
* the host calls on a `/clear`·`/fork` rotation — it re-points the mirror to the
|
|
71
|
+
* new session AND tells the daemon to alias the runner (terminal transfer) so
|
|
72
|
+
* the new session stays injectable. */
|
|
73
|
+
mirrorChannel(localThreadId) {
|
|
74
|
+
const target = { id: localThreadId };
|
|
75
|
+
const emit = (event) => this.transport.send({ t: "mirror", sessionId: target.id, event });
|
|
76
|
+
const retarget = (newId, meta) => {
|
|
77
|
+
target.id = newId;
|
|
78
|
+
this.transport.send({
|
|
79
|
+
t: "rotate",
|
|
80
|
+
from: localThreadId,
|
|
81
|
+
to: newId,
|
|
82
|
+
kind: meta.kind,
|
|
83
|
+
...(meta.agent ? { agent: meta.agent } : {}),
|
|
84
|
+
...(meta.model ? { model: meta.model } : {}),
|
|
85
|
+
...(meta.cwd ? { cwd: meta.cwd } : {}),
|
|
86
|
+
...(meta.parentSessionId ? { parentSessionId: meta.parentSessionId } : {}),
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
return { emit, retarget };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Eagerly bring up a session's codex-native live view: start the persistent
|
|
93
|
+
* forwarder connection (which resume-subscribes to mirror every turn) and
|
|
94
|
+
* launch the detached `codex --remote` TUI, which CREATES the codex thread. The
|
|
95
|
+
* forwarder's connection sees the TUI's broadcast `thread/started`, binds, and
|
|
96
|
+
* subscribes — so the TUI is usable immediately (a fresh `--remote` needs no
|
|
97
|
+
* rollout) and its turns mirror to chat (omnigent's model).
|
|
98
|
+
*/
|
|
99
|
+
async ensureLive(msg) {
|
|
100
|
+
const provider = this.liveProvider;
|
|
101
|
+
try {
|
|
102
|
+
const { emit, retarget } = this.mirrorChannel(msg.localThreadId);
|
|
103
|
+
const started = await provider.ensureLiveCodexSession?.(msg.localThreadId, emit, {
|
|
104
|
+
...(msg.runtime ? { runtime: msg.runtime } : {}),
|
|
105
|
+
...(msg.agentName ? { agentName: msg.agentName } : {}),
|
|
106
|
+
...(msg.agentSpec ? { agentSpec: msg.agentSpec } : {}),
|
|
107
|
+
retargetMirror: retarget,
|
|
108
|
+
});
|
|
109
|
+
if (!started) {
|
|
110
|
+
this.transport.send({ t: "live.ready", reqId: msg.reqId, localThreadId: msg.localThreadId, ok: false });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.liveIds.add(msg.localThreadId);
|
|
114
|
+
// Launch the TUI (fresh `--remote` → it creates the thread) so the forwarder
|
|
115
|
+
// captures the broadcast `thread/started` and binds. Re-launch when the pane
|
|
116
|
+
// is absent OR its process has died (`isAlive` probes `#{pane_dead}`) — so a
|
|
117
|
+
// reconnect after the TUI exited restarts it instead of skipping (a
|
|
118
|
+
// launched-once guard would leave a dead "Pane is dead" husk forever).
|
|
119
|
+
if (!this.terminals.get(`${msg.localThreadId}-main`)?.isAlive()) {
|
|
120
|
+
await this.launchCodexPane(msg.localThreadId, msg.cols, msg.rows);
|
|
121
|
+
}
|
|
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 });
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
this.transport.send({
|
|
129
|
+
t: "live.ready",
|
|
130
|
+
reqId: msg.reqId,
|
|
131
|
+
localThreadId: msg.localThreadId,
|
|
132
|
+
ok: false,
|
|
133
|
+
error: error instanceof Error ? error.message : String(error),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async inject(msg) {
|
|
138
|
+
const provider = this.liveProvider;
|
|
139
|
+
try {
|
|
140
|
+
const outcome = (await provider.injectMessage?.(msg.localThreadId, msg.text)) ?? "notLive";
|
|
141
|
+
this.transport.send({ t: "injected", reqId: msg.reqId, localThreadId: msg.localThreadId, outcome });
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
this.transport.send({
|
|
145
|
+
t: "injected",
|
|
146
|
+
reqId: msg.reqId,
|
|
147
|
+
localThreadId: msg.localThreadId,
|
|
148
|
+
outcome: "failed",
|
|
149
|
+
error: error instanceof Error ? error.message : String(error),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async interruptLive(msg) {
|
|
154
|
+
const provider = this.liveProvider;
|
|
155
|
+
let ok = false;
|
|
156
|
+
let error;
|
|
157
|
+
try {
|
|
158
|
+
ok = (await provider.interruptLive?.(msg.localThreadId)) ?? false;
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
error = e instanceof Error ? e.message : String(e);
|
|
162
|
+
}
|
|
163
|
+
this.transport.send({
|
|
164
|
+
t: "interrupted",
|
|
165
|
+
reqId: msg.reqId,
|
|
166
|
+
localThreadId: msg.localThreadId,
|
|
167
|
+
ok,
|
|
168
|
+
...(error ? { error } : {}),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** Launch (idempotently) the session's codex TUI pane from the executor's
|
|
172
|
+
* `codexTerminalSpec`. Shares the `${id}-main` terminal id with `term.open`,
|
|
173
|
+
* so the web attach reuses the same detached pane. */
|
|
174
|
+
async launchCodexPane(localThreadId, cols, rows) {
|
|
175
|
+
const spec = await this.liveProvider.codexTerminalSpec?.(localThreadId);
|
|
176
|
+
if (!spec)
|
|
177
|
+
return;
|
|
178
|
+
const term = this.terminals.getOrCreate(`${localThreadId}-main`, {
|
|
179
|
+
cwd: spec.cwd,
|
|
180
|
+
command: spec.command,
|
|
181
|
+
args: spec.args,
|
|
182
|
+
cols: cols ?? 120,
|
|
183
|
+
rows: rows ?? 40,
|
|
184
|
+
...(spec.env ? { env: spec.env } : {}),
|
|
185
|
+
});
|
|
186
|
+
this.liveProvider.attachTerminalInjector?.(localThreadId, term);
|
|
187
|
+
}
|
|
188
|
+
stopLive() {
|
|
189
|
+
for (const id of this.liveIds)
|
|
190
|
+
this.liveProvider.stopLiveCodexSession?.(id);
|
|
191
|
+
this.liveIds.clear();
|
|
192
|
+
}
|
|
193
|
+
async openTerminal(msg) {
|
|
194
|
+
try {
|
|
195
|
+
// codex/claude-native session (no explicit command): DUMB ATTACH — omnigent's
|
|
196
|
+
// reattach (codex_native.py:905-942, `app_server=None`). A tab switch ONLY
|
|
197
|
+
// attaches an already-live pane; it NEVER ensures the forwarder or relaunches a
|
|
198
|
+
// dead pane. Creation/relaunch happens on message-send (`live.ensure`) or an
|
|
199
|
+
// explicit restart. A missing/dead pane → `term.error`, so the web shows the
|
|
200
|
+
// dead-pane overlay + restart button instead of silently respawning the codex
|
|
201
|
+
// TUI (and re-ensuring the forwarder) on every tab switch.
|
|
202
|
+
if (!msg.command && msg.localThreadId) {
|
|
203
|
+
const existing = this.terminals.get(msg.terminalId);
|
|
204
|
+
if (!existing || !existing.isAlive()) {
|
|
205
|
+
this.transport.send({ t: "term.error", attachId: msg.attachId, message: "terminal not live" });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
await this.attachExisting(msg);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Login-shell terminal (explicit command): create-or-reuse as before.
|
|
212
|
+
this.terminals.getOrCreate(msg.terminalId, {
|
|
213
|
+
cwd: msg.cwd,
|
|
214
|
+
command: msg.command,
|
|
215
|
+
args: msg.args,
|
|
216
|
+
cols: msg.cols,
|
|
217
|
+
rows: msg.rows,
|
|
218
|
+
});
|
|
219
|
+
await this.attachExisting(msg);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
this.transport.send({
|
|
223
|
+
t: "term.error",
|
|
224
|
+
attachId: msg.attachId,
|
|
225
|
+
message: error instanceof Error ? error.message : String(error),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Attach an already-created terminal and forward its data/exit to the parent. */
|
|
230
|
+
async attachExisting(msg) {
|
|
231
|
+
const { attachment, role } = await this.terminals.attach(msg.terminalId, msg.role, {
|
|
232
|
+
cols: msg.cols,
|
|
233
|
+
rows: msg.rows,
|
|
234
|
+
});
|
|
235
|
+
this.attachments.set(msg.attachId, attachment);
|
|
236
|
+
attachment.onData((chunk) => this.transport.send({
|
|
237
|
+
t: "term.data",
|
|
238
|
+
attachId: msg.attachId,
|
|
239
|
+
dataB64: Buffer.from(chunk, "utf8").toString("base64"),
|
|
240
|
+
}));
|
|
241
|
+
attachment.onExit((info) => {
|
|
242
|
+
this.attachments.delete(msg.attachId);
|
|
243
|
+
this.transport.send({ t: "term.exit", attachId: msg.attachId, exitCode: info.exitCode });
|
|
244
|
+
});
|
|
245
|
+
this.transport.send({ t: "term.opened", attachId: msg.attachId, role });
|
|
246
|
+
}
|
|
247
|
+
async runCap(capId, name, args) {
|
|
248
|
+
try {
|
|
249
|
+
const data = await this.dispatchCap(name, args);
|
|
250
|
+
this.transport.send({ t: "capResult", capId, ok: true, data });
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
this.transport.send({ t: "capResult", capId, ok: false, error: toWireError(error) });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
dispatchCap(name, args) {
|
|
257
|
+
switch (name) {
|
|
258
|
+
case "getGoal":
|
|
259
|
+
return this.executor.getGoal(args[0]);
|
|
260
|
+
case "setGoal":
|
|
261
|
+
return this.executor.setGoal(args[0], args[1]);
|
|
262
|
+
case "clearGoal":
|
|
263
|
+
return this.executor.clearGoal(args[0]);
|
|
264
|
+
case "forkSession":
|
|
265
|
+
return this.executor.forkSession(args[0], args[1]);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent-side runner orchestrator. Replaces {@link LocalAgentHost} at the
|
|
3
|
+
* composition root as the live co-drive + capability surface, but holds **no
|
|
4
|
+
* execution backend** of its own: it spawns one runner child per session
|
|
5
|
+
* (`localThreadId`) that brings up the session's live forwarder + vendor TUI,
|
|
6
|
+
* forwards per-thread capabilities to a shared capability runner, and answers
|
|
7
|
+
* status / model-list locally without a backend.
|
|
8
|
+
*
|
|
9
|
+
* The process boundary carries no live objects: the parent drives a session over
|
|
10
|
+
* the runner transport (`live.ensure` / `inject` / `live.interrupt` + terminal
|
|
11
|
+
* attach), and the child mirrors that session's canonical events back as
|
|
12
|
+
* {@link SessionEvent}s.
|
|
13
|
+
*
|
|
14
|
+
* Today the only transport is local stdio (a spawned child); a remote tunnel
|
|
15
|
+
* transport drops in here later without touching `ConversationRuntime` or any
|
|
16
|
+
* channel.
|
|
17
|
+
*/
|
|
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";
|
|
20
|
+
import { type CodexSessionStore } from "../host.js";
|
|
21
|
+
import { type InjectOutcome, type TerminalRole } from "./protocol.js";
|
|
22
|
+
export interface RunnerManagerOptions {
|
|
23
|
+
config: AppConfig;
|
|
24
|
+
/** Shared session store (used by the backend-free status probe). */
|
|
25
|
+
sessionStore: CodexSessionStore;
|
|
26
|
+
/** Path to the runner entry; defaults to the built `dist/runner-main.js`. */
|
|
27
|
+
runnerEntry?: string;
|
|
28
|
+
/** Idle TTL (ms) after which an unused runner is reaped. */
|
|
29
|
+
idleTtlMs?: number;
|
|
30
|
+
/** Background reap sweep interval (ms); `0` disables the timer (tests). */
|
|
31
|
+
reapIntervalMs?: number;
|
|
32
|
+
/** Injected for tests. */
|
|
33
|
+
spawn?: typeof nodeSpawn;
|
|
34
|
+
now?: () => number;
|
|
35
|
+
/** Extra env merged into every spawned runner child — e.g. the control-plane
|
|
36
|
+
* URL + token so a claude-native PermissionRequest hook can POST back. */
|
|
37
|
+
childEnv?: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
/** A session rotation reported by a runner child (claude `/clear`·`/fork`): the
|
|
40
|
+
* new session `to` is now aliased to `from`'s runner; carries meta to carry over. */
|
|
41
|
+
export interface RotateInfo {
|
|
42
|
+
from: string;
|
|
43
|
+
to: string;
|
|
44
|
+
kind: "clear" | "fork";
|
|
45
|
+
agent?: string;
|
|
46
|
+
model?: string;
|
|
47
|
+
cwd?: string;
|
|
48
|
+
parentSessionId?: string;
|
|
49
|
+
}
|
|
50
|
+
/** Options for {@link RunnerManager.openTerminal}. */
|
|
51
|
+
export interface OpenTerminalOptions {
|
|
52
|
+
role: TerminalRole;
|
|
53
|
+
cwd: string;
|
|
54
|
+
cols: number;
|
|
55
|
+
rows: number;
|
|
56
|
+
/** Inner command for the pane (defaults child-side to the login shell). */
|
|
57
|
+
command?: string;
|
|
58
|
+
args?: string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parent-side handle to a live terminal on a runner child. Bytes flow through
|
|
62
|
+
* {@link onData} / {@link write} (decoded from the base64 wire frames); resize
|
|
63
|
+
* and close are forwarded as control messages. {@link ready} resolves with the
|
|
64
|
+
* granted role once the child attaches (an `owner` request is downgraded to
|
|
65
|
+
* read-only when the owner slot is taken).
|
|
66
|
+
*/
|
|
67
|
+
export interface ParentTerminal {
|
|
68
|
+
readonly attachId: string;
|
|
69
|
+
readonly ready: Promise<{
|
|
70
|
+
role: TerminalRole;
|
|
71
|
+
}>;
|
|
72
|
+
onData(listener: (chunk: string) => void): void;
|
|
73
|
+
onExit(listener: (info: {
|
|
74
|
+
exitCode: number;
|
|
75
|
+
}) => void): void;
|
|
76
|
+
write(data: string): void;
|
|
77
|
+
resize(cols: number, rows: number): void;
|
|
78
|
+
close(): void;
|
|
79
|
+
}
|
|
80
|
+
export declare class RunnerManager implements AgentCapabilities {
|
|
81
|
+
private readonly config;
|
|
82
|
+
private readonly sessionStore;
|
|
83
|
+
private readonly runnerEntry;
|
|
84
|
+
private readonly idleTtlMs;
|
|
85
|
+
private readonly spawn;
|
|
86
|
+
private readonly childEnv;
|
|
87
|
+
private readonly now;
|
|
88
|
+
private readonly defaultRuntime;
|
|
89
|
+
private readonly handles;
|
|
90
|
+
private readonly reapTimer;
|
|
91
|
+
/** Sink for mirrored {@link SessionEvent}s from every session's forwarder. */
|
|
92
|
+
private mirrorListener;
|
|
93
|
+
/** Sink for session rotations (claude `/clear`·`/fork`) — server records meta. */
|
|
94
|
+
private rotateListener;
|
|
95
|
+
/** Session keys with a live codex forwarder — never reaped while present. */
|
|
96
|
+
private readonly liveSessionKeys;
|
|
97
|
+
constructor(opts: RunnerManagerOptions);
|
|
98
|
+
/**
|
|
99
|
+
* Open a live terminal on the session's runner child (spawning it if needed).
|
|
100
|
+
* Phase C hosts one terminal ("main") per session; the returned handle is a
|
|
101
|
+
* single attach client — `owner` (read-write) or a downgraded `read-only`
|
|
102
|
+
* viewer per the child's ownership rule. Not part of `AgentExecutor`; the WS
|
|
103
|
+
* bridge calls it directly.
|
|
104
|
+
*/
|
|
105
|
+
openTerminal(localThreadId: string, opts: OpenTerminalOptions): ParentTerminal;
|
|
106
|
+
/**
|
|
107
|
+
* Register the sink for mirrored {@link SessionEvent}s produced by every
|
|
108
|
+
* session's persistent codex forwarder (web- AND TUI-initiated turns). The
|
|
109
|
+
* server wires this to `persistSessionEvent` (bus + canonical log).
|
|
110
|
+
*/
|
|
111
|
+
onMirror(listener: (sessionId: string, event: SessionEvent) => void): void;
|
|
112
|
+
/** Register the sink for session rotations (claude `/clear`·`/fork`): the server
|
|
113
|
+
* records the new session's meta (carry-over agent/model/title). */
|
|
114
|
+
onRotate(listener: (rotation: RotateInfo) => void): void;
|
|
115
|
+
/**
|
|
116
|
+
* Eagerly bring up a session's codex-native live view (persistent forwarder +
|
|
117
|
+
* detached `codex --remote` TUI) in its runner child, spawning the runner if
|
|
118
|
+
* needed. Idempotent. Resolves true once the codex thread is bound; false for a
|
|
119
|
+
* non-codex / non-live session (the caller then uses the normal run path).
|
|
120
|
+
*/
|
|
121
|
+
ensureLiveSession(localThreadId: string, opts?: {
|
|
122
|
+
cwd?: string;
|
|
123
|
+
cols?: number;
|
|
124
|
+
rows?: number;
|
|
125
|
+
runtime?: AgentRuntimeId;
|
|
126
|
+
agentName?: string;
|
|
127
|
+
agentSpec?: AgentSpec;
|
|
128
|
+
}): Promise<boolean>;
|
|
129
|
+
/**
|
|
130
|
+
* Inject a user turn into a session's live codex thread — omnigent's
|
|
131
|
+
* single-writer web send (`turn/start` / `turn/steer`); the forwarder mirrors
|
|
132
|
+
* all output. Resolves true when the app-server accepted the turn, false when
|
|
133
|
+
* the session has no live forwarder (caller falls back to the run path).
|
|
134
|
+
*/
|
|
135
|
+
injectMessage(localThreadId: string, text: string): Promise<InjectOutcome>;
|
|
136
|
+
/**
|
|
137
|
+
* Interrupt a session's active live turn — the web Stop button (codex
|
|
138
|
+
* `turn/interrupt`, claude Escape). Best-effort: resolves false with NO spawn
|
|
139
|
+
* when the session has no live runner (nothing to interrupt).
|
|
140
|
+
*/
|
|
141
|
+
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;
|
|
148
|
+
listModels(runtime?: AgentRuntimeId): Promise<ModelListResponse | null>;
|
|
149
|
+
getGoal(localThreadId: string): Promise<CapabilityResult<ThreadGoal | null>>;
|
|
150
|
+
setGoal(localThreadId: string, objective: string): Promise<CapabilityResult>;
|
|
151
|
+
clearGoal(localThreadId: string): Promise<CapabilityResult>;
|
|
152
|
+
forkSession(currentLocalThreadId: string, newLocalThreadId: string): Promise<CapabilityResult>;
|
|
153
|
+
/**
|
|
154
|
+
* Backend-free runtime readiness (not part of `AgentCapabilities`; surfaced for
|
|
155
|
+
* the control console). Never spawns a runner.
|
|
156
|
+
*/
|
|
157
|
+
getStatus(options?: {
|
|
158
|
+
force?: boolean;
|
|
159
|
+
runtime?: AgentRuntimeId;
|
|
160
|
+
}): Promise<import("../host.js").CodexRuntimeStatus>;
|
|
161
|
+
/** Stop the runner bound to one session (if any), rejecting its in-flight work. */
|
|
162
|
+
stopRunner(localThreadId: string): void;
|
|
163
|
+
/** Kill every runner. Call on server shutdown. */
|
|
164
|
+
stop(): Promise<void>;
|
|
165
|
+
/** Forward a capability to a runner child. Session-less caps (listModels/status)
|
|
166
|
+
* use the shared `CAP_KEY` child; per-thread caps pass the session's key so they
|
|
167
|
+
* run on that session's child (which owns its private CODEX_HOME). */
|
|
168
|
+
private forwardCap;
|
|
169
|
+
private getOrSpawn;
|
|
170
|
+
private spawnHandle;
|
|
171
|
+
private onChildMessage;
|
|
172
|
+
/** Mark a handle dead and reject every pending run/cap with the exit reason. */
|
|
173
|
+
private failHandle;
|
|
174
|
+
private reapIdle;
|
|
175
|
+
}
|