@rynx-ai/runtime 0.1.11-beta.21 → 0.1.11-beta.22
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/codex-app-server/client.d.ts +4 -3
- package/dist/codex-app-server/client.js +35 -12
- package/dist/codex-app-server/forwarder.d.ts +0 -7
- package/dist/codex-app-server/forwarder.js +0 -48
- package/dist/codex-app-server/protocol.d.ts +1 -2
- package/dist/host.d.ts +27 -21
- package/dist/host.js +237 -376
- package/dist/runner/child.d.ts +12 -5
- package/dist/runner/child.js +115 -36
- package/dist/runner/manager.d.ts +10 -7
- package/dist/runner/manager.js +77 -51
- package/dist/runner/protocol.d.ts +10 -9
- package/dist/runner/startup-policy.d.ts +3 -0
- package/dist/runner/startup-policy.js +5 -0
- package/package.json +2 -2
package/dist/runner/child.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* per-thread capabilities, answered against the same backend so the parent never
|
|
8
8
|
* needs an app-server of its own.
|
|
9
9
|
*/
|
|
10
|
-
import { type AgentCapabilities, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
|
|
10
|
+
import { type AgentCapabilities, type LiveSessionFailure, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
|
|
11
11
|
import type { SessionEvent } from "@rynx-ai/core";
|
|
12
12
|
import type { TerminalInjector } from "../claude/native-integration.js";
|
|
13
13
|
import type { ResolveInteractionResult } from "../interactions.js";
|
|
@@ -38,11 +38,16 @@ interface LiveCodexProvider {
|
|
|
38
38
|
execution: ResolvedExecutionSnapshot;
|
|
39
39
|
retargetMirror?: RetargetMirror;
|
|
40
40
|
}): Promise<boolean>;
|
|
41
|
+
/** Known-thread Codex/Traex resume only: start the independent transcript
|
|
42
|
+
* observer after the replacement TUI has launched. Fresh discovery connects
|
|
43
|
+
* its listener before launch and this method is an idempotent no-op. */
|
|
44
|
+
startLiveCodexObserver?(localThreadId: string): void;
|
|
41
45
|
/** `null` delegates the deadline to RunnerManager. */
|
|
42
46
|
waitLiveReady?(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
|
|
43
47
|
/** `null` delegates the deadline to RunnerManager. */
|
|
44
48
|
waitTerminalReady?(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
|
|
45
49
|
liveSessionError?(localThreadId: string): string | undefined;
|
|
50
|
+
liveSessionFailure?(localThreadId: string): LiveSessionFailure | undefined;
|
|
46
51
|
failLiveStartup?(localThreadId: string, error: Error): boolean;
|
|
47
52
|
failLiveSession?(localThreadId: string, error: Error): boolean;
|
|
48
53
|
injectMessage?(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectOutcome>;
|
|
@@ -84,6 +89,8 @@ export declare class RunnerSession {
|
|
|
84
89
|
private readonly cancelledTerminalOpens;
|
|
85
90
|
/** Session ids with a live codex forwarder started here (stopped on shutdown). */
|
|
86
91
|
private readonly liveIds;
|
|
92
|
+
/** Provider name retained for asynchronous Terminal-exit diagnostics. */
|
|
93
|
+
private readonly liveRuntimes;
|
|
87
94
|
private shuttingDown;
|
|
88
95
|
constructor({ transport, executor, onShutdown }: RunnerSessionDeps);
|
|
89
96
|
private handle;
|
|
@@ -96,10 +103,10 @@ export declare class RunnerSession {
|
|
|
96
103
|
* the new session stays injectable. */
|
|
97
104
|
private mirrorChannel;
|
|
98
105
|
/**
|
|
99
|
-
* Eagerly bring up a session's codex-native live view
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
106
|
+
* Eagerly bring up a session's codex-native live view. Fresh sessions connect
|
|
107
|
+
* the discovery listener before launching the detached TUI; known-thread
|
|
108
|
+
* resumes preload first, launch the replacement TUI, then start their
|
|
109
|
+
* persistent observer in the background.
|
|
103
110
|
*/
|
|
104
111
|
private ensureLive;
|
|
105
112
|
private monitorCodexThreadStartup;
|
package/dist/runner/child.js
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
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 { AgentRuntimeError, } from "@rynx-ai/core";
|
|
1
11
|
import { TerminalRegistry } from "../terminal/registry.js";
|
|
2
|
-
import { toWireError } from "./protocol.js";
|
|
3
|
-
import {
|
|
12
|
+
import { toWireError, } from "./protocol.js";
|
|
13
|
+
import { isCodexLineageProvider } from "./startup-policy.js";
|
|
4
14
|
/** Maximum time for a fresh Codex-lineage TUI to publish its native thread. */
|
|
5
15
|
const CODEX_THREAD_START_TIMEOUT_MS = 30_000;
|
|
16
|
+
function providerDisplayName(runtime) {
|
|
17
|
+
return runtime === "traex" ? "Traex" : runtime === "claude" ? "Claude" : "Codex";
|
|
18
|
+
}
|
|
19
|
+
function providerFailure(provider, localThreadId, fallback) {
|
|
20
|
+
return provider.liveSessionFailure?.(localThreadId) ?? {
|
|
21
|
+
message: provider.liveSessionError?.(localThreadId) ?? fallback.message,
|
|
22
|
+
code: fallback.code,
|
|
23
|
+
statusCode: 503,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function nativePhaseError(runtime, code, phase, cause) {
|
|
27
|
+
const detail = cause === undefined
|
|
28
|
+
? ""
|
|
29
|
+
: `: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
30
|
+
return new AgentRuntimeError(`${providerDisplayName(runtime)} ${phase}${detail}`, 503, code);
|
|
31
|
+
}
|
|
32
|
+
function terminalFailureDetail(pane) {
|
|
33
|
+
return pane
|
|
34
|
+
.split("\n")
|
|
35
|
+
.map((line) => line.trim())
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.slice(-6)
|
|
38
|
+
.join(" ")
|
|
39
|
+
.slice(-1_000);
|
|
40
|
+
}
|
|
6
41
|
const TRAEX_STARTUP_WATCH_MS = 20_000;
|
|
7
42
|
const TRAEX_STARTUP_POLL_MS = 100;
|
|
8
43
|
const TRAEX_AUTHORIZATION_POLL_MS = 500;
|
|
@@ -38,6 +73,8 @@ export class RunnerSession {
|
|
|
38
73
|
cancelledTerminalOpens = new Set();
|
|
39
74
|
/** Session ids with a live codex forwarder started here (stopped on shutdown). */
|
|
40
75
|
liveIds = new Set();
|
|
76
|
+
/** Provider name retained for asynchronous Terminal-exit diagnostics. */
|
|
77
|
+
liveRuntimes = new Map();
|
|
41
78
|
shuttingDown = false;
|
|
42
79
|
constructor({ transport, executor, onShutdown }) {
|
|
43
80
|
this.transport = transport;
|
|
@@ -159,10 +196,10 @@ export class RunnerSession {
|
|
|
159
196
|
return { emit, retarget };
|
|
160
197
|
}
|
|
161
198
|
/**
|
|
162
|
-
* Eagerly bring up a session's codex-native live view
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
199
|
+
* Eagerly bring up a session's codex-native live view. Fresh sessions connect
|
|
200
|
+
* the discovery listener before launching the detached TUI; known-thread
|
|
201
|
+
* resumes preload first, launch the replacement TUI, then start their
|
|
202
|
+
* persistent observer in the background.
|
|
166
203
|
*/
|
|
167
204
|
async ensureLive(msg) {
|
|
168
205
|
const provider = this.liveProvider;
|
|
@@ -174,34 +211,38 @@ export class RunnerSession {
|
|
|
174
211
|
retargetMirror: retarget,
|
|
175
212
|
});
|
|
176
213
|
if (!started) {
|
|
214
|
+
const runtime = msg.execution?.provider;
|
|
177
215
|
this.transport.send({
|
|
178
216
|
t: "live.ready",
|
|
179
217
|
reqId: msg.reqId,
|
|
180
218
|
localThreadId: msg.localThreadId,
|
|
181
219
|
ok: false,
|
|
182
|
-
error: provider
|
|
183
|
-
|
|
220
|
+
error: providerFailure(provider, msg.localThreadId, {
|
|
221
|
+
runtime,
|
|
222
|
+
code: "native_start_failed",
|
|
223
|
+
message: `${providerDisplayName(runtime)} native session did not start`,
|
|
224
|
+
}),
|
|
184
225
|
});
|
|
185
226
|
return;
|
|
186
227
|
}
|
|
187
228
|
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
229
|
const runtime = msg.execution?.provider;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// discovery owns the separate 30s phase below.
|
|
196
|
-
const terminalReadyTimeoutMs = isManagedNativeProvider(runtime) ? null : undefined;
|
|
230
|
+
if (runtime)
|
|
231
|
+
this.liveRuntimes.set(msg.localThreadId, runtime);
|
|
197
232
|
// Launch the TUI attached to the already-bound thread. Re-launch when the
|
|
198
233
|
// pane is absent OR its process has died (`isAlive` probes `#{pane_dead}`) — so a
|
|
199
234
|
// reconnect after the TUI exited restarts it instead of skipping (a
|
|
200
235
|
// launched-once guard would leave a dead "Pane is dead" husk forever).
|
|
201
236
|
if (!this.terminals.get(`${msg.localThreadId}-main`)?.isAlive()) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
237
|
+
// Codex/Traex `ensureLiveCodexSession` already performs the backend
|
|
238
|
+
// preload for an existing thread. Match the native lifecycle: launch
|
|
239
|
+
// the TUI immediately after that preload and let the observer subscribe
|
|
240
|
+
// in the background. Fresh sessions likewise launch before discovery.
|
|
241
|
+
const terminalReady = isCodexLineageProvider(runtime)
|
|
242
|
+
? true
|
|
243
|
+
: provider.waitTerminalReady
|
|
244
|
+
? await provider.waitTerminalReady(msg.localThreadId)
|
|
245
|
+
: true;
|
|
205
246
|
if (!terminalReady) {
|
|
206
247
|
const providerName = runtime === "traex" ? "Traex" : runtime === "codex" ? "Codex" : "Provider";
|
|
207
248
|
this.transport.send({
|
|
@@ -209,14 +250,31 @@ export class RunnerSession {
|
|
|
209
250
|
reqId: msg.reqId,
|
|
210
251
|
localThreadId: msg.localThreadId,
|
|
211
252
|
ok: false,
|
|
212
|
-
error:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
:
|
|
253
|
+
error: providerFailure(provider, msg.localThreadId, {
|
|
254
|
+
runtime,
|
|
255
|
+
code: "native_resume_failed",
|
|
256
|
+
message: runtime === "codex" || runtime === "traex"
|
|
257
|
+
? `${providerName} app-server was ready, but its existing session could not be resumed for the Terminal`
|
|
258
|
+
: "Provider thread was not ready for Terminal resume",
|
|
259
|
+
}),
|
|
216
260
|
});
|
|
217
261
|
return;
|
|
218
262
|
}
|
|
219
|
-
|
|
263
|
+
try {
|
|
264
|
+
await this.launchCodexPane(msg.localThreadId, msg.cols, msg.rows);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
throw error instanceof AgentRuntimeError
|
|
268
|
+
? error
|
|
269
|
+
: nativePhaseError(runtime, "native_terminal_launch_failed", "Terminal failed to launch", error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Omnigent's known-thread path launches `codex --remote resume` first,
|
|
273
|
+
// then creates `_codex_forward_known_thread` as background work. Keep the
|
|
274
|
+
// ordering explicit here: observer connection failure cannot delay or
|
|
275
|
+
// reject cold-resume admission.
|
|
276
|
+
if (isCodexLineageProvider(runtime)) {
|
|
277
|
+
provider.startLiveCodexObserver?.(msg.localThreadId);
|
|
220
278
|
}
|
|
221
279
|
let ready = true;
|
|
222
280
|
if (runtime === "codex" || runtime === "traex") {
|
|
@@ -226,12 +284,15 @@ export class RunnerSession {
|
|
|
226
284
|
this.monitorCodexThreadStartup(msg.localThreadId, runtime, provider);
|
|
227
285
|
}
|
|
228
286
|
else if (msg.waitForReady !== false && provider.waitLiveReady) {
|
|
229
|
-
ready = await provider.waitLiveReady(msg.localThreadId
|
|
287
|
+
ready = await provider.waitLiveReady(msg.localThreadId);
|
|
230
288
|
}
|
|
231
289
|
const readinessError = ready
|
|
232
290
|
? undefined
|
|
233
|
-
: provider
|
|
234
|
-
|
|
291
|
+
: providerFailure(provider, msg.localThreadId, {
|
|
292
|
+
runtime,
|
|
293
|
+
code: "native_readiness_failed",
|
|
294
|
+
message: `${providerDisplayName(runtime)} native session was not ready`,
|
|
295
|
+
});
|
|
235
296
|
this.transport.send({
|
|
236
297
|
t: "live.ready",
|
|
237
298
|
reqId: msg.reqId,
|
|
@@ -246,7 +307,7 @@ export class RunnerSession {
|
|
|
246
307
|
reqId: msg.reqId,
|
|
247
308
|
localThreadId: msg.localThreadId,
|
|
248
309
|
ok: false,
|
|
249
|
-
error:
|
|
310
|
+
error: toWireError(error),
|
|
250
311
|
});
|
|
251
312
|
}
|
|
252
313
|
}
|
|
@@ -269,11 +330,15 @@ export class RunnerSession {
|
|
|
269
330
|
: "";
|
|
270
331
|
const detail = provider.liveSessionError?.(localThreadId)
|
|
271
332
|
?? (paneFailure
|
|
272
|
-
?
|
|
333
|
+
? `${providerDisplayName(runtime)} Terminal exited before native session discovery: ${paneFailure}`
|
|
273
334
|
: `${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
|
|
335
|
+
this.failCodexThreadStartup(localThreadId, provider, new AgentRuntimeError(detail, 503, paneFailure
|
|
336
|
+
? "native_terminal_exited_before_session"
|
|
337
|
+
: "native_thread_discovery_timeout"));
|
|
275
338
|
}).catch((error) => {
|
|
276
|
-
this.failCodexThreadStartup(localThreadId, provider, error instanceof
|
|
339
|
+
this.failCodexThreadStartup(localThreadId, provider, error instanceof AgentRuntimeError
|
|
340
|
+
? error
|
|
341
|
+
: nativePhaseError(runtime, "native_thread_discovery_failed", "native session discovery failed", error));
|
|
277
342
|
});
|
|
278
343
|
}
|
|
279
344
|
/** Discovery failure is terminal for this partial native launch. Publish the
|
|
@@ -306,7 +371,18 @@ export class RunnerSession {
|
|
|
306
371
|
// must not cancel prompt handling for the pane that is still starting.
|
|
307
372
|
const error = outcome === "injected" || outcome === "steered"
|
|
308
373
|
? undefined
|
|
309
|
-
: provider
|
|
374
|
+
: providerFailure(provider, msg.localThreadId, {
|
|
375
|
+
code: outcome === "notReady"
|
|
376
|
+
? "native_thread_not_ready"
|
|
377
|
+
: outcome === "notLive"
|
|
378
|
+
? "native_session_not_live"
|
|
379
|
+
: "native_message_injection_failed",
|
|
380
|
+
message: outcome === "notReady"
|
|
381
|
+
? "Native session was not ready for message injection"
|
|
382
|
+
: outcome === "notLive"
|
|
383
|
+
? "Native session was not live for message injection"
|
|
384
|
+
: "Native message injection failed",
|
|
385
|
+
});
|
|
310
386
|
this.transport.send({
|
|
311
387
|
t: "injected",
|
|
312
388
|
reqId: msg.reqId,
|
|
@@ -321,7 +397,7 @@ export class RunnerSession {
|
|
|
321
397
|
reqId: msg.reqId,
|
|
322
398
|
localThreadId: msg.localThreadId,
|
|
323
399
|
outcome: "failed",
|
|
324
|
-
error:
|
|
400
|
+
error: toWireError(error),
|
|
325
401
|
});
|
|
326
402
|
}
|
|
327
403
|
}
|
|
@@ -333,7 +409,7 @@ export class RunnerSession {
|
|
|
333
409
|
ok = (await provider.interruptLive?.(msg.localThreadId)) ?? false;
|
|
334
410
|
}
|
|
335
411
|
catch (e) {
|
|
336
|
-
error =
|
|
412
|
+
error = toWireError(e);
|
|
337
413
|
}
|
|
338
414
|
this.transport.send({
|
|
339
415
|
t: "interrupted",
|
|
@@ -383,8 +459,10 @@ export class RunnerSession {
|
|
|
383
459
|
clearInterval(timer);
|
|
384
460
|
this.terminalWatchers.delete(localThreadId);
|
|
385
461
|
this.cancelTraexStartupWatcher(localThreadId);
|
|
386
|
-
this.
|
|
387
|
-
|
|
462
|
+
const runtime = this.liveRuntimes.get(localThreadId);
|
|
463
|
+
const paneFailure = terminalFailureDetail(terminal.capturePane?.() ?? "");
|
|
464
|
+
this.liveProvider.failLiveSession?.(localThreadId, nativePhaseError(runtime, "native_terminal_exited", "Terminal exited unexpectedly", paneFailure || undefined));
|
|
465
|
+
this.failCodexThreadStartup(localThreadId, this.liveProvider, nativePhaseError(runtime, "native_terminal_exited_before_session", "Terminal exited before native session discovery completed", paneFailure || undefined));
|
|
388
466
|
}).finally(() => {
|
|
389
467
|
checking = false;
|
|
390
468
|
});
|
|
@@ -469,6 +547,7 @@ export class RunnerSession {
|
|
|
469
547
|
});
|
|
470
548
|
}
|
|
471
549
|
this.liveIds.clear();
|
|
550
|
+
this.liveRuntimes.clear();
|
|
472
551
|
}
|
|
473
552
|
/** Stop event forwarding, kill native terminals/hooks, then synchronously
|
|
474
553
|
* scrub provider handoff files before the child process is allowed to exit. */
|
package/dist/runner/manager.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* channel.
|
|
17
17
|
*/
|
|
18
18
|
import { spawn as nodeSpawn, type ChildProcess } from "node:child_process";
|
|
19
|
-
import { type AdmissionReservation, type AgentCapabilities, type AgentRuntimeId, type AppConfig, type CapabilityResult, type ModelListResponse, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type SessionWorkspaceSnapshot, type ThreadGoal } from "@rynx-ai/core";
|
|
19
|
+
import { type AdmissionReservation, type AgentCapabilities, type AgentRuntimeId, type AppConfig, type CapabilityResult, type ModelListResponse, type LiveSessionFailure, type ResolvedExecutionSnapshot, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type SessionWorkspaceSnapshot, type ThreadGoal } from "@rynx-ai/core";
|
|
20
20
|
import { type CodexSessionStore } from "../host.js";
|
|
21
21
|
import type { ResolveInteractionResult } from "../interactions.js";
|
|
22
22
|
import { type InjectOutcome, type TerminalOpenErrorCode, type TerminalRole } from "./protocol.js";
|
|
@@ -53,12 +53,13 @@ export interface RunnerManagerOptions {
|
|
|
53
53
|
shutdownGraceMs?: number;
|
|
54
54
|
/** Final bounded wait for child exit after SIGKILL. */
|
|
55
55
|
shutdownKillGraceMs?: number;
|
|
56
|
-
/** Max wait for a setup-pane launch acknowledgement.
|
|
56
|
+
/** Max wait for a legacy setup-pane launch acknowledgement. Codex/Traex use
|
|
57
|
+
* their internal phase deadlines and are not wrapped by this timer. */
|
|
57
58
|
liveStartTimeoutMs?: number;
|
|
58
59
|
/** Max wait for a Provider-native thread to become ready. */
|
|
59
60
|
liveReadyTimeoutMs?: number;
|
|
60
|
-
/** Claude SessionStart deadline. Codex/Traex
|
|
61
|
-
*
|
|
61
|
+
/** Claude SessionStart deadline. Codex/Traex have no parent-owned startup
|
|
62
|
+
* deadline; their app-server/discovery/injection phases report directly. */
|
|
62
63
|
nativeLiveStartTimeoutMs?: number;
|
|
63
64
|
/** Max wait for a native interrupt acknowledgement before reporting it
|
|
64
65
|
* unproven. Callers may then use the explicit force-stop path. */
|
|
@@ -247,9 +248,10 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
247
248
|
/**
|
|
248
249
|
* Eagerly bring up a session's codex-native live view (persistent forwarder +
|
|
249
250
|
* detached `codex --remote` TUI) in its runner child, spawning the runner if
|
|
250
|
-
* needed. Idempotent. For Codex-lineage
|
|
251
|
-
*
|
|
252
|
-
*
|
|
251
|
+
* needed. Idempotent. For Codex-lineage sessions, resolves after backend
|
|
252
|
+
* preload and pane launch; known-thread observer attach and fresh-thread
|
|
253
|
+
* discovery continue in the background. Other providers retain their
|
|
254
|
+
* readiness gate.
|
|
253
255
|
*/
|
|
254
256
|
ensureLiveSession(localThreadId: string, opts: {
|
|
255
257
|
workspace: SessionWorkspaceSnapshot;
|
|
@@ -268,6 +270,7 @@ export declare class RunnerManager implements AgentCapabilities {
|
|
|
268
270
|
}): Promise<boolean>;
|
|
269
271
|
private requestLiveSession;
|
|
270
272
|
lastLiveSessionError(localThreadId: string): string | undefined;
|
|
273
|
+
lastLiveSessionFailure(localThreadId: string): LiveSessionFailure | undefined;
|
|
271
274
|
/**
|
|
272
275
|
* Inject a user turn into a session's live codex thread — reference implementation's
|
|
273
276
|
* single-writer web send (`turn/start` / `turn/steer`); the forwarder mirrors
|
package/dist/runner/manager.js
CHANGED
|
@@ -24,7 +24,7 @@ import { listRuntimeModels } from "../models-catalog.js";
|
|
|
24
24
|
import { probeRuntimeStatus } from "../runtime-status.js";
|
|
25
25
|
import { terminateTmuxServer, tmuxHasAttachedClient, tmuxWindowActivityAt, } from "../terminal/tmux.js";
|
|
26
26
|
import { fromWireError, } from "./protocol.js";
|
|
27
|
-
import { isManagedNativeProvider } from "./startup-policy.js";
|
|
27
|
+
import { isCodexLineageProvider, isManagedNativeProvider } from "./startup-policy.js";
|
|
28
28
|
import { StdioRunnerTransport } from "./transport.js";
|
|
29
29
|
/** Routing key for the shared capability runner (slash-command RPCs). */
|
|
30
30
|
const CAP_KEY = "__cap__";
|
|
@@ -34,9 +34,8 @@ const STDERR_TAIL_LINES = 40;
|
|
|
34
34
|
const DEFAULT_SHUTDOWN_GRACE_MS = 5_000;
|
|
35
35
|
/** Time allowed for exit after SIGKILL before shutdown reports failure. */
|
|
36
36
|
const DEFAULT_SHUTDOWN_KILL_GRACE_MS = 5_000;
|
|
37
|
-
/**
|
|
38
|
-
*
|
|
39
|
-
* own 10s gate so scheduling/IPC overhead cannot win the same deadline. */
|
|
37
|
+
/** Legacy setup-pane acknowledgement deadline for providers that do not own
|
|
38
|
+
* phase-specific startup errors. Codex/Traex are deliberately excluded. */
|
|
40
39
|
const DEFAULT_LIVE_START_TIMEOUT_MS = 30_000;
|
|
41
40
|
/** Legacy fallback for a provider without a native startup policy. */
|
|
42
41
|
const DEFAULT_LIVE_READY_TIMEOUT_MS = 25_000;
|
|
@@ -509,9 +508,10 @@ export class RunnerManager {
|
|
|
509
508
|
/**
|
|
510
509
|
* Eagerly bring up a session's codex-native live view (persistent forwarder +
|
|
511
510
|
* detached `codex --remote` TUI) in its runner child, spawning the runner if
|
|
512
|
-
* needed. Idempotent. For Codex-lineage
|
|
513
|
-
*
|
|
514
|
-
*
|
|
511
|
+
* needed. Idempotent. For Codex-lineage sessions, resolves after backend
|
|
512
|
+
* preload and pane launch; known-thread observer attach and fresh-thread
|
|
513
|
+
* discovery continue in the background. Other providers retain their
|
|
514
|
+
* readiness gate.
|
|
515
515
|
*/
|
|
516
516
|
ensureLiveSession(localThreadId, opts) {
|
|
517
517
|
const admission = this.reserveAdmission();
|
|
@@ -550,55 +550,66 @@ export class RunnerManager {
|
|
|
550
550
|
this.liveSessionKeys.add(localThreadId);
|
|
551
551
|
const reqId = randomUUID();
|
|
552
552
|
return new Promise((resolve) => {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
553
|
+
const provider = opts.execution.provider;
|
|
554
|
+
const timeoutMs = isCodexLineageProvider(provider)
|
|
555
|
+
? null
|
|
556
|
+
: !waitForReady
|
|
557
|
+
? this.liveStartTimeoutMs
|
|
558
|
+
: provider === "claude"
|
|
559
|
+
? this.nativeLiveStartTimeoutMs
|
|
559
560
|
: this.liveReadyTimeoutMs;
|
|
560
|
-
const timeout =
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
? `${provider === "traex" ? "Traex" : "Codex"} app-server/observer/Terminal launch was not acknowledged within ${Math.round(timeoutMs / 1_000)}s; the runner process was reset before retry`
|
|
561
|
+
const timeout = timeoutMs === null
|
|
562
|
+
? undefined
|
|
563
|
+
: setTimeout(() => {
|
|
564
|
+
if (!handle.live.delete(reqId))
|
|
565
|
+
return;
|
|
566
|
+
const finishTimeout = () => {
|
|
567
|
+
const reason = !waitForReady
|
|
568
|
+
? `runner did not acknowledge terminal start within ${timeoutMs}ms`
|
|
569
|
+
: provider === "claude"
|
|
570
|
+
? `Claude SessionStart was not observed within ${Math.round(timeoutMs / 1_000)}s; the runner process was reset before retry`
|
|
571
571
|
: `runner did not acknowledge live readiness within ${timeoutMs}ms`;
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
572
|
+
this.liveErrors.set(localThreadId, {
|
|
573
|
+
message: reason,
|
|
574
|
+
code: provider === "claude"
|
|
575
|
+
? "claude_session_start_timeout"
|
|
576
|
+
: "native_runner_ack_timeout",
|
|
577
|
+
statusCode: 503,
|
|
578
|
+
});
|
|
579
|
+
this.liveSessionKeys.delete(localThreadId);
|
|
580
|
+
// Legacy providers without phase-owned startup errors retain the
|
|
581
|
+
// bounded control guard. Codex/Traex never enter this branch.
|
|
582
|
+
void this.terminateHandle(handle, reason).catch((error) => {
|
|
583
|
+
logTerminationFailure(handle, error);
|
|
584
|
+
});
|
|
585
|
+
return false;
|
|
586
|
+
};
|
|
587
|
+
const reservation = allowReservedForkTarget
|
|
588
|
+
? undefined
|
|
589
|
+
: this.forkReservations.get(localThreadId);
|
|
590
|
+
if (reservation) {
|
|
591
|
+
void reservation
|
|
592
|
+
.then(() => finishTimeout())
|
|
593
|
+
.then(resolve, () => resolve(false));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
resolve(finishTimeout());
|
|
597
|
+
}, timeoutMs);
|
|
598
|
+
timeout?.unref?.();
|
|
593
599
|
handle.live.set(reqId, (res) => {
|
|
594
|
-
|
|
600
|
+
if (timeout)
|
|
601
|
+
clearTimeout(timeout);
|
|
595
602
|
const ok = res.ok ?? false;
|
|
596
603
|
const finish = () => {
|
|
597
604
|
if (ok) {
|
|
598
605
|
this.liveErrors.delete(localThreadId);
|
|
599
606
|
}
|
|
600
607
|
else {
|
|
601
|
-
this.liveErrors.set(localThreadId, res.error ??
|
|
608
|
+
this.liveErrors.set(localThreadId, res.error ?? {
|
|
609
|
+
message: "live session did not become ready",
|
|
610
|
+
code: "native_readiness_failed",
|
|
611
|
+
statusCode: 503,
|
|
612
|
+
});
|
|
602
613
|
}
|
|
603
614
|
return ok;
|
|
604
615
|
};
|
|
@@ -626,7 +637,11 @@ export class RunnerManager {
|
|
|
626
637
|
});
|
|
627
638
|
}
|
|
628
639
|
lastLiveSessionError(localThreadId) {
|
|
629
|
-
return this.liveErrors.get(localThreadId);
|
|
640
|
+
return this.liveErrors.get(localThreadId)?.message;
|
|
641
|
+
}
|
|
642
|
+
lastLiveSessionFailure(localThreadId) {
|
|
643
|
+
const failure = this.liveErrors.get(localThreadId);
|
|
644
|
+
return failure ? { ...failure } : undefined;
|
|
630
645
|
}
|
|
631
646
|
/**
|
|
632
647
|
* Inject a user turn into a session's live codex thread — reference implementation's
|
|
@@ -659,7 +674,15 @@ export class RunnerManager {
|
|
|
659
674
|
this.liveErrors.delete(localThreadId);
|
|
660
675
|
}
|
|
661
676
|
else {
|
|
662
|
-
this.liveErrors.set(localThreadId, res.error ??
|
|
677
|
+
this.liveErrors.set(localThreadId, res.error ?? {
|
|
678
|
+
message: `live injection ${outcome}`,
|
|
679
|
+
code: outcome === "notReady"
|
|
680
|
+
? "native_thread_not_ready"
|
|
681
|
+
: outcome === "notLive"
|
|
682
|
+
? "native_session_not_live"
|
|
683
|
+
: "native_message_injection_failed",
|
|
684
|
+
statusCode: 503,
|
|
685
|
+
});
|
|
663
686
|
}
|
|
664
687
|
return outcome;
|
|
665
688
|
};
|
|
@@ -714,7 +737,7 @@ export class RunnerManager {
|
|
|
714
737
|
handle.live.set(reqId, (reply) => {
|
|
715
738
|
resolve(reply.interactionResult ?? {
|
|
716
739
|
disposition: "invalid",
|
|
717
|
-
message: reply.error ?? "runner did not return an interaction result",
|
|
740
|
+
message: reply.error?.message ?? "runner did not return an interaction result",
|
|
718
741
|
});
|
|
719
742
|
});
|
|
720
743
|
handle.transport.send({
|
|
@@ -1318,7 +1341,10 @@ export class RunnerManager {
|
|
|
1318
1341
|
terminal._fail(message);
|
|
1319
1342
|
}
|
|
1320
1343
|
for (const resolve of handle.live.values()) {
|
|
1321
|
-
resolve({
|
|
1344
|
+
resolve({
|
|
1345
|
+
ok: false,
|
|
1346
|
+
error: { message, code: "runner_crashed", statusCode: 500 },
|
|
1347
|
+
});
|
|
1322
1348
|
}
|
|
1323
1349
|
handle.caps.clear();
|
|
1324
1350
|
handle.terminals.clear();
|
|
@@ -64,9 +64,9 @@ export type ToChild = {
|
|
|
64
64
|
}
|
|
65
65
|
/** Eagerly bring up a session's live codex TUI + forwarder (codex-native), so
|
|
66
66
|
* its turns mirror to the bus regardless of whether the web has attached the
|
|
67
|
-
* terminal.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
67
|
+
* terminal. Fresh sessions connect the discovery listener before TUI launch;
|
|
68
|
+
* known-thread resumes launch the TUI after backend preload and attach their
|
|
69
|
+
* observer in the background. */
|
|
70
70
|
| {
|
|
71
71
|
t: "live.ensure";
|
|
72
72
|
reqId: string;
|
|
@@ -155,15 +155,16 @@ export type FromChild = {
|
|
|
155
155
|
execution: ResolvedExecutionSnapshot;
|
|
156
156
|
parentSessionId?: string;
|
|
157
157
|
}
|
|
158
|
-
/** Result of a `live.ensure`: for Codex-lineage sessions `ok` means
|
|
159
|
-
* and
|
|
160
|
-
*
|
|
158
|
+
/** Result of a `live.ensure`: for Codex-lineage sessions `ok` means backend
|
|
159
|
+
* preload and pane launch completed. A resume observer and fresh-thread
|
|
160
|
+
* discovery may continue in the background. Other Providers retain their
|
|
161
|
+
* own readiness gate. */
|
|
161
162
|
| {
|
|
162
163
|
t: "live.ready";
|
|
163
164
|
reqId: string;
|
|
164
165
|
localThreadId: string;
|
|
165
166
|
ok: boolean;
|
|
166
|
-
error?:
|
|
167
|
+
error?: WireError;
|
|
167
168
|
}
|
|
168
169
|
/** Result of an `inject`: `outcome` distinguishes a new turn, an active-turn
|
|
169
170
|
* steer, and failures so the caller never falls back to a second output path.
|
|
@@ -173,7 +174,7 @@ export type FromChild = {
|
|
|
173
174
|
reqId: string;
|
|
174
175
|
localThreadId: string;
|
|
175
176
|
outcome: InjectOutcome;
|
|
176
|
-
error?:
|
|
177
|
+
error?: WireError;
|
|
177
178
|
}
|
|
178
179
|
/** Result of a `live.interrupt`: `ok` when an active turn was interrupted (false
|
|
179
180
|
* when there was none). Echoes the request `reqId`. */
|
|
@@ -182,7 +183,7 @@ export type FromChild = {
|
|
|
182
183
|
reqId: string;
|
|
183
184
|
localThreadId: string;
|
|
184
185
|
ok: boolean;
|
|
185
|
-
error?:
|
|
186
|
+
error?: WireError;
|
|
186
187
|
} | {
|
|
187
188
|
t: "interaction.resolved";
|
|
188
189
|
reqId: string;
|
|
@@ -2,3 +2,6 @@ import type { AgentRuntimeId } from "@rynx-ai/core";
|
|
|
2
2
|
/** Current live providers are native CLI sessions. Keep this explicit so a
|
|
3
3
|
* future non-native provider does not silently inherit their startup budget. */
|
|
4
4
|
export declare function isManagedNativeProvider(provider: AgentRuntimeId | undefined): provider is AgentRuntimeId;
|
|
5
|
+
/** Codex-lineage providers own app-server, thread-discovery, and injection
|
|
6
|
+
* phase errors. Claude instead retains its parent-owned SessionStart gate. */
|
|
7
|
+
export declare function isCodexLineageProvider(provider: AgentRuntimeId | undefined): provider is Exclude<AgentRuntimeId, "claude">;
|
|
@@ -3,3 +3,8 @@
|
|
|
3
3
|
export function isManagedNativeProvider(provider) {
|
|
4
4
|
return provider === "codex" || provider === "traex" || provider === "claude";
|
|
5
5
|
}
|
|
6
|
+
/** Codex-lineage providers own app-server, thread-discovery, and injection
|
|
7
|
+
* phase errors. Claude instead retains its parent-owned SessionStart gate. */
|
|
8
|
+
export function isCodexLineageProvider(provider) {
|
|
9
|
+
return provider === "codex" || provider === "traex";
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynx-ai/runtime",
|
|
3
|
-
"version": "0.1.11-beta.
|
|
3
|
+
"version": "0.1.11-beta.22",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"node-pty": "1.2.0-beta.15",
|
|
28
28
|
"smol-toml": "1.7.1",
|
|
29
29
|
"ws": "^8.21.0",
|
|
30
|
-
"@rynx-ai/core": "0.1.11-beta.
|
|
30
|
+
"@rynx-ai/core": "0.1.11-beta.22"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/ws": "^8.18.1"
|