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

View file

@ -606,63 +606,6 @@ class MemoryStore:
raise RuntimeError(f"Failed to write memory file {path}: {e}")
def _apply_write_gate(action: str, target: str, content: Optional[str],
old_text: Optional[str]) -> Optional[str]:
"""Evaluate the memory write gate. Returns a JSON tool-result string when
the write should NOT proceed normally (blocked or staged), or None when the
caller should perform the real write.
Only the mutating actions (add/replace/remove) are gated.
"""
if action not in {"add", "replace", "remove"}:
return None
try:
from tools import write_approval as wa
except Exception:
# If the gate module can't load, fail open (current behaviour) rather
# than blocking all memory writes.
return None
# Build a small inline summary/detail for the foreground approval prompt.
label = "user profile" if target == "user" else "memory"
if action == "add":
summary = f"add to {label}"
detail = content or ""
elif action == "replace":
summary = f"replace in {label}"
detail = f"old: {old_text}\nnew: {content}"
else: # remove
summary = f"remove from {label}"
detail = old_text or ""
decision = wa.evaluate_gate(wa.MEMORY, inline_summary=summary, inline_detail=detail)
if decision.allow:
return None
if decision.blocked:
return tool_error(decision.message, success=False)
# stage
payload = {
"action": action,
"target": target,
"content": content,
"old_text": old_text,
}
record = wa.stage_write(
wa.MEMORY, payload,
summary=f"{summary}: {detail[:120]}",
origin=wa.current_origin(),
)
return json.dumps(
{"success": True, "staged": True, "pending_id": record["id"],
"message": decision.message},
ensure_ascii=False,
)
def memory_tool(
action: str,
target: str = "memory",
@ -681,29 +624,21 @@ def memory_tool(
if target not in {"memory", "user"}:
return tool_error(f"Invalid target '{target}'. Use 'memory' or 'user'.", success=False)
# Validate required params BEFORE the gate so an invalid write is rejected
# immediately instead of being staged and only failing at approve time.
if action == "add" and not content:
return tool_error("Content is required for 'add' action.", success=False)
if action == "replace" and (not old_text or not content):
missing = "old_text" if not old_text else "content"
return tool_error(f"{missing} is required for 'replace' action.", success=False)
if action == "remove" and not old_text:
return tool_error("old_text is required for 'remove' action.", success=False)
# Approval gate: when on, stages the write (background/gateway) or prompts
# inline (interactive CLI); when off (default) passes straight through.
gate_result = _apply_write_gate(action, target, content, old_text)
if gate_result is not None:
return gate_result
if action == "add":
if not content:
return tool_error("Content is required for 'add' action.", success=False)
result = store.add(target, content)
elif action == "replace":
if not old_text:
return tool_error("old_text is required for 'replace' action.", success=False)
if not content:
return tool_error("content is required for 'replace' action.", success=False)
result = store.replace(target, old_text, content)
elif action == "remove":
if not old_text:
return tool_error("old_text is required for 'remove' action.", success=False)
result = store.remove(target, old_text)
else:
@ -717,23 +652,7 @@ def check_memory_requirements() -> bool:
return True
def apply_memory_pending(payload: Dict[str, Any], store: "MemoryStore") -> Dict[str, Any]:
"""Replay a staged memory write directly against the store, bypassing the
write gate. Called by the /memory approve handler.
Returns the store's result dict.
"""
action = payload.get("action")
target = payload.get("target", "memory")
content = payload.get("content") or ""
old_text = payload.get("old_text") or ""
if action == "add":
return store.add(target, content)
if action == "replace":
return store.replace(target, old_text, content)
if action == "remove":
return store.remove(target, old_text)
return {"success": False, "error": f"Unknown staged action '{action}'."}
# =============================================================================
# OpenAI Function-Calling Schema
# =============================================================================