feat(web): add log epochs to session and terminal cursors
Merged
GN
6d ago+2256+2256+0123456789012345678901234567890123456789-207-207-012345678901234567890123456789
Summary
- A client cursor was a bare number. After a host restart without durable logs, the host reused low numbers for new events. The client then dropped those new events as old ones. A cursor ahead of the log was also honored in silence.
- This change names each log with an epoch. A cursor from another epoch, or ahead of the log head, now gets
stale, and the client installs a snapshot.
Related Issue
Refs #1070
Searched open and closed PRs for the same problem. No PR covers it. This is the second of several PRs for the read-model work in #1070. It builds on #1087.
Type of Change
- feat: new feature
- fix: bug fix
- docs: documentation only
- style: formatting or non-functional styling changes
- refactor: internal cleanup with no behavior change
- perf: performance improvement
- test: adds or updates tests
- build: build or dependency changes
- ci: CI/CD workflow changes
- chore: maintenance or tooling
- revert: reverts a previous change
What Changed
- Each session log has a
logEpoch. A saved session keeps its epoch in its metadata. A session that is not saved gets an epoch for each host start. A legacy session gets an epoch when the host loads it. subscribetakeslogEpoch. Its reply carrieslogEpochandheadSeq. The snapshot, cursor and payload replies carry the epoch too.- A cursor from another epoch, or above
headSeq, getsstale. Nothing is registered in that case. - A live-only event takes a number that never reaches the disk. The first one marks the session. The next host load then gives it a new epoch, so an old cursor goes stale.
- Each terminal spawn has an epoch. An old-epoch or too-high cursor replays the whole kept output with
reset: true. The web and desktop clients clear the terminal first. - A nonzero cursor with no known epoch is not trusted. A terminal sends it with an empty epoch and the host replays everything with
reset. A session client installs a snapshot when the host answers with an epoch. - The client stores
(logEpoch, lastSeq)for each session. A stale answer or a changed epoch starts a snapshot. WS_PROTOCOL_VERSIONgoes from 2 to 3. An old client reloads once.
How It Was Tested
-
bun run ci(Biome lint + format + imports, strict mode) -
bun run typecheck -
bun run test -
cargo clippy --all-targets -- -D warnings(in src-tauri) -
cargo test(in src-tauri) - Manual verification completed
- Not applicable
New tests cover: an old epoch and a too-high cursor get stale; a saved epoch survives a restart; a legacy session keeps one epoch; an unsaved session gets a new epoch after a restart; the terminal reset replay over the socket; the client snapshot after a stale answer.
Screenshots or Recordings
No user-visible UI change. A stale cursor now repairs the transcript with a snapshot. The Rust and client tests above cover the behavior.
CI & Review Gate
- All CI checks pass (PR Validation, Rust Checks, Build Verification, Security Scans)
- Code review comments from CodeRabbit / Claude have been addressed or resolved
- No unresolved review findings remain
Checklist
- My PR title follows the conventional commit format used by this repo
- I linked the related issue or explained why none exists
- I updated docs when needed
- I added or updated tests when needed
- I verified the change does not introduce unrelated modifications
- I read AGENTS.md and followed the contributor guidelines
- A human reviewed the complete diff before submission
Summary by cubic
Session and terminal replay cursors now carry a logEpoch, so a cursor issued by an older host run — or placed ahead of the log head — can no longer silently drop or skip events.
- Durable sessions persist the epoch in their metadata; unsaved sessions and each terminal spawn mint a new one per host start, and legacy sessions get one on load.
- A live-only event whose seq never reaches disk — or a record dropped or rejected before the session registers — marks the log head ahead of the durable frontier; the flag is persisted with writer retry (a failed write is retried and its health mark healed) so an old cursor stays invalid after a restart.
- A stale cursor, or a nonzero cursor with no known epoch (the host now refuses these on terminals too), is never trusted: a session installs a snapshot and a terminal replays the whole retained log with
reset, clearing the terminal first; a terminal gap cursor is advanced past the retained log. subscribe, cursor, and snapshot replies now report(logEpoch, headSeq).WS_PROTOCOL_VERSIONis bumped to 3; older clients reconnect once.
Summary by CodeRabbit
- New Features
- Session and terminal replay cursors now include a log epoch, helping reconnects resume against the correct event or output log.
- Stale session cursors trigger snapshot recovery. Stale terminal cursors replay retained output from the beginning, with the terminal cleared first.
- Session cursor and snapshot responses now include log-head information.
- Durable session epochs persist across restarts and rotate when sequence numbers advance beyond stored events.
- Compatibility
- The WebSocket protocol version is now 3; clients and hosts must use compatible versions.