Conversation
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).
|
CI is queued as End-to-end receipt (mirror into a copy of a live 395-session And the kimi crash this also fixes, before/after: |
There was a problem hiding this comment.
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
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() |
| | **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.
|
All three reproduced failures are fixed in 519f823, plus the nits. Each verified against a copy of a real 395-session 1. Missing store →
2. Title collision keeps a title Suffix is 3. Moved the existence check into Nits done
New tests (22 total, green on py3.9 and py3.12 with the exact CI commands): missing store → End-to-end still holds on a real store — a collision-suffixed mirror resumed through Hermes and answered from the imported history: One note on |
|
Superseded by #2, which now contains the complete Hermes adapter, Agy writable-store, and Agy chat-picker changes. |


What
Adds a Hermes Agent adapter (the 10th agent) — both directions:
/resume/ picker andhermes -c.Hermes stores everything in one SQLite database,
$HERMES_HOME/state.db(default~/.hermes/state.db), insessions+messages. Chats aresource='cli'; sub-agent/compaction children are filtered out viaparent_session_idand the_delegate_frommarker inmodel_config.Mirrors are marked with
origin_json = {"imported_from":{"tool":"samethread"}}, whichread()turns intois_hop— so hop never re-adopts its own mirror. Themessagestable already carries AFTER-INSERT FTS/display triggers, so a plainINSERTkeeps the search index correct; nothing FTS-specific is written.Two things worth a reviewer's eye
modelis left NULL on purpose.hermes --resumereuses the session's stored model, and a placeholder likehop-importis 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.connect()falls back tomode=rwwhenmode=rofails. A WAL-mode db whose-wal/-shmsidecars are gone (Hermes exited cleanly) cannot be opened read-only — andhop syncusually runs precisely when Hermes is not running.sqlite3.connectis lazy, so the open is forced with apragmainside thetry.Also fixes two pre-existing bugs found while wiring this up
kimi:updatedwas the raw ISO string fromstate.json, sosync.adopt_new'ssorted(..., key=...['updated'])crashed withTypeError: '<' not supported between instances of 'str' and 'int'.hop syncdied outright on any machine with a Kimi session. Now converted withcore.iso_ms.kimi: added the missingiso_msimport.Verified
pip install -e . && python -m unittest discover -s tests -v). Suite also stays isolated whenHERMES_HOMEis set (CI has it unset, so this only bit real users).state.db(395 sessions), thenhermes --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-runagainst a real machine now completes (871 mirrors planned across Claude/OpenCode/Hermes/Codex/Kimi) — it previously crashed on the kimi bug.~/.hermes/state.dbwas never modified (all probes on copies).Notes
install()hook: Hermes shell hooks need an allowlist entry plus aconfig.yamledit that a third-party installer shouldn't make. Hermes mirrors land onhop syncor any other agent's sync, same as MiniMax. Easy to add later if a safe path exists.~/.hermesis the default;$HERMES_HOMEis honoured for profiles.