@rynx-ai/runtime 0.1.11-beta.2 → 0.1.11-beta.20
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 +12 -6
- package/dist/claude/native-integration.js +137 -28
- package/dist/codex-app-server/forwarder.d.ts +33 -0
- package/dist/codex-app-server/forwarder.js +159 -0
- 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 +7 -4
- package/dist/codex-app-server/ws-channel.js +19 -19
- package/dist/codex-home.js +2 -4
- package/dist/host.d.ts +27 -7
- package/dist/host.js +298 -55
- 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 +72 -4
- package/dist/runner/manager.js +539 -54
- package/dist/runner/protocol.d.ts +6 -4
- 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 +14 -2
- package/dist/terminal/tmux.js +72 -14
- package/package.json +4 -3
package/dist/host.js
CHANGED
|
@@ -15,6 +15,7 @@ import { CodexAppServerClient, buildRuntimeUserInput, } from "./codex-app-server
|
|
|
15
15
|
import { buildAppServerBaseArgs, CodexTransportError, } from "./codex-app-server/transport.js";
|
|
16
16
|
import { WsRpcChannel, ExternalWsChannel } from "./codex-app-server/ws-channel.js";
|
|
17
17
|
import { CodexSessionForwarder } from "./codex-app-server/forwarder.js";
|
|
18
|
+
import { readMcpStartupPlan } from "./codex-app-server/mcp-startup.js";
|
|
18
19
|
import { buildCodexRemoteArgs } from "./terminal/codex-tui.js";
|
|
19
20
|
import { buildClaudeTuiArgs } from "./terminal/claude-tui.js";
|
|
20
21
|
import { providerAdditionalDirs, threadWorkspaceParams, turnWorkspaceParams, } from "./provider-workspace.js";
|
|
@@ -181,6 +182,8 @@ export class SpawnCodexCommandRunner {
|
|
|
181
182
|
* budget (set proactively); claude has no programmatic compaction, so this
|
|
182
183
|
* signal is the lever consumers surface to the user. */
|
|
183
184
|
const CONTEXT_WARN_RATIO = 0.8;
|
|
185
|
+
/** Wait for native bridge/thread binding before injection gives up. */
|
|
186
|
+
const CODEX_BRIDGE_READY_TIMEOUT_MS = 60_000;
|
|
184
187
|
function providerPluginSkillName(namespace, name) {
|
|
185
188
|
const candidate = `${namespace}-${name}`;
|
|
186
189
|
if (candidate.length <= 128)
|
|
@@ -244,6 +247,7 @@ export class LocalAgentHost {
|
|
|
244
247
|
// multi-connection model). Defaults to an ExternalWsChannel client attached to
|
|
245
248
|
// the backend's app-server; tests inject a fake.
|
|
246
249
|
forwarderClientFactory;
|
|
250
|
+
observerReconnectTimeoutMs;
|
|
247
251
|
// Keyed by `codexBackendKey` — the bare runtime id for budget-less agents, or
|
|
248
252
|
// a `${runtime}::${retryHash}` composite for a per-agent retry budget.
|
|
249
253
|
backends = new Map();
|
|
@@ -263,6 +267,10 @@ export class LocalAgentHost {
|
|
|
263
267
|
// Per-session claude-native live forwarders (parallel to liveSessions; claude
|
|
264
268
|
// has no app-server, so it tails the bridge + transcript instead of RPC).
|
|
265
269
|
liveClaudeSessions = new Map();
|
|
270
|
+
/** Exact pre-live startup failure, retained after the partial handle has been
|
|
271
|
+
* cleaned up so Core/plugin callers receive the failed phase, not a generic
|
|
272
|
+
* `live session unavailable` wrapper. */
|
|
273
|
+
liveStartupErrors = new Map();
|
|
266
274
|
/** Claude forwarders stopped before their terminal is killed. Runner shutdown
|
|
267
275
|
* finalizes these synchronously afterwards to scrub raw interaction answers. */
|
|
268
276
|
pendingClaudeFinalizers = new Set();
|
|
@@ -273,17 +281,18 @@ export class LocalAgentHost {
|
|
|
273
281
|
/** Short-lived dedupe for managed fork notifications delivered after the
|
|
274
282
|
* `thread/fork` response. Values are expected source Provider thread ids. */
|
|
275
283
|
managedForkThreadStarts = new Map();
|
|
276
|
-
constructor({ config, commandRunner, sessionStore = new FileCodexSessionStore(resolveCodexSessionStorePath(config)), allowedRoots = resolveAllowedRoots(config), appServerClient, now = () => Date.now(), backendIdleTtlMs = 300_000, forwarderClientFactory, sessionId, runtimeHomeSessionId, }) {
|
|
284
|
+
constructor({ config, commandRunner, sessionStore = new FileCodexSessionStore(resolveCodexSessionStorePath(config)), allowedRoots = resolveAllowedRoots(config), appServerClient, now = () => Date.now(), backendIdleTtlMs = 300_000, forwarderClientFactory, observerReconnectTimeoutMs = 15_000, sessionId, runtimeHomeSessionId, }) {
|
|
277
285
|
this.config = config;
|
|
278
286
|
this.sessionId = sessionId ?? "__default__";
|
|
279
287
|
this.runtimeHomeSessionId = runtimeHomeSessionId ?? this.sessionId;
|
|
280
288
|
this.sessionStore = sessionStore;
|
|
281
289
|
this.allowedRoots = allowedRoots;
|
|
282
|
-
this.defaultRuntime = config.
|
|
290
|
+
this.defaultRuntime = config.DEFAULT_RUNTIME ?? "codex";
|
|
283
291
|
this.injectedCommandRunner = commandRunner;
|
|
284
292
|
this.injectedAppServerClient = appServerClient;
|
|
285
293
|
this.clock = now;
|
|
286
294
|
this.backendIdleTtlMs = backendIdleTtlMs;
|
|
295
|
+
this.observerReconnectTimeoutMs = observerReconnectTimeoutMs;
|
|
287
296
|
this.forwarderClientFactory =
|
|
288
297
|
forwarderClientFactory ??
|
|
289
298
|
((appServerUrl) => new CodexAppServerClient({ channel: new ExternalWsChannel(appServerUrl) }));
|
|
@@ -472,6 +481,9 @@ export class LocalAgentHost {
|
|
|
472
481
|
// to the same effective sandbox.
|
|
473
482
|
const sandbox = live?.sandbox ?? this.sessionSandbox ?? this.config.AGENT_SANDBOX;
|
|
474
483
|
const approvalPolicy = live?.approvalPolicy ?? this.sessionApprovalPolicy ?? this.config.AGENT_APPROVAL_POLICY;
|
|
484
|
+
const traexYolo = runtime === "traex" &&
|
|
485
|
+
sandbox === "danger-full-access" &&
|
|
486
|
+
approvalPolicy === "never";
|
|
475
487
|
const configOverrides = sandbox === "workspace-write"
|
|
476
488
|
? ["sandbox_workspace_write.network_access=true"]
|
|
477
489
|
: [];
|
|
@@ -500,9 +512,11 @@ export class LocalAgentHost {
|
|
|
500
512
|
remoteUrl,
|
|
501
513
|
...(activeThreadId ? { threadId: activeThreadId } : {}),
|
|
502
514
|
configOverrides,
|
|
515
|
+
codexArgs: traexYolo ? ["--dangerously-bypass-hook-trust"] : [],
|
|
503
516
|
additionalDirs: providerAdditionalDirs(live.workspace),
|
|
504
517
|
}),
|
|
505
518
|
cwd: live.workspace.cwd,
|
|
519
|
+
...(runtime === "traex" ? { skipTraexStartupPrompts: true } : {}),
|
|
506
520
|
// Share the app-server's private CODEX_HOME so the TUI inherits the same
|
|
507
521
|
// login/settings and skips the real home's update/NUX prompt. RYNX_SESSION_ID
|
|
508
522
|
// scopes agent-run CLIs (rynx-emulator) to this session at the daemon.
|
|
@@ -558,6 +572,7 @@ export class LocalAgentHost {
|
|
|
558
572
|
}
|
|
559
573
|
}
|
|
560
574
|
async startLiveCodexSession(localThreadId, emit, opts) {
|
|
575
|
+
this.liveStartupErrors.delete(localThreadId);
|
|
561
576
|
// cancel-before-recreate (reference implementation Layer 2, runner/app.py `_cancel_auto_forwarder_task`):
|
|
562
577
|
// never run a new forwarder alongside a stale one for this id. ensureLive's has()
|
|
563
578
|
// fast-path normally makes this a no-op; it closes the stop→re-ensure race that
|
|
@@ -581,6 +596,7 @@ export class LocalAgentHost {
|
|
|
581
596
|
snapshotSkills = await this.prepareExecutionSkills(execution);
|
|
582
597
|
}
|
|
583
598
|
catch (err) {
|
|
599
|
+
this.liveStartupErrors.set(localThreadId, `${runtime === "traex" ? "Traex" : "Codex"} skill preparation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
584
600
|
console.error(`[session-snapshot] session=${localThreadId} skill materialization failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
585
601
|
return false;
|
|
586
602
|
}
|
|
@@ -593,6 +609,7 @@ export class LocalAgentHost {
|
|
|
593
609
|
}
|
|
594
610
|
catch (err) {
|
|
595
611
|
void snapshotSkills.skillsCleanup();
|
|
612
|
+
this.liveStartupErrors.set(localThreadId, `${runtime === "traex" ? "Traex" : "Codex"} skill persistence failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
596
613
|
console.error(`[session-snapshot] session=${localThreadId} skill persistence failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
597
614
|
return false;
|
|
598
615
|
}
|
|
@@ -612,6 +629,7 @@ export class LocalAgentHost {
|
|
|
612
629
|
};
|
|
613
630
|
const injectClient = this.getBackend(runtime, execution.budget ?? undefined).appServerClient;
|
|
614
631
|
if (!injectClient) {
|
|
632
|
+
this.liveStartupErrors.set(localThreadId, `${runtime === "traex" ? "Traex" : "Codex"} app-server is unavailable for this Session`);
|
|
615
633
|
console.error(`[codex-live] session=${localThreadId} runtime=${runtime} no app-server client`);
|
|
616
634
|
return abandon();
|
|
617
635
|
}
|
|
@@ -619,11 +637,13 @@ export class LocalAgentHost {
|
|
|
619
637
|
await injectClient.ensureInitialized();
|
|
620
638
|
}
|
|
621
639
|
catch (err) {
|
|
640
|
+
this.liveStartupErrors.set(localThreadId, `${runtime === "traex" ? "Traex" : "Codex"} app-server initialization failed during its 10s readiness phase: ${err instanceof Error ? err.message : String(err)}`);
|
|
622
641
|
console.error(`[codex-live] session=${localThreadId} runtime=${runtime} app-server init failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
623
642
|
return abandon();
|
|
624
643
|
}
|
|
625
644
|
const appServerUrl = injectClient.terminalRemoteUrl();
|
|
626
645
|
if (!appServerUrl) {
|
|
646
|
+
this.liveStartupErrors.set(localThreadId, `${runtime === "traex" ? "Traex" : "Codex"} app-server started without a Terminal remote endpoint`);
|
|
627
647
|
console.error(`[codex-live] session=${localThreadId} runtime=${runtime} app-server has no remote url`);
|
|
628
648
|
return abandon(); // needs the ws app-server (live-terminal mode)
|
|
629
649
|
}
|
|
@@ -635,15 +655,20 @@ export class LocalAgentHost {
|
|
|
635
655
|
await forwarderClient.ensureInitialized();
|
|
636
656
|
}
|
|
637
657
|
catch (err) {
|
|
658
|
+
this.liveStartupErrors.set(localThreadId, `${runtime === "traex" ? "Traex" : "Codex"} observer could not attach to the ready app-server: ${err instanceof Error ? err.message : String(err)}`);
|
|
638
659
|
console.error(`[codex-live] session=${localThreadId} runtime=${runtime} forwarder init failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
639
660
|
return abandon();
|
|
640
661
|
}
|
|
641
|
-
const model = execution.model;
|
|
662
|
+
const model = execution.model ?? "";
|
|
642
663
|
const reasoningEffort = execution.reasoningEffort ?? undefined;
|
|
643
664
|
let markReady;
|
|
644
665
|
const ready = new Promise((resolve) => {
|
|
645
666
|
markReady = resolve;
|
|
646
667
|
});
|
|
668
|
+
let markStartupFailed;
|
|
669
|
+
const startupFailed = new Promise((resolve) => {
|
|
670
|
+
markStartupFailed = resolve;
|
|
671
|
+
});
|
|
647
672
|
let markTerminalReady;
|
|
648
673
|
const terminalReady = new Promise((resolve) => {
|
|
649
674
|
markTerminalReady = resolve;
|
|
@@ -663,6 +688,8 @@ export class LocalAgentHost {
|
|
|
663
688
|
threadId: record?.codexSessionId ?? null,
|
|
664
689
|
ready,
|
|
665
690
|
markReady,
|
|
691
|
+
startupFailed,
|
|
692
|
+
markStartupFailed,
|
|
666
693
|
terminalReady,
|
|
667
694
|
markTerminalReady,
|
|
668
695
|
injectLock: Promise.resolve(),
|
|
@@ -671,6 +698,7 @@ export class LocalAgentHost {
|
|
|
671
698
|
subscribing: false,
|
|
672
699
|
rotationPending: false,
|
|
673
700
|
stopped: false,
|
|
701
|
+
observerAvailable: true,
|
|
674
702
|
skillsCleanup: snapshotSkills.skillsCleanup,
|
|
675
703
|
interactionOwners: new Map(),
|
|
676
704
|
interactionStandbys: new Map(),
|
|
@@ -704,9 +732,8 @@ export class LocalAgentHost {
|
|
|
704
732
|
// `turn/started` land on ONE response instead of splitting into random
|
|
705
733
|
// ids (the DB `resp_codex_<uuid>` doubling). Mirrors reference implementation `_response_id`.
|
|
706
734
|
responseId,
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
// label, so report the runtime when the exact model is unknown.
|
|
735
|
+
// A null Session model intentionally leaves selection to the Provider
|
|
736
|
+
// CLI. Canonical events still require a printable label.
|
|
710
737
|
model: model || runtime,
|
|
711
738
|
});
|
|
712
739
|
return normalizer;
|
|
@@ -901,10 +928,19 @@ export class LocalAgentHost {
|
|
|
901
928
|
});
|
|
902
929
|
client.setConnectionListener((state) => {
|
|
903
930
|
if (state === "connected") {
|
|
904
|
-
|
|
931
|
+
// Observer transport recovery alone does not prove that an in-flight
|
|
932
|
+
// interaction request was replayed onto this connection. Its request
|
|
933
|
+
// listener clears disconnectedClients when that duplicate arrives.
|
|
934
|
+
if (client !== live.forwarderClient)
|
|
935
|
+
live.disconnectedClients.delete(client);
|
|
905
936
|
return;
|
|
906
937
|
}
|
|
907
938
|
live.disconnectedClients.add(client);
|
|
939
|
+
if (client === live.forwarderClient && !live.stopped) {
|
|
940
|
+
live.observerAvailable = false;
|
|
941
|
+
this.armObserverReconnectDeadline(live);
|
|
942
|
+
void this.reconnectForwarder(live);
|
|
943
|
+
}
|
|
908
944
|
for (const [interactionId, recovery] of [...live.interactionRecoveries]) {
|
|
909
945
|
if (allInteractionClientsUnavailable(interactionId)) {
|
|
910
946
|
finishRecovery(interactionId, recovery.event);
|
|
@@ -942,6 +978,18 @@ export class LocalAgentHost {
|
|
|
942
978
|
for (const se of n.next(event))
|
|
943
979
|
emitCurrent(se);
|
|
944
980
|
},
|
|
981
|
+
onStatus: (note, statusKind) => {
|
|
982
|
+
const responseId = currentResponseId ??
|
|
983
|
+
live.pendingInjectedInputs.find((entry) => entry.responseId)?.responseId;
|
|
984
|
+
emitCurrent({
|
|
985
|
+
type: "session.status",
|
|
986
|
+
sessionId: currentSessionId,
|
|
987
|
+
...(responseId ? { responseId } : {}),
|
|
988
|
+
status: "running",
|
|
989
|
+
...(note ? { note } : {}),
|
|
990
|
+
...(statusKind ? { statusKind } : {}),
|
|
991
|
+
});
|
|
992
|
+
},
|
|
945
993
|
onTurnEnd: (usage) => {
|
|
946
994
|
closeCanonicalInteractions();
|
|
947
995
|
if (!normalizer)
|
|
@@ -995,6 +1043,7 @@ export class LocalAgentHost {
|
|
|
995
1043
|
turnCompletionGraceMs: runtime === "traex" ? 150 : 0,
|
|
996
1044
|
assistantMessageGraceMs: runtime === "traex" ? 150 : 0,
|
|
997
1045
|
surfaceQueueStatus: runtime === "traex",
|
|
1046
|
+
mcpStartup: readMcpStartupPlan(this.runtimeHome(runtime), runtime),
|
|
998
1047
|
});
|
|
999
1048
|
live.forwarder = forwarder;
|
|
1000
1049
|
live.onNativeThreadRotated = (threadId, kind) => {
|
|
@@ -1068,7 +1117,15 @@ export class LocalAgentHost {
|
|
|
1068
1117
|
threadId: record.codexSessionId,
|
|
1069
1118
|
...threadWorkspaceParams(runtime, workspace, sandbox),
|
|
1070
1119
|
approvalPolicy,
|
|
1120
|
+
// The injection connection only needs to bind/subcribe. Codex
|
|
1121
|
+
// 0.146 can ignore initialTurnsPage and return the whole rollout,
|
|
1122
|
+
// so explicitly suppress history here; it is never replayed.
|
|
1071
1123
|
excludeTurns: true,
|
|
1124
|
+
initialTurnsPage: {
|
|
1125
|
+
limit: 1,
|
|
1126
|
+
sortDirection: "desc",
|
|
1127
|
+
itemsView: "summary",
|
|
1128
|
+
},
|
|
1072
1129
|
});
|
|
1073
1130
|
resumedThreadId = resumed.threadId;
|
|
1074
1131
|
break;
|
|
@@ -1085,9 +1142,22 @@ export class LocalAgentHost {
|
|
|
1085
1142
|
if (resumedThreadId) {
|
|
1086
1143
|
this.onLiveThreadStarted(live, localThreadId, resumedThreadId);
|
|
1087
1144
|
}
|
|
1145
|
+
else if (resumeError &&
|
|
1146
|
+
isThreadNotReadyError(resumeError) &&
|
|
1147
|
+
!record.parentSessionId &&
|
|
1148
|
+
!record.runtimeHomeOwnerSessionId) {
|
|
1149
|
+
// A native thread that still has no rollout after the bounded retry
|
|
1150
|
+
// window has never materialized a Turn. It is safe to forget this
|
|
1151
|
+
// ordinary binding and let the TUI create a fresh native thread.
|
|
1152
|
+
// Fork bindings stay strict because their Provider-native ancestry
|
|
1153
|
+
// must not be silently replaced.
|
|
1154
|
+
await this.sessionStore.delete(localThreadId);
|
|
1155
|
+
live.threadId = null;
|
|
1156
|
+
live.markTerminalReady(true);
|
|
1157
|
+
}
|
|
1088
1158
|
else {
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1159
|
+
// A delayed/missing native index or a fork binding remains explicit:
|
|
1160
|
+
// neither is proof that replacing Provider history is safe.
|
|
1091
1161
|
throw resumeError;
|
|
1092
1162
|
}
|
|
1093
1163
|
}
|
|
@@ -1103,9 +1173,11 @@ export class LocalAgentHost {
|
|
|
1103
1173
|
live.stopped = true;
|
|
1104
1174
|
forwarder.stop();
|
|
1105
1175
|
void forwarderClient.stop().catch(() => undefined);
|
|
1176
|
+
this.liveStartupErrors.set(localThreadId, `${runtime === "traex" ? "Traex" : "Codex"} native thread binding failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1106
1177
|
console.error(`[codex-live] session=${localThreadId} runtime=${runtime} thread bind failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1107
1178
|
return abandon();
|
|
1108
1179
|
}
|
|
1180
|
+
this.liveStartupErrors.delete(localThreadId);
|
|
1109
1181
|
return true;
|
|
1110
1182
|
}
|
|
1111
1183
|
/** Bind a session's codex thread id once known (TUI broadcast or store): persist
|
|
@@ -1165,30 +1237,37 @@ export class LocalAgentHost {
|
|
|
1165
1237
|
* Subscribe the forwarder connection to a thread (reference implementation's
|
|
1166
1238
|
* `_subscribe_until_ready`). A fresh TUI thread has no rollout until its first
|
|
1167
1239
|
* turn, so `thread/resume` is retried: park until the forwarder observes the
|
|
1168
|
-
* thread active, then retry
|
|
1169
|
-
*
|
|
1170
|
-
* succeeds, subsequent turns arrive
|
|
1240
|
+
* thread active, then retry. Resume only fetches the newest summarized Turn:
|
|
1241
|
+
* recovery reconciles the exact active Turn's terminal status and never replays
|
|
1242
|
+
* historical items. Once resume succeeds, subsequent turns arrive live.
|
|
1171
1243
|
*/
|
|
1172
1244
|
async subscribeUntilReady(live, threadId) {
|
|
1173
|
-
let sawNotReady = false;
|
|
1174
1245
|
while (!live.stopped) {
|
|
1175
1246
|
try {
|
|
1176
1247
|
const resp = await live.forwarderClient.threadResume({
|
|
1177
1248
|
threadId,
|
|
1178
1249
|
...threadWorkspaceParams(live.runtime, live.workspace, live.sandbox),
|
|
1179
1250
|
approvalPolicy: live.approvalPolicy,
|
|
1180
|
-
|
|
1251
|
+
initialTurnsPage: {
|
|
1252
|
+
limit: 1,
|
|
1253
|
+
sortDirection: "desc",
|
|
1254
|
+
itemsView: "summary",
|
|
1255
|
+
},
|
|
1181
1256
|
});
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1257
|
+
// Codex 0.146.1 accepts initialTurnsPage but can still return the entire
|
|
1258
|
+
// rollout in chronological order. Never infer newest from array position:
|
|
1259
|
+
// recovery may settle only the exact active turn id already owned here.
|
|
1260
|
+
const activeTurnId = live.forwarder.currentTurnId();
|
|
1261
|
+
const activeTurn = activeTurnId && Array.isArray(resp.thread.turns)
|
|
1262
|
+
? resp.thread.turns.find((turn) => (turn.id ?? turn.turnId) === activeTurnId)
|
|
1263
|
+
: undefined;
|
|
1264
|
+
live.forwarder.reconcileActiveTurn(activeTurn);
|
|
1185
1265
|
return true; // subscribed — live item/turn notifications now flow to the forwarder
|
|
1186
1266
|
}
|
|
1187
1267
|
catch (error) {
|
|
1188
1268
|
if (!isThreadNotReadyError(error)) {
|
|
1189
1269
|
return false; // other failure — injection still works via the backend client
|
|
1190
1270
|
}
|
|
1191
|
-
sawNotReady = true;
|
|
1192
1271
|
// Park until the thread goes active (its first turn materializes the
|
|
1193
1272
|
// rollout); a short poll covers the flush race after "active".
|
|
1194
1273
|
await new Promise((resolve) => {
|
|
@@ -1200,7 +1279,56 @@ export class LocalAgentHost {
|
|
|
1200
1279
|
}
|
|
1201
1280
|
return false;
|
|
1202
1281
|
}
|
|
1203
|
-
/**
|
|
1282
|
+
/** Restore the independent observer after an unexpected exit. The active turn
|
|
1283
|
+
* remains open during the bounded grace so resume can reconcile its exact id. */
|
|
1284
|
+
reconnectForwarder(live) {
|
|
1285
|
+
if (live.observerReconnect)
|
|
1286
|
+
return live.observerReconnect;
|
|
1287
|
+
const reconnect = (async () => {
|
|
1288
|
+
while (!live.stopped && live.threadId) {
|
|
1289
|
+
try {
|
|
1290
|
+
await live.forwarderClient.ensureInitialized();
|
|
1291
|
+
if (await this.subscribeUntilReady(live, live.threadId)) {
|
|
1292
|
+
live.observerAvailable = true;
|
|
1293
|
+
this.clearObserverReconnectDeadline(live);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
catch {
|
|
1298
|
+
// Retry below. Injection uses its independent client and remains usable.
|
|
1299
|
+
}
|
|
1300
|
+
await new Promise((resolve) => {
|
|
1301
|
+
const timer = setTimeout(resolve, 250);
|
|
1302
|
+
timer.unref?.();
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
})();
|
|
1306
|
+
const settled = reconnect.finally(() => {
|
|
1307
|
+
if (live.observerReconnect === settled)
|
|
1308
|
+
live.observerReconnect = undefined;
|
|
1309
|
+
});
|
|
1310
|
+
live.observerReconnect = settled;
|
|
1311
|
+
return settled;
|
|
1312
|
+
}
|
|
1313
|
+
armObserverReconnectDeadline(live) {
|
|
1314
|
+
if (live.observerReconnectTimer || !live.forwarder.isTurnOpen())
|
|
1315
|
+
return;
|
|
1316
|
+
live.observerReconnectTimer = setTimeout(() => {
|
|
1317
|
+
live.observerReconnectTimer = undefined;
|
|
1318
|
+
if (live.stopped)
|
|
1319
|
+
return;
|
|
1320
|
+
const provider = live.runtime === "traex" ? "Traex" : "Codex";
|
|
1321
|
+
live.forwarder.failOpenTurn(new Error(`${provider} observer did not reconnect within ${this.observerReconnectTimeoutMs}ms`));
|
|
1322
|
+
}, this.observerReconnectTimeoutMs);
|
|
1323
|
+
live.observerReconnectTimer.unref?.();
|
|
1324
|
+
}
|
|
1325
|
+
clearObserverReconnectDeadline(live) {
|
|
1326
|
+
if (live.observerReconnectTimer)
|
|
1327
|
+
clearTimeout(live.observerReconnectTimer);
|
|
1328
|
+
live.observerReconnectTimer = undefined;
|
|
1329
|
+
}
|
|
1330
|
+
/** Await a live session's thread binding. `null` leaves the deadline to the
|
|
1331
|
+
* caller; a number keeps the Provider-local bound. Returns false on timeout /
|
|
1204
1332
|
* no live session. Injection and the runner's `live.ready` gate on this. */
|
|
1205
1333
|
async waitLiveReady(localThreadId, timeoutMs = 20_000) {
|
|
1206
1334
|
const claude = this.liveClaudeSessions.get(localThreadId);
|
|
@@ -1209,11 +1337,15 @@ export class LocalAgentHost {
|
|
|
1209
1337
|
const live = this.liveSessions.get(localThreadId);
|
|
1210
1338
|
if (!live)
|
|
1211
1339
|
return false;
|
|
1340
|
+
const ready = live.ready.then(() => true);
|
|
1341
|
+
const failed = live.startupFailed.then(() => false);
|
|
1342
|
+
if (timeoutMs === null)
|
|
1343
|
+
return Promise.race([ready, failed]);
|
|
1212
1344
|
let timer;
|
|
1213
1345
|
const timeout = new Promise((resolve) => {
|
|
1214
1346
|
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
1215
1347
|
});
|
|
1216
|
-
const ok = await Promise.race([
|
|
1348
|
+
const ok = await Promise.race([ready, failed, timeout]);
|
|
1217
1349
|
if (timer)
|
|
1218
1350
|
clearTimeout(timer);
|
|
1219
1351
|
return ok;
|
|
@@ -1230,6 +1362,8 @@ export class LocalAgentHost {
|
|
|
1230
1362
|
const live = this.liveSessions.get(localThreadId);
|
|
1231
1363
|
if (!live)
|
|
1232
1364
|
return false;
|
|
1365
|
+
if (timeoutMs === null)
|
|
1366
|
+
return live.terminalReady;
|
|
1233
1367
|
let timer;
|
|
1234
1368
|
const timeout = new Promise((resolve) => {
|
|
1235
1369
|
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
@@ -1241,7 +1375,21 @@ export class LocalAgentHost {
|
|
|
1241
1375
|
}
|
|
1242
1376
|
/** Diagnostic from the provider adapter when native discovery/resume failed. */
|
|
1243
1377
|
liveSessionError(localThreadId) {
|
|
1244
|
-
return this.liveClaudeSessions.get(localThreadId)?.error
|
|
1378
|
+
return this.liveClaudeSessions.get(localThreadId)?.error
|
|
1379
|
+
?? this.liveSessions.get(localThreadId)?.startupError
|
|
1380
|
+
?? this.liveStartupErrors.get(localThreadId);
|
|
1381
|
+
}
|
|
1382
|
+
/** Publish the background TUI/thread discovery failure so an executor
|
|
1383
|
+
* already waiting in the 60s bridge window exits immediately with the exact
|
|
1384
|
+
* 30s discovery cause. */
|
|
1385
|
+
failLiveStartup(localThreadId, error) {
|
|
1386
|
+
const live = this.liveSessions.get(localThreadId);
|
|
1387
|
+
if (!live || live.threadId || live.stopped)
|
|
1388
|
+
return false;
|
|
1389
|
+
live.startupError = error.message;
|
|
1390
|
+
this.liveStartupErrors.set(localThreadId, error.message);
|
|
1391
|
+
live.markStartupFailed();
|
|
1392
|
+
return true;
|
|
1245
1393
|
}
|
|
1246
1394
|
/**
|
|
1247
1395
|
* Inject a user turn into a session's live codex thread — reference implementation's
|
|
@@ -1302,7 +1450,7 @@ export class LocalAgentHost {
|
|
|
1302
1450
|
return "failed";
|
|
1303
1451
|
// Park until the thread binds (~60s, reference implementation codex_native_executor:177-186),
|
|
1304
1452
|
// not a 20s race that returns false and lets the caller re-run on a 2nd path.
|
|
1305
|
-
const bound = await this.waitLiveReady(localThreadId,
|
|
1453
|
+
const bound = await this.waitLiveReady(localThreadId, CODEX_BRIDGE_READY_TIMEOUT_MS);
|
|
1306
1454
|
const threadId = live.threadId ?? live.forwarder.threadId();
|
|
1307
1455
|
if (!bound || !threadId)
|
|
1308
1456
|
return "notReady";
|
|
@@ -1316,6 +1464,7 @@ export class LocalAgentHost {
|
|
|
1316
1464
|
...(runtimeInput.responseId ? { responseId: runtimeInput.responseId } : {}),
|
|
1317
1465
|
};
|
|
1318
1466
|
live.pendingInjectedInputs.push(pendingInput);
|
|
1467
|
+
let injectionMethod = "turn/start";
|
|
1319
1468
|
const forgetPendingInput = () => {
|
|
1320
1469
|
const index = live.pendingInjectedInputs.indexOf(pendingInput);
|
|
1321
1470
|
if (index >= 0)
|
|
@@ -1326,11 +1475,31 @@ export class LocalAgentHost {
|
|
|
1326
1475
|
if (live.forwarder.isTurnOpen()) {
|
|
1327
1476
|
const turnId = live.forwarder.currentTurnId();
|
|
1328
1477
|
if (turnId) {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1478
|
+
injectionMethod = "turn/steer";
|
|
1479
|
+
let steered;
|
|
1480
|
+
let retryIndex = 0;
|
|
1481
|
+
while (!steered) {
|
|
1482
|
+
try {
|
|
1483
|
+
steered = await live.injectClient.turnSteer({
|
|
1484
|
+
threadId,
|
|
1485
|
+
expectedTurnId: turnId,
|
|
1486
|
+
input: nativeInput,
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
catch (error) {
|
|
1490
|
+
const delayMs = TURN_STEER_ACTIVATION_RETRY_DELAYS_MS[retryIndex];
|
|
1491
|
+
if (delayMs === undefined ||
|
|
1492
|
+
!isTransientTurnSteerActivationRace(error) ||
|
|
1493
|
+
live.stopped ||
|
|
1494
|
+
live.rotationPending ||
|
|
1495
|
+
!live.forwarder.isTurnOpen() ||
|
|
1496
|
+
live.forwarder.currentTurnId() !== turnId) {
|
|
1497
|
+
throw error;
|
|
1498
|
+
}
|
|
1499
|
+
retryIndex += 1;
|
|
1500
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1334
1503
|
if (pendingInput.state === "prepublished") {
|
|
1335
1504
|
// The caller already persisted and published this user input
|
|
1336
1505
|
// before waiting for the native Terminal to become ready.
|
|
@@ -1342,6 +1511,10 @@ export class LocalAgentHost {
|
|
|
1342
1511
|
live.publishInjectedInput(steered.turnId, pendingInput.content);
|
|
1343
1512
|
pendingInput.state = "optimistic";
|
|
1344
1513
|
}
|
|
1514
|
+
live.forwarder.noteTurnAccepted(steered.turnId);
|
|
1515
|
+
if (!live.observerAvailable) {
|
|
1516
|
+
this.armObserverReconnectDeadline(live);
|
|
1517
|
+
}
|
|
1345
1518
|
return "injected";
|
|
1346
1519
|
}
|
|
1347
1520
|
}
|
|
@@ -1366,13 +1539,22 @@ export class LocalAgentHost {
|
|
|
1366
1539
|
live.publishInjectedInput(started.turnId, pendingInput.content);
|
|
1367
1540
|
pendingInput.state = "optimistic";
|
|
1368
1541
|
}
|
|
1542
|
+
// The injection RPC and active-turn write are one serialized operation.
|
|
1543
|
+
// Do not wait for the independent observer connection's `turn/started`:
|
|
1544
|
+
// a second message accepted in that window must steer, not double-start.
|
|
1545
|
+
live.forwarder.noteTurnAccepted(started.turnId);
|
|
1546
|
+
if (!live.observerAvailable) {
|
|
1547
|
+
this.armObserverReconnectDeadline(live);
|
|
1548
|
+
}
|
|
1369
1549
|
return "injected";
|
|
1370
1550
|
}
|
|
1371
1551
|
catch (error) {
|
|
1372
1552
|
forgetPendingInput();
|
|
1373
1553
|
// Preserve the app-server's error instead of collapsing every failure
|
|
1374
1554
|
// into the unactionable `live injection failed` string.
|
|
1375
|
-
const
|
|
1555
|
+
const baseDetail = codexRpcError(error, injectionMethod);
|
|
1556
|
+
const startupDetail = live.forwarder.mcpStartupDetail();
|
|
1557
|
+
const detail = startupDetail ? `${baseDetail} (${startupDetail})` : baseDetail;
|
|
1376
1558
|
console.error(`[codex-live] session=${localThreadId} runtime=${live.runtime} injection failed: ${detail}`);
|
|
1377
1559
|
throw new Error(detail, { cause: error });
|
|
1378
1560
|
}
|
|
@@ -1417,21 +1599,35 @@ export class LocalAgentHost {
|
|
|
1417
1599
|
const live = this.liveSessions.get(localThreadId);
|
|
1418
1600
|
if (!live)
|
|
1419
1601
|
return false;
|
|
1420
|
-
// Only interrupt an OPEN turn — codex's app-server rejects a stale turnId, and
|
|
1421
|
-
// reference implementation no-ops (204) when no turn is active.
|
|
1422
|
-
if (!live.forwarder.isTurnOpen())
|
|
1423
|
-
return false;
|
|
1424
1602
|
const threadId = live.threadId ?? live.forwarder.threadId();
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
return
|
|
1603
|
+
const pendingMcp = live.forwarder.cancelMcpStartup();
|
|
1604
|
+
const turnId = live.forwarder.isTurnOpen()
|
|
1605
|
+
? live.forwarder.currentTurnId()
|
|
1606
|
+
: null;
|
|
1607
|
+
if (!threadId)
|
|
1608
|
+
return pendingMcp.length > 0;
|
|
1609
|
+
let handled = pendingMcp.length > 0;
|
|
1610
|
+
if (pendingMcp.length > 0) {
|
|
1611
|
+
// Codex's TUI cancels a provider-owned MCP startup round with an empty
|
|
1612
|
+
// turn id. Best-effort for Traex: it shares the app-server surface, while
|
|
1613
|
+
// the active-turn interrupt below remains authoritative if it rejects this.
|
|
1614
|
+
try {
|
|
1615
|
+
await live.injectClient.turnInterrupt({ threadId, turnId: "" });
|
|
1616
|
+
}
|
|
1617
|
+
catch (error) {
|
|
1618
|
+
console.warn(`[codex-live] session=${localThreadId} runtime=${live.runtime} MCP startup interrupt failed: ${codexRpcError(error, "turn/interrupt")}`);
|
|
1619
|
+
}
|
|
1431
1620
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1621
|
+
if (turnId) {
|
|
1622
|
+
try {
|
|
1623
|
+
await live.injectClient.turnInterrupt({ threadId, turnId });
|
|
1624
|
+
handled = true;
|
|
1625
|
+
}
|
|
1626
|
+
catch {
|
|
1627
|
+
// The startup cancellation above is still a handled Stop operation.
|
|
1628
|
+
}
|
|
1434
1629
|
}
|
|
1630
|
+
return handled;
|
|
1435
1631
|
}
|
|
1436
1632
|
/** Stop + drop a session's live forwarder and its dedicated connection (session
|
|
1437
1633
|
* close / runner shutdown). The backend inject client is shared — left running. */
|
|
@@ -1457,6 +1653,11 @@ export class LocalAgentHost {
|
|
|
1457
1653
|
return;
|
|
1458
1654
|
this.liveSessions.delete(localThreadId);
|
|
1459
1655
|
live.stopped = true;
|
|
1656
|
+
if (!live.threadId) {
|
|
1657
|
+
live.startupError ??= "native Session stopped before thread discovery completed";
|
|
1658
|
+
live.markStartupFailed();
|
|
1659
|
+
}
|
|
1660
|
+
this.clearObserverReconnectDeadline(live);
|
|
1460
1661
|
live.releaseActive?.();
|
|
1461
1662
|
live.injectClient.cancelInteractions("session_stopped");
|
|
1462
1663
|
live.forwarderClient.cancelInteractions("session_stopped");
|
|
@@ -1522,7 +1723,7 @@ export class LocalAgentHost {
|
|
|
1522
1723
|
});
|
|
1523
1724
|
const settings = { ...effectiveSettings, ...hookSettings };
|
|
1524
1725
|
const settingsPath = writeManagedClaudeSettings(claudeBridgeDir(localThreadId), settings);
|
|
1525
|
-
// The live session carries the
|
|
1726
|
+
// The live session carries the daemon-owned execution state used by Chat.
|
|
1526
1727
|
const args = buildClaudeTuiArgs({
|
|
1527
1728
|
settingsJson: settingsPath,
|
|
1528
1729
|
// A rynx-managed Session must have one interaction owner. Loading host or
|
|
@@ -1530,7 +1731,10 @@ export class LocalAgentHost {
|
|
|
1530
1731
|
// resolve the same native request while rynx still exposes it as pending.
|
|
1531
1732
|
// Explicit rynx hooks and selected-skill plugin dirs remain enabled.
|
|
1532
1733
|
settingSources: "",
|
|
1533
|
-
model: live.execution.model,
|
|
1734
|
+
...(live.execution.model ? { model: live.execution.model } : {}),
|
|
1735
|
+
...(live.execution.reasoningEffort
|
|
1736
|
+
? { reasoningEffort: live.execution.reasoningEffort }
|
|
1737
|
+
: {}),
|
|
1534
1738
|
...(live.execution.instructions
|
|
1535
1739
|
? { appendSystemPrompt: live.execution.instructions }
|
|
1536
1740
|
: {}),
|
|
@@ -1633,8 +1837,16 @@ export class LocalAgentHost {
|
|
|
1633
1837
|
throw failedPluginSkill.reason;
|
|
1634
1838
|
}
|
|
1635
1839
|
const pluginSkills = materializedPluginSkills.flatMap((result) => result.status === "fulfilled" ? result.value : []);
|
|
1840
|
+
const selectedSkills = [...skillEnv.resolved, ...pluginSkills];
|
|
1841
|
+
const selectedNames = new Set();
|
|
1842
|
+
for (const skill of selectedSkills) {
|
|
1843
|
+
if (selectedNames.has(skill.name)) {
|
|
1844
|
+
throw new Error(`duplicate final Session skill name: ${skill.name}`);
|
|
1845
|
+
}
|
|
1846
|
+
selectedNames.add(skill.name);
|
|
1847
|
+
}
|
|
1636
1848
|
return {
|
|
1637
|
-
selectedSkills
|
|
1849
|
+
selectedSkills,
|
|
1638
1850
|
skillsCleanup: () => rm(sessionSkillsDir, { recursive: true, force: true }),
|
|
1639
1851
|
};
|
|
1640
1852
|
}
|
|
@@ -1664,7 +1876,7 @@ export class LocalAgentHost {
|
|
|
1664
1876
|
console.error(`[session-snapshot] session=${localThreadId} skill materialization failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1665
1877
|
return false;
|
|
1666
1878
|
}
|
|
1667
|
-
const model = execution.model;
|
|
1879
|
+
const model = execution.model ?? "";
|
|
1668
1880
|
const skillPlugin = await materializeSkillPlugin(snapshotSkills.selectedSkills);
|
|
1669
1881
|
void snapshotSkills.skillsCleanup();
|
|
1670
1882
|
const permissionMode = execution.permissionMode;
|
|
@@ -1695,7 +1907,7 @@ export class LocalAgentHost {
|
|
|
1695
1907
|
normalizer = new SessionNormalizer({
|
|
1696
1908
|
sessionId: currentSessionId,
|
|
1697
1909
|
responseId,
|
|
1698
|
-
model,
|
|
1910
|
+
model: model || "claude",
|
|
1699
1911
|
});
|
|
1700
1912
|
return normalizer;
|
|
1701
1913
|
};
|
|
@@ -1956,7 +2168,8 @@ export class LocalAgentHost {
|
|
|
1956
2168
|
});
|
|
1957
2169
|
return ok ? "injected" : "failed";
|
|
1958
2170
|
}
|
|
1959
|
-
catch {
|
|
2171
|
+
catch (error) {
|
|
2172
|
+
live.error = error instanceof Error ? error.message : String(error);
|
|
1960
2173
|
return "failed";
|
|
1961
2174
|
}
|
|
1962
2175
|
finally {
|
|
@@ -1986,6 +2199,19 @@ export class LocalAgentHost {
|
|
|
1986
2199
|
if (live)
|
|
1987
2200
|
live.injector = injector;
|
|
1988
2201
|
}
|
|
2202
|
+
/** Close an active native response when its terminal or runner disappears. */
|
|
2203
|
+
failLiveSession(localThreadId, error) {
|
|
2204
|
+
const claude = this.liveClaudeSessions.get(localThreadId);
|
|
2205
|
+
if (claude)
|
|
2206
|
+
return claude.forwarder.failOpenTurn(error);
|
|
2207
|
+
const codex = this.liveSessions.get(localThreadId);
|
|
2208
|
+
if (!codex)
|
|
2209
|
+
return false;
|
|
2210
|
+
const failed = codex.forwarder.failOpenTurn(error);
|
|
2211
|
+
if (failed)
|
|
2212
|
+
this.clearObserverReconnectDeadline(codex);
|
|
2213
|
+
return failed;
|
|
2214
|
+
}
|
|
1989
2215
|
/** List the models a runtime exposes (App Server for codex/traex; static for claude). */
|
|
1990
2216
|
async listModels(runtime) {
|
|
1991
2217
|
const resolved = runtime ?? this.defaultRuntime;
|
|
@@ -2113,7 +2339,7 @@ export class LocalAgentHost {
|
|
|
2113
2339
|
threadId: record.codexSessionId,
|
|
2114
2340
|
excludeTurns: true,
|
|
2115
2341
|
...threadWorkspaceParams(runtime, workspace, execution.sandbox ?? "workspace-write"),
|
|
2116
|
-
model: execution.model,
|
|
2342
|
+
...(execution.model ? { model: execution.model } : {}),
|
|
2117
2343
|
approvalPolicy: execution.approvalPolicy,
|
|
2118
2344
|
});
|
|
2119
2345
|
}
|
|
@@ -2155,12 +2381,26 @@ export class LocalAgentHost {
|
|
|
2155
2381
|
}
|
|
2156
2382
|
}
|
|
2157
2383
|
}
|
|
2158
|
-
function
|
|
2384
|
+
function codexRpcError(error, method) {
|
|
2159
2385
|
if (error instanceof CodexTransportError) {
|
|
2160
|
-
return `Codex app-server rejected
|
|
2386
|
+
return `Codex app-server rejected ${method}: ${error.message} (code ${error.code})`;
|
|
2161
2387
|
}
|
|
2162
|
-
return `Codex
|
|
2388
|
+
return `Codex ${method} failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Codex 0.146 can acknowledge `turn/start` a few milliseconds before its
|
|
2392
|
+
* internal active-turn pointer becomes visible to `turn/steer`. The injection
|
|
2393
|
+
* lock still prevents double-starts, but the immediately following steer must
|
|
2394
|
+
* briefly retry the same expected turn id. Keep this narrow: all other RPC
|
|
2395
|
+
* errors remain terminal and visible to the caller.
|
|
2396
|
+
*/
|
|
2397
|
+
function isTransientTurnSteerActivationRace(error) {
|
|
2398
|
+
return error instanceof CodexTransportError &&
|
|
2399
|
+
error.code === -32600 &&
|
|
2400
|
+
(/no active turn to steer/i.test(error.message) ||
|
|
2401
|
+
/expected active turn id\b.+\bfound\b/i.test(error.message));
|
|
2163
2402
|
}
|
|
2403
|
+
const TURN_STEER_ACTIVATION_RETRY_DELAYS_MS = [10, 20, 40, 80, 160, 250, 440];
|
|
2164
2404
|
export function parseCodexLoginStatus(exitCode, output) {
|
|
2165
2405
|
const normalized = output.trim();
|
|
2166
2406
|
const match = normalized.match(/Logged in using (.+)$/im);
|
|
@@ -2184,15 +2424,18 @@ export function parseCodexLoginStatus(exitCode, output) {
|
|
|
2184
2424
|
issues: normalized ? [normalized] : [],
|
|
2185
2425
|
};
|
|
2186
2426
|
}
|
|
2187
|
-
/** Race a live session's `ready` promise against failure
|
|
2427
|
+
/** Race a live session's `ready` promise against failure and, when supplied,
|
|
2428
|
+
* a timeout. */
|
|
2188
2429
|
function raceReady(ready, timeoutMs, failed) {
|
|
2189
2430
|
let timer;
|
|
2190
|
-
const
|
|
2191
|
-
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
2192
|
-
});
|
|
2193
|
-
const outcomes = [ready.then(() => true), timeout];
|
|
2431
|
+
const outcomes = [ready.then(() => true)];
|
|
2194
2432
|
if (failed)
|
|
2195
2433
|
outcomes.push(failed.then(() => false));
|
|
2434
|
+
if (timeoutMs !== null) {
|
|
2435
|
+
outcomes.push(new Promise((resolve) => {
|
|
2436
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
2437
|
+
}));
|
|
2438
|
+
}
|
|
2196
2439
|
return Promise.race(outcomes).finally(() => {
|
|
2197
2440
|
if (timer)
|
|
2198
2441
|
clearTimeout(timer);
|