Show a replayed session's images instead of dropping them

An imported session showed no screenshots. `text_of` kept only `text`
blocks, so every image in the replayed tail was silently discarded -- while
the *live* translator has always saved them into the session's `files/` and
referenced them. Two readings of the same records, and the one used for
history was the lesser.

`save_image` moves out of `Translator` to a free function both paths call,
since the naming scheme for that directory should exist once. `events_from`
now takes the session directory to write into, which means the conversion
has to happen where that directory exists -- so `Seed` carries the raw
JSONL and `launch` turns it into events, rather than `routes` doing it
before the session is created.

Costs nothing in tokens, which is the point worth recording: this writes
into ai-app's own session directory and the phone fetches a reference only
when it draws one. Nothing here is ever written to the CLI's stdin -- it
reads its own session file, and the only things this app sends it are typed
messages, control requests and `/compact`.

Verified against the 133 MB session behind the 2026-08-29 incident: 45
images in the replayed tail, written as real PNGs and served over the files
route, with the transcript itself staying at 756 KB of references.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VETa8afmpWaYezLCqJhDB8
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-29 05:01:48 -04:00
1 parent d96bc041a7
commit d7c692a4ec
6 files changed
+148 -50

No files matched your search

+6 -6
View File
@@ -478,7 +478,7 @@ async fn spawn_session(
whole thing. Close it there first, then import it here."
)));
}
let events = crate::session::import::replay(&transport, &chosen.path)
let records = crate::session::import::read_tail(&transport, &chosen.path)
.await
.map_err(bad_request)?;
// The recorded directory can outlive itself; resuming into one
@@ -495,7 +495,7 @@ async fn spawn_session(
);
chosen.cwd = String::new();
}
Some((chosen, events))
Some((chosen, records))
}
None => None,
};
@@ -532,11 +532,11 @@ async fn spawn_session(
};
let info = match seed {
Some((chosen, events)) => {
Some((chosen, records)) => {
tracing::info!(
"importing Claude Code session {} ({} events replayed)",
"importing Claude Code session {} ({} lines replayed)",
chosen.id,
events.len()
records.lines().count()
);
manager.spawn_imported(
spec,
@@ -549,7 +549,7 @@ async fn spawn_session(
path: chosen.path,
lines: chosen.lines,
},
events,
records,
},
)
}