Repository navigation
Description
Parked (Paul, 2026-10-30): hand this to Alex only once session import (#744 and its stack) has shipped to finite.chat and been validated end to end there. Until then, move.finite.place's import.md keeps its current step 4.
Below: the replacement step 4 for import.md, what else in the guide changes, five places export.py diverges from Hermes v0.21.7, and the exporter changes media would need (#753). Also ask Alex for the largest chat file his exporter has produced (the importer's per-file cap is 512 MiB, #757).
For Alex, 2026-10-30. Paul asked for one generic CLI command in place of
the hand-built "old chats" app: fragment session import
(#744, the first of a stack). Below are the
replacement text for import.md's step 4, its output, what to change
elsewhere in the guide, five places where export.py diverges from the
Hermes v0.21.7 it reads, and the exporter changes media would need.
When to switch. The new agent runs the fragment CLI that its
computer's Hermes image carries, so step 4 works once an image with #744
is live: a canary round after it merges, then production. Until then
fragment session import answers "unrecognized subcommand". Keep the old
step 4 until the image ships. The sidebar heading and the reader (PR 2)
should follow #744 closely; without them the archive exists but the
shell doesn't show it.
The new step 4 (replaces "## 4. Old chats, in a private app")
## 4. Old chats, in your owner's Session Archive
The old agent's chats go into your owner's Session Archive: a private
place of theirs that their sidebar lists under "Session Archive". They
can read every old chat there and pick one back up with you. One command
does it, and it is safe to run again: it adds nothing twice.
fragment session import ~/finite-import/export
It prints what it brought over:
Session Archive sessions--k3x9 (made now): 212 added, 0 updated, 0 already there
It holds 212 conversations: 180 chats, 30 scheduled runs, 2 side tasks.
- A line `not imported: <id> (…)` names a conversation it left out, and
why (one larger than 8 MiB). Say so in your report.
- A line `could not read: …` names a part of the export that was
damaged. The rest came over.
- If it stops with an error, run it again. A second run picks up where
the first stopped and adds nothing twice.
The old chats are your owner's history with the agent you now are. Read
them when it helps: what your owner asked for, what they care about, what
was decided. Pull the archive into a folder and search it with your own
tools:
fragment sync sessions--k3x9 --dir ~/finite-import/archive --mode pull
grep -ril "<a word>" ~/finite-import/archive/sessions/
Each file there is one conversation (`title`, `started_at`, `ended_at`,
and its `messages`, each with `role`, `at` and `text`).Also in import.md:
- Step 10 ("Tell your owner"): replace the "Chats from the old
Finite" link with: "your old chats are under Session Archive in your
sidebar". - Step 4's guard: export.md's step 6 already ships the zip whole, so
nothing else changes. The import reads onlychats/(index.jsonand
the files it names), and nothing outside it. - Drop the "not yours to learn from" warning (Paul): the new text
above invites the agent to use the archive. - Drop
chat-archive/(index.html.txt, archive.css, archive.js) from
move.finite.place once no guide links it.
What the command does, briefly
- It accepts the export's root, its
chats/, Hermes'state.db, or a
Hermes home. - It writes into one fragment per person, kind
sessions, private, made
at the first import. The agent writes for its owner (FRAGMENT_FOR, as
the bridge sets it), so the archive is the owner's and the agent is an
editor of it. - It stores one file per conversation (
sessions/hermes-<id>.json), plus
index/chat.json,index/cron.jsonandindex/subagent.json, newest
first. The format issession-archive/1, documented in the fragment
repo attemplates/sessions/README.md. Its fields are export.py's
record and message fields, exceptparts(export.py'ssessions) and
that it carries nofile. - A conversation is keyed by its Hermes root id: the same id in an export
and in astate.dbis the same conversation. An older copy never
replaces a newer one. - Limits:
- 20,000 conversations per archive: past that, nothing is written.
--kind chatbrings only the chats. - 8 MiB per conversation: a larger one is left out and named.
- Tool output and reasoning are cut at 2,000 characters, as export.py
cuts them.
- 20,000 conversations per archive: past that, nothing is written.
Five places export.py diverges from Hermes v0.21.7
All five are in chats (export.py's chains, conversation,
message_text). The importer follows Hermes, and repairs (4) in exports
already made.
-
Which continuation. Of several children of a session that ended
compression, export.py takes the earliest started (minover
continuation_rank). Hermes'_CHAIN_STEP_SQLprefers a child that
itself endedcompression, then one still open, then the most
recently active (its newest message or heartbeat), then the latest
started. It also never follows a reset child (_reset_from). After a
stale websocket'sws_orphan_reapsibling, the export can follow the
wrong branch. -
Message order. export.py orders by
timestamp, id. Hermes orders
byid("clocks regress",get_messages). -
Duplicates across a compression. A compression copies the
protected tail into the next session. Hermes shows each message once
(_dedupe_display_generations; an assistant's tool calls are keyed by
their call ids). export.py shows both copies. -
Structured content (the one that bites today). v0.21.x stores
multimodal content as"\x00json:" + json.dumps(parts)
(_encode_content).message_textonly checks for a leading[, so
every message with an image exports as raw\x00json:[{"type": "text", …}, {"type": "image_url", …}]text, base64 data included.
Fix: strip the prefix, then parse as you do now:if isinstance(content, str) and content.startswith("\x00json:"): try: content = json.loads(content[len("\x00json:"):]) except ValueError: return content if isinstance(content, str): return content if isinstance(content, list): return "\n".join(t for t in (part_text(p) for p in content if isinstance(p, dict)) if t)
-
Scaffolding rows. Hermes never shows rows with
display_metadata.model_only, nordisplay_kind = 'hidden'rows that
are not summaries. The export includes both. A summary row can carry
the person's next words after--- END OF CONTEXT SUMMARY — respond to the message below, not the summary above ---. Hermes shows those
words as the person's message; the export folds them into the
summary.
Media: where Hermes keeps it, and what the exporter would change
What the importer side accepts (#753): a message
may carry attachments: [{name, media_type, size, sha256, path}], with
path relative to the export's chats/. The importer uploads each file
at most once, keyed by its SHA-256, as a blob of the archive (files of
1 MiB or more are blobs on this platform anyway). The reader shows images
inline and the rest as files. Without them the importer keeps the
[image]-style placeholders it has now.
Where Hermes v0.21.7 keeps a message's media (read from its source):
-
Almost never in
state.dbitself. A row is written through
_durable_content(agent/session_persistence.py), a text-only
projection: a list of parts keeps its text, and each image becomes
[screenshot]. An image sent to a vision model natively goes to the
model as a base64 data URL (agent/image_routing.py,
build_native_content_parts), but the row keeps the text part, which
names the file:[Image attached at: <path>]. -
In the gateway's caches, briefly. What a person sends through a
gateway platform is saved under$HERMES_HOME/cache/images/,
cache/audio/andcache/documents/(gateway/platforms/base.py).
The person's message text names each file:- an image:
[Image attached at: <path>](native), or a vision
description (text mode); - audio and video:
[The user sent an audio file attachment: '<name>'. It is saved at: <path>. …](gateway/run_inbound.py); - documents: a context note naming the path, with the text inlined
when it is text.
These caches are cleaned of files older than
MEDIA_CACHE_MAX_AGE_HOURS(24 hours,agent/provider_media.py), so
for most old chats the bytes are gone. Only the last day's survive, and
any the person also saved elsewhere. - an image:
-
What the agent sent goes out as
MEDIA:<path>lines in its reply
text, pointing at files in its workspace. Those survive as long as the
workspace file does.
So carrying media is worth it for what survives: files the agent sent
(MEDIA: paths still in the workspace) and the last day's caches. Most
older images, voice notes and documents are already only their
placeholders and names.
Exporter changes this would need:
- Collect each medium a kept message names that still exists:
[Image attached at: <path>]and theIt is saved at: <path>notes
(cache paths),MEDIA:<path>lines (workspace files), and, for rows
older Hermes wrote, data URLs in parts. Read each file at most once,
keyed by SHA-256. A path that's gone stays a name in the text. - Write each one to
chats/media/<sha256>.<ext>(one copy however
many messages name it). On the message, addattachments: [{name, media_type, size, sha256, path: "media/<sha256>.<ext>"}], and keep
the[image]placeholder intextas the fallback for readers that
don't know attachments. - Bound it like the workspace: skip media over a per-file cap (say
25 MB) and count it inskipped(kind: "chat_media",files,
bytes). Count media towardfits; media often pushes an export
over 25 MB, which is when the Drive path applies. - Leave out anything the person didn't send or receive in a chat
(tool caches, screenshots a browser tool took, unless a message names
them). - Manifest: list
chats/media/files inmanifest.jsonwith their
SHA-256, as every other file is.
Activity