Skip to content

Session import: switch move.finite.place import.md step 4 to fragment session import (after it ships) #786

Open

Description

@futurepaul

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 only chats/ (index.json and
    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.json and index/subagent.json, newest
    first. The format is session-archive/1, documented in the fragment
    repo at templates/sessions/README.md. Its fields are export.py's
    record and message fields, except parts (export.py's sessions) and
    that it carries no file.
  • A conversation is keyed by its Hermes root id: the same id in an export
    and in a state.db is the same conversation. An older copy never
    replaces a newer one.
  • Limits:
    • 20,000 conversations per archive: past that, nothing is written.
      --kind chat brings 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.

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.

  1. Which continuation. Of several children of a session that ended
    compression, export.py takes the earliest started (min over
    continuation_rank). Hermes' _CHAIN_STEP_SQL prefers a child that
    itself ended compression, 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's ws_orphan_reap sibling, the export can follow the
    wrong branch.

  2. Message order. export.py orders by timestamp, id. Hermes orders
    by id ("clocks regress", get_messages).

  3. 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.

  4. Structured content (the one that bites today). v0.21.x stores
    multimodal content as "\x00json:" + json.dumps(parts)
    (_encode_content). message_text only 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)
  5. Scaffolding rows. Hermes never shows rows with
    display_metadata.model_only, nor display_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.db itself. 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/ and cache/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.

  • 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:

  1. Collect each medium a kept message names that still exists:
    [Image attached at: <path>] and the It 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.
  2. Write each one to chats/media/<sha256>.<ext> (one copy however
    many messages name it). On the message, add attachments: [{name, media_type, size, sha256, path: "media/<sha256>.<ext>"}], and keep
    the [image] placeholder in text as the fallback for readers that
    don't know attachments.
  3. Bound it like the workspace: skip media over a per-file cap (say
    25 MB) and count it in skipped (kind: "chat_media", files,
    bytes). Count media toward fits; media often pushes an export
    over 25 MB, which is when the Drive path applies.
  4. 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).
  5. Manifest: list chats/media/ files in manifest.json with their
    SHA-256, as every other file is.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions