# Task 01: SessionSeqNo + WAL Format Extension ## Delivers `SessionSeqNo(u64)` type added to `tidal/src/wal/format/session.rs` and `tidal/src/session/state.rs`. Every session write operation carries a monotonically incrementing sequence number. The receiver's high-water-mark (HWM) rejects writes with `seqno <= hwm` as idempotent no-ops. ## Complexity: S ## Dependencies - Phase 8.2 (WAL shipping, SegmentReceiver) ## Technical Design ```rust // tidal/src/wal/format/session.rs /// Monotonic sequence number for session writes. /// /// Incremented once per session write operation (preference signal, /// annotation, search query, interaction). Used by the receiver to /// enforce idempotent replay and exactly-once semantics. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, )] pub struct SessionSeqNo(pub u64); impl SessionSeqNo { pub const ZERO: Self = Self(0); pub fn next(self) -> Self { Self(self.0 + 1) } } impl std::fmt::Display for SessionSeqNo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "ssn:{}", self.0) } } /// Extended session WAL event -- backward-compatible with existing format. /// /// The `session_seqno` and `idempotency_key` fields are appended to the /// existing `SessionWalEvent` bytes. Old readers that don't understand /// the extension fields still decode the core event; they will silently /// ignore the extra bytes (length-prefixed framing ensures this). #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SessionWalEvent { // --- existing fields (unchanged) --- pub session_id: SessionId, pub kind: SessionEventKind, pub timestamp_ns: u64, pub payload: SessionEventPayload, // --- new fields (m8p4 extension) --- /// Monotonically increasing sequence number for this session's writes. /// Starts at 1 for the first write in a session. #[serde(default)] pub session_seqno: Option, /// BLAKE3-derived idempotency key for exactly-once delivery. /// `None` for events written before m8p4. #[serde(default)] pub idempotency_key: Option, } ``` ```rust // tidal/src/session/state.rs (additions only) /// Per-session monotonic write counter. /// /// Tracks the highest seqno applied locally. Writes with seqno <= hwm /// are silently dropped (idempotent replay is safe; the state is already /// reflected in local storage). #[derive(Debug, Default)] pub struct SessionSeqNoTracker { /// Map from SessionId to highest applied SessionSeqNo. hwm: DashMap, } impl SessionSeqNoTracker { pub fn new() -> Self { Self { hwm: DashMap::new() } } /// Returns `true` if this write should be applied (seqno > hwm). /// Returns `false` if the write is a duplicate and should be skipped. /// Updates the HWM on accept. pub fn should_apply(&self, session_id: SessionId, seqno: SessionSeqNo) -> bool { let mut entry = self.hwm.entry(session_id).or_insert(SessionSeqNo::ZERO); if seqno > *entry { *entry = seqno; true } else { false } } /// Current HWM for a session (returns ZERO if unknown). pub fn hwm(&self, session_id: SessionId) -> SessionSeqNo { self.hwm.get(&session_id) .map(|v| *v) .unwrap_or(SessionSeqNo::ZERO) } /// Initialize or reset HWM for a session (used on follower startup). pub fn set_hwm(&self, session_id: SessionId, seqno: SessionSeqNo) { self.hwm.insert(session_id, seqno); } } ``` ### Sequence Number Assignment ```rust // In session write path (tidal/src/session/mod.rs) impl SessionManager { fn next_seqno(&self, session_id: SessionId) -> SessionSeqNo { // Fetch-and-increment per session. let mut counter = self.seqno_counters .entry(session_id) .or_insert(SessionSeqNo::ZERO); *counter = counter.next(); *counter } } ``` ## Acceptance Criteria - [ ] `SessionSeqNo` is `Copy + Clone + Ord + Hash + Serialize + Deserialize` - [ ] `SessionSeqNoTracker::should_apply(id, seqno)` returns `true` for the first call with a given seqno, `false` on duplicate, and `true` again for a higher seqno - [ ] HWM persists in memory; on follower node restart, WAL replay re-establishes HWM by scanning all `SessionWalEvent` entries in order - [ ] `SessionWalEvent` with `session_seqno: None` (pre-m8p4 events) is decoded without error; `should_apply` returns `true` for all legacy events - [ ] `cargo clippy -D warnings` and `cargo fmt` pass