Skip to main content

SessionStore

Struct SessionStore 

pub struct SessionStore { /* private fields */ }
Expand description

Append-only JSONL session store for channel conversations.

Implementations§

Source§

impl SessionStore

Source

pub fn new(workspace_dir: &Path) -> Result<Self>

Create a new session store, ensuring the sessions directory exists.

Source

pub fn set_trim_breadcrumb( &self, session_key: &str, present: bool, ) -> Result<()>

Persist whether the session’s transcript starts with the synthetic trim breadcrumb. Respects the migration fence: fails if the JSONL store has been migrated to SQLite.

Source

pub fn get_trim_breadcrumb(&self, session_key: &str) -> Result<Option<bool>>

Read the persisted breadcrumb flag. None if never recorded.

Only an exact one-byte b"0" or b"1" is a verified reading; any other content (empty, truncated, extra bytes, a stray byte) is not something set_trim_breadcrumb ever wrote, so it is corruption, not a legitimate false. Treating it as Some(false) would let a restore mark an untrimmed transcript as trim-clean and skip persisting the correction. Callers must fail closed on this Err, the same as any other unreadable breadcrumb.

Source

pub fn load(&self, session_key: &str) -> Vec<ChatMessage>

Load all messages for a session from its JSONL file. Returns an empty vec if the path is not a regular JSONL session file or the file is unreadable.

Source

pub fn try_load(&self, session_key: &str) -> Result<Vec<ChatMessage>>

Like load, but fails closed: a missing file returns Ok(empty), while an unreadable file, a line-read error, or a JSON parse failure returns Err so callers can distinguish “no session” from “existing transcript that could not be verified” instead of seeding a new-message-only cache over an existing transcript.

Source

pub fn append(&self, session_key: &str, message: &ChatMessage) -> Result<()>

Append a single message to the session JSONL file.

Source

pub fn remove_last(&self, session_key: &str) -> Result<bool>

Remove the last message from a session’s JSONL file. Rewrite approach: load all messages, drop the last, rewrite. This is O(n) but rollbacks are rare.

Source

pub fn update_last( &self, session_key: &str, message: &ChatMessage, ) -> Result<bool>

Replace the last message without exposing an intermediate truncated session.

Source

pub fn compact(&self, session_key: &str) -> Result<()>

Compact a session file by rewriting only valid messages (removes corrupt lines).

Source

pub fn clear_messages(&self, session_key: &str) -> Result<usize>

Clear all messages from a session by truncating its JSONL file. The file is preserved (empty) so the session key remains in list_sessions. Also removes the breadcrumb sidecar: a reset session has no synthetic marker, so a stale recorded true must not survive into the next first message and be mistaken for a real trim. The sidecar is removed even when the transcript is absent or not a regular JSONL file: it is independently creatable by set_trim_breadcrumb, so a sidecar-only state (e.g. after a transcript write failure or a prior cleanup that removed the transcript but not its provenance file) must not survive a clear_messages call and be misread by a later restore.

Source

pub fn delete_session(&self, session_key: &str) -> Result<bool>

Delete a session’s JSONL file and its breadcrumb sidecar. Returns true if either file existed. The sidecar is removed even when the transcript file is already absent (or not a regular JSONL session file) so stale provenance cannot affect a later session that reuses the same key.

Source

pub fn session_mtime(&self, session_key: &str) -> Option<SystemTime>

Return the modification time of a regular session JSONL file.

Source

pub fn list_sessions(&self) -> Vec<String>

List all session keys that have regular JSONL files on disk.

Trait Implementations§

Source§

impl SessionBackend for SessionStore

Source§

fn session_exists(&self, session_key: &str) -> bool

Quick existence probe using the same regular-file policy as the other JSONL session operations.

Source§

fn load(&self, session_key: &str) -> Vec<ChatMessage>

Load all messages for a session. Returns empty vec if session doesn’t exist.
Source§

fn try_load(&self, session_key: &str) -> Result<Vec<ChatMessage>>

Like load, but distinguishes an unreadable transcript from a genuinely missing/empty session. Ok(vec) is the verified durable transcript (empty means no session), Err means the transcript could not be read or parsed and must not be treated as empty. The default impl delegates to load and never errors; backends that can detect read failures (JSONL) override this so callers can fail closed instead of seeding a new-message-only cache over an existing transcript.
Source§

fn append(&self, session_key: &str, message: &ChatMessage) -> Result<()>

Append a single message to a session.
Source§

fn remove_last(&self, session_key: &str) -> Result<bool>

Remove the last message from a session. Returns true if a message was removed.
Source§

fn rewrite_messages( &self, session_key: &str, messages: &[ChatMessage], ) -> Result<()>

Replace a session’s entire durable transcript with messages. Used to keep the canonical stored history in sync with a caller-owned buffer that trimmed or rewrote turns in place (e.g. history-budget enforcement), rather than appending on top of a now-stale transcript. The default clears the session (clear_messages) and re-appends every message through the required append primitive, so every backend gets real replacement semantics — a backend that never overrides this does not silently drop the caller’s authoritative trimmed history. It is O(n) calls and not atomic; backends with a native batch/transactional rewrite (JSONL, SQLite) override this.
Source§

fn set_session_trim_breadcrumb( &self, session_key: &str, present: bool, ) -> Result<()>

Record whether this session’s persisted transcript currently starts with the synthetic trim breadcrumb, as one canonical fact alongside the transcript itself. Callers must not infer this from message text on restore: a genuine first user turn that happens to equal the localized breadcrumb string must keep its turn-boundary role, and a crumb written under another locale must stay classified as synthetic. No-op for backends that don’t track it (the caller falls back to treating the session as having no breadcrumb).
Source§

fn replace_conversation_state( &self, session_key: &str, messages: &[ChatMessage], breadcrumb_present: bool, ) -> Result<()>

Replace a session’s durable transcript and its breadcrumb provenance with the agent’s authoritative post-turn state. Callers that own a trimmed in-memory history must use this instead of appending only the turn’s delta. The default implementation performs two separate writes (rewrite_messages then set_session_trim_breadcrumb); they are serialized under the caller’s lock (channel per-sender lock or the store’s mutation guard) so concurrent turns cannot interleave, but the pair is not crash-atomic. A crash between the two writes can leave transcript and flag temporarily out of sync, recoverable on the next trim. Both built-in backends override this with a stronger guarantee: SqliteSessionBackend makes the pair atomic inside one transaction, and the JSONL SessionStore rolls the transcript back to its pre-replace content when the breadcrumb write fails, so an in-process failure converges back to the last known-good pair instead of leaving a new transcript paired with a stale flag (a process crash between the two file writes can still split them, since JSONL uses two files). Backends that can provide a transaction should follow SQLite’s example; ones limited to independent writes should follow JSONL’s.
Source§

fn replace_conversation_state_if_exists( &self, session_key: &str, messages: &[ChatMessage], breadcrumb_present: bool, ) -> Result<bool>

Replace the durable transcript and breadcrumb flag only if the session still exists. Returns Ok(true) when the replacement was written and Ok(false) when the session was already deleted, in which case nothing is written and, in particular, no fresh session row or file is recreated for the deleted key. Read more
Source§

fn get_session_trim_breadcrumb(&self, session_key: &str) -> Result<Option<bool>>

Get the recorded breadcrumb provenance for a session. None means the backend doesn’t track it or no session-level state exists yet (callers should assume false, not attempt text inference).
Source§

fn update_last(&self, session_key: &str, message: &ChatMessage) -> Result<bool>

Source§

fn list_sessions(&self) -> Vec<String>

List all session keys.
Source§

fn list_sessions_with_metadata(&self) -> Vec<SessionMetadata>

List sessions with metadata.
Source§

fn compact(&self, session_key: &str) -> Result<()>

Compact a session file (remove duplicates/corruption). No-op by default.
Source§

fn clear_messages(&self, session_key: &str) -> Result<usize>

Source§

fn delete_session(&self, session_key: &str) -> Result<bool>

Delete all messages for a session. Returns true if the session existed.
Source§

fn load_with_timestamps(&self, session_key: &str) -> Vec<TimestampedMessage>

Same as load, but each row carries its persisted created_at when the backend has one. Default impl falls back to load without timestamps so non-SQLite backends keep working.
Source§

fn cleanup_stale(&self, _ttl_hours: u32) -> Result<usize>

Remove sessions that haven’t been active within the given TTL hours.
Source§

fn search(&self, _query: &SessionQuery) -> Vec<SessionMetadata>

Search sessions by keyword. Default returns empty (backends with FTS override).
Source§

fn clear_agent_attribution(&self, _agent_alias: &str) -> Result<usize>

Source§

fn rename_agent_attribution(&self, _from: &str, _to: &str) -> Result<usize>

Source§

fn count_agent_attribution(&self, _agent_alias: &str) -> Result<usize>

Source§

fn set_session_name(&self, _session_key: &str, _name: &str) -> Result<()>

Set or update the human-readable name for a session.
Source§

fn get_session_name(&self, _session_key: &str) -> Result<Option<String>>

Get the human-readable name for a session (if set).
Source§

fn set_session_agent_alias( &self, _session_key: &str, _agent_alias: &str, ) -> Result<()>

Record the agent alias that owns a session. Called on WebSocket handshake when the alias is known. No-op for backends that don’t track per-agent attribution.
Source§

fn get_session_agent_alias(&self, _session_key: &str) -> Result<Option<String>>

Get the agent alias associated with a session, if recorded.
Source§

fn set_session_context( &self, _session_key: &str, _context: SessionContext<'_>, ) -> Result<()>

Source§

fn get_session_metadata(&self, session_key: &str) -> Option<SessionMetadata>

Source§

fn set_session_principal( &self, _session_key: &str, _principal_id: &str, ) -> Result<()>

Record the authenticated principal that owns a session (RFC 7141). Called at session/new when the dispatcher has a real (non-sentinel) principal bound. No-op for backends without the column.
Source§

fn delete_session_owned( &self, _session_key: &str, _owner_principal_id: &str, ) -> Result<bool>

Delete a session ONLY if owner_principal_id matches the stored owner: the ownership check and the destruction of the ownership row are one SQL statement (the RFC 7141 atomic storage predicate), so a concurrent re-stamp cannot race them apart. Backends without ownership support fail closed: Ok(false), nothing deleted.
Source§

fn set_session_state( &self, _session_key: &str, _state: &str, _turn_id: Option<&str>, ) -> Result<()>

Set the session state (e.g. “idle”, “running”, “error”). turn_id identifies the current turn (set when running, cleared on idle).
Source§

fn get_session_state(&self, _session_key: &str) -> Result<Option<SessionState>>

Get the current session state. Returns None if the backend doesn’t track state.
Source§

fn list_running_sessions(&self) -> Vec<SessionMetadata>

List sessions currently in “running” state.
Source§

fn list_stuck_sessions(&self, _threshold_secs: u64) -> Vec<SessionMetadata>

List sessions stuck in “running” state longer than threshold_secs.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more