opentui(phase3): launcher integration — HERMES_TUI_ENGINE dual-engine

hermes --tui launches the native OpenTUI engine (Bun) when
HERMES_TUI_ENGINE=opentui (env) or display.tui_engine=opentui (config);
Ink stays the default and the shipping path is untouched.

- _resolve_tui_engine() (env > config > ink); refuses opentui on
  Windows/Termux (no Bun) -> falls back to ink with a notice.
- _make_opentui_argv() -> [bun, src/entry.real.tsx] (no build step).
- _bun_bin() with HERMES_BUN override.
- Branch at top of _make_tui_argv BEFORE _ensure_tui_node (Bun-only host
  must not bootstrap Node).
- Gate _launch_tui NODE_OPTIONS/--max-old-space-size on engine==ink (Bun
  is JSC; the V8 flag errors/ignores).

Verified end-to-end via tmux: real hermes --tui -> Bun -> OpenTUI ->
real Python gateway streamed a real reply. No-flag default still ink.
This commit is contained in:
alt-glitch
2026-06-08 11:11:54 +00:00
parent 24f74eb888
commit 2bd9c9b881
741 changed files with 17733 additions and 79889 deletions
+9 -75
View File
@@ -147,15 +147,8 @@ class GatewayStreamConsumer:
self._edit_supported = True # Disabled when progressive edits are no longer usable
self._last_edit_time = 0.0
self._last_sent_text = "" # Track last-sent text to skip redundant edits
# True when the most recent _send_or_edit split-and-delivered across
# continuation messages (the adapter adopted a new message id).
self._last_edit_overflowed = False
self._fallback_final_send = False
self._fallback_prefix = ""
# True when fallback is sending only the missing tail after a partial
# Telegram overflow delivery. In that case the already-visible prefix
# is intentional content, not a stale preview to delete.
self._fallback_preserve_partial_messages = False
self._flood_strikes = 0 # Consecutive flood-control edit failures
self._current_edit_interval = self.cfg.edit_interval # Adaptive backoff
self._final_response_sent = False
@@ -268,7 +261,6 @@ class GatewayStreamConsumer:
self._last_sent_text = ""
self._fallback_final_send = False
self._fallback_prefix = ""
self._fallback_preserve_partial_messages = False
# #29346: a tool/segment boundary means what we delivered was an interim
# preamble, not the final answer — clear the flags so a premature setter
# can't fool the gateway. Safe: got_done returns before any reset, and
@@ -589,20 +581,14 @@ class GatewayStreamConsumer:
if self._accumulated:
if self._fallback_final_send:
await self._send_fallback_final(self._accumulated)
elif current_update_visible and (
not self._adapter_requires_finalize
or self._last_edit_overflowed
elif (
current_update_visible
and not self._adapter_requires_finalize
):
# Mid-stream edit above already delivered the
# final accumulated content. Skip the redundant
# final edit for adapters that don't need an
# explicit finalize signal, and for any adapter
# when that edit split-and-delivered across
# continuations: the split edit carried
# finalize=True itself, and re-finalizing with
# the full text would overflow-split again into
# the adopted continuation, duplicating chunks
# on screen.
# final edit — but only for adapters that don't
# need an explicit finalize signal.
self._final_response_sent = True
self._final_content_delivered = True
elif self._message_id:
@@ -661,21 +647,11 @@ class GatewayStreamConsumer:
await asyncio.sleep(0.05) # Small yield to not busy-loop
except asyncio.CancelledError:
# Best-effort final edit on cancellation. finalize=True so
# REQUIRES_EDIT_FINALIZE platforms (Telegram) apply final
# formatting — a plain edit here would leave the entire reply
# rendered as a raw streaming preview while the success flags
# below suppress the gateway's formatted re-send.
# is_turn_final=False keeps _try_fresh_final from setting
# _final_response_sent itself; this handler owns the flags.
# Best-effort final edit on cancellation
_best_effort_ok = False
if self._accumulated and self._message_id:
try:
_best_effort_ok = bool(
await self._send_or_edit(
self._accumulated, finalize=True, is_turn_final=False,
)
)
_best_effort_ok = bool(await self._send_or_edit(self._accumulated))
except Exception:
pass
# Only confirm final delivery if the best-effort send above
@@ -891,21 +867,11 @@ class GatewayStreamConsumer:
self._notify_new_message()
# Remove the frozen partial message so the user only sees the
# complete fallback response. ONLY safe when the fallback re-sent
# the FULL final text (continuation == final_text). When the
# prefix-based dedup above sent only the missing TAIL, the partial
# message IS the head of the answer — deleting it leaves the user
# with only the last part of the response (the "Gemini sent only
# the second half" symptom). Best-effort — if the platform doesn't
# complete fallback response. Best-effort — if the platform doesn't
# implement ``delete_message``, the delete fails (flood control still
# active, bot lacks permission, message too old to delete), the
# partial remains but at least the full answer was delivered.
if (
stale_message_id
and stale_message_id != last_message_id
and not self._fallback_preserve_partial_messages
and continuation == final_text
):
if stale_message_id and stale_message_id != last_message_id:
delete_fn = getattr(self.adapter, "delete_message", None)
if delete_fn is not None:
try:
@@ -922,7 +888,6 @@ class GatewayStreamConsumer:
self._final_content_delivered = True
self._last_sent_text = chunks[-1]
self._fallback_prefix = ""
self._fallback_preserve_partial_messages = False
def _is_flood_error(self, result) -> bool:
"""Check if a SendResult failure is due to flood control / rate limiting."""
@@ -1243,7 +1208,6 @@ class GatewayStreamConsumer:
return True
# Failure already disabled drafts for this run; fall through to
# the regular edit/send path below.
self._last_edit_overflowed = False
try:
if self._message_id is not None:
if self._edit_supported:
@@ -1300,7 +1264,6 @@ class GatewayStreamConsumer:
and result.message_id
and result.message_id != self._message_id
):
self._last_edit_overflowed = True
self._message_id = str(result.message_id)
self._message_created_ts = time.monotonic()
self._last_sent_text = ""
@@ -1311,35 +1274,6 @@ class GatewayStreamConsumer:
self._flood_strikes = 0
return True
else:
raw_response = getattr(result, "raw_response", None)
if isinstance(raw_response, dict) and raw_response.get("partial_overflow"):
# Telegram edited/sent one or more overflow chunks,
# but not the complete response. Preserve the
# visible prefix so the got_done fallback sends the
# missing tail instead of marking a clipped topic
# reply as final delivery.
self._message_id = str(
raw_response.get("last_message_id")
or result.message_id
or self._message_id
)
delivered_prefix = raw_response.get("delivered_prefix")
if isinstance(delivered_prefix, str) and delivered_prefix:
self._last_sent_text = delivered_prefix
self._fallback_prefix = delivered_prefix
self._fallback_preserve_partial_messages = text.startswith(
delivered_prefix
)
else:
self._fallback_prefix = self._visible_prefix()
self._fallback_preserve_partial_messages = False
self._fallback_final_send = True
self._edit_supported = False
self._already_sent = True
if getattr(result, "continuation_message_ids", ()):
self._notify_new_message()
return False
# Edit failed. If this looks like flood control / rate
# limiting, use adaptive backoff: double the edit interval
# and retry on the next cycle. Only permanently disable