Read a session's transcript once at launch, not twice

Reading the last status back from the transcript -- added so a restart
stops claiming an exited session is idle -- walked the whole file a second
time, after `Transcript::open` had just walked it for the sequence number.
Both answers are wanted at the same moment by the same caller, so the cost
was paid per session at exactly the point a restart is trying to be quick.

`Transcript::open` now finds both in its one pass and reports the status it
saw. The free function goes; a transcript knowing what it last recorded is
where that belongs anyway.

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:39:54 -04:00
1 parent 3a74bd9c35
commit c1a468432d
2 files changed
+63 -32

No files matched your search

+62 -29
View File
@@ -30,13 +30,23 @@ pub struct SeqEvent {
pub struct Transcript {
file: File,
next_seq: u64,
last_status: Option<SessionStatus>,
}
impl Transcript {
/// Opens (or creates) the log at `path`, continuing the sequence from
/// the last line if one exists.
pub fn open(path: &Path) -> Result<Self> {
let last_seq = last_seq(path)?;
// One pass for both answers. They are wanted at the same moment by
// the same caller, and reading the file twice to get them doubled
// the cost of starting every session -- which is paid per session,
// at the point a restart is trying to be quick.
let existing = read_after(path, 0)?;
let last_seq = existing.last().map(|entry| entry.seq).unwrap_or(0);
let last_status = existing.iter().rev().find_map(|entry| match entry.event {
Event::Status { state } => Some(state),
_ => None,
});
// Owner-only: a transcript is the whole conversation, including
// whatever the session read, wrote, or was told.
let file = OpenOptions::new()
@@ -48,9 +58,24 @@ impl Transcript {
Ok(Self {
file,
next_seq: last_seq + 1,
last_status,
})
}
/// The state the session was last reported to be in, as of opening.
///
/// Read from the file rather than assumed, because a server that has
/// just restarted has been told nothing yet and this is the only thing
/// it knows. Assuming idle claimed a session was waiting for you when
/// it had exited hours earlier, and would now also claim it of one
/// whose process is still mid-turn.
///
/// `None` for a transcript that never carried a status, which is a new
/// session and genuinely has no prior state.
pub fn last_status(&self) -> Option<SessionStatus> {
self.last_status
}
/// Appends `event`, assigning it the next sequence number. Flushed per
/// event: each line is tiny, and the transcript is the source of truth
/// a crash must not lose the tail of.
@@ -158,34 +183,6 @@ pub fn read_after(path: &Path, after: u64) -> Result<Vec<SeqEvent>> {
Ok(events)
}
/// The state the session was last reported to be in.
///
/// Read from the transcript rather than assumed at launch, because a
/// server that has just restarted has been told nothing yet and the last
/// thing written down is the only thing it knows. Assuming idle claimed a
/// session was waiting for you when it had exited hours earlier, and would
/// now also claim it of one whose process is still mid-turn.
///
/// `None` for a transcript that has never carried a status, which is a new
/// session and genuinely has no prior state.
pub fn last_status(path: &Path) -> Option<SessionStatus> {
read_after(path, 0)
.ok()?
.into_iter()
.rev()
.find_map(|entry| match entry.event {
Event::Status { state } => Some(state),
_ => None,
})
}
fn last_seq(path: &Path) -> Result<u64> {
Ok(read_after(path, 0)?
.last()
.map(|entry| entry.seq)
.unwrap_or(0))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -267,6 +264,42 @@ mod tests {
));
}
#[test]
fn reopening_reports_the_state_it_was_last_left_in() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("transcript.jsonl");
// Nothing recorded yet: no prior state to report, which is not the
// same as reporting idle.
assert_eq!(Transcript::open(&path).expect("open").last_status(), None);
let mut transcript = Transcript::open(&path).expect("open");
transcript
.append(
Event::Status {
state: SessionStatus::Running,
},
1.0,
)
.expect("append");
transcript
.append(
Event::Status {
state: SessionStatus::Exited,
},
2.0,
)
.expect("append");
// Events after the last status must not hide it.
transcript.append(text("trailing"), 3.0).expect("append");
drop(transcript);
let reopened = Transcript::open(&path).expect("reopen");
assert_eq!(reopened.last_status(), Some(SessionStatus::Exited));
// And the same pass still continues the numbering.
assert_eq!(reopened.next_seq, 4);
}
#[test]
fn a_missing_file_reads_as_empty() {
let dir = tempfile::tempdir().expect("tempdir");