Struct SessionStore
pub struct SessionStore { /* private fields */ }Expand description
Append-only JSONL session store for channel conversations.
Implementations§
Source§impl SessionStore
impl SessionStore
Sourcepub fn new(workspace_dir: &Path) -> Result<Self>
pub fn new(workspace_dir: &Path) -> Result<Self>
Create a new session store, ensuring the sessions directory exists.
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.
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.
Sourcepub fn load(&self, session_key: &str) -> Vec<ChatMessage>
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.
Sourcepub fn try_load(&self, session_key: &str) -> Result<Vec<ChatMessage>>
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.
Sourcepub fn append(&self, session_key: &str, message: &ChatMessage) -> Result<()>
pub fn append(&self, session_key: &str, message: &ChatMessage) -> Result<()>
Append a single message to the session JSONL file.
Sourcepub fn remove_last(&self, session_key: &str) -> Result<bool>
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.
Sourcepub fn update_last(
&self,
session_key: &str,
message: &ChatMessage,
) -> Result<bool>
pub fn update_last( &self, session_key: &str, message: &ChatMessage, ) -> Result<bool>
Replace the last message without exposing an intermediate truncated session.
Sourcepub fn compact(&self, session_key: &str) -> Result<()>
pub fn compact(&self, session_key: &str) -> Result<()>
Compact a session file by rewriting only valid messages (removes corrupt lines).
Sourcepub fn clear_messages(&self, session_key: &str) -> Result<usize>
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.
Sourcepub fn delete_session(&self, session_key: &str) -> Result<bool>
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.
Sourcepub fn session_mtime(&self, session_key: &str) -> Option<SystemTime>
pub fn session_mtime(&self, session_key: &str) -> Option<SystemTime>
Return the modification time of a regular session JSONL file.
Sourcepub fn list_sessions(&self) -> Vec<String>
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
impl SessionBackend for SessionStore
Source§fn session_exists(&self, session_key: &str) -> bool
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>
fn load(&self, session_key: &str) -> Vec<ChatMessage>
Source§fn try_load(&self, session_key: &str) -> Result<Vec<ChatMessage>>
fn try_load(&self, session_key: &str) -> Result<Vec<ChatMessage>>
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<()>
fn append(&self, session_key: &str, message: &ChatMessage) -> Result<()>
Source§fn remove_last(&self, session_key: &str) -> Result<bool>
fn remove_last(&self, session_key: &str) -> Result<bool>
true if a message was removed.Source§fn rewrite_messages(
&self,
session_key: &str,
messages: &[ChatMessage],
) -> Result<()>
fn rewrite_messages( &self, session_key: &str, messages: &[ChatMessage], ) -> Result<()>
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 replace_conversation_state(
&self,
session_key: &str,
messages: &[ChatMessage],
breadcrumb_present: bool,
) -> Result<()>
fn replace_conversation_state( &self, session_key: &str, messages: &[ChatMessage], breadcrumb_present: bool, ) -> Result<()>
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>
fn replace_conversation_state_if_exists( &self, session_key: &str, messages: &[ChatMessage], breadcrumb_present: bool, ) -> Result<bool>
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 moreNone means
the backend doesn’t track it or no session-level state exists yet
(callers should assume false, not attempt text inference).fn update_last(&self, session_key: &str, message: &ChatMessage) -> Result<bool>
Source§fn list_sessions(&self) -> Vec<String>
fn list_sessions(&self) -> Vec<String>
Source§fn list_sessions_with_metadata(&self) -> Vec<SessionMetadata>
fn list_sessions_with_metadata(&self) -> Vec<SessionMetadata>
Source§fn compact(&self, session_key: &str) -> Result<()>
fn compact(&self, session_key: &str) -> Result<()>
fn clear_messages(&self, session_key: &str) -> Result<usize>
Source§fn delete_session(&self, session_key: &str) -> Result<bool>
fn delete_session(&self, session_key: &str) -> Result<bool>
true if the session existed.Source§fn load_with_timestamps(&self, session_key: &str) -> Vec<TimestampedMessage>
fn load_with_timestamps(&self, session_key: &str) -> Vec<TimestampedMessage>
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>
fn cleanup_stale(&self, _ttl_hours: u32) -> Result<usize>
Source§fn search(&self, _query: &SessionQuery) -> Vec<SessionMetadata>
fn search(&self, _query: &SessionQuery) -> Vec<SessionMetadata>
fn clear_agent_attribution(&self, _agent_alias: &str) -> Result<usize>
fn rename_agent_attribution(&self, _from: &str, _to: &str) -> Result<usize>
fn count_agent_attribution(&self, _agent_alias: &str) -> Result<usize>
Source§fn set_session_name(&self, _session_key: &str, _name: &str) -> Result<()>
fn set_session_name(&self, _session_key: &str, _name: &str) -> Result<()>
Source§fn get_session_name(&self, _session_key: &str) -> Result<Option<String>>
fn get_session_name(&self, _session_key: &str) -> Result<Option<String>>
Source§fn set_session_agent_alias(
&self,
_session_key: &str,
_agent_alias: &str,
) -> Result<()>
fn set_session_agent_alias( &self, _session_key: &str, _agent_alias: &str, ) -> Result<()>
Source§fn get_session_agent_alias(&self, _session_key: &str) -> Result<Option<String>>
fn get_session_agent_alias(&self, _session_key: &str) -> Result<Option<String>>
fn set_session_context( &self, _session_key: &str, _context: SessionContext<'_>, ) -> Result<()>
fn get_session_metadata(&self, session_key: &str) -> Option<SessionMetadata>
Source§fn set_session_principal(
&self,
_session_key: &str,
_principal_id: &str,
) -> Result<()>
fn set_session_principal( &self, _session_key: &str, _principal_id: &str, ) -> Result<()>
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>
fn delete_session_owned( &self, _session_key: &str, _owner_principal_id: &str, ) -> Result<bool>
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<()>
fn set_session_state( &self, _session_key: &str, _state: &str, _turn_id: Option<&str>, ) -> Result<()>
turn_id identifies the current turn (set when running, cleared on idle).Source§fn get_session_state(&self, _session_key: &str) -> Result<Option<SessionState>>
fn get_session_state(&self, _session_key: &str) -> Result<Option<SessionState>>
None if the backend doesn’t track state.Source§fn list_running_sessions(&self) -> Vec<SessionMetadata>
fn list_running_sessions(&self) -> Vec<SessionMetadata>
Source§fn list_stuck_sessions(&self, _threshold_secs: u64) -> Vec<SessionMetadata>
fn list_stuck_sessions(&self, _threshold_secs: u64) -> Vec<SessionMetadata>
threshold_secs.