@rynx-ai/runtime 0.1.11-beta.42 → 0.1.11-beta.43

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/host.d.ts CHANGED
@@ -275,8 +275,8 @@ export declare class LocalAgentHost implements CodexCapabilities {
275
275
  /** Structured phase + concrete cause used by the runner wire protocol. */
276
276
  liveSessionFailure(localThreadId: string): LiveSessionFailure | undefined;
277
277
  /** Publish the background TUI/thread discovery failure so an executor
278
- * already waiting in the 60s bridge window exits immediately with the exact
279
- * 30s discovery cause. */
278
+ * already waiting in its runtime-specific bridge window exits immediately
279
+ * with the exact discovery cause. */
280
280
  failLiveStartup(localThreadId: string, error: Error): boolean;
281
281
  /**
282
282
  * Inject a user turn into a session's live codex thread — reference implementation's
@@ -288,8 +288,9 @@ export declare class LocalAgentHost implements CodexCapabilities {
288
288
  * Returns an {@link InjectResult}: `notLive` when this session has no live
289
289
  * forwarder (caller may use the run path); `notReady`/`failed` are hard errors
290
290
  * the caller reports WITHOUT re-running (re-running double-writes alongside the
291
- * forwarder). Park-until-ready (~60s), aligning reference implementation's executor waiting for
292
- * the bridge instead of a short race that falls back to a second output path.
291
+ * forwarder). Park until the runtime's discovery window expires, aligning
292
+ * reference implementation's executor waiting for the bridge instead of a
293
+ * short race that falls back to a second output path.
293
294
  */
294
295
  injectMessage(localThreadId: string, input: RuntimeUserInput | string, options?: RuntimeTurnOptions): Promise<InjectResult>;
295
296
  /**
package/dist/host.js CHANGED
@@ -208,6 +208,9 @@ export class SpawnCodexCommandRunner {
208
208
  const CONTEXT_WARN_RATIO = 0.8;
209
209
  /** Wait for native bridge/thread binding before injection gives up. */
210
210
  const CODEX_BRIDGE_READY_TIMEOUT_MS = 60_000;
211
+ /** Match RunnerSession's hard cap when Traex is visibly updating. Its ordinary
212
+ * 30-second startup failure still resolves readiness before this deadline. */
213
+ const TRAEX_BRIDGE_READY_TIMEOUT_MS = 2 * 60_000;
211
214
  function providerPluginSkillName(namespace, name) {
212
215
  const candidate = `${namespace}-${name}`;
213
216
  if (candidate.length <= 128)
@@ -1769,8 +1772,8 @@ export class LocalAgentHost {
1769
1772
  : undefined;
1770
1773
  }
1771
1774
  /** Publish the background TUI/thread discovery failure so an executor
1772
- * already waiting in the 60s bridge window exits immediately with the exact
1773
- * 30s discovery cause. */
1775
+ * already waiting in its runtime-specific bridge window exits immediately
1776
+ * with the exact discovery cause. */
1774
1777
  failLiveStartup(localThreadId, error) {
1775
1778
  const live = this.liveSessions.get(localThreadId);
1776
1779
  if (!live || live.threadId || live.stopped)
@@ -1793,8 +1796,9 @@ export class LocalAgentHost {
1793
1796
  * Returns an {@link InjectResult}: `notLive` when this session has no live
1794
1797
  * forwarder (caller may use the run path); `notReady`/`failed` are hard errors
1795
1798
  * the caller reports WITHOUT re-running (re-running double-writes alongside the
1796
- * forwarder). Park-until-ready (~60s), aligning reference implementation's executor waiting for
1797
- * the bridge instead of a short race that falls back to a second output path.
1799
+ * forwarder). Park until the runtime's discovery window expires, aligning
1800
+ * reference implementation's executor waiting for the bridge instead of a
1801
+ * short race that falls back to a second output path.
1798
1802
  */
1799
1803
  async injectMessage(localThreadId, input, options) {
1800
1804
  const runtimeInput = typeof input === "string"
@@ -1853,9 +1857,13 @@ export class LocalAgentHost {
1853
1857
  const run = live.injectLock.then(async () => {
1854
1858
  if (live.rotationPending || live.stopped)
1855
1859
  return { outcome: "failed" };
1856
- // Park until the thread binds (~60s, reference implementation codex_native_executor:177-186),
1857
- // not a 20s race that returns false and lets the caller re-run on a 2nd path.
1858
- const bound = await this.waitLiveReady(localThreadId, CODEX_BRIDGE_READY_TIMEOUT_MS);
1860
+ // Park until the thread binds, not a short race that lets the caller
1861
+ // re-run on a second path. Traex gets the update-aware Runner deadline;
1862
+ // its ordinary 30s startup failure still wakes this wait immediately.
1863
+ const readyTimeoutMs = live.runtime === "traex"
1864
+ ? TRAEX_BRIDGE_READY_TIMEOUT_MS
1865
+ : CODEX_BRIDGE_READY_TIMEOUT_MS;
1866
+ const bound = await this.waitLiveReady(localThreadId, readyTimeoutMs);
1859
1867
  const threadId = live.threadId ?? live.forwarder.threadId();
1860
1868
  if (!bound || !threadId)
1861
1869
  return { outcome: "notReady" };
@@ -97,6 +97,8 @@ export declare class RunnerSession {
97
97
  private readonly attachmentRoles;
98
98
  private readonly traexStartupWatchers;
99
99
  private readonly traexStartupGates;
100
+ private readonly traexUpdateWatchers;
101
+ private readonly traexStartupUpdateActivity;
100
102
  private readonly codexStartupMonitors;
101
103
  private readonly terminalWatchers;
102
104
  /** Opens are async; a close received before attach resolves tombstones the id. */
@@ -153,6 +155,7 @@ export declare class RunnerSession {
153
155
  */
154
156
  private ensureLive;
155
157
  private monitorCodexThreadStartup;
158
+ private waitForTraexThreadStartup;
156
159
  /** Discovery failure is terminal for this partial native launch. Publish the
157
160
  * exact cause first so an in-flight injection wakes, then drop the observer
158
161
  * and TUI so a later task retry creates a clean launch instead of reusing an
@@ -168,6 +171,11 @@ export declare class RunnerSession {
168
171
  private launchCodexPane;
169
172
  private watchNativeTerminal;
170
173
  private cancelTraexStartupWatcher;
174
+ private cancelTraexUpdateWatcher;
175
+ /** Observe update-only startup screens independently from prompt handling.
176
+ * The low-frequency capture spans the full normal discovery window and only
177
+ * remains alive longer after a positively identified update. */
178
+ private watchTraexStartupUpdates;
171
179
  private skipTraexStartupPrompts;
172
180
  private stopLive;
173
181
  /** Stop event forwarding, kill native terminals/hooks, then synchronously
@@ -18,6 +18,10 @@ import { RunnerTransportWriteError, } from "./transport.js";
18
18
  const TERMINAL_PREPARATION_TTL_MS = 30_000;
19
19
  /** Maximum time for a fresh Codex-lineage TUI to publish its native thread. */
20
20
  const CODEX_THREAD_START_TIMEOUT_MS = 30_000;
21
+ /** Traex may spend most of the normal discovery window installing an update.
22
+ * Keep one fresh discovery window after visible update activity, but never let
23
+ * a stuck updater hold the partial native launch indefinitely. */
24
+ const TRAEX_UPDATE_THREAD_START_MAX_MS = 2 * 60_000;
21
25
  function providerDisplayName(runtime) {
22
26
  return runtime === "traex" ? "Traex" : runtime === "claude" ? "Claude" : "Codex";
23
27
  }
@@ -51,6 +55,7 @@ function terminalFailureDetail(pane) {
51
55
  const TRAEX_STARTUP_WATCH_MS = 20_000;
52
56
  const TRAEX_STARTUP_DISCOVERY_MS = 5_000;
53
57
  const TRAEX_STARTUP_POLL_MS = 100;
58
+ const TRAEX_UPDATE_POLL_MS = 1_000;
54
59
  const TRAEX_AUTHORIZATION_POLL_MS = 500;
55
60
  const TRAEX_AUTHORIZATION_WAIT_MS = 15 * 60_000;
56
61
  const TRAEX_ESCAPE_CONFIRM_MS = 1_000;
@@ -128,7 +133,14 @@ function isTraexAuthorizationPending(pane) {
128
133
  pane.includes("press esc to cancel");
129
134
  }
130
135
  function isTraexStartupActivity(pane) {
131
- return pane.includes("updating traecode cli") || pane.includes("updating trae cli");
136
+ // The visible pane can include old conversation text. A mounted composer is
137
+ // evidence that an update phrase belongs to history rather than the active
138
+ // startup screen.
139
+ const composerReady = pane.includes("❯") &&
140
+ pane.includes("context ") &&
141
+ pane.includes(" left");
142
+ return !composerReady &&
143
+ (pane.includes("updating traecode cli") || pane.includes("updating trae cli"));
132
144
  }
133
145
  function isTerminalProtocolResponse(input) {
134
146
  // xterm answers terminal queries through the same onData channel as real
@@ -191,6 +203,8 @@ export class RunnerSession {
191
203
  attachmentRoles = new Map();
192
204
  traexStartupWatchers = new Map();
193
205
  traexStartupGates = new Map();
206
+ traexUpdateWatchers = new Map();
207
+ traexStartupUpdateActivity = new Map();
194
208
  codexStartupMonitors = new Map();
195
209
  terminalWatchers = new Map();
196
210
  /** Opens are async; a close received before attach resolves tombstones the id. */
@@ -1061,8 +1075,8 @@ export class RunnerSession {
1061
1075
  }
1062
1076
  let ready = true;
1063
1077
  if (runtime === "codex" || runtime === "traex") {
1064
- // Discover thread/started in the background for 30s while injection
1065
- // waits up to 60s for either bridge readiness or that startup error.
1078
+ // Discover thread/started in the background while injection waits for
1079
+ // either bridge readiness or the runtime-specific startup deadline.
1066
1080
  // Do not serialize the two waits here.
1067
1081
  this.monitorCodexThreadStartup(msg.localThreadId, runtime, provider);
1068
1082
  }
@@ -1117,12 +1131,20 @@ export class RunnerSession {
1117
1131
  this.terminals.get(`${localThreadId}-main`) !== expectedTerminal)
1118
1132
  return false;
1119
1133
  this.codexStartupMonitors.delete(localThreadId);
1134
+ if (runtime === "traex") {
1135
+ this.cancelTraexUpdateWatcher(localThreadId);
1136
+ this.traexStartupUpdateActivity.delete(localThreadId);
1137
+ }
1120
1138
  return true;
1121
1139
  };
1122
- void provider.waitLiveReady(localThreadId, CODEX_THREAD_START_TIMEOUT_MS).then((ready) => {
1123
- if (ready) {
1140
+ const handleResult = (result) => {
1141
+ if (result.ready) {
1124
1142
  if (this.codexStartupMonitors.get(localThreadId) === monitor) {
1125
1143
  this.codexStartupMonitors.delete(localThreadId);
1144
+ if (runtime === "traex") {
1145
+ this.cancelTraexUpdateWatcher(localThreadId);
1146
+ this.traexStartupUpdateActivity.delete(localThreadId);
1147
+ }
1126
1148
  }
1127
1149
  return;
1128
1150
  }
@@ -1142,16 +1164,81 @@ export class RunnerSession {
1142
1164
  const detail = provider.liveSessionError?.(localThreadId)
1143
1165
  ?? (paneFailure
1144
1166
  ? `${providerDisplayName(runtime)} Terminal exited before native session discovery: ${paneFailure}`
1145
- : `${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`);
1167
+ : result.updateObserved
1168
+ ? `Traex TUI did not publish thread/started before its update-aware startup deadline (${Math.ceil(result.elapsedMs / 1_000)}s elapsed)`
1169
+ : `${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`);
1146
1170
  this.failCodexThreadStartup(localThreadId, provider, new AgentRuntimeError(detail, 503, paneFailure
1147
1171
  ? "native_terminal_exited_before_session"
1148
1172
  : "native_thread_discovery_timeout"), expectedTerminal);
1149
- }).catch((error) => {
1173
+ };
1174
+ const handleError = (error) => {
1150
1175
  if (!claimCurrentLaunch())
1151
1176
  return;
1152
1177
  this.failCodexThreadStartup(localThreadId, provider, error instanceof AgentRuntimeError
1153
1178
  ? error
1154
1179
  : nativePhaseError(runtime, "native_thread_discovery_failed", "native session discovery failed", error), expectedTerminal);
1180
+ };
1181
+ if (runtime === "traex") {
1182
+ void this.waitForTraexThreadStartup(localThreadId, provider, expectedTerminal, monitor).then(handleResult).catch(handleError);
1183
+ return;
1184
+ }
1185
+ void provider.waitLiveReady(localThreadId, CODEX_THREAD_START_TIMEOUT_MS).then((ready) => {
1186
+ handleResult({
1187
+ ready,
1188
+ updateObserved: false,
1189
+ elapsedMs: CODEX_THREAD_START_TIMEOUT_MS,
1190
+ });
1191
+ }).catch(handleError);
1192
+ }
1193
+ async waitForTraexThreadStartup(localThreadId, provider, expectedTerminal, monitor) {
1194
+ const startedAt = Date.now();
1195
+ const hardDeadline = startedAt + TRAEX_UPDATE_THREAD_START_MAX_MS;
1196
+ let deadline = startedAt + CODEX_THREAD_START_TIMEOUT_MS;
1197
+ let updateObserved = false;
1198
+ const readiness = provider.waitLiveReady(localThreadId, null);
1199
+ return await new Promise((resolve, reject) => {
1200
+ let settled = false;
1201
+ let timer;
1202
+ const finish = (ready, now = Date.now()) => {
1203
+ if (settled)
1204
+ return;
1205
+ settled = true;
1206
+ if (timer)
1207
+ clearTimeout(timer);
1208
+ resolve({ ready, updateObserved, elapsedMs: now - startedAt });
1209
+ };
1210
+ const checkDeadline = () => {
1211
+ if (this.codexStartupMonitors.get(localThreadId) !== monitor ||
1212
+ this.terminals.get(`${localThreadId}-main`) !== expectedTerminal) {
1213
+ finish(false);
1214
+ return;
1215
+ }
1216
+ const now = Date.now();
1217
+ const updateObservedAt = this.traexStartupUpdateActivity.get(localThreadId);
1218
+ if (updateObservedAt !== undefined) {
1219
+ if (!updateObserved) {
1220
+ console.info(`[runner] session=${localThreadId} Traex startup update detected; extending native thread discovery up to ${TRAEX_UPDATE_THREAD_START_MAX_MS / 1_000}s`);
1221
+ }
1222
+ updateObserved = true;
1223
+ deadline = Math.min(hardDeadline, updateObservedAt + CODEX_THREAD_START_TIMEOUT_MS);
1224
+ }
1225
+ const remainingMs = Math.min(deadline, hardDeadline) - now;
1226
+ if (remainingMs <= 0) {
1227
+ finish(false, now);
1228
+ return;
1229
+ }
1230
+ timer = setTimeout(checkDeadline, Math.min(TRAEX_STARTUP_POLL_MS, remainingMs));
1231
+ timer.unref?.();
1232
+ };
1233
+ void readiness.then((ready) => finish(ready), (error) => {
1234
+ if (settled)
1235
+ return;
1236
+ settled = true;
1237
+ if (timer)
1238
+ clearTimeout(timer);
1239
+ reject(error);
1240
+ });
1241
+ checkDeadline();
1155
1242
  });
1156
1243
  }
1157
1244
  /** Discovery failure is terminal for this partial native launch. Publish the
@@ -1167,6 +1254,8 @@ export class RunnerSession {
1167
1254
  clearInterval(watcher);
1168
1255
  this.terminalWatchers.delete(localThreadId);
1169
1256
  this.cancelTraexStartupWatcher(localThreadId);
1257
+ this.cancelTraexUpdateWatcher(localThreadId);
1258
+ this.traexStartupUpdateActivity.delete(localThreadId);
1170
1259
  try {
1171
1260
  this.terminals.close(`${localThreadId}-main`, expectedTerminal);
1172
1261
  }
@@ -1272,6 +1361,7 @@ export class RunnerSession {
1272
1361
  clearInterval(watcher);
1273
1362
  this.terminalWatchers.delete(msg.localThreadId);
1274
1363
  this.cancelTraexStartupWatcher(msg.localThreadId);
1364
+ this.cancelTraexUpdateWatcher(msg.localThreadId);
1275
1365
  for (const [attachId, sessionId] of this.attachmentThreadIds) {
1276
1366
  if (sessionId !== msg.localThreadId)
1277
1367
  continue;
@@ -1328,9 +1418,13 @@ export class RunnerSession {
1328
1418
  : { keepAliveAfterExit: spec.keepAliveAfterExit }),
1329
1419
  });
1330
1420
  if (spec.skipTraexStartupPrompts) {
1331
- const watcher = Symbol(localThreadId);
1421
+ const watcher = Symbol(`${localThreadId}:prompts`);
1422
+ const updateWatcher = Symbol(`${localThreadId}:updates`);
1423
+ this.traexStartupUpdateActivity.delete(localThreadId);
1332
1424
  this.traexStartupWatchers.set(localThreadId, watcher);
1425
+ this.traexUpdateWatchers.set(localThreadId, updateWatcher);
1333
1426
  const gate = this.skipTraexStartupPrompts(localThreadId, term, watcher);
1427
+ void this.watchTraexStartupUpdates(localThreadId, term, updateWatcher);
1334
1428
  this.traexStartupGates.set(localThreadId, gate);
1335
1429
  void gate.finally(() => {
1336
1430
  if (this.traexStartupGates.get(localThreadId) === gate) {
@@ -1367,6 +1461,7 @@ export class RunnerSession {
1367
1461
  clearInterval(timer);
1368
1462
  this.terminalWatchers.delete(localThreadId);
1369
1463
  this.cancelTraexStartupWatcher(localThreadId);
1464
+ this.cancelTraexUpdateWatcher(localThreadId);
1370
1465
  const runtime = this.liveRuntimes.get(localThreadId);
1371
1466
  const lifecycle = this.terminals.lifecycle(terminalId);
1372
1467
  const paneFailure = terminalFailureDetail(terminal.capturePane?.() ?? "");
@@ -1414,6 +1509,47 @@ export class RunnerSession {
1414
1509
  if (localThreadId)
1415
1510
  this.traexStartupWatchers.delete(localThreadId);
1416
1511
  }
1512
+ cancelTraexUpdateWatcher(localThreadId) {
1513
+ if (localThreadId)
1514
+ this.traexUpdateWatchers.delete(localThreadId);
1515
+ }
1516
+ /** Observe update-only startup screens independently from prompt handling.
1517
+ * The low-frequency capture spans the full normal discovery window and only
1518
+ * remains alive longer after a positively identified update. */
1519
+ async watchTraexStartupUpdates(localThreadId, terminal, watcher) {
1520
+ const startedAt = Date.now();
1521
+ const hardDeadline = startedAt + TRAEX_UPDATE_THREAD_START_MAX_MS;
1522
+ let deadline = startedAt + CODEX_THREAD_START_TIMEOUT_MS;
1523
+ const terminalId = `${localThreadId}-main`;
1524
+ while (!this.shuttingDown &&
1525
+ this.traexUpdateWatchers.get(localThreadId) === watcher &&
1526
+ this.terminals.get(terminalId) === terminal) {
1527
+ const now = Date.now();
1528
+ if (now > deadline)
1529
+ break;
1530
+ try {
1531
+ const pane = normalizeTraexPane(terminal.capturePane());
1532
+ if (isTraexStartupActivity(pane)) {
1533
+ this.traexStartupUpdateActivity.set(localThreadId, now);
1534
+ deadline = Math.min(hardDeadline, now + CODEX_THREAD_START_TIMEOUT_MS);
1535
+ }
1536
+ }
1537
+ catch {
1538
+ // A concurrent terminal teardown owns the lifecycle failure. Keep the
1539
+ // bounded observer alive until that path invalidates this watcher.
1540
+ }
1541
+ const remainingMs = deadline - Date.now();
1542
+ if (remainingMs <= 0)
1543
+ break;
1544
+ await new Promise((resolve) => {
1545
+ const timer = setTimeout(resolve, Math.min(TRAEX_UPDATE_POLL_MS, remainingMs));
1546
+ timer.unref();
1547
+ });
1548
+ }
1549
+ if (this.traexUpdateWatchers.get(localThreadId) === watcher) {
1550
+ this.traexUpdateWatchers.delete(localThreadId);
1551
+ }
1552
+ }
1417
1553
  async skipTraexStartupPrompts(localThreadId, terminal, watcher) {
1418
1554
  const prompts = [
1419
1555
  {
@@ -1459,7 +1595,8 @@ export class RunnerSession {
1459
1595
  const prompt = prompts.find((candidate) => candidate.matches(pane));
1460
1596
  const now = Date.now();
1461
1597
  const authorizationPending = isTraexAuthorizationPending(pane);
1462
- if (isTraexStartupActivity(pane)) {
1598
+ const updateActive = isTraexStartupActivity(pane);
1599
+ if (updateActive) {
1463
1600
  discoveryDeadline = now + TRAEX_STARTUP_DISCOVERY_MS;
1464
1601
  }
1465
1602
  // Human device authorization routinely takes longer than the normal
@@ -1483,7 +1620,7 @@ export class RunnerSession {
1483
1620
  // updater keeps the full startup window because prompts may follow it.
1484
1621
  if (now >= discoveryDeadline &&
1485
1622
  !authorizationPending &&
1486
- !isTraexStartupActivity(pane))
1623
+ !updateActive)
1487
1624
  break;
1488
1625
  }
1489
1626
  else if (!prompt.requiresEscapeConfirmation) {
@@ -1542,6 +1679,8 @@ export class RunnerSession {
1542
1679
  clearInterval(timer);
1543
1680
  this.terminalWatchers.clear();
1544
1681
  this.codexStartupMonitors.clear();
1682
+ this.traexUpdateWatchers.clear();
1683
+ this.traexStartupUpdateActivity.clear();
1545
1684
  for (const id of this.liveIds) {
1546
1685
  this.liveProvider.stopLiveCodexSession?.(id, {
1547
1686
  deferClaudeInteractionCleanup: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/runtime",
3
- "version": "0.1.11-beta.42",
3
+ "version": "0.1.11-beta.43",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "smol-toml": "1.7.1",
28
28
  "ws": "^8.21.0",
29
- "@rynx-ai/core": "0.1.11-beta.42"
29
+ "@rynx-ai/core": "0.1.11-beta.43"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/ws": "^8.18.1"