@rynx-ai/runtime 0.1.11-beta.2 → 0.1.11-beta.21
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/models.d.ts +0 -5
- package/dist/claude/models.js +1 -7
- package/dist/claude/native-integration.d.ts +35 -7
- package/dist/claude/native-integration.js +204 -32
- package/dist/claude/session-status.d.ts +39 -0
- package/dist/claude/session-status.js +163 -0
- package/dist/codex-app-server/forwarder.d.ts +66 -2
- package/dist/codex-app-server/forwarder.js +329 -11
- package/dist/codex-app-server/mapping.d.ts +2 -0
- package/dist/codex-app-server/mapping.js +97 -23
- package/dist/codex-app-server/mcp-startup.d.ts +13 -0
- package/dist/codex-app-server/mcp-startup.js +63 -0
- package/dist/codex-app-server/protocol.d.ts +8 -5
- package/dist/codex-app-server/ws-channel.js +19 -19
- package/dist/codex-home.js +2 -4
- package/dist/host.d.ts +28 -7
- package/dist/host.js +356 -92
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/models-catalog.d.ts +2 -1
- package/dist/models-catalog.js +82 -3
- package/dist/runner/child.d.ts +22 -6
- package/dist/runner/child.js +237 -30
- package/dist/runner/manager.d.ts +87 -4
- package/dist/runner/manager.js +610 -56
- package/dist/runner/protocol.d.ts +11 -15
- package/dist/runner/startup-policy.d.ts +4 -0
- package/dist/runner/startup-policy.js +5 -0
- package/dist/terminal/claude-tui.d.ts +3 -1
- package/dist/terminal/claude-tui.js +3 -1
- package/dist/terminal/tmux.d.ts +29 -2
- package/dist/terminal/tmux.js +122 -14
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,6 @@ export type { InjectOutcome, TerminalOpenErrorCode, TerminalRole } from "./runne
|
|
|
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 } from "./terminal/tmux.js";
|
|
16
|
+
export { TmuxTerminal, isTmuxAvailable, terminateTmuxServer, tmuxSocketPath, } from "./terminal/tmux.js";
|
|
17
17
|
export type { TerminalAttachment, 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 } from "./terminal/tmux.js";
|
|
15
|
+
export { TmuxTerminal, isTmuxAvailable, terminateTmuxServer, tmuxSocketPath, } from "./terminal/tmux.js";
|
|
16
16
|
export { TerminalRegistry } from "./terminal/registry.js";
|
package/dist/models-catalog.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type AgentRuntimeId, type AppConfig } from "@rynx-ai/core";
|
|
2
2
|
import type { ModelListResponse } from "./codex-app-server/protocol.js";
|
|
3
3
|
export interface RuntimeModelCatalogDeps {
|
|
4
|
+
readCodexModels?: () => Promise<unknown>;
|
|
4
5
|
readTraexModels?: () => Promise<unknown>;
|
|
5
6
|
readTraexDebugModels?: () => Promise<unknown>;
|
|
6
7
|
}
|
|
7
8
|
/**
|
|
8
9
|
* The model list for a runtime, without an execution backend.
|
|
9
|
-
* Falls back to the configured model if
|
|
10
|
+
* Falls back to the configured model if local/native discovery is unavailable.
|
|
10
11
|
*/
|
|
11
12
|
export declare function listRuntimeModels(config: AppConfig, runtime: AgentRuntimeId, deps?: RuntimeModelCatalogDeps): Promise<ModelListResponse | null>;
|
package/dist/models-catalog.js
CHANGED
|
@@ -3,24 +3,37 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The parent control plane no longer holds an app-server, so `/models` can't be
|
|
5
5
|
* a live `model/list` RPC anymore. Following reference implementation's static-catalog model, we
|
|
6
|
-
* serve
|
|
6
|
+
* serve Codex's local model cache and Traex's native `models --json` catalog.
|
|
7
7
|
* claude already has its own static list ({@link listClaudeModels}).
|
|
8
8
|
*/
|
|
9
9
|
import { execFile } from "node:child_process";
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
10
12
|
import { promisify } from "node:util";
|
|
11
|
-
import { resolveRuntimeBinary, resolveRuntimeModel, } from "@rynx-ai/core";
|
|
13
|
+
import { getRuntimeProfile, resolveRuntimeBinary, resolveRuntimeHome, resolveRuntimeModel, } from "@rynx-ai/core";
|
|
12
14
|
import { listClaudeModels } from "./claude/models.js";
|
|
13
15
|
const execFileAsync = promisify(execFile);
|
|
14
16
|
const TRAEX_MODELS_TIMEOUT_MS = 8_000;
|
|
15
17
|
const TRAEX_MODELS_MAX_BYTES = 2 * 1024 * 1024;
|
|
16
18
|
/**
|
|
17
19
|
* The model list for a runtime, without an execution backend.
|
|
18
|
-
* Falls back to the configured model if
|
|
20
|
+
* Falls back to the configured model if local/native discovery is unavailable.
|
|
19
21
|
*/
|
|
20
22
|
export async function listRuntimeModels(config, runtime, deps = {}) {
|
|
21
23
|
if (runtime === "claude") {
|
|
22
24
|
return listClaudeModels();
|
|
23
25
|
}
|
|
26
|
+
if (runtime === "codex") {
|
|
27
|
+
try {
|
|
28
|
+
const configuredDefault = resolveRuntimeModel(config, runtime).trim();
|
|
29
|
+
const models = normalizeCodexModels(await (deps.readCodexModels ?? readCodexModels)(), configuredDefault);
|
|
30
|
+
if (models.length > 0)
|
|
31
|
+
return { data: models };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Keep the configured fallback usable before Codex has populated its cache.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
24
37
|
if (runtime === "traex") {
|
|
25
38
|
try {
|
|
26
39
|
const value = await (deps.readTraexModels ?? readTraexModels)();
|
|
@@ -48,6 +61,72 @@ export async function listRuntimeModels(config, runtime, deps = {}) {
|
|
|
48
61
|
}
|
|
49
62
|
return { data: [{ id: model, model, isDefault: true }] };
|
|
50
63
|
}
|
|
64
|
+
async function readCodexModels() {
|
|
65
|
+
const cachePath = join(resolveRuntimeHome(getRuntimeProfile("codex")), "models_cache.json");
|
|
66
|
+
return JSON.parse(await readFile(cachePath, "utf8"));
|
|
67
|
+
}
|
|
68
|
+
function normalizeCodexModels(value, configuredDefault) {
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
70
|
+
return [];
|
|
71
|
+
const entries = value.models;
|
|
72
|
+
if (!Array.isArray(entries))
|
|
73
|
+
return [];
|
|
74
|
+
const models = [];
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
for (const item of entries) {
|
|
77
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
78
|
+
continue;
|
|
79
|
+
const record = item;
|
|
80
|
+
const id = typeof record.slug === "string" ? record.slug.trim() : "";
|
|
81
|
+
if (!id || seen.has(id) || record.visibility === "hide")
|
|
82
|
+
continue;
|
|
83
|
+
seen.add(id);
|
|
84
|
+
const supportedReasoningEfforts = Array.isArray(record.supported_reasoning_levels)
|
|
85
|
+
? record.supported_reasoning_levels.flatMap((raw) => {
|
|
86
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
87
|
+
return [];
|
|
88
|
+
const effort = typeof raw.effort === "string"
|
|
89
|
+
? raw.effort.trim()
|
|
90
|
+
: "";
|
|
91
|
+
if (!effort)
|
|
92
|
+
return [];
|
|
93
|
+
const description = raw.description;
|
|
94
|
+
return [{
|
|
95
|
+
reasoningEffort: effort,
|
|
96
|
+
...(typeof description === "string" && description.trim()
|
|
97
|
+
? { description: description.trim() }
|
|
98
|
+
: {}),
|
|
99
|
+
}];
|
|
100
|
+
})
|
|
101
|
+
: [];
|
|
102
|
+
const displayName = typeof record.display_name === "string"
|
|
103
|
+
? record.display_name.trim()
|
|
104
|
+
: "";
|
|
105
|
+
const description = typeof record.description === "string"
|
|
106
|
+
? record.description.trim()
|
|
107
|
+
: "";
|
|
108
|
+
const defaultReasoningEffort = typeof record.default_reasoning_level === "string"
|
|
109
|
+
? record.default_reasoning_level.trim()
|
|
110
|
+
: "";
|
|
111
|
+
models.push({
|
|
112
|
+
id,
|
|
113
|
+
model: id,
|
|
114
|
+
...(displayName ? { displayName } : {}),
|
|
115
|
+
...(description ? { description } : {}),
|
|
116
|
+
isDefault: id === configuredDefault,
|
|
117
|
+
...(supportedReasoningEfforts.length > 0 ? { supportedReasoningEfforts } : {}),
|
|
118
|
+
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (configuredDefault && !seen.has(configuredDefault)) {
|
|
122
|
+
models.unshift({
|
|
123
|
+
id: configuredDefault,
|
|
124
|
+
model: configuredDefault,
|
|
125
|
+
isDefault: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return models;
|
|
129
|
+
}
|
|
51
130
|
async function readTraexModels() {
|
|
52
131
|
const { stdout } = await execFileAsync(resolveRuntimeBinary("traex"), ["models", "--json"], {
|
|
53
132
|
encoding: "utf8",
|
package/dist/runner/child.d.ts
CHANGED
|
@@ -31,15 +31,20 @@ interface LiveCodexProvider {
|
|
|
31
31
|
args: string[];
|
|
32
32
|
cwd: string;
|
|
33
33
|
env?: Record<string, string>;
|
|
34
|
+
skipTraexStartupPrompts?: boolean;
|
|
34
35
|
} | null>;
|
|
35
36
|
ensureLiveCodexSession?(localThreadId: string, emit: (event: SessionEvent) => void, opts: {
|
|
36
37
|
workspace: SessionWorkspaceSnapshot;
|
|
37
38
|
execution: ResolvedExecutionSnapshot;
|
|
38
39
|
retargetMirror?: RetargetMirror;
|
|
39
40
|
}): Promise<boolean>;
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
/** `null` delegates the deadline to RunnerManager. */
|
|
42
|
+
waitLiveReady?(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
|
|
43
|
+
/** `null` delegates the deadline to RunnerManager. */
|
|
44
|
+
waitTerminalReady?(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
|
|
42
45
|
liveSessionError?(localThreadId: string): string | undefined;
|
|
46
|
+
failLiveStartup?(localThreadId: string, error: Error): boolean;
|
|
47
|
+
failLiveSession?(localThreadId: string, error: Error): boolean;
|
|
43
48
|
injectMessage?(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectOutcome>;
|
|
44
49
|
interruptLive?(localThreadId: string): Promise<boolean>;
|
|
45
50
|
stopLiveCodexSession?(localThreadId: string, opts?: {
|
|
@@ -71,6 +76,9 @@ export declare class RunnerSession {
|
|
|
71
76
|
/** Live terminals hosted by this session, and per-attach client handles. */
|
|
72
77
|
private readonly terminals;
|
|
73
78
|
private readonly attachments;
|
|
79
|
+
private readonly attachmentThreadIds;
|
|
80
|
+
private readonly traexStartupWatchers;
|
|
81
|
+
private readonly terminalWatchers;
|
|
74
82
|
/** Opens are async; a close received before attach resolves tombstones the id. */
|
|
75
83
|
private readonly pendingTerminalOpens;
|
|
76
84
|
private readonly cancelledTerminalOpens;
|
|
@@ -89,18 +97,26 @@ export declare class RunnerSession {
|
|
|
89
97
|
private mirrorChannel;
|
|
90
98
|
/**
|
|
91
99
|
* Eagerly bring up a session's codex-native live view: start the persistent
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* so the TUI is usable immediately and its turns mirror to chat.
|
|
100
|
+
* observer connection before launching the detached Codex TUI. Fresh-session
|
|
101
|
+
* thread discovery then continues in the background while the first
|
|
102
|
+
* injection waits for the same bridge state.
|
|
96
103
|
*/
|
|
97
104
|
private ensureLive;
|
|
105
|
+
private monitorCodexThreadStartup;
|
|
106
|
+
/** Discovery failure is terminal for this partial native launch. Publish the
|
|
107
|
+
* exact cause first so an in-flight injection wakes, then drop the observer
|
|
108
|
+
* and TUI so a later task retry creates a clean launch instead of reusing an
|
|
109
|
+
* already-rejected readiness promise. */
|
|
110
|
+
private failCodexThreadStartup;
|
|
98
111
|
private inject;
|
|
99
112
|
private interruptLive;
|
|
100
113
|
/** Launch (idempotently) the session's codex TUI pane from the executor's
|
|
101
114
|
* `codexTerminalSpec`. Shares the `${id}-main` terminal id with `term.open`,
|
|
102
115
|
* so the web attach reuses the same detached pane. */
|
|
103
116
|
private launchCodexPane;
|
|
117
|
+
private watchNativeTerminal;
|
|
118
|
+
private cancelTraexStartupWatcher;
|
|
119
|
+
private skipTraexStartupPrompts;
|
|
104
120
|
private stopLive;
|
|
105
121
|
/** Stop event forwarding, kill native terminals/hooks, then synchronously
|
|
106
122
|
* scrub provider handoff files before the child process is allowed to exit. */
|
package/dist/runner/child.js
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { TerminalRegistry } from "../terminal/registry.js";
|
|
2
2
|
import { toWireError } from "./protocol.js";
|
|
3
|
+
import { isManagedNativeProvider } from "./startup-policy.js";
|
|
4
|
+
/** Maximum time for a fresh Codex-lineage TUI to publish its native thread. */
|
|
5
|
+
const CODEX_THREAD_START_TIMEOUT_MS = 30_000;
|
|
6
|
+
const TRAEX_STARTUP_WATCH_MS = 20_000;
|
|
7
|
+
const TRAEX_STARTUP_POLL_MS = 100;
|
|
8
|
+
const TRAEX_AUTHORIZATION_POLL_MS = 500;
|
|
9
|
+
const TRAEX_AUTHORIZATION_WAIT_MS = 15 * 60_000;
|
|
10
|
+
const TRAEX_PROMPT_RETRY_MS = 500;
|
|
11
|
+
function normalizeTraexPane(pane) {
|
|
12
|
+
return pane.toLowerCase().replace(/\s+/g, " ").trim();
|
|
13
|
+
}
|
|
14
|
+
function isTraexAuthorizationPending(pane) {
|
|
15
|
+
return pane.includes("waiting for authorization") &&
|
|
16
|
+
pane.includes("open this link in your browser") &&
|
|
17
|
+
pane.includes("press esc to cancel");
|
|
18
|
+
}
|
|
19
|
+
function isTerminalProtocolResponse(input) {
|
|
20
|
+
// xterm answers terminal queries through the same onData channel as real
|
|
21
|
+
// keystrokes. CSI carries device/focus/position reports; OSC carries color
|
|
22
|
+
// query replies such as `OSC 10;rgb:... ST` and `OSC 11;rgb:... ST`.
|
|
23
|
+
// Neither is evidence that the user has taken over startup prompt handling.
|
|
24
|
+
return /^(?:\x1b\[(?:(?:\?|>)[0-9;]*c|[0-9;]*(?:n|R|t)|[IO])|\x1b\][0-9]+;[^\x07\x1b]*(?:\x07|\x1b\\))+$/.test(input);
|
|
25
|
+
}
|
|
3
26
|
export class RunnerSession {
|
|
4
27
|
transport;
|
|
5
28
|
executor;
|
|
@@ -7,6 +30,9 @@ export class RunnerSession {
|
|
|
7
30
|
/** Live terminals hosted by this session, and per-attach client handles. */
|
|
8
31
|
terminals = new TerminalRegistry();
|
|
9
32
|
attachments = new Map();
|
|
33
|
+
attachmentThreadIds = new Map();
|
|
34
|
+
traexStartupWatchers = new Map();
|
|
35
|
+
terminalWatchers = new Map();
|
|
10
36
|
/** Opens are async; a close received before attach resolves tombstones the id. */
|
|
11
37
|
pendingTerminalOpens = new Set();
|
|
12
38
|
cancelledTerminalOpens = new Set();
|
|
@@ -46,7 +72,17 @@ export class RunnerSession {
|
|
|
46
72
|
});
|
|
47
73
|
return;
|
|
48
74
|
case "term.input":
|
|
49
|
-
|
|
75
|
+
{
|
|
76
|
+
const localThreadId = this.attachmentThreadIds.get(msg.attachId);
|
|
77
|
+
const input = Buffer.from(msg.dataB64, "base64").toString("utf8");
|
|
78
|
+
// xterm sends device/focus reports through the same onData channel as
|
|
79
|
+
// keystrokes. They must reach the TUI without pretending the user has
|
|
80
|
+
// taken over startup prompt handling.
|
|
81
|
+
if (!isTerminalProtocolResponse(input)) {
|
|
82
|
+
this.cancelTraexStartupWatcher(localThreadId);
|
|
83
|
+
}
|
|
84
|
+
this.attachments.get(msg.attachId)?.write(input);
|
|
85
|
+
}
|
|
50
86
|
return;
|
|
51
87
|
case "term.resize":
|
|
52
88
|
this.attachments.get(msg.attachId)?.resize(msg.cols, msg.rows);
|
|
@@ -57,6 +93,7 @@ export class RunnerSession {
|
|
|
57
93
|
}
|
|
58
94
|
const attachment = this.attachments.get(msg.attachId);
|
|
59
95
|
this.attachments.delete(msg.attachId);
|
|
96
|
+
this.attachmentThreadIds.delete(msg.attachId);
|
|
60
97
|
attachment?.kill();
|
|
61
98
|
return;
|
|
62
99
|
}
|
|
@@ -123,10 +160,9 @@ export class RunnerSession {
|
|
|
123
160
|
}
|
|
124
161
|
/**
|
|
125
162
|
* Eagerly bring up a session's codex-native live view: start the persistent
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* so the TUI is usable immediately and its turns mirror to chat.
|
|
163
|
+
* observer connection before launching the detached Codex TUI. Fresh-session
|
|
164
|
+
* thread discovery then continues in the background while the first
|
|
165
|
+
* injection waits for the same bridge state.
|
|
130
166
|
*/
|
|
131
167
|
async ensureLive(msg) {
|
|
132
168
|
const provider = this.liveProvider;
|
|
@@ -143,51 +179,59 @@ export class RunnerSession {
|
|
|
143
179
|
reqId: msg.reqId,
|
|
144
180
|
localThreadId: msg.localThreadId,
|
|
145
181
|
ok: false,
|
|
146
|
-
error:
|
|
182
|
+
error: provider.liveSessionError?.(msg.localThreadId)
|
|
183
|
+
?? "live provider did not start",
|
|
147
184
|
});
|
|
148
185
|
return;
|
|
149
186
|
}
|
|
150
187
|
this.liveIds.add(msg.localThreadId);
|
|
188
|
+
// Codex-lineage startup is phase-bounded: app-server readiness is
|
|
189
|
+
// handled by its channel, then the already-connected
|
|
190
|
+
// observer gets a full window for the TUI's thread/started event.
|
|
191
|
+
// Claude retains the parent-owned SessionStart deadline.
|
|
192
|
+
const runtime = msg.execution?.provider;
|
|
193
|
+
// Do not add another provider-local deadline around resume/preload. The
|
|
194
|
+
// parent control request remains bounded for process safety; fresh-thread
|
|
195
|
+
// discovery owns the separate 30s phase below.
|
|
196
|
+
const terminalReadyTimeoutMs = isManagedNativeProvider(runtime) ? null : undefined;
|
|
151
197
|
// Launch the TUI attached to the already-bound thread. Re-launch when the
|
|
152
198
|
// pane is absent OR its process has died (`isAlive` probes `#{pane_dead}`) — so a
|
|
153
199
|
// reconnect after the TUI exited restarts it instead of skipping (a
|
|
154
200
|
// launched-once guard would leave a dead "Pane is dead" husk forever).
|
|
155
201
|
if (!this.terminals.get(`${msg.localThreadId}-main`)?.isAlive()) {
|
|
156
202
|
const terminalReady = provider.waitTerminalReady
|
|
157
|
-
? await provider.waitTerminalReady(msg.localThreadId)
|
|
203
|
+
? await provider.waitTerminalReady(msg.localThreadId, terminalReadyTimeoutMs)
|
|
158
204
|
: true;
|
|
159
205
|
if (!terminalReady) {
|
|
206
|
+
const providerName = runtime === "traex" ? "Traex" : runtime === "codex" ? "Codex" : "Provider";
|
|
160
207
|
this.transport.send({
|
|
161
208
|
t: "live.ready",
|
|
162
209
|
reqId: msg.reqId,
|
|
163
210
|
localThreadId: msg.localThreadId,
|
|
164
211
|
ok: false,
|
|
165
|
-
error:
|
|
212
|
+
error: runtime === "codex" || runtime === "traex"
|
|
213
|
+
? provider.liveSessionError?.(msg.localThreadId)
|
|
214
|
+
?? `${providerName} app-server was ready, but its existing thread could not be resumed for the Terminal`
|
|
215
|
+
: "Provider thread was not ready for Terminal resume",
|
|
166
216
|
});
|
|
167
217
|
return;
|
|
168
218
|
}
|
|
169
219
|
await this.launchCodexPane(msg.localThreadId, msg.cols, msg.rows);
|
|
170
220
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
.slice(-1_000)
|
|
186
|
-
: "";
|
|
187
|
-
const readinessError = provider.liveSessionError?.(msg.localThreadId)
|
|
188
|
-
?? (paneFailure
|
|
189
|
-
? `Provider terminal exited before session discovery: ${paneFailure}`
|
|
190
|
-
: "live session was not ready before timeout");
|
|
221
|
+
let ready = true;
|
|
222
|
+
if (runtime === "codex" || runtime === "traex") {
|
|
223
|
+
// Discover thread/started in the background for 30s while injection
|
|
224
|
+
// waits up to 60s for either bridge readiness or that startup error.
|
|
225
|
+
// Do not serialize the two waits here.
|
|
226
|
+
this.monitorCodexThreadStartup(msg.localThreadId, runtime, provider);
|
|
227
|
+
}
|
|
228
|
+
else if (msg.waitForReady !== false && provider.waitLiveReady) {
|
|
229
|
+
ready = await provider.waitLiveReady(msg.localThreadId, terminalReadyTimeoutMs);
|
|
230
|
+
}
|
|
231
|
+
const readinessError = ready
|
|
232
|
+
? undefined
|
|
233
|
+
: provider.liveSessionError?.(msg.localThreadId)
|
|
234
|
+
?? "live session was not ready before timeout";
|
|
191
235
|
this.transport.send({
|
|
192
236
|
t: "live.ready",
|
|
193
237
|
reqId: msg.reqId,
|
|
@@ -206,12 +250,70 @@ export class RunnerSession {
|
|
|
206
250
|
});
|
|
207
251
|
}
|
|
208
252
|
}
|
|
253
|
+
monitorCodexThreadStartup(localThreadId, runtime, provider) {
|
|
254
|
+
if (!provider.waitLiveReady)
|
|
255
|
+
return;
|
|
256
|
+
void provider.waitLiveReady(localThreadId, CODEX_THREAD_START_TIMEOUT_MS).then((ready) => {
|
|
257
|
+
if (ready)
|
|
258
|
+
return;
|
|
259
|
+
const terminal = this.terminals.get(`${localThreadId}-main`);
|
|
260
|
+
const paneFailure = terminal && !terminal.isAlive()
|
|
261
|
+
? terminal
|
|
262
|
+
.capturePane()
|
|
263
|
+
.split("\n")
|
|
264
|
+
.map((line) => line.trim())
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
.slice(-6)
|
|
267
|
+
.join(" ")
|
|
268
|
+
.slice(-1_000)
|
|
269
|
+
: "";
|
|
270
|
+
const detail = provider.liveSessionError?.(localThreadId)
|
|
271
|
+
?? (paneFailure
|
|
272
|
+
? `Provider terminal exited before session discovery: ${paneFailure}`
|
|
273
|
+
: `${runtime === "traex" ? "Traex" : "Codex"} TUI did not publish thread/started within ${CODEX_THREAD_START_TIMEOUT_MS / 1_000}s after app-server and observer readiness`);
|
|
274
|
+
this.failCodexThreadStartup(localThreadId, provider, new Error(detail));
|
|
275
|
+
}).catch((error) => {
|
|
276
|
+
this.failCodexThreadStartup(localThreadId, provider, error instanceof Error ? error : new Error(String(error)));
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/** Discovery failure is terminal for this partial native launch. Publish the
|
|
280
|
+
* exact cause first so an in-flight injection wakes, then drop the observer
|
|
281
|
+
* and TUI so a later task retry creates a clean launch instead of reusing an
|
|
282
|
+
* already-rejected readiness promise. */
|
|
283
|
+
failCodexThreadStartup(localThreadId, provider, error) {
|
|
284
|
+
if (!provider.failLiveStartup?.(localThreadId, error))
|
|
285
|
+
return;
|
|
286
|
+
const watcher = this.terminalWatchers.get(localThreadId);
|
|
287
|
+
if (watcher)
|
|
288
|
+
clearInterval(watcher);
|
|
289
|
+
this.terminalWatchers.delete(localThreadId);
|
|
290
|
+
this.cancelTraexStartupWatcher(localThreadId);
|
|
291
|
+
try {
|
|
292
|
+
this.terminals.close(`${localThreadId}-main`);
|
|
293
|
+
}
|
|
294
|
+
catch (closeError) {
|
|
295
|
+
console.warn(`[runner] session=${localThreadId} failed to close native Terminal after startup failure: ${closeError instanceof Error ? closeError.message : String(closeError)}`);
|
|
296
|
+
}
|
|
297
|
+
provider.stopLiveCodexSession?.(localThreadId);
|
|
298
|
+
this.liveIds.delete(localThreadId);
|
|
299
|
+
}
|
|
209
300
|
async inject(msg) {
|
|
210
301
|
const provider = this.liveProvider;
|
|
211
302
|
try {
|
|
212
303
|
const input = msg.input ?? msg.text;
|
|
213
304
|
const outcome = (await provider.injectMessage?.(msg.localThreadId, input)) ?? "notLive";
|
|
214
|
-
|
|
305
|
+
// App-server injection is independent of the Terminal TUI startup, so it
|
|
306
|
+
// must not cancel prompt handling for the pane that is still starting.
|
|
307
|
+
const error = outcome === "injected" || outcome === "steered"
|
|
308
|
+
? undefined
|
|
309
|
+
: provider.liveSessionError?.(msg.localThreadId);
|
|
310
|
+
this.transport.send({
|
|
311
|
+
t: "injected",
|
|
312
|
+
reqId: msg.reqId,
|
|
313
|
+
localThreadId: msg.localThreadId,
|
|
314
|
+
outcome,
|
|
315
|
+
...(error ? { error } : {}),
|
|
316
|
+
});
|
|
215
317
|
}
|
|
216
318
|
catch (error) {
|
|
217
319
|
this.transport.send({
|
|
@@ -248,7 +350,8 @@ export class RunnerSession {
|
|
|
248
350
|
const spec = await this.liveProvider.codexTerminalSpec?.(localThreadId);
|
|
249
351
|
if (!spec)
|
|
250
352
|
return;
|
|
251
|
-
const
|
|
353
|
+
const terminalId = `${localThreadId}-main`;
|
|
354
|
+
const term = this.terminals.getOrCreate(terminalId, {
|
|
252
355
|
cwd: spec.cwd,
|
|
253
356
|
command: spec.command,
|
|
254
357
|
args: spec.args,
|
|
@@ -256,9 +359,110 @@ export class RunnerSession {
|
|
|
256
359
|
rows: rows ?? 40,
|
|
257
360
|
...(spec.env ? { env: spec.env } : {}),
|
|
258
361
|
});
|
|
362
|
+
if (spec.skipTraexStartupPrompts) {
|
|
363
|
+
const watcher = Symbol(localThreadId);
|
|
364
|
+
this.traexStartupWatchers.set(localThreadId, watcher);
|
|
365
|
+
void this.skipTraexStartupPrompts(localThreadId, term, watcher);
|
|
366
|
+
}
|
|
259
367
|
this.liveProvider.attachTerminalInjector?.(localThreadId, term);
|
|
368
|
+
this.watchNativeTerminal(localThreadId, terminalId, term);
|
|
369
|
+
}
|
|
370
|
+
watchNativeTerminal(localThreadId, terminalId, terminal) {
|
|
371
|
+
const previous = this.terminalWatchers.get(localThreadId);
|
|
372
|
+
if (previous)
|
|
373
|
+
clearInterval(previous);
|
|
374
|
+
let checking = false;
|
|
375
|
+
const timer = setInterval(() => {
|
|
376
|
+
if (checking || this.shuttingDown || this.terminals.get(terminalId) !== terminal)
|
|
377
|
+
return;
|
|
378
|
+
checking = true;
|
|
379
|
+
const probe = terminal.isAliveAsync?.() ?? Promise.resolve(terminal.isAlive());
|
|
380
|
+
void probe.then((alive) => {
|
|
381
|
+
if (alive || this.shuttingDown || this.terminals.get(terminalId) !== terminal)
|
|
382
|
+
return;
|
|
383
|
+
clearInterval(timer);
|
|
384
|
+
this.terminalWatchers.delete(localThreadId);
|
|
385
|
+
this.cancelTraexStartupWatcher(localThreadId);
|
|
386
|
+
this.liveProvider.failLiveSession?.(localThreadId, new Error("Provider terminal exited unexpectedly"));
|
|
387
|
+
this.failCodexThreadStartup(localThreadId, this.liveProvider, new Error("Provider terminal exited before native thread discovery completed"));
|
|
388
|
+
}).finally(() => {
|
|
389
|
+
checking = false;
|
|
390
|
+
});
|
|
391
|
+
}, 1_000);
|
|
392
|
+
timer.unref?.();
|
|
393
|
+
this.terminalWatchers.set(localThreadId, timer);
|
|
394
|
+
}
|
|
395
|
+
cancelTraexStartupWatcher(localThreadId) {
|
|
396
|
+
if (localThreadId)
|
|
397
|
+
this.traexStartupWatchers.delete(localThreadId);
|
|
398
|
+
}
|
|
399
|
+
async skipTraexStartupPrompts(localThreadId, terminal, watcher) {
|
|
400
|
+
const prompts = [
|
|
401
|
+
{
|
|
402
|
+
id: "welcome",
|
|
403
|
+
matches: (pane) => pane.includes("welcome to trae cli") && pane.includes("press enter to continue"),
|
|
404
|
+
dismiss: () => terminal.sendEnter(),
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
id: "migration",
|
|
408
|
+
matches: (pane) => pane.includes("legacy trae cli data detected") &&
|
|
409
|
+
pane.includes("select what to import") &&
|
|
410
|
+
(pane.includes("skip for now") || pane.includes("don't ask again")),
|
|
411
|
+
dismiss: () => terminal.interrupt(),
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: "hooks",
|
|
415
|
+
matches: (pane) => pane.includes("hooks need review") &&
|
|
416
|
+
pane.includes("trust all and continue") &&
|
|
417
|
+
pane.includes("continue without trusting"),
|
|
418
|
+
dismiss: () => terminal.interrupt(),
|
|
419
|
+
},
|
|
420
|
+
];
|
|
421
|
+
let activePromptId;
|
|
422
|
+
let lastDismissedAt = 0;
|
|
423
|
+
const startedAt = Date.now();
|
|
424
|
+
let deadline = startedAt + TRAEX_STARTUP_WATCH_MS;
|
|
425
|
+
const authorizationDeadline = startedAt + TRAEX_AUTHORIZATION_WAIT_MS;
|
|
426
|
+
const terminalId = `${localThreadId}-main`;
|
|
427
|
+
while (!this.shuttingDown &&
|
|
428
|
+
Date.now() < deadline &&
|
|
429
|
+
this.traexStartupWatchers.get(localThreadId) === watcher &&
|
|
430
|
+
this.terminals.get(terminalId) === terminal) {
|
|
431
|
+
// Do not treat a composer frame as completion: Traex can render it before
|
|
432
|
+
// the startup modals arrive. The bounded deadline stops this watcher.
|
|
433
|
+
const pane = normalizeTraexPane(terminal.capturePane());
|
|
434
|
+
const prompt = prompts.find((candidate) => candidate.matches(pane));
|
|
435
|
+
const now = Date.now();
|
|
436
|
+
const authorizationPending = isTraexAuthorizationPending(pane);
|
|
437
|
+
// Human device authorization routinely takes longer than the normal
|
|
438
|
+
// startup-modal window. Keep watching while that known screen remains,
|
|
439
|
+
// then preserve a full window for welcome/migration/hooks after sign-in.
|
|
440
|
+
// The separate cap prevents an abandoned auth screen from polling forever.
|
|
441
|
+
if (authorizationPending && now < authorizationDeadline) {
|
|
442
|
+
deadline = now + TRAEX_STARTUP_WATCH_MS;
|
|
443
|
+
}
|
|
444
|
+
if (!prompt) {
|
|
445
|
+
activePromptId = undefined;
|
|
446
|
+
}
|
|
447
|
+
else if (prompt.id !== activePromptId ||
|
|
448
|
+
now - lastDismissedAt >= TRAEX_PROMPT_RETRY_MS) {
|
|
449
|
+
prompt.dismiss();
|
|
450
|
+
activePromptId = prompt.id;
|
|
451
|
+
lastDismissedAt = now;
|
|
452
|
+
}
|
|
453
|
+
await new Promise((resolve) => {
|
|
454
|
+
const timer = setTimeout(resolve, authorizationPending ? TRAEX_AUTHORIZATION_POLL_MS : TRAEX_STARTUP_POLL_MS);
|
|
455
|
+
timer.unref();
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (this.traexStartupWatchers.get(localThreadId) === watcher) {
|
|
459
|
+
this.traexStartupWatchers.delete(localThreadId);
|
|
460
|
+
}
|
|
260
461
|
}
|
|
261
462
|
stopLive() {
|
|
463
|
+
for (const timer of this.terminalWatchers.values())
|
|
464
|
+
clearInterval(timer);
|
|
465
|
+
this.terminalWatchers.clear();
|
|
262
466
|
for (const id of this.liveIds) {
|
|
263
467
|
this.liveProvider.stopLiveCodexSession?.(id, {
|
|
264
468
|
deferClaudeInteractionCleanup: true,
|
|
@@ -336,6 +540,8 @@ export class RunnerSession {
|
|
|
336
540
|
return;
|
|
337
541
|
}
|
|
338
542
|
this.attachments.set(msg.attachId, attachment);
|
|
543
|
+
if (msg.localThreadId)
|
|
544
|
+
this.attachmentThreadIds.set(msg.attachId, msg.localThreadId);
|
|
339
545
|
attachment.onData((chunk) => this.transport.send({
|
|
340
546
|
t: "term.data",
|
|
341
547
|
attachId: msg.attachId,
|
|
@@ -343,6 +549,7 @@ export class RunnerSession {
|
|
|
343
549
|
}));
|
|
344
550
|
attachment.onExit((info) => {
|
|
345
551
|
this.attachments.delete(msg.attachId);
|
|
552
|
+
this.attachmentThreadIds.delete(msg.attachId);
|
|
346
553
|
this.transport.send({ t: "term.exit", attachId: msg.attachId, exitCode: info.exitCode });
|
|
347
554
|
});
|
|
348
555
|
this.transport.send({ t: "term.opened", attachId: msg.attachId, role });
|