feat(web): add log epochs to session and terminal cursors

Merged
GN
6d ago
+2256-207

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.
  • subscribe takes logEpoch. Its reply carries logEpoch and headSeq. The snapshot, cursor and payload replies carry the epoch too.
  • A cursor from another epoch, or above headSeq, gets stale. 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_VERSION goes 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_VERSION is 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.