In-place compaction is the default. It soft-archives every earlier row of a session under the same id (active = 0, compacted = 1), and Desktop and the dashboard still show those turns. The md/qmd export read the session through export_session -> get_messages with the default live-only clause, so it wrote only the compaction summary and the carried tail. verify_export_file then compared the file with that same dict, and delete_session removed every row of the session, including the archived turns that never reached the file. The md/qmd export now reads the display history (include_compacted), for a single session and for --lineage logical. export_session and export_session_lineage take include_compacted, off by default: import_sessions inserts every message as live context, so the JSON export and stranded-session adoption keep reading live rows only. The other transcripts people read had the same hole without the delete: `/save md` and `/save html` (CLI and gateway), `sessions export --format html` (one session or all of them) and `--only user-prompts` each held 1 of 6 answers on a six-turn session after one compaction. They now read the display history too (SAVE_TRANSCRIPT_FORMATS; export_all gains include_compacted and reads per session then, since the display read dedupes per session). `/save json`, JSONL and the dashboard's JSON export stay live-only for the import reason above. Before deleting, the verify step also re-counts the store's display rows for every session the file covers and refuses on a mismatch. A message that lands while the files are written, or a later export change that reads a narrower view, now refuses the delete instead of being removed unseen. Like the adoption retire loop, the re-count runs just before delete_session, not inside its transaction. Rewind rows (undone turns, the superseded originals of a carried tail) are still deleted without being exported, as `hermes sessions delete` does: they are not part of the history the session shows. Measured through the real CLI on a session with 6 turns and one default in-place compaction (15 rows, 13 shown): before, 3 messages were exported and all 15 rows deleted, with answers 1-5 missing from the file; after, 13 messages are exported in display order, then deleted. (cherry picked from commit adeaff1e33f1ae2b8a066ef2374bb29ade50b3b8)
44 lines
1.3 KiB
Python
44 lines
1.3 KiB
Python
"""`hermes sessions export` single-file formats accept a directory as OUTPUT."""
|
|
|
|
import json
|
|
import sys
|
|
|
|
import hermes_state
|
|
import hermes_cli.main as main_mod
|
|
|
|
|
|
class _FakeDB:
|
|
def resolve_session_id(self, session_id):
|
|
return "sess-123"
|
|
|
|
def export_session(self, session_id, include_compacted=False):
|
|
return {"id": "sess-123", "source": "cli", "messages": [{"role": "user", "content": "hi"}]}
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
|
|
def _export(monkeypatch, *argv):
|
|
monkeypatch.setattr(hermes_state, "SessionDB", lambda *args, **kwargs: _FakeDB())
|
|
monkeypatch.setattr(sys, "argv", ["hermes", "sessions", "export", "--session-id", "sess", *argv])
|
|
main_mod.main()
|
|
|
|
|
|
def test_jsonl_export_into_directory_writes_default_named_file(monkeypatch, tmp_path):
|
|
out_dir = tmp_path / "saved"
|
|
out_dir.mkdir()
|
|
|
|
_export(monkeypatch, f"{out_dir}/")
|
|
|
|
written = out_dir / "hermes_session_sess-123.jsonl"
|
|
assert json.loads(written.read_text(encoding="utf-8"))["id"] == "sess-123"
|
|
|
|
|
|
def test_jsonl_export_to_file_path_is_unchanged(monkeypatch, tmp_path):
|
|
target = tmp_path / "one.jsonl"
|
|
|
|
_export(monkeypatch, str(target))
|
|
|
|
assert json.loads(target.read_text(encoding="utf-8"))["id"] == "sess-123"
|
|
assert target.is_file() and not (tmp_path / "hermes_session_sess-123.jsonl").exists()
|