Skip to content

Add a Hermes Agent adapter (10th agent) + fix kimi sync crash - #1

Closed
KennyUMN wants to merge 2 commits into
GodrezJr2:mainfrom
KennyUMN:add-hermes-agent-adapter
Closed

KennyUMN wants to merge 2 commits into
GodrezJr2:mainfrom
KennyUMN:add-hermes-agent-adapter

Conversation

@KennyUMN

Copy link
Copy Markdown

What

Adds a Hermes Agent adapter (the 10th agent) — both directions:

  • reads Hermes chats into every other agent, and
  • writes mirrors into Hermes' own store, so they appear in Hermes' native /resume / picker and hermes -c.

Hermes stores everything in one SQLite database, $HERMES_HOME/state.db (default ~/.hermes/state.db), in sessions + messages. Chats are source='cli'; sub-agent/compaction children are filtered out via parent_session_id and the _delegate_from marker in model_config.

Mirrors are marked with origin_json = {"imported_from":{"tool":"samethread"}}, which read() turns into is_hop — so hop never re-adopts its own mirror. The messages table already carries AFTER-INSERT FTS/display triggers, so a plain INSERT keeps the search index correct; nothing FTS-specific is written.

Two things worth a reviewer's eye

  1. model is left NULL on purpose. hermes --resume reuses the session's stored model, and a placeholder like hop-import is not a real provider model — Hermes rejects it (HTTP 400: hop-import is not a valid model ID) and the mirror becomes unresumable. Claude/Codex ignore that field; Hermes does not.
  2. connect() falls back to mode=rw when mode=ro fails. A WAL-mode db whose -wal/-shm sidecars are gone (Hermes exited cleanly) cannot be opened read-only — and hop sync usually runs precisely when Hermes is not running. sqlite3.connect is lazy, so the open is forced with a pragma inside the try.

Also fixes two pre-existing bugs found while wiring this up

  • kimi: updated was the raw ISO string from state.json, so sync.adopt_new's sorted(..., key=...['updated']) crashed with TypeError: '<' not supported between instances of 'str' and 'int'. hop sync died outright on any machine with a Kimi session. Now converted with core.iso_ms.
  • kimi: added the missing iso_ms import.

Verified

  • Full suite green: 20 tests, on macOS py3.9 (and the repo's own CI command pip install -e . && python -m unittest discover -s tests -v). Suite also stays isolated when HERMES_HOME is set (CI has it unset, so this only bit real users).
  • Real end-to-end: mirror written into a copy of a live state.db (395 sessions), then hermes --resume <mirror-id> -z "what codename…" → the model answered the codename from the mirrored history. Writes succeed against the real schema with its triggers.
  • hop sync --dry-run against a real machine now completes (871 mirrors planned across Claude/OpenCode/Hermes/Codex/Kimi) — it previously crashed on the kimi bug.
  • The machine's real ~/.hermes/state.db was never modified (all probes on copies).

Notes

  • No install() hook: Hermes shell hooks need an allowlist entry plus a config.yaml edit that a third-party installer shouldn't make. Hermes mirrors land on hop sync or any other agent's sync, same as MiniMax. Easy to add later if a safe path exists.
  • ~/.hermes is the default; $HERMES_HOME is honoured for profiles.

Mirrors chats into Hermes' SQLite store ($HERMES_HOME/state.db) so they show
up in Hermes' own /resume, and reads Hermes chats into every other agent.

Writes sessions + messages rows directly (the schema is stable and already
maintained by AFTER-INSERT triggers), marked via origin_json so hop never
re-adopts its own mirror. model is left NULL on purpose: --resume reuses the
stored model, and a placeholder like 'hop-import' makes the mirror unresumable.

Also fixes two pre-existing bugs found while wiring this up:
- kimi: updated was the raw ISO string from state.json, so sync's
  sort-by-updated crashed with str/int comparison (TypeError).
- kimi: list() now converts it with core.iso_ms.

Tests: 3 new (Hermes mirror round-trip + title collision, reading a real
session, WAL store without -wal/-shm sidecars; Kimi ISO updatedAt).
Copilot AI lite review requested due to automatic review settings September 23, 2026 09:45
@KennyUMN

Copy link
Copy Markdown
Author

CI is queued as action_required (first-time contributor — needs a maintainer to approve the run). Meanwhile I ran the exact CI steps locally on both matrix Python versions against this branch:

# python 3.12
pip install -e . && hop --version && python -m unittest discover -s tests -v
  samethread 0.2.0
  Ran 20 tests in 0.012s — OK

# python 3.9
pip install -e . && hop --version && python -m unittest discover -s tests
  samethread 0.2.0
  Ran 20 tests in 0.014s — OK

End-to-end receipt (mirror into a copy of a live 395-session state.db, then resume it through Hermes itself):

$ hop write ...  -> 20260923_164505_84dc07  (source=cli, model=NULL, title='PR handoff (Agy)')
$ hermes --resume 20260923_164505_84dc07 -z 'What codename? Reply with just the codename.'
CODENAME-PAPAYA-99

And the kimi crash this also fixes, before/after:

$ hop sync --dry-run
TypeError: '<' not supported between instances of 'str' and 'int'   # before
done: create 871, new 194                                            # after

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Hermes detection can schedule writes to a missing database, and documentation omits Hermes from resume references.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Adds a Hermes Agent adapter for native SQLite session synchronization and fixes Kimi timestamp handling.

Changes:

  • Adds and registers Hermes read/write support.
  • Normalizes Kimi timestamps.
  • Updates CLI/docs and adds regression coverage.
File Summary Final review notes
tests/​test_core.py Adds Hermes and Kimi coverage. No final review comments.
src/​samethread/​cli.py Updates resume help. No final review comments.
src/​samethread/​agents/​kimi.py Converts ISO timestamps to epoch milliseconds. No final review comments.
src/​samethread/​agents/​hermes.py Implements Hermes SQLite discovery, reading, writing, and mirroring. Moderate (3 votes): missing databases can be detected as writable, causing sync failures.
src/​samethread/​agents/​__init__.py Registers Hermes. No final review comments.
README.md Documents Hermes support. Nit (2 votes): resume references and the flowchart omit Hermes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

key, name, aliases, binary, writable = 'hermes', 'Hermes', ('hermes-agent',), 'hermes', True

def detect(self):
return os.path.exists(DB) or super().detect()
Comment thread README.md
| **Kimi Code** 2.x | ✅ | ✅ event log + custom title | `[[hooks]]` `Stop` in `config.toml` | resume ✔ · hook ✔ |
| **Gemini CLI** | ✅ | ✅ in folders Gemini already knows | `AfterAgent` + `SessionEnd` hooks | listed with title ✔ |
| **Antigravity CLI** (`agy`) | ✅ | ➖ no import API: `hop resume agy` seeds a new conversation | `Stop` hook | read ✔ · hook ✔ |
| **Hermes Agent** | ✅ | ✅ SQLite `sessions`/`messages` + `hermes --resume` | none yet: picked up by any other agent's sync | resume ✔ |
…ision, delete)

1. write() on a store that does not exist yet (Hermes installed but never run)
   raised a raw OperationalError, which is not a HopError, so sync queued the
   chat for retry on every turn forever. It now raises HopError('run Hermes
   once so its state.db exists') before touching anything, like Gemini/Kimi.
2. A title collision retried with no title at all, leaving mirrors untitled
   (Hermes requires unique titles). It now retries with ' · <last 6 of id>'.
3. delete() (hop forget) and retitle() crashed when ~/.hermes was gone. The
   existence check moved into connect() so every rw caller treats a missing
   store as a no-op.

Also from the same review:
- started_at is the first message's time, not the last.
- The delete in write() moved inside the transaction block.
- list() uses one grouped query for message counts instead of two per session.
- Tests: per-test store reset (no test depends on another having run), the WAL
  test seeds its own session, HOP renamed to HERMES_HOME_DIR.
- Docs: pyproject description/keywords and the README 'Tested on' line mention
  Hermes. Hero image left for the maintainer's make_hero.py.
- Added 3 tests: missing store -> HopError + delete no-op, title collision
  keeps a suffixed title, WAL store without sidecars.
@KennyUMN

Copy link
Copy Markdown
Author

All three reproduced failures are fixed in 519f823, plus the nits. Each verified against a copy of a real 395-session state.db (its triggers present):

1. Missing store → HopError, not OperationalError

FIX1  HopError: run Hermes once so its state.db exists

write() raises before touching anything; no more per-turn retry queue.

2. Title collision keeps a title

FIX2  1st='Greeting (OpenCode)'  2nd='Greeting (OpenCode) · f548ad'

Suffix is · <last 6 of session id> — same shape the existing OpenCode collision produces.

3. delete() / retitle() with ~/.hermes gone

FIX3  delete with no store: no crash
FIX3  list with no store -> {}

Moved the existence check into connect(), so every rw caller treats a missing store as a no-op.

Nits done

  • started_at is now the first message's time (first<last verified).
  • The delete from messages moved inside the with c: block.
  • list() uses one grouped query for message counts (was 2 per session), like OpenCode.
  • Tests: per-test store reset so no test depends on another having run (the WAL test was order-dependent on real1); the WAL test now seeds its own session; HOP → HERMES_HOME_DIR.
  • pyproject description + keywords and the README 'Tested on' line mention Hermes. Hero image left to you (make_hero.py).

New tests (22 total, green on py3.9 and py3.12 with the exact CI commands): missing store → HopError + delete no-op, title collision keeps a suffixed title, WAL store without sidecars.

End-to-end still holds on a real store — a collision-suffixed mirror resumed through Hermes and answered from the imported history:

$ hermes --resume 20260923_171221_1cf3d1 -z 'what was this conversation about?'
Ngebandingin 5 ide hackathon IFEST dari Google Doc pakai prompt di tab PROMPT, terus nulis
hasilnya ke tab Hasil AI Kenny, termasuk bikin revisi KONEK v3 (ganti WhatsApp API ke
WebRTC + Telegram).

One note on failed 15 in a sync run: those are all OpenCode, from a malformed opencode.json in one workspace — pre-existing and unrelated.

@KennyUMN

Copy link
Copy Markdown
Author

Superseded by #2, which now contains the complete Hermes adapter, Agy writable-store, and Agy chat-picker changes.

@KennyUMN KennyUMN closed this Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants