Repository navigation
sessions: Hermes' own state.db as a source for fragment session import - #750
Conversation
`fragment session import` reads Hermes' state.db (or a Hermes home), read-only in one transaction, following Hermes v0.21.7: a conversation is a session and its compression continuations (markers bound to the parent, as Hermes' pruning and display lineage bind them); its messages what Hermes shows, each once. A delegate's or a tool's run is a side task, its parent gone or not. A better read than the export's (reader 20 to 10). Bounded: SQLite hands over at most what a conversation could keep of each row (a tool's output and reasoning their cut's worth, with their length, NUL-safe), a conversation past twice its limit is refused alone, a database past 200,000 sessions whole; each conversation goes to the spool as it is read. Any database under this computer's Hermes root is refused. Checked against what Hermes v0.21.7 writes: hermes-v0.21.7.sql, made by its own SessionDB in our image (`regenerate_the_hermes_fixture`, run by name), read by a host test and by the e2e's sessions section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for this stack layer at 9d24d115800767750f349bebd53b55560304bbea, against #744 at 222c0c1ae07b50cf49cfde5e3477136065f17889.
Blocking:
- Enforce the conversation read budget while iterating messages (
cli/src/session/hermes.rs:617–625).rows.collect::<rusqlite::Result<Vec<_>>>()materializes every message in one Hermes session beforeheldis measured. The per-cell SQL cuts do not bound the number of rows. Importing a large session (including retained compacted/inactive rows) therefore consumes memory proportional to that session and can exhaust memory instead of refusing just that conversation and continuing.--kinddoes not avoid this read either.
I reproduced this with a temporary host test using one session with 64 one-MiB message rows: the refusal reported 64.0 MiB retained, rather than stopping at the first row past the 16 MiB read budget (17 MiB). The probe was removed afterward. Consume rows incrementally, account for their retained allocation before accumulating them, and abandon the conversation immediately at the budget. Include row overhead/metadata or a row-count bound so empty-content rows cannot bypass the memory bound. Test a single oversized session, a chain crossing the budget, and a following small conversation that still imports.
The existing session tests pass (16 passed, 1 ignored: fixture regeneration). CI check and images check pass; e2e shards and real-Hermes Docker remain pending. I did not run those integration suites locally.
Non-blocking: the SQLite reader is isolated and reuses the existing spool, but the CLI now owns substantial version-specific Hermes semantics. The image-export alternative already discussed in the PR would keep that maintenance coupled to the pinned runtime; this placement concern is not the blocker above.
The re-review's must-fixes: SQLite now hands over only what a conversation keeps of each row, parsing structured content in place (`kept_content`: text parts, a mark for each medium, never a picture's data) and tool calls (`kept_calls`: ids, name, the start of the arguments with their whole length), so a screenshot's result reads as its words and `[image]` again, and the size check counts kept bytes: a chat of photos, or of long file writes, is imported, not refused. And its should-fixes: the size check stops as rows are read; a platform computer's own databases are refused only as its agent (a person's own Hermes, which sets HERMES_HOME too, is theirs to import), the check pure and tested so; the too-many-sessions refusal no longer suggests --kind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for this stack layer at ad1dbaae35bf5d02b896c6b4ca3d6c8c9a361b2a, against #744 at 222c0c1ae07b50cf49cfde5e3477136065f17889.
The earlier whole-session collection is fixed: the read budget is now checked during iteration. The row-overhead part of the earlier feedback remains unaddressed (noted below). The new SQL content projection introduces a reproducible data-loss blocker.
Blocking:
- Plain JSON arrays can truncate the imported conversation (
cli/src/session/hermes.rs:524, 537–542, 704–706). For an ordinary message containing["red", "blue"],json_eachyields SQL stringsredandblue; passing those tojson_type(p.value, '$.type')orjson_extract(p.value, '$.text')raisesmalformed JSON. The message iterator then ends, butread_instill spools the accumulated prefix as a successfully read conversation. A temporary host test with three messages—before,["red", "blue"],after—produced that diagnostic and archived onlybefore. This affects ordinary user/assistant text and tool outputs containing string arrays, not just malformed databases.
Guard element types before applying JSON-object functions in both kept_content and kept_length, preserving arrays that are not typed content parts as plain text. Also refuse the affected conversation on a SQLite iteration failure instead of publishing a partial prefix. Cover string and mixed arrays, genuine structured parts, and a read failure followed by another importable conversation.
Non-blocking: held still counts only content, calls and reasoning, excluding MessageRow storage and its other strings; empty-content rows can accumulate without advancing the budget. Add a row-count/allocation bound to complete the earlier bounded-memory feedback. The reader remains isolated and reuses the spool; its version-specific SQL projection is the main new maintenance cost.
Validation: cargo test -p fragment-cli session:: -- --nocapture passes (17 passed, fixture regeneration ignored). The added three-message reproduction fails and was removed afterward; the checkout is clean. CI check and images check pass; e2e shards and real-Hermes Docker are pending. Those integration suites were not run locally. The referenced engineering-style file is absent on this machine.
…ream' into claude/session-archive-hermes # Conflicts: # templates/sessions/README.md
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for this stack layer at 9074270b25be5ec7ce80dbc20cebfeaf27e2eae6, against claude/session-archive-export-stream at 63c309f1b6451804ff2971c2dbbccfb560c287ee.
The prior JSON-array/partial-import blocker remains: hermes.rs is unchanged from my previous reviewed head. The earlier whole-session collection fix remains intact.
Blocking:
- Ordinary JSON-array text truncates a conversation (
cli/src/session/hermes.rs:524, 537–542, 704–706).json_eachyields SQL strings for elements of["red", "blue"]; the object JSON functions applied to those values raisemalformed JSON. The iteration error is logged, but the accumulated prefix is still spooled as a successful conversation. On this head, a temporary host test withbefore,["red", "blue"],afteragain archived onlybefore. This affects user/assistant text and tool output containing string arrays.
Guard element types in both kept_content and kept_length, preserving non-part arrays as plain text. Refuse the entire affected conversation on query/iteration failure, including failures in a compression continuation, instead of publishing a partial prefix. Cover string/mixed arrays, genuine structured parts, and an affected conversation followed by another valid one.
Non-blocking: the previously noted allocation accounting gap remains (held omits row overhead and other strings). The isolated reader reuses the spool appropriately; the version-specific SQL projection remains its main complexity cost.
Validation: existing session tests pass (20 passed, fixture regeneration ignored). The temporary three-message regression test fails (1 archived message versus 3 expected); removed afterward, checkout clean. CI check, images check, e2e shards and real-Hermes Docker were pending when inspected. Full integration suites were not run locally; the referenced engineering-style file is absent on this machine.
…on-archive-hermes
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for this stack layer at 03c9a57491999d4bb94d295155dac8aeea4b2f90, against #757 at 31e8cdf76c2888c4f8627ac42e398bc7505841af.
The previous JSON-array/partial-import blocker remains: cli/src/session/hermes.rs is unchanged from my last reviewed head. The earlier incremental read-budget fix remains intact.
Blocking:
- Ordinary JSON-array text truncates a conversation (
cli/src/session/hermes.rs:524, 537–542, 704–706). For["red", "blue"],json_eachyields SQL strings; the JSON-object functions applied to those values raisemalformed JSON. The iteration error is recorded, butread_instill spools the accumulated prefix as a successful conversation. A temporary host test on this head withbefore,["red", "blue"],afterarchived onlybefore(1 message instead of 3). Ordinary user/assistant text and tool output can trigger this.
Guard element types in both kept_content and kept_length, preserving non-part arrays as plain text. Refuse the entire affected conversation on query/iteration failure, including a failure in a compression continuation, rather than publishing a partial prefix. Cover string/mixed arrays, genuine structured parts, and a failed conversation followed by a valid one.
Non-blocking: the previously noted allocation accounting gap remains: held excludes row storage and other strings, so empty-content rows do not advance the budget. The reader appropriately reuses the existing spool; its version-specific SQL projection remains the principal complexity cost.
Validation: ran cargo test -p fragment-cli session:: -- --nocapture with the temporary regression test: all 22 existing tests passed, fixture regeneration was ignored, and the added reproduction failed as above. Removed the probe afterward; checkout clean. CI images check passed; check, e2e shards and real-Hermes Docker were pending when inspected. Full integration suites were not run locally. The referenced engineering-style file is absent on this machine.
…on-archive-hermes
…read in part is refused Alex's review of #750: - A message whose text is a JSON list but no list of typed parts (a person's `["red", "blue"]`) handed its string elements to SQLite's object functions, which failed the row with "malformed JSON"; the row iterator ended and the part read before was spooled as the whole conversation. `typed_parts` now checks each element is an object with a string `type` inside a `CASE` (evaluated in order) before any object function sees it, in `kept_content` and `kept_length` alike; a list that is no parts (strings, mixed, empty) is kept as the text it is. `kept_calls` keeps only the entries that are objects. - A read that fails anywhere in a conversation (its root's session or a continuation's) now refuses that conversation whole, saying why; it is never kept as the part read, and the next is read. The reader has no other problems to say, so it returns `()`. - Each row read counts itself (`MessageRow`) and every string it read toward the conversation's bound, so rows with nothing in them cannot pass it. Tests (each fails before): text lists of strings, mixed and empty lists, a tool output that is a list, typed parts with an image and a call list holding a string, all kept with the messages around them; a conversation whose continuation SQLite fails on (a view raising for one row) refused whole, the next imported; a session of more empty rows than the bound has room for refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Fixed at 49f93f9 ( 1. Ordinary JSON-array text truncated a conversation.
Non-blocking, row accounting: done too. Each row now counts Placement: Paul decided the reader stays in the CLI (option A). The PR body now says so.
|
alexlwn123
left a comment
There was a problem hiding this comment.
Pass for this stack layer at 49f93f9afde3346e47a13a44c8d9ecf5a53aec03, against #757 at 2ba6eb983d34a3febb8cd2d3315fc984175a38cd.
The earlier JSON-array/partial-import blocker is fixed: both content projections guard element types, non-part arrays remain text, and a query or iteration failure refuses the whole conversation, including its compression continuations, before anything is spooled. The earlier incremental-budget fix remains intact; row storage and all retained strings now count too, resolving the empty-row accounting feedback.
The reader adds Hermes databases to the existing import/spool path without changing the archive format. Its version-specific SQL remains the main maintenance cost, but this correction reuses one typed-parts predicate across both projections and stays within the reader. I found no blocking regression in this layer.
Validation: cargo test -p fragment-cli session:: -- --nocapture passed (25 passed, fixture regeneration ignored), including string/mixed/empty arrays, genuine content parts, a failing continuation followed by a valid conversation, and empty-row budgeting. CI check and images check pass; e2e shards and real-Hermes Docker were pending when inspected. Full integration suites were not run locally. The referenced engineering-style file is absent on this machine.
Hermes' own
state.dbas a source forfragment session import, split out of #744 at the review's request. Stacked on #757 (the export read as typed records, streaming), on #744.It serves people who ran Hermes elsewhere (a
state.db, or a Hermes home). The old Finite's customers don't need it: their export never includesstate.db.What it adds
fragment session import <state.db | Hermes home>(cli/src/session/hermes.rs). It opens the database read-only, in one read transaction, and follows Hermes v0.21.7:_CHAIN_STEP_SQL's choice among several). Fork markers are bound to the parent's id, as Hermes' pruning and display lineage bind them, so a continuation that inherits a marker still joins its chain. A reset that compresses later is one conversation.cron: a scheduled job's run.subagent: a delegate's or a tool's run, whether its parent is gone or not, per the review. Hermes' picker hides_delegate_fromrows, and a deleted or pruned parent leaves them orphaned.chat: a root, a branch, or a reset.subagent: a passed-over continuation. Hermes shows it nowhere; it's kept, folded away, because it may hold messages._dedupe_display_generations). It leaves out rows taken back,model_onlyrows (including metadata an older Hermes double-encoded) and hidden scaffolding. It keeps summaries, marked as such, and the person's words a summary carried after it.call). The Hermes profile becomesagent.reader20, above the export's 10. As late a copy from Hermes replaces the export's, which kept a compression's tail twice; the export never replaces Hermes'.kept_content, with JSON functions): its text parts and a mark for each medium, never a picture's data;kept_calls);Screenshot taken\n[image], and a chat of photos or of long file writes is imported rather than refused.fragment session import(the old Finite's export) #744's spool as it is read. A multi-GBstate.dbis read in bounded memory.$HERMES_HOME, or the folder holding itsprofiles/). A person's own Hermes on their laptop, which setsHERMES_HOMEtoo, is theirs to import.hermes-v0.21.7.sqlholds Hermes' ownsessions/messagesDDL and the rows itsSessionDBwrote. It covers a compression with a copied tail, an image, a summary carrying the next words, a model-only row, a branch, a reset that compressed, a cron run, a delegate, and a pruned delegate. A host test reads it; the e2e imports it.regenerate_the_hermes_fixture(ignored, run by name withHERMES_IMAGE) remakes it in our image when Hermes moves.Where it lives
In the CLI (Paul, 2026-10-30): it serves anyone with the
fragmentCLI, including someone moving from a self-hosted Hermes who never runs our image.readerand the committed fixture make a fix reach archives made before it, and make drift from Hermes visible.Complexity ledger
cli/src/session/hermes.rs(~560 lines + tests),hermes-v0.21.7.sql(11 KB fixture)mod.rs(source)cli/Cargo.toml,crates/e2e/Cargo.toml(already in the lock via core's host side)state.dbrelease.yml(run it before the next CLI tag)crates/e2e/src/lanes/sessions.rs(the fixture as astate.db)Tests
hermes.rshost tests:\0json:with an image, legacy part lists, call ids, a long tool output and reasoning cut, saying how many more there were;write_filecalls of 1 MiB each (cut, saying how much more), a list that is no parts, a string Hermes encoded;sessions(11 passed locally): adds "Hermes' own database is imported as Hermes joins its conversations, by its better read".cargo xtask checkpasses.🤖 Generated with Claude Code