Skip to content

feat(hermes): pin the adapter to Hermes Agent 0.22.2 and fix the Windows routes it exposed - #2

Merged
gabszap merged 31 commits into
mainfrom
feat/hermes-schema-31
Oct 17, 2026
Merged

gabszap merged 31 commits into
mainfrom
feat/hermes-schema-31

Conversation

@gabszap

@gabszap gabszap commented Oct 16, 2026

Copy link
Copy Markdown
Owner

PR 2 de 3 — empilhado: merge depois do PR 1 (o PR 3 vem em cima deste).

O que entra

Leva o adapter do Hermes Agent de 0.20.6 / schema 26 para 0.22.2 (tag v2026.9.24) / schema 31 e corrige as falhas de Windows que essa rota expôs. É o mesmo trabalho que rodou contra um install 0.22.2 real no Windows (24 sessões, transcrições de 73 MB).

Adapter Hermes

  • Pin 0.22.2 / schema 31, mantendo SessionDB.import_sessions como fronteira de escrita nativa. O --version agora anexa proveniência de build, então o gate casa o prefixo pinado com regex em vez de igualdade exata.
  • Runtime via hermes --print-runtime-command (fallback: o bootstrap do próprio launcher). No Windows o launcher é um zipapp compilado sem shebang utilizável, e o store Python não traz pacotes de terceiros — o helper reproduz o prelúdio do launcher (hermes_bootstrap) antes de importar hermes_state.
  • Resolução do home igual à do CLI: HERMES_HOME quando definido, senão %LOCALAPPDATA%\hermes no Windows e ~/.hermes nos outros. O fallback anterior (~/.hermes) apontava para um diretório onde o CLI nunca escreve.
  • Título duplicado: instala sem título (warning target_title_omitted) em vez de recusar o import atômico inteiro — o índice único de títulos não-nulos é parcial e o importador nativo trata o campo como cosmético.
  • UTF-8 ponta a ponta: o bundle atravessa o processo como bytes (sys.stdin.buffer, decode explícito de stdout/stderr, filho com -X utf8). Como texto ele era codificado com o codec de locale do chamador e trocava todo caractere não-ASCII por U+FFFD dentro do store.
  • Orçamento de import: sobe o cap por sessão do store para o que o bundle precisa — nunca além dos 25 MiB que o import web dele aceita — e registra target_import_budget_raised no manifesto, em vez de recusar sessão com screenshots colados (base64 inline).

Windows

  • Fecha os snapshots de banco: hermes.database_snapshot copiava, commitava e lia o banco temporário com a conexão de destino ainda aberta, então todo root Hermes falhava no scan com WinError 32 e o catálogo reportava zero sessões.
  • write_private_atomic publica com o descritor fechado e aplica chmod no arquivo temporário antes.
  • Identidade de stat assinada: o Windows reporta st_dev/st_ino como inteiros unsigned de 64 bits que o SQLite não consegue bindar, o que derrubava todo root SQLite com scan_failed. Os dois campos passam a ser dobrados para a faixa assinada na gravação e na comparação.
  • Ordem do rollback: fecha a guarda de identidade do manifesto antes do unlink nos seis caminhos de install de banco compartilhado. Antes disso, um install que falhava (claude, codex, hermes, opencode, kilo, …) reportava o WinError 32 da limpeza e escondia a causa.

Fonte Claude Code

  • Loop de compaction: aceita o loop de segmento preservado quando o summary não é filho direto do boundary. Valida a declaração em vez da forma que um build emitiu (attachments intercalam, então o summary pode ficar 3–4 saltos abaixo do boundary, e preservedMessages também nomeia ids do branch resumido). Declaração que não fecha continua falhando. Sem isso, uma transcrição legível de 73 MB com duas compactions era recusada como Claude active graph contains an ancestry cycle.

Testes

Windows, Python 3.12, test_hermes_format.py test_cli.py test_conversion.py test_catalog.py:

falhas
main (a3b0fae) 40
esta branch 8

As 8 restantes são falhas pré-existentes de Windows (temp-file/shim binário) sem relação com a mudança, e nenhuma falha da main some aqui. A suíte completa no Windows ainda tem falhas pré-existentes não relacionadas (fcntl/termios/pty na coleta, locks de temp file).

Validação ponta a ponta contra store real: 24 sessões indexadas; transcrição Claude de 73 MB migrada com 1.794 mensagens, 740 tool calls e imagens inline intactas; segunda migração (Claude → OpenCode) com 1.065 session_message e 31 mídias.

Notas

  • Esta branch é a da pilha (contém o PR 1). Inclui os commits de release 0.12.0 e 0.12.1 do fork.
  • A branch pr/hermes-schema-31 tem o mesmo conteúdo sem os releases e sem o antigravity — reservada para um PR no upstream, não é a deste merge.

elijahr and others added 23 commits October 16, 2026 18:44
- add bidirectional format adapter for Antigravity Desktop (antigravity-desktop)
- support reading and writing SQLite conversation databases, conversation_summaries.db, and agyhub_summaries_proto.pb
- handle both 19-column and 21-column schemas (with raw_summary, group_id) in conversation_summaries.db
- support up to 512 MB native sessions in Antigravity CLI and Desktop
- auto-register projects in app_storage.json and ~/.gemini/config/projects/ when installing desktop sessions
- document the desktop storage architecture in docs/antigravity-desktop-interoperability.md
- add comprehensive unit tests and native language server verification
…tform guards

- add default_language_server_path() supporting darwin, linux, and win32
- add electron_user_data_dir() resolving app_storage.json on macOS, Linux (XDG), and Windows (APPDATA)
- update verify_pinned_desktop() with clear platform diagnostics and unvalidated bypass for Linux/Windows
- document platform support status in docs/antigravity-desktop-interoperability.md
- add platform resolution tests across macOS, Linux, and Windows
…, and Windows

- add verified binary sizes and SHA-256 digests for Linux x64 and Windows x64
- implement PINNED_DESKTOP_SPECS table validating binaries against platform-specific digests
- enhance Linux search order (PATH launcher, /opt, ~/.local/share) and Windows case-insensitivity
- update documentation with complete multi-platform installer matrix
…tion arguments

- rename docs/antigravity-desktop-interoperability.md to docs/antigravity-desktop-format.md to match repository naming convention
- update invocation flags and arguments to reflect verified package launcher details
- update reference in CHANGELOG.md
…file URI paths

- Close SQLite connection handles explicitly before unlinking or cleaning up temporary directories (prevents WinError 32 on Windows).

- Bypass directory fd fsync on Windows NT in conversion.py, jsonl.py, and devin.py to avoid PermissionError [Errno 13].

- Normalize Windows drive letter parsing in file:/// URIs (strip leading slash before drive letter).

- Adjust stat permission test assertion for Windows NT (0o666 vs 0o600).

- Restore standard PyPI endpoints in uv.lock.
…a 31

- Move the Hermes pin from 0.20.6 (v2026.8.27, schema 26) to 0.22.2
  (v2026.9.24, schema 31). Schema 31 keeps every column the adapter requires
  from schema 26, so the source projection and the required-column gates are
  unchanged.
- Match the version line on its pinned prefix instead of the whole line: 0.22.2
  appends build provenance (" Hermes Agent v0.22.2 (2026.9.24) · stable ·
  local <sha>"), which made the exact-equality gate fail on a correct install.
- Keep the full --version report, because it publishes the install directory
  the runtime resolution needs (resolved in the next commit, which completes
  the write path for this release).
- Track the new pin in the native corpus fixture script and parser docs.
- Ask the launcher for its runtime command (hermes --print-runtime-command) and
  reproduce the prelude the published launcher applies to itself: clear
  PYTHONHOME, PYTHONPATH and VIRTUAL_ENV, insert the install root and import
  hermes_bootstrap, which selects and activates the store's dependency
  generation. Store Python ships without third-party packages, so importing
  hermes_state failed on ruamel before this.
- Recover the install root from the "Install directory:" line, or from the
  launcher's own sys.path.insert bootstrap when that line is absent. A Windows
  launcher is a compiled zipapp, so the file head carries no usable shebang;
  the shebang stays as the fallback for builds predating the flag.
- Keep the Windows essentials (USERPROFILE, HOMEDRIVE/HOMEPATH, APPDATA,
  LOCALAPPDATA, SYSTEMROOT, SYSTEMDRIVE, WINDIR, PATHEXT, COMSPEC, TEMP, TMP)
  in the sanitized child environment and mirror HOME into USERPROFILE. Without
  them the CLI cannot resolve a home directory, returns non-zero and prints
  nothing, which surfaced as "observed <unavailable>".
- Initialize a missing state.db through the native SessionDB constructor:
  hermes sessions list no longer creates the store on a read of an absent one.
- Report the launcher's stderr tail when initialization fails instead of a bare
  "failed to initialize" message.
…nlink

Windows refuses to delete a file that still has an open handle, so every failed
shared-database install died with WinError 32 while unlinking its reserved
manifest and masked the original install error. Close the guard first in each of
the six install rollback paths and re-close it as a no-op in the surrounding
finally, so the guard is still released exactly once on every path.
…d range

Windows reports st_dev as an unsigned 64-bit volume serial (15332589137577146414
for an NTFS system volume), which SQLite cannot bind at all: the session INSERT
raised OverflowError, the root scan failed closed, and every SQLite-backed root
(OpenCode, Kilo, Hermes, MastraCode, Devin) ended as scan_failed. Store the
folded value and compare through the same helper so refresh reuse keeps
matching. Adds a regression test with a simulated unsigned identity, covering
storage and the second-refresh unchanged path.
…s limits

- Pin table moves to 0.22.2 (v2026.9.24, schema 31); drop the source-archive
  hash pin, since a git install reports its own build provenance on the version
  line and rebuilds would invalidate a recorded hash.
- Document how the installer reaches the Hermes runtime: the published
  --print-runtime-command, the store Python plus hermes_bootstrap prelude, the
  install-directory recovery, and the Windows essentials the sanitized child
  environment must keep.
- Record the native re-validation performed for this promotion (read-only store
  snapshot, isolated-bundle install, and a real Claude Code to Hermes transfer
  with linked tool calls) and state plainly what was not verified: a live
  resume against a provider, native compaction after import, and WAL snapshots
  under a concurrent writer.
- Update the compatibility, specification, development, troubleshooting, and
  credential-free testing tables to the new pin, and move the native corpus
  fixture script to the new release, with an explicit note that its
  entry-point wrapper bytes are per-install and must be re-recorded.
…title

Hermes enforces idx_sessions_title_unique, a partial unique index over non-null
session titles, and its own importer treats the field as cosmetic: it wraps its
title write in a suppressed exception. The migrator inherited the raw refusal,
so re-migrating a conversation into a store that already held its title failed
the whole bundle with "UNIQUE constraint failed: sessions.title".

- Add hermes.title_taken(), hermes.without_title() and hermes.bundle_title():
  an advisory read-only lookup, a bundle whose only change is the cleared
  cosmetic field, and the title a bundle currently carries.
- Sample the target store before the import and drop the title when it is
  already taken, counting the omission as title:duplicate_in_target and
  reporting a target_title_omitted warning in the manifest instead of failing
  the migration over a cosmetic field.
- The import transaction still enforces the constraint itself: a title created
  by a concurrent writer in between fails closed, with no partial session.
- Covered by a credential-free round-trip test and a conversion-level test;
  verified end to end against a live 0.22.2 store, where the untitled session
  lists in the native picker with all 22 records intact.
The migration manifest carries the title:duplicate_in_target counter and the
target_title_omitted warning; the CLI's content-free stdout summary is built
before any installer runs for every harness, and a dry run writes no manifest.
The bundle crossed the process boundary as text: the caller encoded it with its
own locale codec while the store interpreter always decodes UTF-8, so on a
Windows console whose code page is not UTF-8 (no PYTHONUTF8/PYTHONIOENCODING set)
the install either died with UnicodeEncodeError or replaced every non-ASCII
character of the imported conversation — title and message bodies alike — with
U+FFFD inside the target store.

- Move the payload as bytes (`input=data`, no text mode) and have the helper read
  `sys.stdin.buffer`; decode the helper's stdout/stderr explicitly as UTF-8.
- Start the child interpreter with `-X utf8`, and read the launcher probes with
  an explicit UTF-8 encoding, so neither direction depends on the console code
  page.
- Extract `_run_helper()` so the transport has a single seam, covered by a
  credential-free regression test asserting bytes on stdin and the
  `sys.stdin.buffer` helper.
- Give the opt-in native trajectory accented title/body text (which would have
  caught this) and the Windows essentials in its child environment.
HERMES_HOME still wins, but the fallback is now the CLI's own platform default:
%LOCALAPPDATA%\hermes on Windows (or ~/AppData/Local/hermes without that
variable) and ~/.hermes elsewhere. A bare ~/.hermes on Windows named a directory
the CLI never writes to, so `--to hermes` and automatic root discovery missed
the real store unless the caller exported HERMES_HOME by hand — the CLI finds
its store with no environment at all, and now so does the migrator.

- Thread a `platform` override through hermes_home()/state_database_path() so the
  resolution is testable off-platform, matching the Devin adapter's pattern.
- Extend the resolution test and add one covering both platform defaults, the
  unset-LOCALAPPDATA fallback, blank HERMES_HOME, and the explicit root.
- Update the CLI reference, format doc, and changelog.
The adapter work in this branch (Hermes 0.22.2 / schema 31, the Windows runtime
and home resolution, the UTF-8 import bundle, the duplicate-title rule, and the
signed stat identity fix) plus the Antigravity Desktop Windows fixes are the
0.12.0 release, so stop reporting 0.11.0 from a checkout that behaves
differently.

- pyproject.toml, __init__.__version__, uv.lock, and the CLI assertion move to
  0.12.0.
- Fold the changelog's Unreleased section into "0.12.0 - 2026-10-16".
- Point the CLI reference and the specification at 0.12.0.

The website/ npm package keeps its own 0.11.0 version field; nothing reads it.
A real 73 MB transcript with two compactions failed with "Claude active graph
contains an ancestry cycle" even though its active graph is intact.  The
declaration is the same as the one the validator was written for, but the layout
under it moved: the boundary is now followed by attachment records, then the
compact summary, then the preserved tail — so the summary is no longer the
boundary's direct child — and preservedMessages also names ids from the
summarized branch that are no longer on the active path.

Validate the loop the way the declaration describes it rather than by the shape
one build happened to use: the tail must walk up to the summary and the summary
up to the boundary, with every hop already on the walked path; head/tail must
still appear in preservedMessages, and a boundary whose declaration does not
close over the walk still raises.  Requiring every declared id to be on the path
rejected readable transcripts, because the declared set spans branches the walk
never enters.

- Test the 2.1.300 layout (boundary -> attachment -> summary -> tail) and the
  still-fatal unclosed declaration.
The two boundaries in one real session carry different shapes under the same
declaration — one is the summary's direct parent, the other sits four hops below
it, with the attachments the CLI interleaves in between.  Attribute the variance
to the interleaved records rather than to a build change, and say what the
validator actually requires.
The store's CLI import refuses any one session above 5 MiB while its own web
import accepts 25 MiB, so a portable session carrying a pasted screenshot
(inline base64 image) was rejected as "session exceeds the import size limit"
although nothing about the bundle is invalid — a real 73 MB Claude transcript
produced a 6.88 MB bundle whose rows are 4.5 MB of user-pasted PNGs.

- Size the bundle exactly the way the store does (`measured_session_bytes`,
  ignoring the derived `timings`) and request `max(5 MiB, min(measured, 25 MiB))`
  from the import helper, which applies it to `SessionDB._IMPORT_MAX_SESSION_BYTES`
  before opening the database.  The vendor ceiling is the same one its web import
  uses, so the migrator never exceeds what Hermes accepts elsewhere.
- Report `target_import_budget_raised` (bundle bytes, default, applied budget) in
  the manifest whenever the budget moves, so a raised cap is never silent.
- Carry the session payload on `ParsedHermesBundle` instead of re-parsing it.
Both changes after 0.12.0 are backwards-compatible bug fixes: a readable Claude
transcript refused as an ancestry cycle, and a valid bundle refused by the
store's 5 MiB import cap. No CLI surface changed and the manifest schema stays
at 2; the only additions are a keyword-only `platform` override and the
`target_import_budget_raised` warning.

- pyproject.toml, __init__.__version__, uv.lock, and the CLI assertion move to
  0.12.1.
- Add the 0.12.1 changelog section covering both fixes.
- Point the CLI reference and the specification at 0.12.1.
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

🟢 Contributor eligibility requirements met for @gabszap.

  • 🟢 Account created: 2020-07-15; age: 2285 days (required: at least six calendar months).
  • 🟢 Contributions dated on or before 2026-09-17: 336 or more (required: at least 100).

Pull request authors must have a GitHub account at least six calendar months old and at least 100 GitHub contributions dated on or before the date one calendar month before the check.

See the contributor eligibility policy.

The new Hermes code carried five over-length lines and three files the pinned
ruff 0.16.10 would reformat, so the python matrix jobs failed at the first gate
before any test ran.
The adapter pin moved to 0.22.2 (v2026.9.24, d923a5eb) but the credential-free
gate still installed 0.20.6 (5fc308a7), so the exact-client check compared the
installed revision against the new pin and failed.

- install-native-test-clis.sh: tarball url, recorded revision and the expected
  archive hash for the 0.22.2 source tree.
- rename the native trajectory test and the corpus skip messages to the new
  release.
0.22.2 adds auto_archived, compression_overload_streak,
compression_recovery_deadline, created_source, timings, tool_names and
transport_profile to the official session export, plus absorbed_message_uids,
message_uid, tool_call_uid and tool_call_uids on each message. tool_names
carries the runtime tool catalog, so join system_prompt in the private-field
scrub.

Also scrub the JSON-escaped variant of the private cwd: on Windows the raw
cwd contains backslash separators, which json.dumps escapes in
tool_calls arguments, defeating the plain-text replace.
Recaptured portable-rich against the pinned v0.22.2 build (d923a5eb) on
linux-x86_64 through the loopback provider: schema 31 state.db, 8 messages,
2 tool calls/results, user image accepted (textual fallback), document,
audio and video rejected by the CLI image surface. The sanitizer run lists
schema-driven session and message scrubs plus runtime-ownership removal.

Route and source consumers now load the hermes fixture from
sources/hermes/0.22.2/portable-rich; the loopback context in
capture-hermes.py and test_hermes_native.py rises to 131072 tokens so the
0.22.2 client (which requires a 64k minimum) accepts the scenario.
…0.22.2

install-native-test-clis.sh synced the pinned Hermes tree with Python 3.13,
but the d923a5eb lockfile's supported environments require at least 3.14, so
the exact-client gate aborted before running any Hermes tests. And the
central catalog test fixture hand-wrote a schema_version 26 store, which the
0.22.2 adapter now refuses, so its shared-database assertions saw a root
error. Raise both: sync with 3.14 and stamp the fixture store as 31.
The exact-client gate pins Hermes Agent 0.22.2, but the installed tree is a
gitless tarball extraction, so the client resolves its own version report as
"Hermes Agent vunknown (2026.9.24)" and verify_client refuses it before any
Hermes probe runs. Recreate a minimal checkout in the install directory with
the pinned release tag so the runtime reports Hermes Agent v0.22.2 (the
emulated CI flow was validated on WSL: reported version, wrapper shape, and
uv sync with Python 3.14 all match the pinned environment).
Hermes 0.22.2 names a session that closes without a title, so the request
logged last after a resumed CLI run is the title-generation call; the test
read the wrong provider request and failed against the literal chat call.
Select the last chat request (skipping the "You name chat sessions" system
prompt) as the replay target. Validated on WSL against the emulated CI
install: the exact 0.22.2 client runs the resumed flow and the markers land
in the chat request as before.
@gabszap
gabszap force-pushed the feat/hermes-schema-31 branch from bbabb68 to 6dbfdab Compare October 17, 2026 03:42
@gabszap
gabszap merged commit 4446946 into main Oct 17, 2026
23 checks passed
@gabszap gabszap mentioned this pull request Oct 17, 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.

2 participants