@rynx-ai/runtime 0.1.11-beta.30 → 0.1.11-beta.31

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.
@@ -52,13 +52,13 @@ function isTurnScopedMethod(method) {
52
52
  function canRecoverMissedTurnFrom(method) {
53
53
  return method === "item/agentMessage/delta" || method === "item/plan/delta";
54
54
  }
55
- /** Omnigent forwards these live deltas only for the bridge's exact active
56
- * Codex turn. Assistant/plan deltas may recover a missed `turn/started` when
55
+ /** Live deltas require the bridge's exact active Codex turn. Assistant/plan
56
+ * deltas may recover a missed `turn/started` when
57
57
  * fully scoped; command output never opens or recovers a Turn. */
58
58
  function requiresMatchingActiveTurn(method) {
59
59
  return canRecoverMissedTurnFrom(method) || method === "item/commandExecution/outputDelta";
60
60
  }
61
- /** Content Omnigent forwards independently from the active-turn status edge. */
61
+ /** Turn content that remains valid independently of lifecycle status. */
62
62
  function isIndependentTurnContent(method) {
63
63
  return method === "item/started" ||
64
64
  method === "item/reasoning/textDelta" ||
@@ -412,8 +412,8 @@ export class CodexSessionForwarder {
412
412
  }
413
413
  if (requiresMatchingActiveTurn(method)) {
414
414
  if (this.currentTurnIdValue !== null) {
415
- // An id-less delta is not attributable to the active Turn. Omnigent's
416
- // `_is_active_turn_delta` applies the same exact-id requirement.
415
+ // An id-less delta is not attributable to the active Turn. Require
416
+ // the exact active Turn id before forwarding it.
417
417
  if (carriedTurnId !== this.currentTurnIdValue)
418
418
  return;
419
419
  }
@@ -465,8 +465,8 @@ export class CodexSessionForwarder {
465
465
  if (canonicalEvents.some((event) => event.type !== "reasoning_delta" && event.type !== "reasoning_completed")) {
466
466
  this.flushDeferredAssistantMessage();
467
467
  }
468
- // Omnigent's item/content channel is independent from its turn-status
469
- // channel. A scoped item or reasoning delta that arrives after the terminal
468
+ // Item/content delivery is independent from turn lifecycle status. A
469
+ // scoped item or reasoning delta that arrives after the terminal
470
470
  // edge remains visible, but must not synthesize another running response.
471
471
  if (!this.turnOpen &&
472
472
  this.currentTurnIdValue === null &&
package/dist/host.js CHANGED
@@ -21,7 +21,7 @@ import { buildCodexRemoteArgs } from "./terminal/codex-tui.js";
21
21
  import { buildClaudeTuiArgs } from "./terminal/claude-tui.js";
22
22
  import { providerAdditionalDirs, threadWorkspaceParams, turnWorkspaceParams, } from "./provider-workspace.js";
23
23
  import { ensureProjectTrusted } from "./claude/trust.js";
24
- import { claudeAttachmentToken, claudeInputText, codexUserEchoContent, runtimeUserContent, } from "./input-resources.js";
24
+ import { claudeAttachmentToken, claudeInputText, runtimeUserContent, } from "./input-resources.js";
25
25
  import { claudeBridgeDir, prepareClaudeBridgeDir, removeManagedClaudeSettings, writeManagedClaudeSettings, } from "./claude/native-bridge.js";
26
26
  import { ClaudeLiveSession, injectViaTerminal, } from "./claude/native-integration.js";
27
27
  import { buildClaudeHookSettings } from "./claude/native-hooks.js";
@@ -873,7 +873,6 @@ export class LocalAgentHost {
873
873
  markTerminalReady,
874
874
  injectLock: Promise.resolve(),
875
875
  pendingInjectedInputs: [],
876
- publishInjectedInput: () => undefined,
877
876
  publishTurnAdmission: () => undefined,
878
877
  publishTurnAdmissionFailure: () => undefined,
879
878
  publishInterrupted: () => undefined,
@@ -930,14 +929,6 @@ export class LocalAgentHost {
930
929
  });
931
930
  return normalizer;
932
931
  };
933
- live.publishInjectedInput = (turnId, content) => {
934
- const n = startNormalizer(turnId);
935
- const pending = live.pendingInjectedInputs.find((entry) => entry.signature === JSON.stringify(content) && entry.responseId === undefined);
936
- if (pending && currentResponseId)
937
- pending.responseId = currentResponseId;
938
- for (const se of n.userInput(content))
939
- emitCurrent(se);
940
- };
941
932
  live.publishTurnAdmission = () => {
942
933
  emitCurrent({
943
934
  type: "session.status",
@@ -970,7 +961,7 @@ export class LocalAgentHost {
970
961
  contentNormalizers.set(responseId, contentNormalizer);
971
962
  }
972
963
  for (const event of produce(contentNormalizer)) {
973
- // Omnigent posts item/transient content under the Turn's response id
964
+ // Item/transient content is attached to the Turn's response id
974
965
  // independently of lifecycle status. Reuse Rynx's canonical item
975
966
  // normalization but suppress its synthetic lifecycle edges.
976
967
  if (event.type !== "response.created" && event.type !== "session.status") {
@@ -982,14 +973,16 @@ export class LocalAgentHost {
982
973
  const normalizedContent = typeof content === "string"
983
974
  ? [{ type: "input_text", text: content }]
984
975
  : content;
985
- const signature = JSON.stringify(normalizedContent);
986
- const pending = live.pendingInjectedInputs.find((entry) => entry.signature === signature || entry.providerEchoSignature === signature);
987
- if (pending?.state === "optimistic") {
988
- live.pendingInjectedInputs.splice(live.pendingInjectedInputs.indexOf(pending), 1);
989
- return null;
990
- }
991
- if (pending)
976
+ // The Provider does not echo a caller id and may reformat the text, so the
977
+ // first native user record drains the oldest injected input instead of
978
+ // comparing message text.
979
+ const pending = live.pendingInjectedInputs[0];
980
+ if (pending) {
992
981
  pending.observed = true;
982
+ if (pending.state === "accepted") {
983
+ live.pendingInjectedInputs.shift();
984
+ }
985
+ }
993
986
  return normalizedContent;
994
987
  };
995
988
  const rememberSettledInteraction = (interactionId) => {
@@ -1293,16 +1286,12 @@ export class LocalAgentHost {
1293
1286
  },
1294
1287
  onUserMessage: (content) => {
1295
1288
  const normalizedContent = observedUserContent(content);
1296
- if (!normalizedContent)
1297
- return;
1298
1289
  const n = normalizer ?? startNormalizer();
1299
1290
  for (const se of n.userInput(normalizedContent))
1300
1291
  emitCurrent(se);
1301
1292
  },
1302
1293
  onTurnContentUserMessage: (turnId, content) => {
1303
1294
  const normalizedContent = observedUserContent(content);
1304
- if (!normalizedContent)
1305
- return;
1306
1295
  turnContentEvents(turnId, (n) => n.userInput(normalizedContent));
1307
1296
  },
1308
1297
  onEvent: (event) => {
@@ -1823,8 +1812,6 @@ export class LocalAgentHost {
1823
1812
  const text = claudeInputText(runtimeInput);
1824
1813
  const content = runtimeUserContent(runtimeInput);
1825
1814
  const pendingInput = {
1826
- content,
1827
- signature: JSON.stringify(content),
1828
1815
  state: "awaiting",
1829
1816
  observed: false,
1830
1817
  };
@@ -1855,8 +1842,12 @@ export class LocalAgentHost {
1855
1842
  if (token)
1856
1843
  claude.pendingImageInputs.delete(token);
1857
1844
  }
1858
- else if (pendingInput.observed) {
1859
- forgetPendingInput();
1845
+ else {
1846
+ pendingInput.responseId = result.responseId;
1847
+ if (pendingInput.observed)
1848
+ forgetPendingInput();
1849
+ else
1850
+ pendingInput.state = "accepted";
1860
1851
  }
1861
1852
  return result;
1862
1853
  }
@@ -1873,17 +1864,7 @@ export class LocalAgentHost {
1873
1864
  if (!bound || !threadId)
1874
1865
  return { outcome: "notReady" };
1875
1866
  const nativeInput = buildRuntimeUserInput(runtimeInput);
1876
- const content = runtimeUserContent(runtimeInput);
1877
- const providerEcho = codexUserEchoContent(nativeInput);
1878
- const providerEchoContent = typeof providerEcho === "string"
1879
- ? [{ type: "input_text", text: providerEcho }]
1880
- : providerEcho;
1881
1867
  const pendingInput = {
1882
- content,
1883
- signature: JSON.stringify(content),
1884
- ...(providerEchoContent
1885
- ? { providerEchoSignature: JSON.stringify(providerEchoContent) }
1886
- : {}),
1887
1868
  state: "awaiting",
1888
1869
  observed: false,
1889
1870
  };
@@ -1921,13 +1902,11 @@ export class LocalAgentHost {
1921
1902
  expectedTurnId: turnId,
1922
1903
  input: nativeInput,
1923
1904
  });
1924
- if (pendingInput.observed) {
1905
+ pendingInput.responseId = `resp_codex_${steered.turnId}`;
1906
+ if (pendingInput.observed)
1925
1907
  forgetPendingInput();
1926
- }
1927
- else {
1928
- live.publishInjectedInput(steered.turnId, pendingInput.content);
1929
- pendingInput.state = "optimistic";
1930
- }
1908
+ else
1909
+ pendingInput.state = "accepted";
1931
1910
  live.forwarder.noteTurnAccepted(steered.turnId);
1932
1911
  return { outcome: "steered", responseId: `resp_codex_${steered.turnId}` };
1933
1912
  }
@@ -1985,13 +1964,11 @@ export class LocalAgentHost {
1985
1964
  ...turnWorkspaceParams(live.runtime, live.workspace, live.sandbox),
1986
1965
  approvalPolicy: live.approvalPolicy,
1987
1966
  });
1988
- if (pendingInput.observed) {
1967
+ pendingInput.responseId = `resp_codex_${started.turnId}`;
1968
+ if (pendingInput.observed)
1989
1969
  forgetPendingInput();
1990
- }
1991
- else {
1992
- live.publishInjectedInput(started.turnId, pendingInput.content);
1993
- pendingInput.state = "optimistic";
1994
- }
1970
+ else
1971
+ pendingInput.state = "accepted";
1995
1972
  // The injection RPC and active-turn write are one serialized operation.
1996
1973
  // Do not wait for the independent observer connection's `turn/started`:
1997
1974
  // a second message accepted in that window must steer, not double-start.
@@ -2515,14 +2492,13 @@ export class LocalAgentHost {
2515
2492
  if (token && content)
2516
2493
  live.pendingImageInputs.delete(token);
2517
2494
  const normalizedContent = content ?? [{ type: "input_text", text }];
2518
- const signature = JSON.stringify(normalizedContent);
2519
- const pending = live.pendingInjectedInputs.find((entry) => entry.signature === signature);
2495
+ const pending = live.pendingInjectedInputs[0];
2520
2496
  if (pending) {
2521
2497
  pending.responseId = currentResponseId;
2522
- live.pendingInjectedInputs.splice(live.pendingInjectedInputs.indexOf(pending), 1);
2523
- if (pending.state === "optimistic")
2524
- return;
2525
2498
  pending.observed = true;
2499
+ if (pending.state === "accepted") {
2500
+ live.pendingInjectedInputs.shift();
2501
+ }
2526
2502
  }
2527
2503
  for (const se of n.userInput(normalizedContent))
2528
2504
  emitCurrent(se);
@@ -2574,7 +2550,7 @@ export class LocalAgentHost {
2574
2550
  const message = error.message || "Agent turn failed";
2575
2551
  if (!normalizer) {
2576
2552
  // Native pane/forwarder death is Session-level even when Claude is
2577
- // between Turns. Omnigent publishes the same bare failed edge; the
2553
+ // between Turns. Emit a bare failed edge; the
2578
2554
  // explicit zero also retires any sticky background-shell tally.
2579
2555
  emitCurrent({
2580
2556
  type: "session.status",
@@ -80,6 +80,7 @@ export declare class RunnerSession {
80
80
  private readonly attachments;
81
81
  private readonly attachmentThreadIds;
82
82
  private readonly traexStartupWatchers;
83
+ private readonly traexStartupGates;
83
84
  private readonly terminalWatchers;
84
85
  /** Opens are async; a close received before attach resolves tombstones the id. */
85
86
  private readonly pendingTerminalOpens;
@@ -45,10 +45,13 @@ function terminalFailureDetail(pane) {
45
45
  .slice(-1_000);
46
46
  }
47
47
  const TRAEX_STARTUP_WATCH_MS = 20_000;
48
+ const TRAEX_STARTUP_DISCOVERY_MS = 5_000;
48
49
  const TRAEX_STARTUP_POLL_MS = 100;
49
50
  const TRAEX_AUTHORIZATION_POLL_MS = 500;
50
51
  const TRAEX_AUTHORIZATION_WAIT_MS = 15 * 60_000;
51
- const TRAEX_PROMPT_RETRY_MS = 500;
52
+ const TRAEX_ESCAPE_CONFIRM_MS = 1_000;
53
+ const TRAEX_ESCAPE_COOLDOWN_MS = 1_000;
54
+ const TRAEX_REPEAT_PROMPT_CONFIRM_MS = 5_000;
52
55
  const MIRROR_IMAGE_ACK_TIMEOUT_MS = 30_000;
53
56
  function normalizeTraexPane(pane) {
54
57
  return pane.toLowerCase().replace(/\s+/g, " ").trim();
@@ -58,6 +61,9 @@ function isTraexAuthorizationPending(pane) {
58
61
  pane.includes("open this link in your browser") &&
59
62
  pane.includes("press esc to cancel");
60
63
  }
64
+ function isTraexStartupActivity(pane) {
65
+ return pane.includes("updating traecode cli") || pane.includes("updating trae cli");
66
+ }
61
67
  function isTerminalProtocolResponse(input) {
62
68
  // xterm answers terminal queries through the same onData channel as real
63
69
  // keystrokes. CSI carries device/focus/position reports; OSC carries color
@@ -116,6 +122,7 @@ export class RunnerSession {
116
122
  attachments = new Map();
117
123
  attachmentThreadIds = new Map();
118
124
  traexStartupWatchers = new Map();
125
+ traexStartupGates = new Map();
119
126
  terminalWatchers = new Map();
120
127
  /** Opens are async; a close received before attach resolves tombstones the id. */
121
128
  pendingTerminalOpens = new Set();
@@ -437,6 +444,13 @@ export class RunnerSession {
437
444
  else if (msg.waitForReady !== false && provider.waitLiveReady) {
438
445
  ready = await provider.waitLiveReady(msg.localThreadId);
439
446
  }
447
+ // Message delivery is serialized behind Traex's startup modal handling.
448
+ // Otherwise app-server can accept a Turn while the TUI watcher is still
449
+ // sending Escape, and the same key becomes `turn/interrupt` as soon as
450
+ // the modal disappears. Setup-terminal opens remain non-blocking.
451
+ if (runtime === "traex" && msg.waitForReady !== false) {
452
+ await this.traexStartupGates.get(msg.localThreadId);
453
+ }
440
454
  const readinessError = ready
441
455
  ? undefined
442
456
  : providerFailure(provider, msg.localThreadId, {
@@ -619,7 +633,13 @@ export class RunnerSession {
619
633
  if (spec.skipTraexStartupPrompts) {
620
634
  const watcher = Symbol(localThreadId);
621
635
  this.traexStartupWatchers.set(localThreadId, watcher);
622
- void this.skipTraexStartupPrompts(localThreadId, term, watcher);
636
+ const gate = this.skipTraexStartupPrompts(localThreadId, term, watcher);
637
+ this.traexStartupGates.set(localThreadId, gate);
638
+ void gate.finally(() => {
639
+ if (this.traexStartupGates.get(localThreadId) === gate) {
640
+ this.traexStartupGates.delete(localThreadId);
641
+ }
642
+ });
623
643
  }
624
644
  this.liveProvider.attachTerminalInjector?.(localThreadId, term);
625
645
  this.watchNativeTerminal(localThreadId, terminalId, term);
@@ -694,6 +714,7 @@ export class RunnerSession {
694
714
  id: "welcome",
695
715
  matches: (pane) => pane.includes("welcome to trae cli") && pane.includes("press enter to continue"),
696
716
  dismiss: () => terminal.sendEnter(),
717
+ requiresEscapeConfirmation: false,
697
718
  },
698
719
  {
699
720
  id: "migration",
@@ -702,6 +723,7 @@ export class RunnerSession {
702
723
  pane.includes("select what to import") &&
703
724
  (pane.includes("skip for now") || pane.includes("don't ask again")),
704
725
  dismiss: () => terminal.interrupt(),
726
+ requiresEscapeConfirmation: true,
705
727
  },
706
728
  {
707
729
  id: "hooks",
@@ -709,12 +731,16 @@ export class RunnerSession {
709
731
  pane.includes("trust all and continue") &&
710
732
  pane.includes("continue without trusting"),
711
733
  dismiss: () => terminal.interrupt(),
734
+ requiresEscapeConfirmation: true,
712
735
  },
713
736
  ];
714
- let activePromptId;
715
- let lastDismissedAt = 0;
737
+ let activeWelcome = false;
738
+ let pendingEscapePromptId;
739
+ const dismissedPromptIds = new Set();
740
+ let dismissedAnyPrompt = false;
716
741
  const startedAt = Date.now();
717
742
  let deadline = startedAt + TRAEX_STARTUP_WATCH_MS;
743
+ let discoveryDeadline = startedAt + TRAEX_STARTUP_DISCOVERY_MS;
718
744
  const authorizationDeadline = startedAt + TRAEX_AUTHORIZATION_WAIT_MS;
719
745
  const terminalId = `${localThreadId}-main`;
720
746
  while (!this.shuttingDown &&
@@ -727,6 +753,9 @@ export class RunnerSession {
727
753
  const prompt = prompts.find((candidate) => candidate.matches(pane));
728
754
  const now = Date.now();
729
755
  const authorizationPending = isTraexAuthorizationPending(pane);
756
+ if (isTraexStartupActivity(pane)) {
757
+ discoveryDeadline = now + TRAEX_STARTUP_DISCOVERY_MS;
758
+ }
730
759
  // Human device authorization routinely takes longer than the normal
731
760
  // startup-modal window. Keep watching while that known screen remains,
732
761
  // then preserve a full window for welcome/migration/hooks after sign-in.
@@ -735,13 +764,63 @@ export class RunnerSession {
735
764
  deadline = now + TRAEX_STARTUP_WATCH_MS;
736
765
  }
737
766
  if (!prompt) {
738
- activePromptId = undefined;
767
+ activeWelcome = false;
768
+ pendingEscapePromptId = undefined;
769
+ // Every dismissal is followed by a one-second cooldown before the next
770
+ // capture. If no known modal remains then, startup prompt handling is
771
+ // complete and message injection may safely begin.
772
+ if (dismissedAnyPrompt)
773
+ break;
774
+ // A clean Traex home may have no startup modal at all. Preserve a
775
+ // five-second discovery window for late modals, but do not impose the
776
+ // old full 20-second watcher lifetime on every cold message. A visible
777
+ // updater keeps the full startup window because prompts may follow it.
778
+ if (now >= discoveryDeadline &&
779
+ !authorizationPending &&
780
+ !isTraexStartupActivity(pane))
781
+ break;
739
782
  }
740
- else if (prompt.id !== activePromptId ||
741
- now - lastDismissedAt >= TRAEX_PROMPT_RETRY_MS) {
783
+ else if (!prompt.requiresEscapeConfirmation) {
784
+ pendingEscapePromptId = undefined;
785
+ if (!activeWelcome && !dismissedPromptIds.has(prompt.id)) {
786
+ prompt.dismiss();
787
+ dismissedPromptIds.add(prompt.id);
788
+ dismissedAnyPrompt = true;
789
+ await new Promise((resolve) => {
790
+ const timer = setTimeout(resolve, TRAEX_ESCAPE_COOLDOWN_MS);
791
+ timer.unref();
792
+ });
793
+ continue;
794
+ }
795
+ activeWelcome = true;
796
+ }
797
+ else if (prompt.id !== pendingEscapePromptId) {
798
+ activeWelcome = false;
799
+ pendingEscapePromptId = prompt.id;
800
+ await new Promise((resolve) => {
801
+ // Traex can show hooks -> migration -> hooks during one startup. The
802
+ // trailing hooks screen normally disappears on its own, so observe a
803
+ // previously handled prompt type for five seconds before acting.
804
+ const timer = setTimeout(resolve, dismissedPromptIds.has(prompt.id)
805
+ ? TRAEX_REPEAT_PROMPT_CONFIRM_MS
806
+ : TRAEX_ESCAPE_CONFIRM_MS);
807
+ timer.unref();
808
+ });
809
+ continue;
810
+ }
811
+ else {
742
812
  prompt.dismiss();
743
- activePromptId = prompt.id;
744
- lastDismissedAt = now;
813
+ dismissedPromptIds.add(prompt.id);
814
+ dismissedAnyPrompt = true;
815
+ // Keep the id through the cooldown. If the same prompt is genuinely
816
+ // still active after a sent key, the next check retries it; a prompt
817
+ // that disappeared during the cooldown never receives another key.
818
+ pendingEscapePromptId = prompt.id;
819
+ await new Promise((resolve) => {
820
+ const timer = setTimeout(resolve, TRAEX_ESCAPE_COOLDOWN_MS);
821
+ timer.unref();
822
+ });
823
+ continue;
745
824
  }
746
825
  await new Promise((resolve) => {
747
826
  const timer = setTimeout(resolve, authorizationPending ? TRAEX_AUTHORIZATION_POLL_MS : TRAEX_STARTUP_POLL_MS);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/runtime",
3
- "version": "0.1.11-beta.30",
3
+ "version": "0.1.11-beta.31",
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"
30
+ "@rynx-ai/core": "0.1.11-beta.31"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/ws": "^8.18.1"