77 lines
3.6 KiB
Python
77 lines
3.6 KiB
Python
"""Turn-completion ("idle") poster state for the hermes-native harness.
|
|
|
|
The completion->parent-wake path needs a harness to POST an
|
|
``external_session_status: idle`` event to the Sessions API: the server maps
|
|
that edge to a sub-agent turn-terminal and wakes the parent orchestrator's
|
|
inbox (the SAME contract claude-/codex-/opencode-/cursor-native use). Hermes'
|
|
PTY-activity watcher only emits a ``session.status: idle`` SSE edge that drives
|
|
the web "Working…" spinner and never wakes a parent — so without an explicit
|
|
post a hermes-native sub-agent finishes silently and the orchestrator hangs.
|
|
|
|
Unlike cursor-agent, hermes-agent exposes NO per-turn ``stop`` hook (only a
|
|
``pre_tool_call`` hook, used for policy enforcement), so there is no separate
|
|
process writing a turn-end marker. Instead the runner-owned
|
|
:func:`omnigent.hermes_native_forwarder.forward_hermes_store_to_session` poll
|
|
loop *derives* turn completion from Hermes' ``state.db`` itself — an
|
|
``assistant`` row with no ``tool_calls`` is the agentic loop's terminal step,
|
|
i.e. one completed turn (see ``_count_completed_turns`` in that module). The
|
|
``messages`` table is the append-only "marker store"; this module owns only the
|
|
*poster* state: how many of those completed turns have already been turned into
|
|
an ``external_session_status: idle`` post.
|
|
|
|
It is the hermes analog of the poster-state half of
|
|
:mod:`omnigent.cursor_native_status`. Persisting the posted-count means a
|
|
supervisor restart never re-wakes the parent for a turn it already reported.
|
|
Stdlib-only (no httpx) so it stays a cheap, dependency-free state file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
#: Durable poster state: how many completed turns the forwarder has already
|
|
#: turned into an ``external_session_status: idle`` post. Persisted so a
|
|
#: supervisor restart never re-wakes the parent for a turn it already reported.
|
|
_STATE_FILE = "hermes_status_forwarder.json"
|
|
|
|
|
|
def read_posted_count(bridge_dir: Path) -> int:
|
|
"""Load the count of completed turns already POSTed as idle (0 on cold/unreadable)."""
|
|
try:
|
|
data = json.loads((bridge_dir / _STATE_FILE).read_text(encoding="utf-8"))
|
|
except (OSError, ValueError):
|
|
return 0
|
|
posted = data.get("posted") if isinstance(data, dict) else None
|
|
return posted if isinstance(posted, int) and posted >= 0 else 0
|
|
|
|
|
|
def write_posted_count(bridge_dir: Path, posted: int) -> None:
|
|
"""Atomically persist the count of completed turns already POSTed as idle.
|
|
|
|
Persisted only AFTER a successful idle POST so a failed flush is retried (the
|
|
unreported turns stay unreported until the post lands).
|
|
"""
|
|
bridge_dir.mkdir(parents=True, exist_ok=True)
|
|
tmp = bridge_dir / (_STATE_FILE + ".tmp")
|
|
tmp.write_text(json.dumps({"posted": posted}), encoding="utf-8")
|
|
os.replace(tmp, bridge_dir / _STATE_FILE)
|
|
|
|
|
|
def clear_hermes_status_state(bridge_dir: Path) -> None:
|
|
"""Remove the idle poster state so a re-created terminal starts clean.
|
|
|
|
Sibling of :func:`omnigent.hermes_native_forwarder.clear_hermes_bridge_state`:
|
|
the runner calls this when it re-creates a hermes terminal so a stale
|
|
posted-count from a prior terminal can't make the new forwarder skip (or
|
|
re-fire) the ``external_session_status: idle`` parent-wake edge. The
|
|
completed-turn count is derived per ``hermes_session_id`` (not per terminal),
|
|
so after an in-session compaction re-pins to a forked child the forwarder
|
|
rebases this posted-count to the child's current count; only the poster state
|
|
is persisted here, so only it needs clearing.
|
|
"""
|
|
with contextlib.suppress(OSError):
|
|
(bridge_dir / _STATE_FILE).unlink()
|