The persist / restore / carry logic for session YOLO had grown three copies (the CLI mixin, tui_gateway, the gateway runner), each with its own key choice, ordering and error handling. tools/approval_yolo.py now owns it: - toggle_session_yolo: flip or set, persist BEFORE the live flip (a racing restore never revives an OFF), stored-copy-aware so a toggle after a restart turns it OFF; a failed write logs a warning and never blocks the flip. - restore_session_yolo: re-arm a persisted flag in a fresh process; skipped under the frozen process --yolo. - transfer_session_yolo: move the live flag to a continuation id. - with_session_yolo: a row being created records a live flag (lazy first row, compression child, /branch). Each surface keeps only its storage: the session row for CLI/TUI/Desktop, the routing entry for the gateway. Removed: HermesCLI._transfer_session_yolo / _persist_session_yolo, GatewayRunner._restore_session_yolo, the TUI-only restore helper, and the TUI's post-publish set_session_yolo carry (the compression publish now writes the child row with the flag). Closes a gap the unification exposed: a CLI manual /compress that rotated the session dropped YOLO (the next dangerous command prompted, and a --resume of the continuation came back with it off).
73 lines
3.3 KiB
Python
73 lines
3.3 KiB
Python
"""Session ``/yolo``: the one toggle / restore / carry contract every surface calls.
|
|
|
|
The bypass itself is ``tools.approval``'s in-memory per-key set, so every process starts with it empty.
|
|
Each surface persists it where its session identity lives: CLI, TUI and Desktop on the session row
|
|
(``model_config.yolo_mode``), the messaging gateway on the routing entry, whose key outlives compression
|
|
rotations. The surface hands its writer in as ``persist(enabled)``; everything else is decided here, so
|
|
the CLI ``/yolo``, the TUI Shift+Tab, the Desktop zap and a Telegram ``/yolo`` behave the same.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any, Callable, Optional
|
|
|
|
from tools import approval
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
Persist = Optional[Callable[[bool], Any]]
|
|
|
|
|
|
def _persist(persist: Persist, enabled: bool, session_key: str) -> None:
|
|
# Best-effort: the live flag is authoritative for this process; a failed write only costs the restore.
|
|
if persist is None:
|
|
return
|
|
try:
|
|
persist(enabled)
|
|
except Exception:
|
|
logger.warning("failed to persist session yolo=%s for %s", enabled, session_key, exc_info=True)
|
|
|
|
|
|
def toggle_session_yolo(session_key: str, enabled: Optional[bool] = None, *, persisted: bool = False,
|
|
persist: Persist = None) -> bool:
|
|
"""Flip (``enabled=None``) or set the bypass for *session_key*; returns the new state.
|
|
|
|
*persisted* is the stored flag: after a restart only it is set and the user still sees ON, so a toggle
|
|
turns it OFF. The write runs BEFORE the live flip, so a turn restoring in between never revives a
|
|
bypass that is being switched off.
|
|
"""
|
|
if enabled is None:
|
|
enabled = not (persisted or approval.is_session_yolo_enabled(session_key))
|
|
_persist(persist, enabled, session_key)
|
|
(approval.enable_session_yolo if enabled else approval.disable_session_yolo)(session_key)
|
|
return enabled
|
|
|
|
|
|
def restore_session_yolo(session_key: str, persisted: bool) -> bool:
|
|
"""Re-arm a persisted bypass in a fresh process; True when this call turned it on.
|
|
|
|
Key it on the id approvals are checked under (the stored session id / routing key), never a
|
|
transport-local id. Skipped under the frozen process ``--yolo``, which already bypasses everything.
|
|
"""
|
|
if (not session_key or not persisted or approval._YOLO_MODE_FROZEN
|
|
or approval.is_session_yolo_enabled(session_key)):
|
|
return False
|
|
approval.enable_session_yolo(session_key)
|
|
return True
|
|
|
|
|
|
def transfer_session_yolo(old_key: str, new_key: str) -> None:
|
|
"""Move the live bypass when the conversation continues under a new id (compression rotation, /branch).
|
|
The new row records it at creation (``with_session_yolo``), so nothing is written here."""
|
|
if not old_key or not new_key or old_key == new_key or not approval.is_session_yolo_enabled(old_key):
|
|
return
|
|
approval.enable_session_yolo(new_key)
|
|
approval.disable_session_yolo(old_key)
|
|
|
|
|
|
def with_session_yolo(model_config: Optional[dict], session_key: str) -> Optional[dict]:
|
|
"""``model_config`` for a session row being created, carrying a live bypass. Rows are created lazily on
|
|
the first turn, so this is where a toggle made before the row existed gets recorded."""
|
|
if not approval.is_session_yolo_enabled(session_key):
|
|
return model_config
|
|
return {**(model_config or {}), "yolo_mode": True}
|