Skip to content

last: add Hermes CLI host — transcripts live in SQLite and Herdr reports a session id, not a path #25

Description

@jonathannd02

Summary

plannotator-tui last cannot resolve transcripts for Hermes CLI panes. hermes is not in the supported host list, so herdr last (and Herdr Annotate's annotate.last / prefix+shift+o) falls back to the pane's recent screen output instead of the agent's last reply.

Hermes differs from every host you support today in two ways that make it worth its own reader:

  • Herdr reports its session as an id, not a path (AgentSessionRefKind is ["id", "path"]).
  • Hermes stores conversations in SQLite, not in per-session JSONL files. There is no transcript file, so --session <transcript> cannot be used as a workaround either.

Environment

  • plannotator-tui 0.3.0, as bundled by herdr-annotate 0.3.0 (commit 8dc0de1)
  • Herdr 0.8.2; Linux x86_64 and macOS arm64
  • Agent: Hermes CLI; Herdr integration hermes reports current (v5)

What happens

omp-style flow: herdr last passes only PLANNOTATOR_TUI_HOST=hermes plus a pid to the doc pane. hermes is not among claude, codex, copilot, droid, pi, so it takes the documented fallback — … is not supported yet; supported hosts: …— showing the pane's recent output instead — and the header reads hermes · screen instead of hermes · last message.

What Herdr provides

The Herdr integration Hermes ships (~/.hermes/plugins/herdr-agent-state/__init__.py, HERDR_INTEGRATION_VERSION=5) reports identity on on_session_start, on_session_reset and pre_llm_call with:

command = [herdr, "pane", "report-agent-session", pane_id,
           "--source", "herdr:hermes", "--agent", "hermes",
           "--seq", str(time.time_ns()),
           "--agent-session-id", session_id,
           "--session-start-source", start_source]

So herdr agent get <pane> yields agent_session with kind: "id" and value = the Hermes session id. Note the string agent_session does not occur anywhere in the shipped 0.3.0 binary, so this field appears to be unused today — for Hermes it is the only usable handle, since there is no file to discover.

Where the messages live

SQLite at ~/.hermes/state.db (WAL mode; state.db-wal / state.db-shm alongside). Relevant schema, verbatim:

CREATE TABLE sessions (
    id TEXT PRIMARY KEY,
    source TEXT NOT NULL,
    display_name TEXT,
    model TEXT,
    parent_session_id TEXT,
    started_at REAL NOT NULL,
    ended_at REAL,
    message_count INTEGER DEFAULT 0,
    ...
);

CREATE TABLE messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL REFERENCES sessions(id),
    role TEXT NOT NULL,
    content TEXT,
    tool_call_id TEXT,
    tool_calls TEXT,
    tool_name TEXT,
    timestamp REAL NOT NULL,
    reasoning TEXT,
    observed INTEGER DEFAULT 0,
    active INTEGER NOT NULL DEFAULT 1,
    compacted INTEGER NOT NULL DEFAULT 0,
    ...
);

CREATE INDEX idx_messages_session_active
    ON messages(session_id, active, timestamp);

sessions.id is exactly the id Herdr reports. Observed role values: user, assistant, tool, session_meta.

Getting the last reply is one indexed query — verified against a live database (3,215 sessions, 165,079 messages, 1.6 GB):

SELECT content FROM messages
WHERE session_id = ?1 AND role = 'assistant' AND active = 1
ORDER BY timestamp DESC
LIMIT 1;

That returned the correct newest assistant reply. idx_messages_session_active(session_id, active, timestamp) covers it, which matters at this size — a scan is not an option.

Expected

last on a Hermes pane shows that pane's newest assistant reply, with the header hermes · last message, and the recent-replies picker works as it does for Claude and Codex.

Suggested fix

  1. Add crates/plannotator-tui-hosts/src/hermes.rs resolving a session id against ~/.hermes/state.db with the query above; --pick N maps to LIMIT N+1 / OFFSET N over the same ordering. Fixture can be a tiny generated .db next to the existing JSONL fixtures.
  2. Open the database read-only and non-destructively: file:…state.db?mode=ro while honouring the WAL, so a live Hermes process is never disturbed. A stale-read is preferable to touching the agent's own store.
  3. This host has no file transcript, so the reader must accept an id, not a path. That likely means threading Herdr's agent_session ({kind, value}) through instead of only host+pid — which would also fix omp and the other Herdr-integrated agents in one go (herdr integration status lists 17 against your 5 hosts).

HERMES_HOME may be worth honouring for the database root; the default is ~/.hermes.

Happy to test a patch against a real Hermes setup.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions