#![allow(clippy::module_name_repetitions)]
use std::{
collections::BTreeMap,
fs,
path::{Path, PathBuf},
sync::Arc,
};
use crate::current_schema::{global_migrations, session_migrations};
use crate::db_artifact::{finalized_artifact_reference_from_row, project_artifact_references};
use crate::db_connection::{
init_turso_local_with_retry, is_database_lock_error, is_database_lock_error_message,
};
use crate::db_context::{
ContextHistoryRole, canonical_model_context_from_events, context_history_role,
is_model_context_event_type, model_context_event_kind_name,
};
use crate::db_event_store::{
event_created_at_ms, event_kind_name, insert_event, last_sequence, seq_to_value,
strict_event_from_row, strict_events,
};
pub use crate::db_path::{global_catalog_db_path, session_db_path, session_dir_path};
pub use crate::db_projection::MaterializedProjection;
use crate::db_projection::{
BASE_MATERIALIZED_PROJECTIONS, projection_checkpoint_snapshot, update_projection_checkpoint,
};
use crate::db_projection_row::{
input_history_entry_from_row, runtime_work_from_row, session_summary_from_catalog_row,
tool_run_from_row, transcript_item_from_row,
};
use crate::db_row::{
i64_to_u64, optional_i64, optional_string, required_i64, required_non_negative_u64,
required_string,
};
use crate::db_runtime_work::{runtime_work_kind_name, runtime_work_status_name};
const fn bool_to_value(value: bool) -> DatabaseValue {
DatabaseValue::Int32(if value { 1 } else { 0 })
}
use crate::db_validation::{
compaction_boundary, validate_append_identity, validate_append_tail,
validate_canonical_event_identity, validate_context_occupancy_precondition,
validate_model_context_postcondition, validate_model_context_precondition,
validate_projection_checkpoint_snapshot,
};
use crate::persisted::{PersistedSessionEventError, decode_session_event, encode_session_event};
use async_trait::async_trait;
use bcode_session_migration_target::MigrationTarget as _;
use sha2::{Digest as _, Sha256};
use bcode_database_observability::ObservedDatabase;
use bcode_metrics::{DatabaseMetrics, DatabaseOperation, MetricsRegistry};
use bcode_session_models::{
ExecutionSessionProvenance, MAX_SESSION_HISTORY_READ_EVENTS, MAX_SESSION_INSPECTION_EVENTS,
RuntimeWorkKind, RuntimeWorkStatus, SessionEvent, SessionEventKind, SessionHistoryAroundQuery,
SessionHistoryCursor, SessionHistoryDirection, SessionHistoryPage, SessionHistoryQuery,
SessionHistoryWindow, SessionId, SessionInputHistoryEntry, SessionInspectionCategory,
SessionInspectionPage, SessionInspectionQuery, SessionSummary, SessionTokenUsage,
SessionUsageSummary, SessionVisibility, ToolInvocationResult, WorkId,
};
use switchy::{
database::{
Database, DatabaseError, DatabaseValue,
query::{FilterableQuery, SortDirection},
},
schema::{MigrationError, migration::MigrationSource, runner::MigrationRunner},
};
use thiserror::Error;
const GLOBAL_MIGRATIONS_TABLE: &str = "__bcode_global_migrations";
const SESSION_MIGRATIONS_TABLE: &str = "__bcode_session_migrations";
#[cfg(test)]
fn abort_at_migration_crash_boundary(boundary: &str) {
if std::env::var("BCODE_MIGRATION_CRASH_PHASE").as_deref() == Ok(boundary) {
std::process::abort();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MigrationFaultPhase {
Disabled,
CanonicalDecode,
Projection,
FinalValidation,
Receipt,
WriterEpochFinalization,
}
fn inject_migration_fault(
configured: MigrationFaultPhase,
phase: MigrationFaultPhase,
) -> SessionDbResult<()> {
if configured == phase && configured != MigrationFaultPhase::Disabled {
return Err(SessionDbError::MigrationHistoryIncompatible {
reason: format!("injected migration fault at {phase:?}"),
});
}
Ok(())
}
const MIGRATION_EVENT_PAGE_SIZE: usize = 1_000;
pub const CURRENT_SESSION_STORAGE_WRITER_EPOCH: u32 =
crate::lease::CURRENT_SESSION_STORAGE_WRITER_EPOCH;
pub(crate) const LEGACY_SESSION_STORAGE_WRITER_EPOCH: u32 = 2;
#[cfg(test)]
const PREVIOUS_SESSION_STORAGE_WRITER_EPOCH: u32 = 3;
const SESSION_STORAGE_CONTRACT_ID: i32 = 1;
const SESSION_STORAGE_CONTRACT_SCHEMA_VERSION: u32 = 1;
const MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION: u32 = 3;
const MODEL_CONTEXT_PROJECTION_ID: i32 = 1;
const CONTEXT_OCCUPANCY_PROJECTION_SCHEMA_VERSION: u32 = 4;
const CONTEXT_OCCUPANCY_PROJECTION_ID: i32 = 1;
#[derive(Debug, Error)]
pub enum SessionDbError {
#[error("invalid turn evidence: {0}")]
InvalidData(String),
#[error("failed to initialize database connection: {0}")]
Connection(#[from] switchy::database_connection::InitTursoError),
#[error(transparent)]
Database(#[from] DatabaseError),
#[error(transparent)]
Migration(#[from] MigrationError),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error(transparent)]
Lease(#[from] crate::lease::SessionLeaseError),
#[error(transparent)]
Serialize(#[from] serde_json::Error),
#[error(transparent)]
PersistedEvent(#[from] PersistedSessionEventError),
#[error("transient tool contribution {contribution_id} cannot be persisted")]
TransientContribution { contribution_id: String },
#[error(
"model-context projection is stale: checkpoint #{checkpoint}, canonical history ends at #{expected}"
)]
ModelContextProjectionStale { checkpoint: u64, expected: u64 },
#[error("unsupported model-context projection schema version {actual}; expected {expected}")]
ModelContextProjectionVersion { actual: u64, expected: u64 },
#[error(
"unsupported session storage writer epoch: actual={actual:?} expected={expected}; explicit migration is required"
)]
WriterIncompatible { actual: Option<u64>, expected: u64 },
#[error("invalid canonical append sequence: expected #{expected}, found #{actual}")]
InvalidCanonicalAppendSequence { expected: u64, actual: u64 },
#[error("invalid canonical event sequence for reindex: expected #{expected}, found #{actual}")]
InvalidCanonicalSequence { expected: u64, actual: u64 },
#[error(
"session DB projection is stale: {projection} checkpoint={checkpoint:?} expected={expected}"
)]
ProjectionStale {
projection: &'static str,
checkpoint: Option<u64>,
expected: u64,
},
#[error(
"unsupported session DB projection schema: {projection} actual={actual} expected={expected}"
)]
ProjectionIncompatible {
projection: &'static str,
actual: u64,
expected: u64,
},
#[error("invalid context compaction marker at event #{sequence}: {message}")]
InvalidCompactionMarker { sequence: u64, message: String },
#[error("invalid session database row: missing or invalid column {column}")]
InvalidRow { column: String },
#[error("unsupported or inconsistent session migration history: {reason}")]
MigrationHistoryIncompatible { reason: String },
#[error("session history read limit {requested} exceeds maximum {maximum}")]
HistoryReadLimitExceeded { requested: usize, maximum: usize },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionStorageCompatibility {
Current { writer_epoch: u64 },
KnownLegacy { writer_epoch: u64 },
}
impl SessionDbError {
#[must_use]
pub fn is_lock_error(&self) -> bool {
matches!(self, Self::Connection(error) if is_database_lock_error(error))
|| matches!(self, Self::Database(error) if is_database_lock_error_message(&error.to_string()))
}
}
pub type SessionDbResult<T> = Result<T, SessionDbError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelContextProjectionStatus {
Missing,
Fresh { checkpoint: u64 },
Stale { checkpoint: u64, expected: u64 },
Incompatible { actual: u64, expected: u64 },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolRun {
pub tool_call_id: String,
pub event_seq_start: u64,
pub event_seq_end: Option<u64>,
pub status: String,
pub tool_name: Option<String>,
pub started_at_ms: Option<u64>,
pub completed_at_ms: Option<u64>,
pub output_bytes: Option<u64>,
pub is_error: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeWorkProjection {
pub work_id: WorkId,
pub event_seq_start: u64,
pub event_seq_end: Option<u64>,
pub kind: RuntimeWorkKind,
pub label: String,
pub status: RuntimeWorkStatus,
pub parent_work_id: Option<WorkId>,
pub started_at_ms: Option<u64>,
pub finished_at_ms: Option<u64>,
pub message: Option<String>,
pub cancellable: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct ReasoningByModelEntry {
provider: String,
model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
effort: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
summary: Option<String>,
}
fn decode_model_selection_source(
value: &str,
) -> Option<bcode_session_models::ModelSelectionSource> {
serde_json::from_str(value).ok()
}
fn encode_model_selection_source(source: bcode_session_models::ModelSelectionSource) -> String {
serde_json::to_string(&source).expect("model selection source serializes")
}
type ReasoningMemory = (
std::collections::BTreeMap<
bcode_session_models::ModelScopeKey,
crate::state::RememberedReasoning,
>,
Vec<bcode_session_models::ModelScopeKey>,
);
fn decode_reasoning_by_model(value: &str) -> ReasoningMemory {
let Ok(entries) = serde_json::from_str::<Vec<ReasoningByModelEntry>>(value) else {
return ReasoningMemory::default();
};
let mut map = std::collections::BTreeMap::new();
let mut order = Vec::with_capacity(entries.len());
for entry in entries {
let scope = bcode_session_models::ModelScopeKey::new(entry.provider, entry.model);
order.retain(|existing: &bcode_session_models::ModelScopeKey| existing != &scope);
map.insert(
scope.clone(),
crate::state::RememberedReasoning {
effort: entry.effort,
summary: entry.summary,
},
);
order.push(scope);
}
(map, order)
}
fn encode_reasoning_by_model(
memory: &std::collections::BTreeMap<
bcode_session_models::ModelScopeKey,
crate::state::RememberedReasoning,
>,
order: &[bcode_session_models::ModelScopeKey],
) -> String {
let entries = order
.iter()
.filter_map(|scope| {
memory.get(scope).map(|reasoning| ReasoningByModelEntry {
provider: scope.provider.clone(),
model: scope.model.clone(),
effort: reasoning.effort.clone(),
summary: reasoning.summary.clone(),
})
})
.collect::<Vec<_>>();
serde_json::to_string(&entries).expect("reasoning memory serializes")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionDbState {
pub session_id: SessionId,
pub last_event_seq: u64,
pub title: Option<String>,
pub explicit_name: Option<String>,
pub derived_title: Option<String>,
pub automatic_title: Option<String>,
pub automatic_title_source_sequence: Option<u64>,
pub working_directory: std::path::PathBuf,
pub current_agent: Option<String>,
pub current_provider: Option<String>,
pub current_model: Option<String>,
pub model_selection_source: bcode_session_models::ModelSelectionSource,
pub reasoning_effort: Option<String>,
pub reasoning_summary: Option<String>,
pub reasoning_by_model: std::collections::BTreeMap<
bcode_session_models::ModelScopeKey,
crate::state::RememberedReasoning,
>,
pub reasoning_by_model_order: Vec<bcode_session_models::ModelScopeKey>,
pub updated_at_ms: Option<u64>,
pub has_user_message: bool,
pub latest_compaction_sequence: Option<u64>,
pub visibility: SessionVisibility,
pub parent_id: Option<SessionId>,
pub execution: Option<ExecutionSessionProvenance>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TranscriptItem {
pub transcript_seq: u64,
pub event_seq_start: u64,
pub event_seq_end: u64,
pub role: String,
pub kind: String,
pub status: String,
pub content: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FinalizedArtifactReference {
pub artifact_id: String,
pub reference_key: String,
pub producer_plugin_id: String,
pub schema: String,
pub schema_version: u32,
pub storage_uri: Option<String>,
pub content_type: Option<String>,
pub byte_len: Option<u64>,
pub availability: Option<String>,
pub complete: Option<bool>,
pub checksum_sha256: Option<String>,
pub finalized_event_seq: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogComposerDraft {
pub launch_working_directory: PathBuf,
pub text: String,
pub updated_at_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CatalogComposerDraftInventory {
TableAbsent,
Rows(Vec<CatalogComposerDraft>),
}
#[derive(Debug, Clone)]
pub struct GlobalSessionDb {
db: Arc<Box<dyn Database>>,
catalog_lock: Arc<std::sync::Mutex<Option<crate::lease::CatalogLockGuard>>>,
}
impl GlobalSessionDb {
async fn open_observed_connection(
path: &Path,
metrics: MetricsRegistry,
catalog_lock: Option<crate::lease::CatalogLockGuard>,
) -> SessionDbResult<Self> {
let open_started = std::time::Instant::now();
let db = init_turso_local_with_retry(path).await;
DatabaseMetrics::new(metrics.clone(), "session_catalog", "turso").record(
DatabaseOperation::Open,
None,
"none",
db.is_ok(),
u64::try_from(open_started.elapsed().as_millis()).unwrap_or(u64::MAX),
);
let db = db?;
let db: Box<dyn Database> = Box::new(ObservedDatabase::new(
db,
metrics,
"session_catalog",
"turso",
));
Ok(Self {
db: Arc::new(db),
catalog_lock: Arc::new(std::sync::Mutex::new(catalog_lock)),
})
}
pub async fn initialize_turso_in_root(root: &Path) -> SessionDbResult<Self> {
Self::initialize_turso_in_root_observed(root, MetricsRegistry::disabled()).await
}
pub async fn initialize_turso_in_root_observed(
root: &Path,
metrics: MetricsRegistry,
) -> SessionDbResult<Self> {
let path = global_catalog_db_path(root);
fs::create_dir_all(root)?;
let catalog_lock = crate::lease::acquire_catalog_lock(root)?;
let catalog = Self::open_observed_connection(&path, metrics, Some(catalog_lock)).await?;
run_global_migrations(&**catalog.db).await?;
Ok(catalog)
}
pub async fn open_existing_turso_in_root(root: &Path) -> SessionDbResult<Self> {
Self::open_existing_turso_in_root_observed(root, MetricsRegistry::disabled()).await
}
pub async fn open_existing_turso_in_root_observed(
root: &Path,
metrics: MetricsRegistry,
) -> SessionDbResult<Self> {
let path = global_catalog_db_path(root);
if !path.exists() {
return Err(SessionDbError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("catalog database does not exist: {}", path.display()),
)));
}
let catalog = Self::open_observed_connection(
&path,
metrics,
Some(crate::lease::acquire_catalog_lock(root)?),
)
.await?;
catalog.verify_schema().await?;
Ok(catalog)
}
async fn verify_schema(&self) -> SessionDbResult<()> {
for table in ["sessions", "composer_drafts"] {
if !self.db.table_exists(table).await? {
return Err(SessionDbError::MigrationHistoryIncompatible {
reason: format!("global catalog is missing required table {table}"),
});
}
}
for column in ["session_id", "parent_id", "db_path", "updated_at_ms"] {
if !self.db.column_exists("sessions", column).await? {
return Err(SessionDbError::MigrationHistoryIncompatible {
reason: format!(
"global catalog lacks required column {column}; explicit catalog maintenance required"
),
});
}
}
Ok(())
}
pub async fn open_turso_in_root(root: &Path) -> SessionDbResult<Self> {
Self::open_turso_in_root_observed(root, MetricsRegistry::disabled()).await
}
pub async fn open_turso_in_root_observed(
root: &Path,
metrics: MetricsRegistry,
) -> SessionDbResult<Self> {
let path = global_catalog_db_path(root);
Self::open_turso_observed(&path, metrics).await
}
pub async fn open_turso(path: &Path) -> SessionDbResult<Self> {
Self::open_turso_observed(path, MetricsRegistry::disabled()).await
}
pub async fn open_turso_observed(
path: &Path,
metrics: MetricsRegistry,
) -> SessionDbResult<Self> {
let root = path.parent().unwrap_or_else(|| Path::new("."));
fs::create_dir_all(root)?;
let catalog_lock = crate::lease::acquire_catalog_lock(root)?;
let catalog = Self::open_observed_connection(path, metrics, Some(catalog_lock)).await?;
run_global_migrations(&**catalog.db).await?;
Ok(catalog)
}
pub async fn open_existing_turso_without_catalog_lock(path: &Path) -> SessionDbResult<Self> {
if !path.exists() {
return Err(SessionDbError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("catalog database does not exist: {}", path.display()),
)));
}
let catalog =
Self::open_observed_connection(path, MetricsRegistry::disabled(), None).await?;
catalog.verify_schema().await?;
Ok(catalog)
}
pub async fn open_retired_turso_without_catalog_lock(path: &Path) -> SessionDbResult<Self> {
if !path.exists() {
return Err(SessionDbError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("catalog database does not exist: {}", path.display()),
)));
}
let catalog =
Self::open_observed_connection(path, MetricsRegistry::disabled(), None).await?;
if !catalog.db.table_exists("sessions").await? {
return Err(SessionDbError::MigrationHistoryIncompatible {
reason: "global catalog is missing required table sessions".to_owned(),
});
}
Ok(catalog)
}
pub async fn retired_composer_draft_inventory(
&self,
) -> SessionDbResult<CatalogComposerDraftInventory> {
if !self.db.table_exists("composer_drafts").await? {
return Ok(CatalogComposerDraftInventory::TableAbsent);
}
self.list_draft_session_composer_drafts()
.await
.map(CatalogComposerDraftInventory::Rows)
}
pub async fn open_turso_without_catalog_lock(path: &Path) -> SessionDbResult<Self> {
let catalog =
Self::open_observed_connection(path, MetricsRegistry::disabled(), None).await?;
run_global_migrations(&**catalog.db).await?;
Ok(catalog)
}
pub async fn is_closed(&self) -> SessionDbResult<bool> {
match self.db.check_connection().await {
Ok(()) => Ok(false),
Err(error) if error.is_connection_error() => Ok(true),
Err(error) => Err(error.into()),
}
}
pub async fn close(&self) -> SessionDbResult<()> {
self.db.close().await?;
self.catalog_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
Ok(())
}
async fn upsert_session_on(
db: &dyn Database,
summary: &SessionSummary,
db_path: &Path,
) -> SessionDbResult<()> {
let existing = db
.select("sessions")
.columns(&["session_id", "updated_at_ms"])
.where_eq("session_id", summary.id.to_string())
.execute_first(db)
.await?;
if existing.as_ref().is_some_and(|row| {
optional_i64(row, "updated_at_ms")
.map(i64_to_u64)
.is_some_and(|updated_at_ms| updated_at_ms > summary.updated_at_ms)
}) {
return Ok(());
}
let title = summary.name.clone();
let working_directory = summary.working_directory.to_string_lossy().to_string();
let db_path = db_path.to_string_lossy().to_string();
if existing.is_some() {
db.update("sessions")
.value("parent_id", summary.parent_id.map(|id| id.to_string()))
.value("db_path", db_path)
.value("title", title)
.value("working_directory", working_directory)
.value("created_at_ms", seq_to_value(summary.created_at_ms))
.value("updated_at_ms", seq_to_value(summary.updated_at_ms))
.value("state", "active")
.value("projection_status", "fresh")
.where_eq("session_id", summary.id.to_string())
.execute(db)
.await?;
} else {
db.insert("sessions")
.value("parent_id", summary.parent_id.map(|id| id.to_string()))
.value("session_id", summary.id.to_string())
.value("db_path", db_path)
.value("title", title)
.value("working_directory", working_directory)
.value("created_at_ms", seq_to_value(summary.created_at_ms))
.value("updated_at_ms", seq_to_value(summary.updated_at_ms))
.value("state", "active")
.value("projection_status", "fresh")
.execute(db)
.await?;
}
Ok(())
}
pub async fn apply_catalog_batch(
&self,
root: &Path,
summaries: &[SessionSummary],
deleted: impl IntoIterator<Item = SessionId>,
) -> SessionDbResult<()> {
let transaction = self.db.begin_transaction().await?;
configure_turso_connection(&*transaction).await?;
let result = async {
for session_id in deleted {
transaction
.delete("sessions")
.where_eq("session_id", session_id.to_string())
.execute(&*transaction)
.await?;
}
for summary in summaries {
Self::upsert_session_on(&*transaction, summary, &session_db_path(root, summary.id))
.await?;
}
Ok::<(), SessionDbError>(())
}
.await;
if let Err(error) = result {
transaction.rollback().await?;
return Err(error);
}
transaction.commit().await?;
Ok(())
}
pub async fn upsert_sessions(
&self,
root: &Path,
summaries: &[SessionSummary],
) -> SessionDbResult<()> {
self.apply_catalog_batch(root, summaries, std::iter::empty())
.await
}
pub async fn delete_session(&self, session_id: SessionId) -> SessionDbResult<()> {
self.db
.delete("sessions")
.where_eq("session_id", session_id.to_string())
.execute(&**self.db)
.await?;
Ok(())
}
pub async fn list_sessions(&self) -> SessionDbResult<Vec<SessionSummary>> {
self.db
.select("sessions")
.columns(&[
"session_id",
"title",
"working_directory",
"created_at_ms",
"updated_at_ms",
])
.sort("updated_at_ms", SortDirection::Desc)
.execute(&**self.db)
.await?
.iter()
.map(session_summary_from_catalog_row)
.collect()
}
pub async fn list_sessions_page(
&self,
after: Option<SessionId>,
limit: usize,
) -> SessionDbResult<Vec<SessionSummary>> {
if limit == 0 {
return Ok(Vec::new());
}
self.db
.select("sessions")
.columns(&[
"session_id",
"title",
"working_directory",
"created_at_ms",
"updated_at_ms",
])
.where_gt(
"session_id",
after.map_or_else(String::new, |id| id.to_string()),
)
.sort("session_id", SortDirection::Asc)
.limit(limit.min(256))
.execute(&**self.db)
.await?
.iter()
.map(session_summary_from_catalog_row)
.collect()
}
pub async fn list_sessions_activity_page(
&self,
after: Option<bcode_session_models::SessionCatalogPosition>,
include_boundary: bool,
limit: usize,
) -> SessionDbResult<Vec<SessionSummary>> {
use switchy::database::query::{
where_and, where_eq, where_gt, where_gte, where_lt, where_or,
};
let mut query = self.db.select("sessions").columns(&[
"session_id",
"title",
"working_directory",
"created_at_ms",
"updated_at_ms",
]);
if let Some(after) = after {
let identity: Box<dyn switchy::database::query::BooleanExpression> = if include_boundary
{
Box::new(where_gte("session_id", after.id.to_string()))
} else {
Box::new(where_gt("session_id", after.id.to_string()))
};
query = query.filter(Box::new(where_or(vec![
Box::new(where_lt("updated_at_ms", seq_to_value(after.updated_at_ms))),
Box::new(where_and(vec![
Box::new(where_eq("updated_at_ms", seq_to_value(after.updated_at_ms))),
Box::new(where_lt("created_at_ms", seq_to_value(after.created_at_ms))),
])),
Box::new(where_and(vec![
Box::new(where_eq("updated_at_ms", seq_to_value(after.updated_at_ms))),
Box::new(where_eq("created_at_ms", seq_to_value(after.created_at_ms))),
identity,
])),
])));
}
query
.sort("updated_at_ms", SortDirection::Desc)
.sort("created_at_ms", SortDirection::Desc)
.sort("session_id", SortDirection::Asc)
.limit(limit.min(256))
.execute(&**self.db)
.await?
.iter()
.map(session_summary_from_catalog_row)
.collect()
}
pub async fn child_session_ids_page(
&self,
parent: SessionId,
after: Option<SessionId>,
limit: usize,
) -> SessionDbResult<Vec<SessionId>> {
self.db
.select("sessions")
.columns(&["session_id"])
.where_eq("parent_id", parent.to_string())
.where_gt(
"session_id",
after.map_or_else(String::new, |id| id.to_string()),
)
.sort("session_id", SortDirection::Asc)
.limit(limit.min(129))
.execute(&**self.db)
.await?
.iter()
.map(|row| {
required_string(row, "session_id")?.parse().map_err(|_| {
SessionDbError::InvalidRow {
column: "session_id".into(),
}
})
})
.collect()
}
pub async fn list_draft_session_composer_drafts(
&self,
) -> SessionDbResult<Vec<CatalogComposerDraft>> {
self.db
.select("composer_drafts")
.columns(&["scope_key", "text", "updated_at_ms"])
.where_eq("scope_kind", "draft_session")
.execute(&**self.db)
.await?
.iter()
.map(|row| {
Ok(CatalogComposerDraft {
launch_working_directory: PathBuf::from(required_string(row, "scope_key")?),
text: required_string(row, "text")?,
updated_at_ms: i64_to_u64(required_i64(row, "updated_at_ms")?),
})
})
.collect()
}
pub async fn draft_session_composer_draft_record(
&self,
launch_working_directory: &Path,
) -> SessionDbResult<Option<CatalogComposerDraft>> {
let scope_key = launch_working_directory.to_string_lossy().to_string();
self.db
.select("composer_drafts")
.columns(&["scope_key", "text", "updated_at_ms"])
.where_eq("scope_kind", "draft_session")
.where_eq("scope_key", scope_key)
.execute_first(&**self.db)
.await?
.as_ref()
.map(|row| {
Ok(CatalogComposerDraft {
launch_working_directory: PathBuf::from(required_string(row, "scope_key")?),
text: required_string(row, "text")?,
updated_at_ms: i64_to_u64(required_i64(row, "updated_at_ms")?),
})
})
.transpose()
}
pub async fn draft_session_composer_draft(
&self,
launch_working_directory: &Path,
) -> SessionDbResult<Option<String>> {
let scope_key = launch_working_directory.to_string_lossy().to_string();
let row = self
.db
.select("composer_drafts")
.columns(&["text"])
.where_eq("scope_kind", "draft_session")
.where_eq("scope_key", scope_key)
.execute_first(&**self.db)
.await?;
row.as_ref()
.map(|row| required_string(row, "text"))
.transpose()
}
pub async fn set_draft_session_composer_draft(
&self,
launch_working_directory: &Path,
text: &str,
updated_at_ms: u64,
) -> SessionDbResult<()> {
let scope_key = launch_working_directory.to_string_lossy().to_string();
if text.is_empty() {
self.db
.delete("composer_drafts")
.where_eq("scope_kind", "draft_session")
.where_eq("scope_key", scope_key)
.execute(&**self.db)
.await?;
return Ok(());
}
let existing = self
.db
.select("composer_drafts")
.columns(&["scope_key", "updated_at_ms"])
.where_eq("scope_kind", "draft_session")
.where_eq("scope_key", scope_key.clone())
.execute_first(&**self.db)
.await?;
if existing.as_ref().is_some_and(|row| {
optional_i64(row, "updated_at_ms")
.map(i64_to_u64)
.is_some_and(|existing_updated_at_ms| existing_updated_at_ms >= updated_at_ms)
}) {
return Ok(());
}
if existing.is_some() {
self.db
.update("composer_drafts")
.value("launch_working_directory", scope_key.clone())
.value("text", text.to_owned())
.value("updated_at_ms", seq_to_value(updated_at_ms))
.where_eq("scope_kind", "draft_session")
.where_eq("scope_key", scope_key)
.execute(&**self.db)
.await?;
} else {
self.db
.insert("composer_drafts")
.value("scope_kind", "draft_session")
.value("scope_key", scope_key.clone())
.value("launch_working_directory", scope_key)
.value("text", text.to_owned())
.value("updated_at_ms", seq_to_value(updated_at_ms))
.execute(&**self.db)
.await?;
}
Ok(())
}
#[must_use]
pub fn database(&self) -> &dyn Database {
&**self.db
}
}
pub type SessionMigrationProgressCallback =
Arc<dyn Fn(bcode_session_models::SessionMigrationProgress) + Send + Sync>;
#[derive(Debug, Clone)]
pub struct SessionDb {
session_id: SessionId,
pub(crate) db: Arc<Box<dyn Database>>,
}
#[cfg(test)]
fn test_migration_policy_callbacks() -> bcode_session_migration_target::MigrationPolicyCallbacks {
bcode_session_migration_target::MigrationPolicyCallbacks {
normalize: Arc::new(|row| {
let event = serde_json::from_str::<SessionEvent>(&row.payload)
.map_err(|error| error.to_string())?;
Ok(bcode_session_migration_target::NormalizedCanonicalRow {
event,
metric_counter: None,
historical_source: None,
retired_known: false,
})
}),
build_receipt: Arc::new(|facts| {
let source_writer_epoch =
u32::try_from(facts.source_writer_epoch).map_err(|error| error.to_string())?;
Ok(bcode_session_migration_target::MigrationReceipt {
operation_id: facts
.operation_id
.unwrap_or_else(|| "explicit-maintenance".to_owned()),
session_id: facts.session_id,
source_writer_epoch,
target_writer_epoch: CURRENT_SESSION_STORAGE_WRITER_EPOCH,
migration_step_ids: vec!["test-current-materialization".to_owned()],
source_event_count: facts.event_count,
source_event_tail: facts.event_tail,
source_payload_digest_sha256: facts.replay.source_payload_digest_sha256,
target_event_count: facts.event_count,
target_event_tail: facts.event_tail,
target_payload_digest_sha256: facts.target_payload_digest_sha256,
converted_events: facts.replay.converted_events,
retired_known_events: facts.replay.retired_known_events,
completed_at_ms: facts.completed_at_ms,
})
}),
}
}
impl SessionDb {
pub async fn open_existing_turso_in_root(
session_id: SessionId,
root: &Path,
) -> SessionDbResult<Self> {
Self::open_existing_turso_in_root_observed(session_id, root, MetricsRegistry::disabled())
.await
}
pub async fn open_existing_turso_in_root_observed(
session_id: SessionId,
root: &Path,
metrics: MetricsRegistry,
) -> SessionDbResult<Self> {
let path = session_db_path(root, session_id);
Self::open_existing_turso_observed(session_id, &path, metrics).await
}
pub async fn initialize_turso_in_root(
session_id: SessionId,
root: &Path,
) -> SessionDbResult<Self> {
let path = session_db_path(root, session_id);
if path.exists() {
return Err(SessionDbError::Io(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("session database already exists at {}", path.display()),
)));
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
Self::initialize_turso_observed(session_id, &path, MetricsRegistry::disabled()).await
}
pub(crate) async fn open_runtime_turso_in_root_observed(
session_id: SessionId,
root: &Path,
metrics: MetricsRegistry,
) -> SessionDbResult<Self> {
let db = Self::open_existing_turso_in_root_observed(session_id, root, metrics).await?;
db.validate_write_readiness().await?;
Ok(db)
}
pub(crate) async fn initialize_turso_in_root_observed(
session_id: SessionId,
root: &Path,
metrics: MetricsRegistry,
) -> SessionDbResult<Self> {
let path = session_db_path(root, session_id);
if path.exists() {
return Err(SessionDbError::Io(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("session database already exists at {}", path.display()),
)));
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
Self::initialize_turso_observed(session_id, &path, metrics).await
}
#[cfg(test)]
pub(crate) async fn migrate_turso_in_root(
session_id: SessionId,
root: &Path,
maintenance: &crate::lease::SessionMaintenanceGuard,
write: &crate::lease::SessionWriteGuard,
) -> SessionDbResult<Self> {
Self::migrate_turso_in_root_observed(
session_id,
root,
maintenance,
write,
MetricsRegistry::disabled(),
None,
)
.await
}
#[cfg(test)]
pub(crate) async fn migrate_turso_in_root_observed(
session_id: SessionId,
root: &Path,
maintenance: &crate::lease::SessionMaintenanceGuard,
write: &crate::lease::SessionWriteGuard,
metrics: MetricsRegistry,
progress: Option<SessionMigrationProgressCallback>,
) -> SessionDbResult<Self> {
Self::migrate_turso_in_root_observed_with_operation(
session_id,
root,
maintenance,
write,
metrics,
progress,
None,
test_migration_policy_callbacks(),
)
.await
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
pub async fn migrate_turso_in_root_observed_with_operation(
session_id: SessionId,
root: &Path,
maintenance: &crate::lease::SessionMaintenanceGuard,
write: &crate::lease::SessionWriteGuard,
metrics: MetricsRegistry,
progress: Option<SessionMigrationProgressCallback>,
operation_id: Option<bcode_session_models::SessionOpenOperationId>,
policy: bcode_session_migration_target::MigrationPolicyCallbacks,
) -> SessionDbResult<Self> {
Self::migrate_turso_in_root_observed_with_fault(
session_id,
root,
maintenance,
write,
metrics,
progress,
operation_id,
policy,
MigrationFaultPhase::Disabled,
)
.await
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
async fn migrate_turso_in_root_observed_with_fault(
session_id: SessionId,
root: &Path,
_maintenance: &crate::lease::SessionMaintenanceGuard,
_write: &crate::lease::SessionWriteGuard,
metrics: MetricsRegistry,
progress: Option<SessionMigrationProgressCallback>,
operation_id: Option<bcode_session_models::SessionOpenOperationId>,
policy: bcode_session_migration_target::MigrationPolicyCallbacks,
fault: MigrationFaultPhase,
) -> SessionDbResult<Self> {
let path = session_db_path(root, session_id);
let db = Self::open_existing_turso_observed(session_id, &path, metrics.clone()).await?;
let source_writer_epoch = db.storage_writer_epoch().await?;
#[cfg(test)]
abort_at_migration_crash_boundary("before_transaction");
#[cfg(test)]
abort_at_migration_crash_boundary("transaction_start");
let tx = db.db.begin_transaction().await?;
configure_turso_connection(&*tx).await?;
let mut target = SessionMigrationTarget::new(&*tx, &metrics);
report_migration_stage(
progress.as_ref(),
bcode_session_models::SessionMigrationStage::PreparingSchema,
"Preparing session storage schema",
);
let schema_timer = metrics.timer();
target.materialize_current_schema().await?;
metrics.record_histogram(
"session.migration.schema_duration_ms",
schema_timer.elapsed_ms(),
);
let migration_outcome = migrate_session_storage(
&mut target,
session_id,
&metrics,
progress.as_ref(),
&policy.normalize,
fault,
)
.await?;
let receipt = build_target_migration_receipt(
&*tx,
session_id,
source_writer_epoch,
operation_id,
&migration_outcome,
&policy.build_receipt,
)
.await?;
inject_migration_fault(fault, MigrationFaultPhase::Receipt)?;
target.persist_migration_receipt(&receipt).await?;
#[cfg(test)]
abort_at_migration_crash_boundary("before_epoch_update");
inject_migration_fault(fault, MigrationFaultPhase::WriterEpochFinalization)?;
if CURRENT_SESSION_STORAGE_WRITER_EPOCH == 0 {
return Err(SessionDbError::MigrationHistoryIncompatible {
reason: "current writer epoch must be non-zero".to_owned(),
});
}
target.finalize_writer_contract().await?;
#[cfg(test)]
abort_at_migration_crash_boundary("after_epoch_update_before_commit");
validate_storage_writer_contract(&*tx).await?;
#[cfg(test)]
abort_at_migration_crash_boundary("during_transaction");
report_migration_stage(
progress.as_ref(),
bcode_session_models::SessionMigrationStage::Committing,
"Committing session migration",
);
let commit_timer = metrics.timer();
tx.commit().await?;
#[cfg(test)]
abort_at_migration_crash_boundary("after_commit");
#[cfg(test)]
abort_at_migration_crash_boundary("post_commit_checkpoint");
metrics.record_histogram(
"session.migration.commit_duration_ms",
commit_timer.elapsed_ms(),
);
let checkpoint_timer = metrics.timer();
db.db.sqlite_checkpoint_truncate().await?;
metrics.record_histogram(
"session.migration.wal_checkpoint_duration_ms",
checkpoint_timer.elapsed_ms(),
);
Ok(db)
}
pub async fn open_turso_in_root(session_id: SessionId, root: &Path) -> SessionDbResult<Self> {
Self::open_turso_in_root_observed(session_id, root, MetricsRegistry::disabled()).await
}
pub async fn open_turso_in_root_observed(
session_id: SessionId,
root: &Path,
metrics: MetricsRegistry,
) -> SessionDbResult<Self> {
let path = session_db_path(root, session_id);
if path.exists() {
Self::open_existing_turso_observed(session_id, &path, metrics).await
} else {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
Self::initialize_turso_observed(session_id, &path, metrics).await
}
}
pub async fn open_turso(session_id: SessionId, path: &Path) -> SessionDbResult<Self> {
if path.exists() {
Self::open_existing_turso_observed(session_id, path, MetricsRegistry::disabled()).await
} else {
Self::initialize_turso_observed(session_id, path, MetricsRegistry::disabled()).await
}
}
async fn open_existing_turso_observed(
session_id: SessionId,
path: &Path,
metrics: MetricsRegistry,
) -> SessionDbResult<Self> {
if !path.exists() {
return Err(SessionDbError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("session database does not exist at {}", path.display()),
)));
}
let path = crate::db_path::resolve_existing_session_db(path)?;
Self::connect_turso_observed(session_id, &path, metrics).await
}
async fn initialize_turso_observed(
session_id: SessionId,
path: &Path,
metrics: MetricsRegistry,
) -> SessionDbResult<Self> {
let db = Self::connect_turso_observed(session_id, path, metrics).await?;
let stage_timer = crate::child_creation_observation::timer(
session_id,
crate::child_creation_observation::ChildCreationStage::SchemaInitialization,
);
Box::pin(run_session_migrations(&**db.db)).await?;
drop(stage_timer);
let stage_timer = crate::child_creation_observation::timer(
session_id,
crate::child_creation_observation::ChildCreationStage::StorageContract,
);
initialize_current_storage_contract(&**db.db).await?;
drop(stage_timer);
Ok(db)
}
async fn connect_turso_observed(
session_id: SessionId,
path: &Path,
metrics: MetricsRegistry,
) -> SessionDbResult<Self> {
let stage_timer = crate::child_creation_observation::timer(
session_id,
crate::child_creation_observation::ChildCreationStage::DatabaseConnect,
);
let open_started = std::time::Instant::now();
let db = init_turso_local_with_retry(path).await;
drop(stage_timer);
metrics.increment_counter("session.db.open_total");
DatabaseMetrics::new(metrics.clone(), "session", "turso").record(
DatabaseOperation::Open,
None,
"none",
db.is_ok(),
u64::try_from(open_started.elapsed().as_millis()).unwrap_or(u64::MAX),
);
let db = db?;
let db: Box<dyn Database> =
Box::new(ObservedDatabase::new(db, metrics, "session", "turso"));
let stage_timer = crate::child_creation_observation::timer(
session_id,
crate::child_creation_observation::ChildCreationStage::DatabaseConfiguration,
);
configure_turso_connection(&*db).await?;
drop(stage_timer);
Ok(Self {
session_id,
db: Arc::new(db),
})
}
pub(crate) async fn close(&self) -> SessionDbResult<()> {
self.db.close().await?;
Ok(())
}
pub(crate) async fn is_closed(&self) -> SessionDbResult<bool> {
match self.db.check_connection().await {
Ok(()) => Ok(false),
Err(error) if error.is_connection_error() => Ok(true),
Err(error) => Err(error.into()),
}
}
pub async fn session_composer_draft(&self) -> SessionDbResult<Option<String>> {
let row = self
.db
.select("session_drafts")
.columns(&["text"])
.where_eq("session_id", self.session_id.to_string())
.execute_first(&**self.db)
.await?;
row.as_ref()
.map(|row| required_string(row, "text"))
.transpose()
}
pub async fn set_session_composer_draft(
&self,
text: &str,
updated_at_ms: u64,
) -> SessionDbResult<()> {
validate_storage_writer_contract(&**self.db).await?;
if text.is_empty() {
self.db
.delete("session_drafts")
.where_eq("session_id", self.session_id.to_string())
.execute(&**self.db)
.await?;
return Ok(());
}
let existing = self
.db
.select("session_drafts")
.columns(&["session_id"])
.where_eq("session_id", self.session_id.to_string())
.execute_first(&**self.db)
.await?;
if existing.is_some() {
self.db
.update("session_drafts")
.value("text", text.to_owned())
.value("updated_at_ms", seq_to_value(updated_at_ms))
.where_eq("session_id", self.session_id.to_string())
.execute(&**self.db)
.await?;
} else {
self.db
.insert("session_drafts")
.value("session_id", self.session_id.to_string())
.value("text", text.to_owned())
.value("updated_at_ms", seq_to_value(updated_at_ms))
.execute(&**self.db)
.await?;
}
Ok(())
}
pub async fn active_tool_runs(&self) -> SessionDbResult<Vec<ToolRun>> {
self.tool_runs_by_status("running").await
}
pub async fn tool_run(&self, tool_call_id: &str) -> SessionDbResult<Option<ToolRun>> {
let row = self
.db
.select("tool_runs")
.columns(&[
"tool_call_id",
"event_seq_start",
"event_seq_end",
"status",
"tool_name",
"started_at_ms",
"completed_at_ms",
"output_bytes",
"is_error",
])
.where_eq("tool_call_id", tool_call_id.to_owned())
.execute_first(&**self.db)
.await?;
row.as_ref().map(tool_run_from_row).transpose()
}
pub async fn tool_runs_by_status(&self, status: &str) -> SessionDbResult<Vec<ToolRun>> {
let rows = self
.db
.select("tool_runs")
.columns(&[
"tool_call_id",
"event_seq_start",
"event_seq_end",
"status",
"tool_name",
"started_at_ms",
"completed_at_ms",
"output_bytes",
"is_error",
])
.where_eq("status", status)
.sort("event_seq_start", SortDirection::Asc)
.execute(&**self.db)
.await?;
rows.iter().map(tool_run_from_row).collect()
}
pub async fn active_runtime_work(&self) -> SessionDbResult<Vec<RuntimeWorkProjection>> {
let rows = self
.db
.select("runtime_work")
.columns(&[
"work_id",
"event_seq_start",
"event_seq_end",
"kind",
"label",
"status",
"parent_work_id",
"started_at_ms",
"finished_at_ms",
"message",
"cancellable",
])
.where_in(
"status",
[
RuntimeWorkStatus::Queued,
RuntimeWorkStatus::Running,
RuntimeWorkStatus::Cancelling,
]
.map(|status| DatabaseValue::String(runtime_work_status_name(status).to_owned()))
.to_vec(),
)
.sort("event_seq_start", SortDirection::Asc)
.execute(&**self.db)
.await?;
rows.iter().map(runtime_work_from_row).collect()
}
pub async fn runtime_work_history(
&self,
limit: usize,
) -> SessionDbResult<Vec<RuntimeWorkProjection>> {
let mut query = self
.db
.select("runtime_work")
.columns(&[
"work_id",
"event_seq_start",
"event_seq_end",
"kind",
"label",
"status",
"parent_work_id",
"started_at_ms",
"finished_at_ms",
"message",
"cancellable",
])
.sort("event_seq_start", SortDirection::Desc);
if limit > 0 {
query = query.limit(limit);
}
let mut work = query
.execute(&**self.db)
.await?
.iter()
.map(runtime_work_from_row)
.collect::<SessionDbResult<Vec<_>>>()?;
work.reverse();
Ok(work)
}
pub async fn session_state(&self) -> SessionDbResult<Option<SessionDbState>> {
let Some(row) = self
.db
.select("session_state")
.columns(&[
"session_id",
"last_event_seq",
"current_model",
"current_provider",
"working_directory",
"title",
"reasoning_effort",
"reasoning_summary",
"model_selection_source",
"reasoning_by_model",
"updated_at_ms",
"visibility",
"parent_id",
"execution_provenance",
"explicit_name",
"derived_title",
"automatic_title",
"automatic_title_source_sequence",
])
.where_eq("session_id", self.session_id.to_string())
.execute_first(&**self.db)
.await?
else {
return Ok(None);
};
let row_session_id = required_string(&row, "session_id")?.parse().map_err(|_| {
SessionDbError::InvalidRow {
column: "session_id".to_string(),
}
})?;
let has_user_message = self.input_message_count().await? > 0;
let latest_compaction_sequence = self.latest_context_compaction_sequence().await?;
let current_agent = self.latest_agent_selection().await?;
let execution = optional_string(&row, "execution_provenance")
.map(|value| serde_json::from_str::<ExecutionSessionProvenance>(&value))
.transpose()
.map_err(|_| SessionDbError::InvalidRow {
column: "execution_provenance".to_string(),
})?;
let visibility = optional_string(&row, "visibility")
.map(|value| serde_json::from_str::<SessionVisibility>(&value))
.transpose()
.map_err(|_| SessionDbError::InvalidRow {
column: "visibility".to_string(),
})?
.unwrap_or_default();
let (reasoning_by_model, reasoning_by_model_order) =
optional_string(&row, "reasoning_by_model")
.as_deref()
.map_or_else(Default::default, decode_reasoning_by_model);
Ok(Some(SessionDbState {
session_id: row_session_id,
last_event_seq: required_i64(&row, "last_event_seq").map(i64_to_u64)?,
title: optional_string(&row, "title"),
explicit_name: optional_string(&row, "explicit_name"),
derived_title: optional_string(&row, "derived_title"),
automatic_title: optional_string(&row, "automatic_title"),
automatic_title_source_sequence: optional_i64(&row, "automatic_title_source_sequence")
.map(i64_to_u64),
working_directory: std::path::PathBuf::from(required_string(
&row,
"working_directory",
)?),
current_agent,
current_provider: optional_string(&row, "current_provider"),
current_model: optional_string(&row, "current_model"),
reasoning_effort: optional_string(&row, "reasoning_effort"),
reasoning_summary: optional_string(&row, "reasoning_summary"),
model_selection_source: optional_string(&row, "model_selection_source")
.as_deref()
.and_then(decode_model_selection_source)
.unwrap_or_default(),
reasoning_by_model,
reasoning_by_model_order,
updated_at_ms: row
.get("updated_at_ms")
.and_then(|value| value.as_i64())
.map(i64_to_u64),
has_user_message,
latest_compaction_sequence,
visibility,
parent_id: optional_string(&row, "parent_id")
.map(|value| value.parse())
.transpose()
.map_err(|_| SessionDbError::InvalidRow {
column: "parent_id".into(),
})?,
execution,
}))
}
async fn latest_agent_selection(&self) -> SessionDbResult<Option<String>> {
let row = self
.db
.select("events")
.columns(&["event_seq", "payload"])
.where_eq("event_type", "agent_changed")
.sort("event_seq", SortDirection::Desc)
.limit(1)
.execute_first(&**self.db)
.await?;
let message_row = self
.db
.select("events")
.columns(&["event_seq", "payload"])
.where_eq("event_type", "user_message")
.sort("event_seq", SortDirection::Desc)
.limit(1)
.execute_first(&**self.db)
.await?;
if let Some(message_row) = message_row {
let message = strict_event_from_row(&message_row, self.session_id)?;
let legacy_sequence = row
.as_ref()
.map(|row| strict_event_from_row(row, self.session_id))
.transpose()?
.map(|event| event.sequence);
if legacy_sequence.is_none_or(|sequence| message.sequence > sequence)
&& let SessionEventKind::UserMessage { admission, .. } = message.kind
&& let Some(agent_id) = admission.execution.agent_profile
{
return Ok(Some(agent_id));
}
}
let Some(row) = row else {
return Ok(None);
};
let event = strict_event_from_row(&row, self.session_id)?;
Ok(match event.kind {
SessionEventKind::AgentChanged { agent_id } => Some(agent_id),
_ => None,
})
}
async fn input_message_count(&self) -> SessionDbResult<usize> {
Ok(self
.db
.select("input_messages")
.columns(&["event_seq"])
.execute(&**self.db)
.await?
.len())
}
async fn latest_context_compaction_sequence(&self) -> SessionDbResult<Option<u64>> {
Ok(self
.latest_context_compaction_event()
.await?
.and_then(|event| match event.kind {
SessionEventKind::ContextCompacted {
compacted_through_sequence,
..
}
| SessionEventKind::ProviderContextCompacted {
compacted_through_sequence,
..
} => Some(compacted_through_sequence),
_ => None,
}))
}
#[cfg(test)]
pub(crate) async fn compress_history_payload_page(
&self,
start: u64,
level: i32,
) -> SessionDbResult<crate::history_compression::HistoryCompressionPage> {
self.compress_history_payload_page_checked(start, level, None, || Ok(()))
.await
}
#[cfg(test)]
pub(crate) async fn compress_history_payload_page_checked(
&self,
start: u64,
level: i32,
cutoff_ms: Option<u64>,
check_cancelled: impl FnMut() -> std::io::Result<()> + Send,
) -> SessionDbResult<crate::history_compression::HistoryCompressionPage> {
self.compress_history_payload_page_through(start, level, cutoff_ms, None, check_cancelled)
.await
}
pub(crate) async fn compress_history_payload_page_through(
&self,
start: u64,
level: i32,
cutoff_ms: Option<u64>,
through_sequence: Option<u64>,
mut check_cancelled: impl FnMut() -> std::io::Result<()> + Send,
) -> SessionDbResult<crate::history_compression::HistoryCompressionPage> {
check_cancelled()?;
let tx = self.db.begin_transaction().await?;
configure_turso_connection(&*tx).await?;
validate_storage_writer_contract(&*tx).await?;
let current_tail = last_sequence(&*tx).await?;
if let Some(requested) = through_sequence
&& current_tail.is_none_or(|current| current < requested)
{
return Err(SessionDbError::InvalidRow {
column: "history_compression.through_sequence".into(),
});
}
let through_sequence = through_sequence.or(current_tail);
let mut report = crate::history_compression::HistoryCompressionPage {
through_sequence,
..Default::default()
};
let Some(tail) = through_sequence.filter(|tail| start <= *tail) else {
return Ok(report);
};
let limit =
usize::try_from(tail.saturating_sub(start).saturating_add(1).min(16)).unwrap_or(16);
let rows = read_canonical_payload_page(&*tx, start, limit).await?;
if rows.is_empty() {
return Err(SessionDbError::InvalidRow {
column: "history_compression.missing_page".into(),
});
}
let mut expected_sequence = start;
for row in rows {
if row.sequence != expected_sequence {
return Err(SessionDbError::InvalidCanonicalSequence {
expected: expected_sequence,
actual: row.sequence,
});
}
expected_sequence =
expected_sequence
.checked_add(1)
.ok_or_else(|| SessionDbError::InvalidRow {
column: "event_seq".into(),
})?;
if row.sequence > tail {
break;
}
check_cancelled()?;
let event = decode_session_event(&row.payload)?;
validate_canonical_event_identity(&event, row.sequence, self.session_id)?;
report.inspected += 1;
report.next_sequence = row.sequence.checked_add(1);
if cutoff_ms.is_some_and(|cutoff| event.timestamp_ms > cutoff) {
continue;
}
let stored = tx
.select("events")
.columns(&["payload"])
.where_eq("event_seq", seq_to_value(row.sequence))
.execute_first(&*tx)
.await?
.ok_or_else(|| SessionDbError::InvalidRow {
column: "events.payload".into(),
})?;
let stored = required_string(&stored, "payload")?;
if row.payload.len() <= crate::event_compression::MAX_COMPRESSED_EVENT_BYTES {
let candidate =
crate::event_compression::compress_event_payload(&row.payload, level)?;
if candidate.len() < stored.len() {
if crate::event_compression::decode_event_payload(&candidate)?.as_ref()
!= row.payload
{
return Err(std::io::Error::other(
"history compression verification failed",
)
.into());
}
tx.update("events")
.value("payload", candidate.clone())
.where_eq("event_seq", seq_to_value(row.sequence))
.execute(&*tx)
.await?;
report.compressed += 1;
report.saved_bytes += (stored.len() - candidate.len()) as u64;
}
}
}
check_cancelled()?;
tx.commit().await?;
Ok(report)
}
#[must_use]
pub const fn session_id(&self) -> SessionId {
self.session_id
}
#[must_use]
pub fn database(&self) -> &dyn Database {
&**self.db
}
pub async fn first_non_current_event_envelope(&self) -> SessionDbResult<Option<(u16, String)>> {
let current = bcode_session_models::CURRENT_SESSION_EVENT_SCHEMA_VERSION;
let row = self
.db
.select("events")
.columns(&["schema_version", "event_type"])
.where_not_eq("schema_version", i64::from(current))
.sort("event_seq", SortDirection::Asc)
.limit(1)
.execute_first(&**self.db)
.await?;
row.as_ref()
.map(|row| {
Ok((
u16::try_from(required_non_negative_u64(row, "schema_version")?).map_err(
|_| SessionDbError::InvalidRow {
column: "events.schema_version".to_owned(),
},
)?,
required_string(row, "event_type")?,
))
})
.transpose()
}
pub async fn event_envelope_inventory(&self) -> SessionDbResult<(Vec<(u16, String)>, bool)> {
const MAX_ENVELOPES: usize = 256;
let rows = self
.db
.select("events")
.project(switchy::database::query::identifier("event_type"))
.project(switchy::database::query::identifier("schema_version"))
.project_as(
switchy::database::query::min(switchy::database::query::identifier("event_seq")),
"first_event_seq",
)
.group_by(switchy::database::query::identifier("event_type"))
.group_by(switchy::database::query::identifier("schema_version"))
.sort("first_event_seq", SortDirection::Asc)
.limit(MAX_ENVELOPES + 1)
.execute(&**self.db)
.await?;
let truncated = rows.len() > MAX_ENVELOPES;
let mut envelopes = Vec::with_capacity(rows.len().min(MAX_ENVELOPES));
for row in rows.into_iter().take(MAX_ENVELOPES) {
let event_kind = required_string(&row, "event_type")?;
let event_schema = u16::try_from(required_non_negative_u64(&row, "schema_version")?)
.map_err(|_| SessionDbError::InvalidRow {
column: "events.schema_version".to_owned(),
})?;
envelopes.push((event_schema, event_kind));
}
Ok((envelopes, truncated))
}
pub async fn first_unknown_event_envelope(&self) -> SessionDbResult<Option<(u16, String)>> {
let rows = self
.db
.select("events")
.columns(&["event_type", "schema_version"])
.sort("event_seq", SortDirection::Asc)
.limit(256)
.execute(&**self.db)
.await?;
for row in rows {
let event_kind = required_string(&row, "event_type")?;
let event_schema = u16::try_from(required_non_negative_u64(&row, "schema_version")?)
.map_err(|_| SessionDbError::InvalidRow {
column: "events.schema_version".to_owned(),
})?;
if event_schema > bcode_session_models::CURRENT_SESSION_EVENT_SCHEMA_VERSION {
return Ok(Some((event_schema, event_kind)));
}
}
Ok(None)
}
pub async fn first_non_current_event_schema(&self) -> SessionDbResult<Option<u16>> {
let current = bcode_session_models::CURRENT_SESSION_EVENT_SCHEMA_VERSION;
let row = self
.db
.select("events")
.columns(&["schema_version"])
.where_not_eq("schema_version", i64::from(current))
.sort("schema_version", SortDirection::Asc)
.limit(1)
.execute_first(&**self.db)
.await?;
row.as_ref()
.and_then(|row| optional_i64(row, "schema_version"))
.map(i64_to_u64)
.map(u16::try_from)
.transpose()
.map_err(|_| SessionDbError::InvalidRow {
column: "events.schema_version".to_owned(),
})
}
pub async fn canonical_event_inventory(
&self,
) -> SessionDbResult<(u64, Option<u64>, BTreeMap<u16, u64>, BTreeMap<String, u64>)> {
let rows = self
.db
.select("events")
.columns(&["event_seq", "event_type", "schema_version"])
.sort("event_seq", SortDirection::Asc)
.execute(&**self.db)
.await?;
let mut schemas = BTreeMap::new();
let mut kinds = BTreeMap::new();
let mut tail = None;
for (index, row) in rows.iter().enumerate() {
let sequence = required_non_negative_u64(row, "event_seq")?;
let expected = u64::try_from(index).unwrap_or(u64::MAX);
if sequence != expected {
return Err(SessionDbError::InvalidCanonicalSequence {
expected,
actual: sequence,
});
}
tail = Some(sequence);
let schema = required_non_negative_u64(row, "schema_version")?;
let schema = u16::try_from(schema).map_err(|_| SessionDbError::InvalidRow {
column: "events.schema_version".to_owned(),
})?;
let count = schemas.entry(schema).or_insert(0_u64);
*count = count.saturating_add(1);
let kind = required_string(row, "event_type")?;
let count = kinds.entry(kind).or_insert(0_u64);
*count = count.saturating_add(1);
}
Ok((
u64::try_from(rows.len()).unwrap_or(u64::MAX),
tail,
schemas,
kinds,
))
}
pub async fn canonical_rows_page(
&self,
start_sequence: u64,
limit: usize,
) -> SessionDbResult<Vec<bcode_session_migration_target::CanonicalRow>> {
read_canonical_payload_page(&**self.db, start_sequence, limit).await
}
#[allow(clippy::too_many_lines)]
pub async fn storage_compatibility_facts(
&self,
) -> SessionDbResult<bcode_session_migration_target::StorageCompatibilityFacts> {
let current_migration_ids = session_migrations()
.migrations()
.await?
.into_iter()
.map(|migration| migration.id().to_owned())
.collect::<Vec<_>>();
let migration_rows = if self.db.table_exists(SESSION_MIGRATIONS_TABLE).await? {
self.db
.select(SESSION_MIGRATIONS_TABLE)
.columns(&["id", "status"])
.execute(&**self.db)
.await?
.into_iter()
.map(|row| {
Ok(bcode_session_migration_target::MigrationLedgerRow {
id: required_string(&row, "id")?,
status: required_string(&row, "status")?,
})
})
.collect::<SessionDbResult<Vec<_>>>()?
} else {
Vec::new()
};
let contract_table_exists = self.db.table_exists("session_storage_contract").await?;
let contract = if contract_table_exists {
self.db
.select("session_storage_contract")
.columns(&["schema_version", "writer_epoch"])
.where_eq("contract_id", SESSION_STORAGE_CONTRACT_ID)
.execute_first(&**self.db)
.await?
.as_ref()
.map(|row| {
Ok::<_, SessionDbError>((
required_non_negative_u64(row, "schema_version")?,
required_non_negative_u64(row, "writer_epoch")?,
))
})
.transpose()?
} else {
None
};
Ok(bcode_session_migration_target::StorageCompatibilityFacts {
current_migration_ids,
migration_rows,
contract_table_exists,
contract_schema_version: contract.map(|(schema, _)| schema),
writer_epoch: contract.map(|(_, writer)| writer),
expected_contract_schema_version: u64::from(SESSION_STORAGE_CONTRACT_SCHEMA_VERSION),
legacy_writer_epoch: LEGACY_SESSION_STORAGE_WRITER_EPOCH,
})
}
pub async fn storage_compatibility(&self) -> SessionDbResult<SessionStorageCompatibility> {
match bcode_session_migration_target::classify_storage_facts(
&self.storage_compatibility_facts().await?,
) {
Ok(bcode_session_migration_target::StorageCompatibility::Current { writer_epoch }) => {
Ok(SessionStorageCompatibility::Current { writer_epoch })
}
Ok(bcode_session_migration_target::StorageCompatibility::MigrationRequired {
writer_epoch,
}) => Ok(SessionStorageCompatibility::KnownLegacy { writer_epoch }),
Err(bcode_session_migration_target::StorageCompatibilityError::WriterEpoch {
actual,
expected,
}) => Err(SessionDbError::WriterIncompatible {
actual: Some(actual),
expected,
}),
Err(error) => Err(SessionDbError::MigrationHistoryIncompatible {
reason: error.to_string(),
}),
}
}
pub async fn storage_writer_epoch(&self) -> SessionDbResult<u64> {
if !self.db.table_exists("session_storage_contract").await? {
return Ok(u64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH));
}
let row = self
.db
.select("session_storage_contract")
.columns(&["writer_epoch"])
.where_eq("contract_id", SESSION_STORAGE_CONTRACT_ID)
.execute_first(&**self.db)
.await?;
row.as_ref().map_or_else(
|| Ok(u64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)),
|row| required_non_negative_u64(row, "writer_epoch"),
)
}
pub(crate) async fn accepted_turn_event(
&self,
sequence: u64,
) -> SessionDbResult<Option<SessionEvent>> {
self.validate_write_readiness().await?;
self.db
.select("events")
.columns(&["event_seq", "event_type", "schema_version", "payload"])
.where_eq("event_seq", seq_to_value(sequence))
.execute_first(&**self.db)
.await?
.as_ref()
.map(|row| strict_event_from_row(row, self.session_id))
.transpose()
}
pub async fn turn_receipt(
&self,
producer: &str,
idempotency_key: &str,
) -> SessionDbResult<Option<bcode_session_models::TurnReceipt>> {
let row = self
.db
.select("turn_receipts")
.columns(&["accepted_event_seq", "turn_id", "work_id"])
.where_eq("producer", producer.to_owned())
.where_eq("idempotency_key", idempotency_key.to_owned())
.execute_first(&**self.db)
.await?;
row.as_ref()
.map(|row| {
Ok(bcode_session_models::TurnReceipt {
accepted_event_sequence: i64_to_u64(required_i64(row, "accepted_event_seq")?),
turn_id: bcode_session_models::TurnId(required_string(row, "turn_id")?),
work_id: WorkId::new(required_string(row, "work_id")?),
})
})
.transpose()
}
pub async fn append_event(&self, event: &SessionEvent) -> SessionDbResult<()> {
self.append_event_with_activity_timestamp(event, None).await
}
pub(crate) async fn append_event_batch(
&self,
events: &[(SessionEvent, Option<u64>)],
) -> SessionDbResult<()> {
if events.is_empty() {
return Ok(());
}
let tx = self.db.begin_transaction().await?;
configure_turso_connection(&*tx).await?;
validate_storage_writer_contract(&*tx).await?;
for (event, activity_timestamp_ms) in events {
validate_append_preconditions_without_writer(&*tx, event).await?;
append_event_projections(&*tx, event, *activity_timestamp_ms).await?;
validate_append_postconditions(&*tx, event).await?;
}
tx.commit().await?;
Ok(())
}
async fn append_event_for_writer_epoch(
&self,
event: &SessionEvent,
activity_timestamp_ms: Option<u64>,
writer_epoch: u32,
) -> SessionDbResult<()> {
let tx = self.db.begin_transaction().await?;
configure_turso_connection(&*tx).await?;
validate_storage_writer_contract_for_epoch(&*tx, writer_epoch).await?;
validate_append_preconditions_without_writer(&*tx, event).await?;
append_event_projections(&*tx, event, activity_timestamp_ms).await?;
validate_append_postconditions(&*tx, event).await?;
tx.commit().await?;
Ok(())
}
pub async fn append_event_with_activity_timestamp(
&self,
event: &SessionEvent,
activity_timestamp_ms: Option<u64>,
) -> SessionDbResult<()> {
self.append_event_for_writer_epoch(
event,
activity_timestamp_ms,
CURRENT_SESSION_STORAGE_WRITER_EPOCH,
)
.await
}
pub async fn append_event_with_cost(
&self,
event: &SessionEvent,
cost: Option<&bcode_session_models::SessionCostEstimate>,
activity_timestamp_ms: Option<u64>,
) -> SessionDbResult<()> {
if let (SessionEventKind::ModelUsage { usage, .. }, Some(cost)) = (&event.kind, cost) {
let mut validation = usage.clone();
validation.cost = Some(cost.clone());
validation
.validate()
.map_err(|column| SessionDbError::InvalidRow { column })?;
}
let tx = self.db.begin_transaction().await?;
configure_turso_connection(&*tx).await?;
validate_storage_writer_contract(&*tx).await?;
validate_append_preconditions_without_writer(&*tx, event).await?;
append_event_projections(&*tx, event, activity_timestamp_ms).await?;
if let Some(cost) = cost {
set_projected_request_cost(&*tx, event, cost).await?;
}
validate_append_postconditions(&*tx, event).await?;
tx.commit().await?;
Ok(())
}
pub async fn append_event_with_original(
&self,
event: &SessionEvent,
cost: Option<&bcode_session_models::SessionCostEstimate>,
original: Option<&bcode_session_models::OriginalUsage>,
activity_timestamp_ms: Option<u64>,
) -> SessionDbResult<()> {
if original.is_none() {
return self
.append_event_with_cost(event, cost, activity_timestamp_ms)
.await;
}
if let SessionEventKind::ModelUsage { usage, .. } = &event.kind {
let mut validation = usage.clone();
if let Some(cost) = cost {
validation.cost = Some(cost.clone());
}
validation
.validate()
.map_err(|column| SessionDbError::InvalidRow { column })?;
}
let payload = crate::persisted::encode_session_event_with_original(event, original)
.map_err(|column| SessionDbError::InvalidRow { column })?;
let tx = self.db.begin_transaction().await?;
configure_turso_connection(&*tx).await?;
validate_storage_writer_contract(&*tx).await?;
validate_append_preconditions_without_writer(&*tx, event).await?;
if matches!(event.kind, SessionEventKind::ModelUsage { .. }) {
validate_original_usage_duplicate(&*tx, event, original).await?;
}
crate::db_event_store::insert_event_payload(&*tx, event, activity_timestamp_ms, payload)
.await?;
project_appended_event(&*tx, event).await?;
if let Some(cost) = cost {
set_projected_request_cost(&*tx, event, cost).await?;
}
if let Some(original) = original {
let mut summary = read_session_usage_summary(&*tx).await?;
summary.original_usage_capture_issue = original.capture_issue;
tx.update("session_usage_projection")
.value("summary_json", serde_json::to_string(&summary)?)
.where_eq("projection_id", 1)
.execute(&*tx)
.await?;
}
validate_append_postconditions(&*tx, event).await?;
tx.commit().await?;
Ok(())
}
pub async fn validate_write_readiness(&self) -> SessionDbResult<()> {
validate_storage_writer_contract(&**self.db).await?;
let next_sequence = self
.last_event_sequence()
.await?
.map_or(0, |sequence| sequence.saturating_add(1));
let probe = SessionEvent {
schema_version: bcode_session_models::CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence: next_sequence,
timestamp_ms: 0,
session_id: self.session_id,
provenance: None,
kind: SessionEventKind::SessionRenamed { name: None },
};
validate_append_preconditions_without_writer(&**self.db, &probe).await
}
pub async fn validate_read_readiness(&self) -> SessionDbResult<()> {
let transaction = self.db.begin_transaction().await?;
let result = async {
validate_storage_writer_contract(&*transaction).await?;
let next_sequence = last_sequence(&*transaction)
.await?
.map_or(0, |sequence| sequence.saturating_add(1));
let probe = SessionEvent {
schema_version: bcode_session_models::CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence: next_sequence,
timestamp_ms: 0,
session_id: self.session_id,
provenance: None,
kind: SessionEventKind::SessionRenamed { name: None },
};
validate_append_preconditions_without_writer(&*transaction, &probe).await
}
.await;
let release = transaction.rollback().await;
result?;
release?;
Ok(())
}
pub async fn current_open_readiness_known_current(&self) -> SessionDbResult<()> {
self.validate_write_readiness().await
}
pub async fn materialized_projection_schema_version(
&self,
projection: MaterializedProjection,
) -> SessionDbResult<Option<u64>> {
let row = self
.db
.select("projection_checkpoints")
.columns(&["projection_version"])
.where_eq("projection_name", projection.as_str())
.execute_first(&**self.db)
.await?;
row.as_ref()
.map(|row| required_i64(row, "projection_version").map(i64_to_u64))
.transpose()
}
pub async fn materialized_projection_checkpoint(
&self,
projection: MaterializedProjection,
) -> SessionDbResult<Option<u64>> {
self.projection_checkpoint(projection.as_str()).await
}
async fn projection_checkpoint(&self, projection_name: &str) -> SessionDbResult<Option<u64>> {
let row = self
.db
.select("projection_checkpoints")
.columns(&["last_event_seq"])
.where_eq("projection_name", projection_name)
.execute_first(&**self.db)
.await?;
row.as_ref()
.map(|row| required_i64(row, "last_event_seq").map(i64_to_u64))
.transpose()
}
pub async fn usage_page(
&self,
query: &bcode_session_models::SessionUsageQuery,
) -> SessionDbResult<bcode_session_models::SessionUsagePage> {
use bcode_session_models::{SessionUsageEntry, SessionUsageGeneration, SessionUsagePage};
let invalid = |column| SessionDbError::InvalidRow { column };
query.validate().map_err(invalid)?;
let before = self.session_usage_summary().await?;
let generation = SessionUsageGeneration {
through_sequence: before.through_sequence,
cost_revision: before.cost_revision,
};
if query
.generation
.is_some_and(|expected| expected != generation)
{
return Err(invalid(
"usage accounting generation changed; restart query".into(),
));
}
let rows = self
.db
.select("session_usage_requests")
.columns(&[
"request_key",
"event_seq",
"first_observed_at_ms",
"usage_json",
"cost_json",
])
.where_gt("request_key", query.after.clone().unwrap_or_default())
.sort("request_key", SortDirection::Asc)
.limit(query.limit as usize)
.execute(&**self.db)
.await?;
let scanned =
u32::try_from(rows.len()).map_err(|_| invalid("usage page overflow".into()))?;
let mut entries = Vec::new();
let mut last_key = None;
for row in rows {
let key = required_string(&row, "request_key")?;
let first_observed_at_ms = u64::try_from(required_i64(&row, "first_observed_at_ms")?)
.map_err(|_| invalid("negative request timestamp".into()))?;
last_key = Some(key.clone());
if !query.range.contains(first_observed_at_ms) {
continue;
}
let mut usage: SessionTokenUsage =
serde_json::from_str(&required_string(&row, "usage_json")?)?;
usage.cost = optional_string(&row, "cost_json")
.map(|json| serde_json::from_str(&json))
.transpose()?;
usage.validate().map_err(invalid)?;
let canonical = self
.db
.select("events")
.columns(&["payload"])
.where_eq("event_seq", required_i64(&row, "event_seq")?)
.execute_first(&**self.db)
.await?
.ok_or_else(|| invalid("missing canonical usage evidence".into()))?;
let evidence =
crate::persisted::original_usage(&required_string(&canonical, "payload")?)
.map_err(invalid)?
.map(|original| bcode_session_models::SessionUsageEvidence {
complete: original.complete,
report_count: original.reports.len(),
capture_issue: original.capture_issue,
});
entries.push(SessionUsageEntry {
key,
first_observed_at_ms,
usage,
evidence,
});
}
let after = self.session_usage_summary().await?;
if before.through_sequence != after.through_sequence
|| before.cost_revision != after.cost_revision
{
return Err(invalid(
"usage accounting generation changed; restart query".into(),
));
}
Ok(SessionUsagePage {
generation,
entries,
scanned,
next_after: if scanned == query.limit {
last_key
} else {
None
},
})
}
pub async fn session_usage_summary(&self) -> SessionDbResult<SessionUsageSummary> {
let expected = self.last_event_sequence().await?.unwrap_or_default();
let checkpoint = self
.materialized_projection_checkpoint(MaterializedProjection::SessionUsage)
.await?;
if checkpoint != Some(expected) {
return Err(SessionDbError::ProjectionStale {
projection: MaterializedProjection::SessionUsage.as_str(),
checkpoint,
expected,
});
}
let mut summary = read_session_usage_summary(&**self.db).await?;
summary.through_sequence = Some(expected);
Ok(summary)
}
pub async fn reprice_usage(
&self,
range: bcode_session_models::SessionCostRange,
price: &(
dyn Fn(&SessionTokenUsage) -> bcode_session_models::SessionCostEstimate + Send + Sync
),
) -> SessionDbResult<(u64, SessionUsageSummary)> {
range
.validate()
.map_err(|column| SessionDbError::InvalidRow { column })?;
self.session_usage_summary().await?;
let tx = self.db.begin_transaction().await?;
configure_turso_connection(&*tx).await?;
validate_storage_writer_contract(&*tx).await?;
let mut summary = read_session_usage_summary(&*tx).await?;
let latest_usage = summary.latest_usage.clone();
let mut cursor = String::new();
let mut count = 0_u64;
loop {
let rows = tx
.select("session_usage_requests")
.columns(&["request_key", "usage_json", "cost_json"])
.where_gte(
"first_observed_at_ms",
seq_to_value(range.from_timestamp_ms),
)
.where_lt("first_observed_at_ms", seq_to_value(range.to_timestamp_ms))
.where_gt("request_key", cursor.clone())
.sort("request_key", SortDirection::Asc)
.limit(128)
.execute(&*tx)
.await?;
if rows.is_empty() {
break;
}
for row in rows {
cursor = required_string(&row, "request_key")?;
let mut previous: SessionTokenUsage =
serde_json::from_str(&required_string(&row, "usage_json")?)?;
previous.cost = optional_string(&row, "cost_json")
.map(|json| serde_json::from_str(&json))
.transpose()?;
let mut facts = previous.clone();
facts.cost = None;
let cost = price(&facts);
let mut next = previous.clone();
next.cost = Some(cost.clone());
next.validate()
.map_err(|column| SessionDbError::InvalidRow { column })?;
crate::usage::replace(&mut summary, Some(&previous), &next)
.map_err(|column| SessionDbError::InvalidRow { column })?;
tx.update("session_usage_requests")
.value("cost_json", serde_json::to_string(&cost)?)
.where_eq("request_key", cursor.clone())
.execute(&*tx)
.await?;
count += 1;
}
}
summary.latest_usage = latest_usage;
summary.cost_revision =
summary
.cost_revision
.checked_add(1)
.ok_or_else(|| SessionDbError::InvalidRow {
column: "cost_revision overflow".to_owned(),
})?;
tx.upsert("session_usage_projection")
.unique(&["projection_id"])
.value("projection_id", DatabaseValue::Int32(1))
.value("summary_json", serde_json::to_string(&summary)?)
.execute(&*tx)
.await?;
tx.commit().await?;
summary.through_sequence = self.last_event_sequence().await?;
Ok((count, summary))
}
pub async fn renormalize_usage(
&self,
range: bcode_session_models::SessionCostRange,
normalize: crate::actor::UsageNormalizer,
) -> SessionDbResult<(u64, SessionUsageSummary)> {
range
.validate()
.map_err(|column| SessionDbError::InvalidRow { column })?;
let initial = self.session_usage_summary().await?;
self.clear_usage_staging().await?;
let mut cursor = String::new();
let mut count = 0;
loop {
let rows = self
.db
.select("session_usage_requests")
.columns(&["request_key", "usage_json", "event_seq"])
.where_gte(
"first_observed_at_ms",
seq_to_value(range.from_timestamp_ms),
)
.where_lt("first_observed_at_ms", seq_to_value(range.to_timestamp_ms))
.where_gt("request_key", cursor.clone())
.sort("request_key", SortDirection::Asc)
.limit(128)
.execute(&**self.db)
.await?;
if rows.is_empty() {
break;
}
for row in rows {
cursor = required_string(&row, "request_key")?;
let sequence = required_i64(&row, "event_seq")?.cast_unsigned();
let usage: SessionTokenUsage =
serde_json::from_str(&required_string(&row, "usage_json")?)?;
let canonical = self
.db
.select("events")
.columns(&["payload"])
.where_eq("event_seq", seq_to_value(sequence))
.execute_first(&**self.db)
.await?
.ok_or_else(|| SessionDbError::InvalidRow {
column: "missing usage event".into(),
})?;
let original =
crate::persisted::original_usage(&required_string(&canonical, "payload")?)
.map_err(|column| SessionDbError::InvalidRow { column })?;
let valuation = normalize(bcode_session_models::SessionUsageSource {
sequence,
usage: usage.clone(),
original,
})
.await
.map_err(|column| SessionDbError::InvalidRow { column })?;
validate_renormalized_usage(&usage, &valuation)?;
self.db
.insert("usage_valuation_staging")
.value("request_key", cursor.clone())
.value("usage_json", serde_json::to_string(&valuation.usage)?)
.value("cost_json", serde_json::to_string(&valuation.cost)?)
.execute(&**self.db)
.await?;
count += 1;
}
}
self.publish_staged_usage(initial, count).await
}
pub async fn clear_usage_staging(&self) -> SessionDbResult<()> {
self.db
.delete("usage_valuation_staging")
.execute(&**self.db)
.await?;
Ok(())
}
async fn publish_staged_usage(
&self,
initial: SessionUsageSummary,
count: u64,
) -> SessionDbResult<(u64, SessionUsageSummary)> {
let tx = self.db.begin_transaction().await?;
configure_turso_connection(&*tx).await?;
validate_storage_writer_contract(&*tx).await?;
let mut summary = read_session_usage_summary(&*tx).await?;
if last_sequence(&*tx).await? != initial.through_sequence
|| summary.cost_revision != initial.cost_revision
{
return Err(SessionDbError::InvalidRow {
column: "usage changed during repricing".into(),
});
}
let latest_request = summary
.latest_usage
.as_ref()
.and_then(|usage| usage.request_id.clone());
let mut cursor = String::new();
loop {
let rows = tx
.select("usage_valuation_staging")
.columns(&["request_key", "usage_json", "cost_json"])
.where_gt("request_key", cursor.clone())
.sort("request_key", SortDirection::Asc)
.limit(128)
.execute(&*tx)
.await?;
if rows.is_empty() {
break;
}
for row in rows {
cursor = required_string(&row, "request_key")?;
let current = tx
.select("session_usage_requests")
.columns(&["usage_json", "cost_json"])
.where_eq("request_key", cursor.clone())
.execute_first(&*tx)
.await?
.ok_or_else(|| SessionDbError::InvalidRow {
column: "request missing during repricing".into(),
})?;
let mut previous: SessionTokenUsage =
serde_json::from_str(&required_string(¤t, "usage_json")?)?;
previous.cost = serde_json::from_str(&required_string(¤t, "cost_json")?)?;
let mut next: SessionTokenUsage =
serde_json::from_str(&required_string(&row, "usage_json")?)?;
next.cost = Some(serde_json::from_str(&required_string(&row, "cost_json")?)?);
let latest = summary.latest_usage.clone();
crate::usage::replace(&mut summary, Some(&previous), &next)
.map_err(|column| SessionDbError::InvalidRow { column })?;
if next.request_id != latest_request {
summary.latest_usage = latest;
}
tx.update("session_usage_requests")
.value("usage_json", required_string(&row, "usage_json")?)
.value("cost_json", required_string(&row, "cost_json")?)
.where_eq("request_key", cursor.clone())
.execute(&*tx)
.await?;
}
}
summary.cost_revision =
summary
.cost_revision
.checked_add(1)
.ok_or_else(|| SessionDbError::InvalidRow {
column: "cost revision overflow".into(),
})?;
tx.update("session_usage_projection")
.value("summary_json", serde_json::to_string(&summary)?)
.where_eq("projection_id", 1)
.execute(&*tx)
.await?;
tx.delete("usage_valuation_staging").execute(&*tx).await?;
tx.commit().await?;
summary.through_sequence = initial.through_sequence;
Ok((count, summary))
}
pub async fn input_history(&self) -> SessionDbResult<Vec<SessionInputHistoryEntry>> {
let rows = self
.db
.select("input_messages")
.columns(&["event_seq", "created_at_ms", "text"])
.sort("input_seq", SortDirection::Asc)
.execute(&**self.db)
.await?;
rows.iter().map(input_history_entry_from_row).collect()
}
pub async fn input_history_page(
&self,
before_sequence: Option<u64>,
limit: usize,
) -> SessionDbResult<Vec<SessionInputHistoryEntry>> {
let mut select = self
.db
.select("input_messages")
.columns(&["event_seq", "created_at_ms", "text"])
.sort("input_seq", SortDirection::Desc)
.limit(limit);
if let Some(before_sequence) = before_sequence {
select = select.where_lt("event_seq", seq_to_value(before_sequence));
}
let rows = select.execute(&**self.db).await?;
rows.iter().map(input_history_entry_from_row).collect()
}
pub async fn activity_bounds(&self) -> SessionDbResult<Option<(u64, u64)>> {
let first = self
.db
.select("events")
.columns(&["created_at_ms"])
.sort("event_seq", SortDirection::Asc)
.limit(1)
.execute_first(&**self.db)
.await?
.and_then(|row| row.get("created_at_ms").and_then(|value| value.as_i64()))
.map(i64_to_u64);
let last = self
.db
.select("events")
.columns(&["created_at_ms"])
.sort("event_seq", SortDirection::Desc)
.limit(1)
.execute_first(&**self.db)
.await?
.and_then(|row| row.get("created_at_ms").and_then(|value| value.as_i64()))
.map(i64_to_u64);
Ok(first.zip(last))
}
pub async fn all_events(&self) -> SessionDbResult<Vec<SessionEvent>> {
let rows = self
.db
.select("events")
.columns(&["event_seq", "payload"])
.sort("event_seq", SortDirection::Asc)
.execute(&**self.db)
.await?;
rows.iter()
.map(|row| strict_event_from_row(row, self.session_id))
.collect()
}
pub async fn all_events_strict(&self) -> SessionDbResult<Vec<SessionEvent>> {
strict_events(&**self.db).await
}
pub async fn history_page(
&self,
query: SessionHistoryQuery,
) -> SessionDbResult<SessionHistoryPage> {
let limit = query.limit.max(1);
if limit > MAX_SESSION_HISTORY_READ_EVENTS {
return Err(SessionDbError::HistoryReadLimitExceeded {
requested: limit,
maximum: MAX_SESSION_HISTORY_READ_EVENTS,
});
}
let fetch_limit = limit.saturating_add(1);
let mut select = self
.db
.select("events")
.columns(&["event_seq", "payload"])
.limit(fetch_limit);
select = match query.direction {
SessionHistoryDirection::Forward => {
let select = if let Some(cursor) = query.cursor {
select.where_gte("event_seq", seq_to_value(cursor.sequence))
} else {
select
};
select.sort("event_seq", SortDirection::Asc)
}
SessionHistoryDirection::Backward => {
let select = if let Some(cursor) = query.cursor {
select.where_lte("event_seq", seq_to_value(cursor.sequence))
} else {
select
};
select.sort("event_seq", SortDirection::Desc)
}
};
let rows = select.execute(&**self.db).await?;
let has_more = rows.len() > limit;
let next_cursor = rows
.get(limit)
.map(|row| -> SessionDbResult<SessionHistoryCursor> {
Ok(SessionHistoryCursor {
sequence: required_non_negative_u64(row, "event_seq")?,
})
})
.transpose()?;
let decoded = rows
.iter()
.take(limit)
.map(|row| strict_event_from_row(row, self.session_id))
.collect::<SessionDbResult<Vec<_>>>()?;
let compatibility_issues = Vec::new();
let events = decoded;
let events = if matches!(query.direction, SessionHistoryDirection::Backward) {
events.into_iter().rev().collect()
} else {
events
};
Ok(SessionHistoryPage {
session_id: self.session_id,
events,
compatibility_issues,
next_cursor,
has_more,
})
}
pub async fn history_around(
&self,
query: SessionHistoryAroundQuery,
) -> SessionDbResult<SessionHistoryWindow> {
let limit = query.event_limit();
if limit > MAX_SESSION_HISTORY_READ_EVENTS {
return Err(SessionDbError::HistoryReadLimitExceeded {
requested: limit,
maximum: MAX_SESSION_HISTORY_READ_EVENTS,
});
}
let start_sequence = query
.sequence
.saturating_sub(u64::try_from(query.before).unwrap_or(u64::MAX));
let end_sequence = query
.sequence
.saturating_add(u64::try_from(query.after).unwrap_or(u64::MAX));
let events = self
.events_range(start_sequence, end_sequence, limit)
.await?;
Ok(SessionHistoryWindow {
session_id: self.session_id,
requested_sequence: query.sequence,
anchor_present: events.iter().any(|event| event.sequence == query.sequence),
events,
first_available_sequence: self.first_event_sequence().await?,
last_available_sequence: self.last_event_sequence().await?,
compatibility_issues: Vec::new(),
})
}
pub async fn turn_evidence(&self, turn_id: &str) -> SessionDbResult<Vec<SessionEvent>> {
if let Some(tail) = self.last_event_sequence().await? {
let snapshot = projection_checkpoint_snapshot(&**self.db).await?;
validate_projection_checkpoint_snapshot(&snapshot, tail)?;
}
let indexes = self
.db
.select("sqlite_master")
.columns(&["name"])
.where_eq("type", "index")
.where_eq("name", "idx_turn_evidence_sequence")
.limit(1)
.execute(&**self.db)
.await?;
if indexes.is_empty() {
return Err(SessionDbError::InvalidData(
"turn evidence index missing; explicit maintenance required".into(),
));
}
let mut events = Vec::new();
for kind in ["terminal", "output", "retry"] {
let mut query = self
.db
.select("turn_evidence")
.columns(&["event_seq"])
.where_eq("turn_id", turn_id.to_owned())
.where_eq("evidence_kind", kind)
.limit(1);
if kind == "output" {
query = query.sort("segment_order", SortDirection::Desc);
if let Some(terminal) = events.first() {
let terminal: &SessionEvent = terminal;
query = query.where_lte("event_seq", seq_to_value(terminal.sequence));
}
}
let rows = query
.sort(
"event_seq",
if kind == "terminal" {
SortDirection::Asc
} else {
SortDirection::Desc
},
)
.execute(&**self.db)
.await?;
let Some(locator) = rows.first() else {
continue;
};
let sequence = required_i64(locator, "event_seq")?;
let rows = self
.db
.select("events")
.columns(&["event_seq", "payload"])
.where_eq("event_seq", DatabaseValue::Int64(sequence))
.limit(1)
.execute(&**self.db)
.await?;
let row = rows.first().ok_or_else(|| {
SessionDbError::InvalidData(
"turn evidence references a missing canonical event".into(),
)
})?;
let event = strict_event_from_row(row, self.session_id)?;
let matches = match &event.kind {
SessionEventKind::TraceEvent { trace } => {
kind == "retry"
&& trace.turn_id.as_deref() == Some(turn_id)
&& matches!(&trace.payload, bcode_session_models::SessionTracePayload::ProviderEvent { event_type, .. } if event_type == bcode_session_models::PROVIDER_RETRY_CHECKPOINT_EVENT)
}
SessionEventKind::ModelTurnFinished { turn_id: id, .. } => {
kind == "terminal" && id == turn_id
}
SessionEventKind::AssistantResponseSegment { turn_id: id, .. }
| SessionEventKind::PositionedAssistantResponseSegment { turn_id: id, .. } => {
kind == "output" && id == turn_id
}
_ => false,
};
if !matches {
return Err(SessionDbError::InvalidData(
"turn evidence disagrees with canonical event".into(),
));
}
events.push(event);
}
events.sort_by_key(|event| event.sequence);
Ok(events)
}
pub async fn inspection_page(
&self,
query: SessionInspectionQuery,
) -> SessionDbResult<SessionInspectionPage> {
let limit = query.limit.max(1);
if limit > MAX_SESSION_INSPECTION_EVENTS {
return Err(SessionDbError::HistoryReadLimitExceeded {
requested: limit,
maximum: MAX_SESSION_INSPECTION_EVENTS,
});
}
let candidate_limit = MAX_SESSION_HISTORY_READ_EVENTS.saturating_add(1);
let event_types = inspection_event_types(query.category)
.iter()
.map(|event_type| DatabaseValue::String((*event_type).to_owned()))
.collect::<Vec<_>>();
let mut select = self
.db
.select("events")
.columns(&["event_seq", "payload"])
.where_in("event_type", event_types)
.limit(candidate_limit);
select = match query.direction {
SessionHistoryDirection::Forward => {
let select = if let Some(cursor) = query.cursor {
select.where_gte("event_seq", seq_to_value(cursor.sequence))
} else {
select
};
select.sort("event_seq", SortDirection::Asc)
}
SessionHistoryDirection::Backward => {
let select = if let Some(cursor) = query.cursor {
select.where_lte("event_seq", seq_to_value(cursor.sequence))
} else {
select
};
select.sort("event_seq", SortDirection::Desc)
}
};
let rows = select.execute(&**self.db).await?;
let scan_limit = rows.len().min(MAX_SESSION_HISTORY_READ_EVENTS);
let mut events = Vec::new();
let mut scanned_events = 0;
let mut continuation_index = None;
for (index, row) in rows.iter().take(scan_limit).enumerate() {
scanned_events += 1;
let event = strict_event_from_row(row, self.session_id)?;
if inspection_event_matches(query.category, &event.kind) {
events.push(event);
if events.len() == limit {
continuation_index = Some(index.saturating_add(1));
break;
}
}
}
if continuation_index.is_none() && rows.len() > scan_limit {
continuation_index = Some(scan_limit);
}
let next_cursor = continuation_index
.and_then(|index| rows.get(index))
.map(|row| -> SessionDbResult<SessionHistoryCursor> {
Ok(SessionHistoryCursor {
sequence: required_non_negative_u64(row, "event_seq")?,
})
})
.transpose()?;
if matches!(query.direction, SessionHistoryDirection::Backward) {
events.reverse();
}
Ok(SessionInspectionPage {
session_id: self.session_id,
category: query.category,
events,
scanned_events,
compatibility_issues: Vec::new(),
next_cursor,
has_more: next_cursor.is_some(),
})
}
pub async fn plugin_status_note_events(
&self,
plugin_id: &str,
note_id: &str,
) -> SessionDbResult<Vec<SessionEvent>> {
let rows = self
.db
.select("events")
.columns(&["event_seq", "payload"])
.where_eq(
"event_type",
DatabaseValue::String("plugin_status_note".to_owned()),
)
.sort("event_seq", SortDirection::Asc)
.execute(&**self.db)
.await?;
let mut events = Vec::new();
for row in rows {
let event = strict_event_from_row(&row, self.session_id)?;
if matches!(
&event.kind,
SessionEventKind::PluginStatusNote {
plugin_id: event_plugin_id,
note_id: event_note_id,
..
} if event_plugin_id == plugin_id && event_note_id == note_id
) {
events.push(event);
}
}
Ok(events)
}
pub(crate) async fn submission_queue_head(
&self,
) -> SessionDbResult<Option<bcode_session_models::SubmissionRecord>> {
self.validate_write_readiness().await?;
self.db
.select("submissions")
.columns(&["record"])
.where_eq("queue_visible", 1_i64)
.sort("accepted_seq", switchy::database::query::SortDirection::Asc)
.limit(1)
.execute_first(&**self.db)
.await?
.map(|r| Ok(serde_json::from_str(&required_string(&r, "record")?)?))
.transpose()
}
pub(crate) async fn submission_recovery_operation(
&self,
operation_id: &str,
) -> SessionDbResult<Option<(SessionEventKind, bcode_session_models::SubmissionRecord)>> {
self.validate_write_readiness().await?;
crate::submission::recovery_operation(&**self.db, operation_id).await
}
pub async fn submission(
&self,
id: &bcode_session_models::SubmissionId,
) -> SessionDbResult<Option<bcode_session_models::SubmissionRecord>> {
self.validate_write_readiness().await?;
crate::submission::lookup(&**self.db, id).await
}
pub async fn pending_submissions(
&self,
after: Option<u64>,
limit: usize,
) -> SessionDbResult<Vec<bcode_session_models::SubmissionRecord>> {
self.validate_write_readiness().await?;
let mut query = self
.db
.select("submissions")
.columns(&["record"])
.where_eq("pending", 1_i64)
.sort("accepted_seq", SortDirection::Asc)
.limit(limit.min(256));
if let Some(after) = after {
query = query.where_gt("accepted_seq", seq_to_value(after));
}
query
.execute(&**self.db)
.await?
.iter()
.map(|row| Ok(serde_json::from_str(&required_string(row, "record")?)?))
.collect()
}
pub async fn pending_submissions_for_turn(
&self,
turn_id: &bcode_session_models::TurnId,
after: Option<u64>,
limit: usize,
) -> SessionDbResult<Vec<bcode_session_models::SubmissionRecord>> {
self.validate_write_readiness().await?;
let mut query = self
.db
.select("submissions")
.columns(&["record"])
.where_eq("target_turn_id", turn_id.0.clone())
.where_eq("pending", 1_i64)
.sort("accepted_seq", SortDirection::Asc)
.limit(limit.min(256));
if let Some(after) = after {
query = query.where_gt("accepted_seq", seq_to_value(after));
}
query
.execute(&**self.db)
.await?
.iter()
.map(|row| Ok(serde_json::from_str(&required_string(row, "record")?)?))
.collect()
}
pub async fn activated_conversation_snapshot(
&self,
) -> SessionDbResult<bcode_session_models::ActivatedConversationSnapshot> {
let through_sequence = self.last_event_sequence().await?.unwrap_or_default();
let events = self.model_context_events().await?;
if self.last_event_sequence().await?.unwrap_or_default() != through_sequence {
return Err(SessionDbError::InvalidData(
"conversation changed during snapshot; retry".into(),
));
}
Ok(bcode_session_models::ActivatedConversationSnapshot {
through_sequence,
events,
})
}
pub async fn model_context_events(&self) -> SessionDbResult<Vec<SessionEvent>> {
if let Some(events) = self.projected_model_context_events().await? {
return Ok(events);
}
Err(SessionDbError::ProjectionStale {
projection: "model_context",
checkpoint: None,
expected: self.last_event_sequence().await?.unwrap_or_default(),
})
}
pub async fn model_context_projection_status(
&self,
) -> SessionDbResult<ModelContextProjectionStatus> {
let Some(state) = self
.db
.select("model_context_projection_state")
.columns(&["schema_version", "last_event_seq"])
.where_eq("projection_id", MODEL_CONTEXT_PROJECTION_ID)
.execute_first(&**self.db)
.await?
else {
return Ok(ModelContextProjectionStatus::Missing);
};
let schema_version = required_i64(&state, "schema_version").map(i64_to_u64)?;
if schema_version != u64::from(MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION) {
return Ok(ModelContextProjectionStatus::Incompatible {
actual: schema_version,
expected: u64::from(MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION),
});
}
let checkpoint = required_i64(&state, "last_event_seq").map(i64_to_u64)?;
let expected = self.last_event_sequence().await?.unwrap_or_default();
if checkpoint == expected {
Ok(ModelContextProjectionStatus::Fresh { checkpoint })
} else {
Ok(ModelContextProjectionStatus::Stale {
checkpoint,
expected,
})
}
}
pub async fn reindex_session_projections(
&self,
_maintenance: &crate::lease::SessionMaintenanceGuard,
_write: &crate::lease::SessionWriteGuard,
) -> SessionDbResult<usize> {
let tx = self.db.begin_transaction().await?;
configure_turso_connection(&*tx).await?;
let events = strict_events(&*tx).await?;
validate_contiguous_canonical_events(&events)?;
rebuild_model_context_projection_from_events(&*tx, &events).await?;
rebuild_transcript_projection_from_events(&*tx, &events).await?;
tx.delete("submissions").execute(&*tx).await?;
tx.delete("submission_recovery_operations")
.execute(&*tx)
.await?;
for event in &events {
crate::submission::project(&*tx, event).await?;
}
if let Some(tail) = events.last() {
update_projection_checkpoint(&*tx, MaterializedProjection::Submissions, tail).await?;
}
tx.commit().await?;
Ok(events.len())
}
pub async fn reindex_model_context(
&self,
_maintenance: &crate::lease::SessionMaintenanceGuard,
_write: &crate::lease::SessionWriteGuard,
) -> SessionDbResult<usize> {
let tx = self.db.begin_transaction().await?;
configure_turso_connection(&*tx).await?;
let event_count = rebuild_model_context_projection(&*tx).await?;
tx.commit().await?;
Ok(event_count)
}
async fn projected_model_context_events(&self) -> SessionDbResult<Option<Vec<SessionEvent>>> {
let Some(state) = self
.db
.select("model_context_projection_state")
.columns(&["schema_version", "last_event_seq"])
.where_eq("projection_id", MODEL_CONTEXT_PROJECTION_ID)
.execute_first(&**self.db)
.await?
else {
return Ok(None);
};
let schema_version = required_i64(&state, "schema_version").map(i64_to_u64)?;
if schema_version != u64::from(MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION) {
return Err(SessionDbError::ModelContextProjectionVersion {
actual: schema_version,
expected: u64::from(MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION),
});
}
let checkpoint = required_i64(&state, "last_event_seq").map(i64_to_u64)?;
let expected = self.last_event_sequence().await?.unwrap_or_default();
if checkpoint != expected {
return Err(SessionDbError::ModelContextProjectionStale {
checkpoint,
expected,
});
}
let rows = self
.db
.select("model_context_entries")
.columns(&["event_seq", "event_type", "payload"])
.sort("event_seq", SortDirection::Asc)
.execute(&**self.db)
.await?;
let mut events = Vec::with_capacity(rows.len());
for row in rows {
let event_seq = required_i64(&row, "event_seq").map(i64_to_u64)?;
let event_type = required_string(&row, "event_type")?;
let payload = required_string(&row, "payload")?;
let event = decode_session_event(&payload)?;
if event.sequence != event_seq
|| model_context_event_kind_name(&event.kind) != event_type
|| !is_model_context_event_type(&event_type)
{
return Err(SessionDbError::InvalidRow {
column: "model_context_entries".to_string(),
});
}
events.push(event);
}
Ok(Some(canonical_model_context_from_events(events)))
}
pub async fn current_context_epoch(&self) -> SessionDbResult<u64> {
let row = self
.db
.select("context_occupancy_projection")
.columns(&["schema_version", "context_epoch"])
.where_eq("projection_id", CONTEXT_OCCUPANCY_PROJECTION_ID)
.execute_first(&**self.db)
.await?;
let Some(row) = row.as_ref() else {
return Ok(0);
};
let schema_version = required_i64(row, "schema_version").map(i64_to_u64)?;
if schema_version != u64::from(CONTEXT_OCCUPANCY_PROJECTION_SCHEMA_VERSION) {
return Err(SessionDbError::ProjectionIncompatible {
projection: MaterializedProjection::RequestContextOccupancy.as_str(),
actual: schema_version,
expected: u64::from(CONTEXT_OCCUPANCY_PROJECTION_SCHEMA_VERSION),
});
}
required_i64(row, "context_epoch").map(i64_to_u64)
}
pub async fn current_context_occupancy(
&self,
) -> SessionDbResult<Option<bcode_session_models::RequestContextOccupancy>> {
let expected = self.last_event_sequence().await?.unwrap_or_default();
let checkpoint = self
.materialized_projection_checkpoint(MaterializedProjection::RequestContextOccupancy)
.await?;
if checkpoint != Some(expected) {
return Err(SessionDbError::ProjectionStale {
projection: MaterializedProjection::RequestContextOccupancy.as_str(),
checkpoint,
expected,
});
}
let row = self
.db
.select("context_occupancy_projection")
.columns(&["schema_version", "occupancy_json"])
.where_eq("projection_id", CONTEXT_OCCUPANCY_PROJECTION_ID)
.execute_first(&**self.db)
.await?;
let Some(row) = row.as_ref() else {
return Ok(None);
};
let schema_version = required_i64(row, "schema_version").map(i64_to_u64)?;
if schema_version != u64::from(CONTEXT_OCCUPANCY_PROJECTION_SCHEMA_VERSION) {
return Err(SessionDbError::ProjectionIncompatible {
projection: MaterializedProjection::RequestContextOccupancy.as_str(),
actual: schema_version,
expected: u64::from(CONTEXT_OCCUPANCY_PROJECTION_SCHEMA_VERSION),
});
}
optional_string(row, "occupancy_json")
.map(|json| serde_json::from_str(&json).map_err(SessionDbError::from))
.transpose()
}
async fn latest_context_compaction_event(&self) -> SessionDbResult<Option<SessionEvent>> {
let mut latest = None;
for event_type in ["context_compacted", "provider_context_compacted"] {
let row = self
.db
.select("events")
.columns(&["payload"])
.where_eq("event_type", event_type)
.sort("event_seq", SortDirection::Desc)
.limit(1)
.execute_first(&**self.db)
.await?;
let Some(row) = row.as_ref() else {
continue;
};
let payload = required_string(row, "payload")?;
let event = decode_session_event(&payload)?;
let boundary = match (&event.kind, event_type) {
(
SessionEventKind::ContextCompacted {
compacted_through_sequence,
..
},
"context_compacted",
)
| (
SessionEventKind::ProviderContextCompacted {
compacted_through_sequence,
..
},
"provider_context_compacted",
) => *compacted_through_sequence,
_ => {
return Err(SessionDbError::InvalidCompactionMarker {
sequence: event.sequence,
message: format!(
"event_type {event_type:?} does not match the persisted event kind"
),
});
}
};
if boundary > event.sequence {
return Err(SessionDbError::InvalidCompactionMarker {
sequence: event.sequence,
message: format!("compacted boundary #{boundary} is later than its marker"),
});
}
if latest
.as_ref()
.is_none_or(|current: &SessionEvent| event.sequence > current.sequence)
{
latest = Some(event);
}
}
Ok(latest)
}
pub async fn events_range(
&self,
start_sequence: u64,
end_sequence: u64,
max_events: usize,
) -> SessionDbResult<Vec<SessionEvent>> {
let rows = self
.db
.select("events")
.columns(&["event_seq", "payload"])
.where_gte("event_seq", seq_to_value(start_sequence))
.where_lte("event_seq", seq_to_value(end_sequence))
.sort("event_seq", SortDirection::Asc)
.limit(max_events)
.execute(&**self.db)
.await?;
rows.iter()
.map(|row| strict_event_from_row(row, self.session_id))
.collect()
}
pub async fn latest_transcript_items(
&self,
limit: usize,
) -> SessionDbResult<Vec<TranscriptItem>> {
self.transcript_items_for_latest_window(limit, limit, usize::MAX)
.await
}
pub async fn transcript_items_for_latest_window(
&self,
min_items: usize,
max_items: usize,
max_bytes: usize,
) -> SessionDbResult<Vec<TranscriptItem>> {
let fetch_limit = max_items.max(min_items).max(1);
let rows = self
.latest_transcript_rows(fetch_limit)
.await?
.into_iter()
.map(|row| transcript_item_from_row(&row))
.collect::<SessionDbResult<Vec<_>>>()?;
let mut items = Vec::new();
let mut bytes = 0usize;
for item in rows {
let item_bytes = item.content.as_ref().map_or(0, String::len);
if items.len() >= min_items
&& (items.len() >= max_items || bytes.saturating_add(item_bytes) > max_bytes)
{
break;
}
bytes = bytes.saturating_add(item_bytes);
items.push(item);
if items.len() >= max_items {
break;
}
}
items.sort_by_key(|item| item.transcript_seq);
Ok(items)
}
pub async fn first_event_sequence(&self) -> SessionDbResult<Option<u64>> {
let row = self
.db
.select("events")
.columns(&["event_seq"])
.sort("event_seq", SortDirection::Asc)
.limit(1)
.execute_first(&**self.db)
.await?;
row.as_ref()
.map(|row| required_i64(row, "event_seq").map(i64_to_u64))
.transpose()
}
pub async fn last_event_sequence(&self) -> SessionDbResult<Option<u64>> {
let row = self
.db
.select("events")
.columns(&["event_seq"])
.sort("event_seq", SortDirection::Desc)
.limit(1)
.execute_first(&**self.db)
.await?;
row.as_ref()
.and_then(|row| optional_i64(row, "event_seq"))
.map(i64_to_u64)
.map_or(Ok(None), |value| Ok(Some(value)))
}
pub async fn artifact_maintenance_page(
&self,
after: Option<(&str, &str)>,
) -> SessionDbResult<Vec<(String, String)>> {
self.artifact_maintenance_page_through(after, i64::MAX as u64)
.await
}
pub async fn artifact_maintenance_page_through(
&self,
after: Option<(&str, &str)>,
through_sequence: u64,
) -> SessionDbResult<Vec<(String, String)>> {
validate_storage_writer_contract(&**self.db).await?;
let expected = self.last_event_sequence().await?.unwrap_or_default();
if self
.materialized_projection_checkpoint(MaterializedProjection::ArtifactReferences)
.await?
!= Some(expected)
{
return Err(SessionDbError::ProjectionStale {
projection: "artifact_references",
checkpoint: None,
expected,
});
}
let mut result = Vec::new();
if let Some((artifact, reference)) = after {
let rows = self
.db
.select("artifact_references")
.columns(&["artifact_id", "reference_key"])
.where_eq("artifact_id", artifact)
.where_gt("reference_key", reference)
.where_lte("finalized_event_seq", seq_to_value(through_sequence))
.where_eq("complete", true)
.where_eq("availability", "complete")
.sort("reference_key", SortDirection::Asc)
.limit(16)
.execute(&**self.db)
.await?;
for row in rows {
result.push((
required_string(&row, "artifact_id")?,
required_string(&row, "reference_key")?,
));
}
}
if result.len() < 16 {
let mut query = self
.db
.select("artifact_references")
.columns(&["artifact_id", "reference_key"])
.where_lte("finalized_event_seq", seq_to_value(through_sequence))
.where_eq("complete", true)
.where_eq("availability", "complete")
.sort("artifact_id", SortDirection::Asc)
.sort("reference_key", SortDirection::Asc)
.limit(16 - result.len());
if let Some((artifact, _)) = after {
query = query.where_gt("artifact_id", artifact);
}
for row in query.execute(&**self.db).await? {
result.push((
required_string(&row, "artifact_id")?,
required_string(&row, "reference_key")?,
));
}
}
Ok(result)
}
pub async fn artifact_finalized_at_ms(&self, sequence: u64) -> SessionDbResult<u64> {
let row = self
.db
.select("events")
.columns(&["created_at_ms"])
.where_eq("event_seq", seq_to_value(sequence))
.execute_first(&**self.db)
.await?
.ok_or_else(|| SessionDbError::InvalidRow {
column: "artifact.finalized_event_seq".to_owned(),
})?;
required_non_negative_u64(&row, "created_at_ms")
}
pub async fn finalized_artifact_reference(
&self,
artifact_id: &str,
reference_key: &str,
) -> SessionDbResult<Option<FinalizedArtifactReference>> {
validate_storage_writer_contract(&**self.db).await?;
let expected = self.last_event_sequence().await?.unwrap_or_default();
let checkpoint = self
.materialized_projection_checkpoint(MaterializedProjection::ArtifactReferences)
.await?;
if checkpoint != Some(expected) {
return Err(SessionDbError::ProjectionStale {
projection: MaterializedProjection::ArtifactReferences.as_str(),
checkpoint,
expected,
});
}
let row = self
.db
.select("artifact_references")
.columns(&[
"artifact_id",
"reference_key",
"producer_plugin_id",
"schema",
"schema_version",
"storage_uri",
"content_type",
"byte_len",
"availability",
"complete",
"checksum_sha256",
"finalized_event_seq",
])
.where_eq("artifact_id", artifact_id)
.where_eq("reference_key", reference_key)
.execute_first(&**self.db)
.await?;
let mut reference = row
.as_ref()
.map(finalized_artifact_reference_from_row)
.transpose()?;
if let Some(reference) = &mut reference {
let events = self
.events_range(
reference.finalized_event_seq,
reference.finalized_event_seq,
1,
)
.await?;
reference.checksum_sha256 = crate::db_artifact::whole_content_checksum(
events.first(),
artifact_id,
reference_key,
)?;
}
Ok(reference)
}
pub async fn latest_transcript_rows(
&self,
limit: usize,
) -> Result<Vec<switchy::database::Row>, DatabaseError> {
self.db
.select("transcript_items")
.columns(&[
"transcript_seq",
"event_seq_start",
"event_seq_end",
"role",
"kind",
"status",
"content",
])
.sort("transcript_seq", SortDirection::Desc)
.limit(limit)
.execute(&**self.db)
.await
}
}
async fn run_global_migrations(db: &dyn Database) -> Result<(), MigrationError> {
let runner = MigrationRunner::new(Box::new(global_migrations()))
.with_table_name(GLOBAL_MIGRATIONS_TABLE.to_string());
runner.run(db).await
}
async fn run_session_migrations(db: &dyn Database) -> Result<(), MigrationError> {
let runner = MigrationRunner::new(Box::new(session_migrations()))
.with_table_name(SESSION_MIGRATIONS_TABLE.to_string());
runner.run(db).await
}
const CANONICAL_PAGE_DECODED_BYTES: usize = 32 * 1024 * 1024;
async fn canonical_payload_fetch_limit(
db: &dyn Database,
start: u64,
limit: usize,
) -> SessionDbResult<usize> {
let rows = db
.select("events")
.project_as(
switchy::database::query::byte_length(switchy::database::query::identifier("payload")),
"payload_bytes",
)
.where_gte("event_seq", seq_to_value(start))
.sort("event_seq", SortDirection::Asc)
.limit(limit.min(MIGRATION_EVENT_PAGE_SIZE))
.execute(db)
.await?;
let mut remaining = CANONICAL_PAGE_DECODED_BYTES as u64;
let mut count = 0;
for row in rows {
let bytes = required_non_negative_u64(&row, "payload_bytes")?;
if bytes > remaining {
if count == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::OutOfMemory,
"canonical row exceeds physical page budget",
)
.into());
}
break;
}
remaining -= bytes;
count += 1;
}
Ok(count)
}
pub(super) async fn read_canonical_payload_page(
db: &dyn Database,
start_sequence: u64,
limit: usize,
) -> SessionDbResult<Vec<bcode_session_migration_target::CanonicalRow>> {
if limit == 0 || start_sequence > i64::MAX as u64 {
return Ok(Vec::new());
}
let mut remaining = CANONICAL_PAGE_DECODED_BYTES;
let mut result = Vec::new();
let fetch_limit = canonical_payload_fetch_limit(db, start_sequence, limit).await?;
if fetch_limit == 0 {
return Ok(Vec::new());
}
let rows = db
.select("events")
.columns(&["event_seq", "event_type", "schema_version", "payload"])
.where_gte("event_seq", seq_to_value(start_sequence))
.sort("event_seq", SortDirection::Asc)
.limit(fetch_limit)
.execute(db)
.await?;
for row in rows {
let stored = required_string(&row, "payload")?;
let payload =
match crate::event_compression::decode_event_payload_with_limit(&stored, remaining) {
Ok(payload) => payload.into_owned(),
Err(error)
if error.kind() == std::io::ErrorKind::OutOfMemory && !result.is_empty() =>
{
break;
}
Err(error) => return Err(error.into()),
};
remaining -= payload.len();
result.push(bcode_session_migration_target::CanonicalRow {
sequence: required_non_negative_u64(&row, "event_seq")?,
event_kind: required_string(&row, "event_type")?,
schema_version: u16::try_from(required_non_negative_u64(&row, "schema_version")?)
.map_err(|_| SessionDbError::InvalidRow {
column: "events.schema_version".to_owned(),
})?,
payload,
});
}
Ok(result)
}
async fn build_target_migration_receipt(
db: &dyn Database,
session_id: SessionId,
source_writer_epoch: u64,
operation_id: Option<bcode_session_models::SessionOpenOperationId>,
migration_outcome: &MigrationReplayOutcome,
builder: &bcode_session_migration_target::MigrationReceiptBuilder,
) -> SessionDbResult<bcode_session_migration_target::MigrationReceipt> {
let (event_count, event_tail, target_payload_digest_sha256) =
migration_payload_digest(db).await?;
builder(bcode_session_migration_target::MigrationReceiptFacts {
operation_id: operation_id.map(|id| id.to_string()),
session_id,
source_writer_epoch,
event_count,
event_tail,
target_payload_digest_sha256,
replay: migration_outcome.evidence.clone(),
completed_at_ms: bcode_session_models::current_unix_timestamp_ms(),
})
.map_err(|reason| SessionDbError::MigrationHistoryIncompatible { reason })
}
async fn migration_payload_digest(
db: &dyn Database,
) -> SessionDbResult<(u64, Option<u64>, String)> {
let mut event_count = 0_u64;
let mut event_tail = None;
let mut digest = Sha256::new();
loop {
let mut query = db
.select("events")
.columns(&["event_seq", "payload"])
.sort("event_seq", SortDirection::Asc)
.limit(MIGRATION_EVENT_PAGE_SIZE);
if let Some(tail) = event_tail {
query = query.where_gt("event_seq", seq_to_value(tail));
}
let rows = query.execute(db).await?;
if rows.is_empty() {
break;
}
for row in rows {
let sequence = required_non_negative_u64(&row, "event_seq")?;
if sequence != event_count {
return Err(SessionDbError::InvalidCanonicalSequence {
expected: event_count,
actual: sequence,
});
}
let stored = required_string(&row, "payload")?;
let payload = crate::event_compression::decode_event_payload(&stored)?;
digest.update(
u64::try_from(payload.len())
.unwrap_or(u64::MAX)
.to_le_bytes(),
);
digest.update(payload.as_bytes());
event_count += 1;
event_tail = Some(sequence);
}
}
Ok((event_count, event_tail, hex::encode(digest.finalize())))
}
async fn set_storage_writer_contract(db: &dyn Database, writer_epoch: u32) -> SessionDbResult<()> {
db.upsert("session_storage_contract")
.value("contract_id", SESSION_STORAGE_CONTRACT_ID)
.value(
"schema_version",
DatabaseValue::Int64(i64::from(SESSION_STORAGE_CONTRACT_SCHEMA_VERSION)),
)
.value(
"writer_epoch",
DatabaseValue::Int64(i64::from(writer_epoch)),
)
.value(
"updated_by_build",
option_env!("BCODE_BUILD_FINGERPRINT").map(str::to_owned),
)
.execute(db)
.await?;
Ok(())
}
async fn initialize_current_storage_contract(db: &dyn Database) -> SessionDbResult<()> {
let tx = db.begin_transaction().await?;
set_storage_writer_contract(&*tx, CURRENT_SESSION_STORAGE_WRITER_EPOCH).await?;
tx.commit().await?;
Ok(())
}
#[cfg(test)]
async fn validate_all_projection_checkpoints_at_tail(
db: &dyn Database,
expected: Option<u64>,
) -> SessionDbResult<()> {
let Some(expected) = expected else {
return Ok(());
};
let snapshot = projection_checkpoint_snapshot(db).await?;
validate_projection_checkpoint_snapshot(&snapshot, expected)
}
async fn migrate_session_storage(
target: &mut SessionMigrationTarget<'_>,
session_id: SessionId,
metrics: &MetricsRegistry,
progress: Option<&SessionMigrationProgressCallback>,
normalizer: &bcode_session_migration_target::CanonicalNormalizer,
fault: MigrationFaultPhase,
) -> SessionDbResult<MigrationReplayOutcome> {
let event_total = last_sequence(target.db)
.await?
.map_or(0, |tail| tail.saturating_add(1));
report_migration_progress(
progress,
bcode_session_models::SessionMigrationStage::ReadingCanonicalHistory,
0,
event_total,
"Reading canonical session history",
);
metrics.add_counter("session.migration.canonical_events_total", event_total);
let replay = rebuild_migration_projections(
target,
event_total,
session_id,
metrics,
progress,
normalizer,
fault,
)
.await?;
inject_migration_fault(fault, MigrationFaultPhase::FinalValidation)?;
#[cfg(test)]
abort_at_migration_crash_boundary("final_validation");
validate_migrated_storage(target, replay.tail.as_ref(), metrics, progress).await?;
Ok(replay)
}
struct MigrationReplayOutcome {
tail: Option<SessionEvent>,
evidence: bcode_session_migration_target::ReplayEvidence,
}
#[allow(clippy::too_many_lines)]
async fn rebuild_migration_projections(
target: &mut SessionMigrationTarget<'_>,
event_total: u64,
session_id: SessionId,
metrics: &MetricsRegistry,
progress: Option<&SessionMigrationProgressCallback>,
normalizer: &bcode_session_migration_target::CanonicalNormalizer,
fault: MigrationFaultPhase,
) -> SessionDbResult<MigrationReplayOutcome> {
let replay_timer = metrics.timer();
let mut state = MigrationProjectionState::new();
let mut source_digest = Sha256::new();
let mut converted_events = BTreeMap::new();
let mut retired_known_events = BTreeMap::new();
let mut tail = None;
let mut completed = 0_u64;
while completed < event_total {
let page = target
.canonical_page(completed, MIGRATION_EVENT_PAGE_SIZE)
.await?;
if page.is_empty() {
return Err(SessionDbError::InvalidCanonicalSequence {
expected: completed,
actual: event_total,
});
}
for row in page {
let event_seq = row.sequence;
if event_seq != completed {
return Err(SessionDbError::InvalidCanonicalSequence {
expected: completed,
actual: event_seq,
});
}
let payload = row.payload;
let payload_length = u64::try_from(payload.len()).unwrap_or(u64::MAX);
source_digest.update(payload_length.to_le_bytes());
source_digest.update(payload.as_bytes());
let decode_timer = metrics.timer();
inject_migration_fault(fault, MigrationFaultPhase::CanonicalDecode)?;
#[cfg(test)]
abort_at_migration_crash_boundary("normalization");
let current_row = normalizer(&bcode_session_migration_target::CanonicalRow {
sequence: event_seq,
event_kind: row.event_kind,
schema_version: row.schema_version,
payload: payload.clone(),
})
.map_err(|reason| SessionDbError::MigrationHistoryIncompatible { reason })?;
metrics.record_histogram(
"session.migration.canonical_decode_duration_ms",
decode_timer.elapsed_ms(),
);
if let Some(source) = current_row.historical_source.as_ref() {
let counts = if current_row.retired_known {
&mut retired_known_events
} else {
&mut converted_events
};
*counts.entry(source.clone()).or_insert(0_u64) += 1;
}
if let Some(counter) = current_row.metric_counter.as_deref() {
metrics.increment_counter(counter);
}
validate_canonical_event_identity(¤t_row.event, event_seq, session_id)?;
let event = current_row.event;
let original = crate::persisted::original_usage(&payload)
.map_err(|column| SessionDbError::InvalidRow { column })?;
let current_payload =
crate::persisted::encode_session_event_with_original(&event, original.as_ref())
.map_err(|column| SessionDbError::InvalidRow { column })?;
target
.replace_canonical_row(bcode_session_migration_target::CanonicalRow {
sequence: event_seq,
event_kind: event_kind_name(&event.kind).to_owned(),
schema_version: event.schema_version,
payload: current_payload,
})
.await?;
inject_migration_fault(fault, MigrationFaultPhase::Projection)?;
#[cfg(test)]
abort_at_migration_crash_boundary("projection_rebuild");
target.ingest_projectors(&event).await?;
state.authoritative.ingest(&event);
completed = completed.saturating_add(1);
if completed == event_total || completed.is_multiple_of(100) {
report_migration_progress(
progress,
bcode_session_models::SessionMigrationStage::ReadingCanonicalHistory,
completed,
event_total,
"Reading canonical history and rebuilding session indexes",
);
}
tail = Some(event);
}
}
target
.write_authoritative_state(
state.authoritative.context_epoch,
state
.authoritative
.context_occupancy
.as_ref()
.map(serde_json::to_string)
.transpose()?,
)
.await?;
report_migration_progress(
progress,
bcode_session_models::SessionMigrationStage::RebuildingProjections,
event_total,
event_total,
"Rebuilt session indexes",
);
metrics.record_histogram(
"session.migration.projection_rebuild_duration_ms",
replay_timer.elapsed_ms(),
);
metrics.add_counter("session.migration.projected_events_total", event_total);
Ok(MigrationReplayOutcome {
tail,
evidence: bcode_session_migration_target::ReplayEvidence {
source_payload_digest_sha256: hex::encode(source_digest.finalize()),
converted_events,
retired_known_events,
},
})
}
async fn migration_target_validation_facts(
db: &dyn Database,
canonical_tail: Option<u64>,
) -> SessionDbResult<bcode_session_migration_target::StrictValidation> {
if canonical_tail.is_none() {
return Ok(bcode_session_migration_target::StrictValidation {
canonical_tail,
projections: Vec::new(),
model_context_schema_version: None,
expected_model_context_schema_version: u64::from(
MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION,
),
model_context_checkpoint: None,
});
}
let snapshot = projection_checkpoint_snapshot(db).await?;
let projections = MaterializedProjection::all()
.iter()
.map(|projection| {
let state = snapshot.get(projection.as_str());
bcode_session_migration_target::ProjectionValidation {
projection: projection.as_str().to_owned(),
actual_schema_version: state.map(|state| state.version),
expected_schema_version: u64::from(projection.schema_version()),
checkpoint: state.map(|state| state.checkpoint),
}
})
.collect();
let model_state = db
.select("model_context_projection_state")
.columns(&["schema_version", "last_event_seq"])
.where_eq("projection_id", MODEL_CONTEXT_PROJECTION_ID)
.execute_first(db)
.await?;
let model_context_schema_version = model_state
.as_ref()
.map(|row| required_non_negative_u64(row, "schema_version"))
.transpose()?;
let model_context_checkpoint = model_state
.as_ref()
.map(|row| required_non_negative_u64(row, "last_event_seq"))
.transpose()?;
Ok(bcode_session_migration_target::StrictValidation {
canonical_tail,
projections,
model_context_schema_version,
expected_model_context_schema_version: u64::from(MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION),
model_context_checkpoint,
})
}
fn validate_strict_target_facts(
facts: &bcode_session_migration_target::StrictValidation,
) -> SessionDbResult<()> {
if facts.canonical_tail.is_none() {
return Ok(());
}
for projection in &facts.projections {
if projection.actual_schema_version != Some(projection.expected_schema_version) {
return Err(SessionDbError::MigrationHistoryIncompatible {
reason: format!(
"projection {} has incompatible schema",
projection.projection
),
});
}
if projection.checkpoint != facts.canonical_tail {
return Err(SessionDbError::MigrationHistoryIncompatible {
reason: format!("projection {} is stale", projection.projection),
});
}
}
if facts.model_context_schema_version != Some(facts.expected_model_context_schema_version) {
return Err(SessionDbError::MigrationHistoryIncompatible {
reason: "model-context projection has incompatible schema".to_owned(),
});
}
if facts.model_context_checkpoint != facts.canonical_tail {
return Err(SessionDbError::MigrationHistoryIncompatible {
reason: "model-context projection is stale".to_owned(),
});
}
Ok(())
}
async fn validate_migrated_storage(
target: &mut SessionMigrationTarget<'_>,
tail: Option<&SessionEvent>,
metrics: &MetricsRegistry,
progress: Option<&SessionMigrationProgressCallback>,
) -> SessionDbResult<()> {
let validation_timer = metrics.timer();
report_migration_stage(
progress,
bcode_session_models::SessionMigrationStage::ValidatingProjections,
"Validating rebuilt session indexes",
);
let canonical_tail = tail.map(|event| event.sequence);
target.finalize_projectors(canonical_tail).await?;
let validation = target.validate_strict_current().await?;
validate_strict_target_facts(&validation)?;
metrics.record_histogram(
"session.migration.validation_duration_ms",
validation_timer.elapsed_ms(),
);
Ok(())
}
struct CurrentAuthoritativeProjectionState {
context_epoch: u64,
context_occupancy: Option<bcode_session_models::RequestContextOccupancy>,
}
impl CurrentAuthoritativeProjectionState {
fn ingest(&mut self, event: &SessionEvent) {
use bcode_session_models::{RequestContextOccupancy, SessionEventKind};
match &event.kind {
SessionEventKind::ModelChanged { .. }
| SessionEventKind::ContextCompacted { .. }
| SessionEventKind::ProviderContextCompacted { .. } => {
self.context_epoch = event.sequence;
self.context_occupancy = None;
}
SessionEventKind::RequestContextObserved { observation } => {
self.context_occupancy = RequestContextOccupancy::reconcile(
self.context_occupancy.as_ref(),
self.context_epoch,
event.sequence,
observation.clone(),
);
}
_ => {}
}
}
}
struct MigrationProjectionState {
model_context_checkpoint: Option<u64>,
authoritative: CurrentAuthoritativeProjectionState,
}
impl MigrationProjectionState {
const fn new() -> Self {
Self {
model_context_checkpoint: None,
authoritative: CurrentAuthoritativeProjectionState {
context_epoch: 0,
context_occupancy: None,
},
}
}
}
async fn project_migration_event(
db: &dyn Database,
event: &SessionEvent,
metrics: &MetricsRegistry,
state: &mut MigrationProjectionState,
) -> SessionDbResult<()> {
let timer = metrics.timer();
project_materialized_event_without_checkpoints(db, event).await?;
metrics.record_histogram(
"session.migration.projector.materialized_duration_ms",
timer.elapsed_ms(),
);
let timer = metrics.timer();
project_migration_model_context_event(db, event, state).await?;
metrics.record_histogram(
"session.migration.projector.model_context_duration_ms",
timer.elapsed_ms(),
);
let timer = metrics.timer();
state.authoritative.ingest(event);
metrics.record_histogram(
"session.migration.projector.context_occupancy_duration_ms",
timer.elapsed_ms(),
);
let timer = metrics.timer();
project_turn_receipt(db, event).await?;
metrics.record_histogram(
"session.migration.projector.turn_receipt_duration_ms",
timer.elapsed_ms(),
);
Ok(())
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) struct SessionMigrationTarget<'a> {
db: &'a dyn Database,
metrics: &'a MetricsRegistry,
projection_state: MigrationProjectionState,
pending_tail: Option<SessionEvent>,
}
impl<'a> SessionMigrationTarget<'a> {
#[must_use]
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) const fn new(db: &'a dyn Database, metrics: &'a MetricsRegistry) -> Self {
Self {
db,
metrics,
projection_state: MigrationProjectionState::new(),
pending_tail: None,
}
}
}
#[async_trait]
impl bcode_session_migration_target::MigrationTarget for SessionMigrationTarget<'_> {
type Error = SessionDbError;
async fn materialize_current_schema(&mut self) -> Result<(), Self::Error> {
run_session_migrations(self.db).await?;
for table in [
"turn_evidence",
"turn_receipts",
"submissions",
"submission_recovery_operations",
"session_children",
"milestone_evaluations",
"milestone_scheduling",
"milestone_turns",
"session_milestones",
"session_children_checkpoint",
"model_context_entries",
"model_context_projection_state",
"context_occupancy_projection",
"session_usage_requests",
"session_usage_projection",
"projection_checkpoints",
"artifact_references",
"runtime_work",
"tool_runs",
"transcript_items",
"input_messages",
"session_state",
] {
self.db.delete(table).execute(self.db).await?;
}
Ok(())
}
async fn canonical_page(
&mut self,
start_sequence: u64,
limit: usize,
) -> Result<Vec<bcode_session_migration_target::CanonicalRow>, Self::Error> {
read_canonical_payload_page(self.db, start_sequence, limit).await
}
async fn replace_canonical_row(
&mut self,
row: bcode_session_migration_target::CanonicalRow,
) -> Result<(), Self::Error> {
self.db
.update("events")
.value("event_type", row.event_kind)
.value(
"schema_version",
DatabaseValue::Int32(i32::from(row.schema_version)),
)
.value("payload", row.payload)
.where_eq("event_seq", seq_to_value(row.sequence))
.execute(self.db)
.await?;
Ok(())
}
async fn write_authoritative_state(
&mut self,
context_epoch: u64,
context_occupancy_json: Option<String>,
) -> Result<(), Self::Error> {
self.projection_state.authoritative.context_epoch = context_epoch;
self.projection_state.authoritative.context_occupancy = context_occupancy_json
.map(|json| serde_json::from_str(&json))
.transpose()?;
Ok(())
}
async fn ingest_projectors(&mut self, event: &SessionEvent) -> Result<(), Self::Error> {
project_migration_event(self.db, event, self.metrics, &mut self.projection_state).await?;
self.pending_tail = Some(event.clone());
Ok(())
}
async fn finalize_projectors(
&mut self,
canonical_tail: Option<u64>,
) -> Result<(), Self::Error> {
match (canonical_tail, self.pending_tail.as_ref()) {
(None, None) => Ok(()),
(Some(expected), Some(tail)) if tail.sequence == expected => {
finalize_migration_session_state(self.db, tail).await?;
finalize_migration_model_context(self.db, tail).await?;
finalize_migration_context_occupancy(self.db, tail, &self.projection_state).await?;
project_migration_materialized_checkpoints_at_tail(self.db, tail).await?;
Ok(())
}
_ => Err(SessionDbError::MigrationHistoryIncompatible {
reason: "migration target canonical tail does not match projector ingestion"
.to_owned(),
}),
}
}
async fn validate_strict_current(
&mut self,
) -> Result<bcode_session_migration_target::StrictValidation, Self::Error> {
let canonical_tail = last_sequence(self.db).await?;
let facts = migration_target_validation_facts(self.db, canonical_tail).await?;
Ok(facts)
}
async fn persist_migration_receipt(
&mut self,
receipt: &bcode_session_migration_target::MigrationReceipt,
) -> Result<(), Self::Error> {
self.db
.insert("session_migration_receipts")
.value("operation_id", receipt.operation_id.clone())
.value("session_id", receipt.session_id.to_string())
.value(
"source_writer_epoch",
DatabaseValue::Int64(i64::from(receipt.source_writer_epoch)),
)
.value(
"target_writer_epoch",
DatabaseValue::Int64(i64::from(receipt.target_writer_epoch)),
)
.value("receipt", serde_json::to_string(receipt)?)
.value("completed_at_ms", seq_to_value(receipt.completed_at_ms))
.execute(self.db)
.await?;
Ok(())
}
async fn finalize_writer_contract(&mut self) -> Result<(), Self::Error> {
self.db
.update("session_children_checkpoint")
.value("coverage", 0_i64)
.where_eq("singleton", 1_i64)
.execute(self.db)
.await?;
set_storage_writer_contract(self.db, CURRENT_SESSION_STORAGE_WRITER_EPOCH).await
}
}
fn report_migration_stage(
progress: Option<&SessionMigrationProgressCallback>,
stage: bcode_session_models::SessionMigrationStage,
message: &str,
) {
if let Some(progress) = progress {
progress(bcode_session_models::SessionMigrationProgress {
stage,
completed_units: None,
total_units: None,
unit: None,
message: message.to_owned(),
});
}
}
fn report_migration_progress(
progress: Option<&SessionMigrationProgressCallback>,
stage: bcode_session_models::SessionMigrationStage,
completed: u64,
total: u64,
message: &str,
) {
if let Some(progress) = progress {
progress(bcode_session_models::SessionMigrationProgress {
stage,
completed_units: Some(completed),
total_units: Some(total),
unit: Some(bcode_session_models::SessionMigrationProgressUnit::Events),
message: message.to_owned(),
});
}
}
async fn rebuild_model_context_projection(db: &dyn Database) -> SessionDbResult<usize> {
let events = strict_events(db).await?;
validate_contiguous_canonical_events(&events)?;
rebuild_model_context_projection_from_events(db, &events).await?;
Ok(events.len())
}
fn validate_contiguous_canonical_events(events: &[SessionEvent]) -> SessionDbResult<()> {
for (index, event) in events.iter().enumerate() {
let expected = u64::try_from(index).unwrap_or(u64::MAX);
if event.sequence != expected {
return Err(SessionDbError::InvalidCanonicalSequence {
expected,
actual: event.sequence,
});
}
}
Ok(())
}
async fn rebuild_model_context_projection_from_events(
db: &dyn Database,
events: &[SessionEvent],
) -> SessionDbResult<()> {
db.delete("model_context_entries").execute(db).await?;
db.delete("model_context_projection_state")
.execute(db)
.await?;
for event in events {
project_model_context_event(db, event).await?;
}
Ok(())
}
async fn rebuild_transcript_projection_from_events(
db: &dyn Database,
events: &[SessionEvent],
) -> SessionDbResult<()> {
db.delete("transcript_items").execute(db).await?;
for event in events {
rebuild_transcript_event(db, event).await?;
}
if let Some(tail) = events.last() {
update_projection_checkpoint(db, MaterializedProjection::Transcript, tail).await?;
}
Ok(())
}
async fn rebuild_transcript_event(db: &dyn Database, event: &SessionEvent) -> SessionDbResult<()> {
match &event.kind {
SessionEventKind::UserMessage { text, .. }
| SessionEventKind::SubmissionAccepted {
submission: bcode_session_models::PromptSubmission { text, .. },
} => {
insert_transcript_item(db, event, "user", "message", "complete", Some(text.clone()))
.await?;
}
SessionEventKind::AssistantMessage { text }
| SessionEventKind::AssistantResponseSegment { text, .. }
| SessionEventKind::PositionedAssistantResponseSegment { text, .. } => {
insert_transcript_item(
db,
event,
"assistant",
"message",
"complete",
Some(text.clone()),
)
.await?;
}
SessionEventKind::PluginStatusNote { text, .. } => {
insert_transcript_item(
db,
event,
"system",
"plugin_status_note",
"complete",
Some(text.clone()),
)
.await?;
}
SessionEventKind::ToolCallRequested { .. }
| SessionEventKind::PositionedToolCallRequested { .. } => {
insert_transcript_item(db, event, "tool", "invocation", "running", None).await?;
}
SessionEventKind::ToolInvocationResultRecorded { record } => {
finalize_tool_transcript_item(
db,
&record.invocation_id,
event.sequence,
record.is_error,
)
.await?;
}
SessionEventKind::ToolInvocationLifecycle { event: lifecycle } => {
use bcode_session_models::ToolInvocationLifecycleStage as S;
if matches!(lifecycle.stage, S::Completed | S::Cancelled | S::Failed) {
finalize_tool_transcript_item(
db,
&lifecycle.invocation_id,
event.sequence,
matches!(lifecycle.stage, S::Cancelled | S::Failed),
)
.await?;
}
}
SessionEventKind::ToolExchangeRequested { request } => {
insert_transcript_item(
db,
event,
"runtime",
"tool_exchange_requested",
"running",
Some(serde_json::to_string(request)?),
)
.await?;
}
SessionEventKind::ToolExchangeResolved { event: resolution } => {
insert_transcript_item(
db,
event,
"runtime",
"tool_exchange_resolved",
"complete",
Some(serde_json::to_string(resolution)?),
)
.await?;
}
SessionEventKind::ToolContribution {
event: contribution,
} => {
insert_transcript_item(
db,
event,
"runtime",
"tool_contribution",
"complete",
Some(serde_json::to_string(contribution)?),
)
.await?;
}
_ => {}
}
Ok(())
}
async fn validate_storage_writer_contract_for_epoch(
db: &dyn Database,
expected_writer_epoch: u32,
) -> SessionDbResult<()> {
crate::db_contract::validate_writer_contract(
db,
expected_writer_epoch,
SESSION_STORAGE_CONTRACT_SCHEMA_VERSION,
LEGACY_SESSION_STORAGE_WRITER_EPOCH,
SESSION_STORAGE_CONTRACT_ID,
)
.await
}
async fn validate_storage_writer_contract(db: &dyn Database) -> SessionDbResult<()> {
validate_storage_writer_contract_for_epoch(db, CURRENT_SESSION_STORAGE_WRITER_EPOCH).await
}
async fn configure_turso_connection(db: &dyn Database) -> SessionDbResult<()> {
db.sqlite_temp_store_memory().await?;
Ok(())
}
async fn validate_append_preconditions_without_writer(
db: &dyn Database,
event: &SessionEvent,
) -> SessionDbResult<()> {
let canonical_tail = last_sequence(db).await?;
validate_append_identity(event, canonical_tail)?;
crate::submission::validate_activation(db, event).await?;
validate_model_context_precondition(
db,
event,
MODEL_CONTEXT_PROJECTION_ID,
MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION,
)
.await?;
validate_context_occupancy_precondition(
db,
event,
CONTEXT_OCCUPANCY_PROJECTION_ID,
CONTEXT_OCCUPANCY_PROJECTION_SCHEMA_VERSION,
)
.await?;
if let Some(expected) = canonical_tail {
let snapshot = projection_checkpoint_snapshot(db).await?;
validate_projection_checkpoint_snapshot(&snapshot, expected)?;
}
Ok(())
}
async fn validate_append_postconditions(
db: &dyn Database,
event: &SessionEvent,
) -> SessionDbResult<()> {
let canonical_tail = last_sequence(db).await?;
validate_append_tail(canonical_tail, event)?;
let snapshot = projection_checkpoint_snapshot(db).await?;
validate_projection_checkpoint_snapshot(&snapshot, event.sequence)?;
validate_model_context_postcondition(
db,
event,
MODEL_CONTEXT_PROJECTION_ID,
MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION,
)
.await?;
validate_context_occupancy_precondition(
db,
event,
CONTEXT_OCCUPANCY_PROJECTION_ID,
CONTEXT_OCCUPANCY_PROJECTION_SCHEMA_VERSION,
)
.await
}
async fn project_migration_model_context_event(
db: &dyn Database,
event: &SessionEvent,
state: &mut MigrationProjectionState,
) -> SessionDbResult<()> {
if let Some(checkpoint) = state.model_context_checkpoint {
let expected = event.sequence.saturating_sub(1);
if event.sequence == 0 || checkpoint != expected {
return Err(SessionDbError::ModelContextProjectionStale {
checkpoint,
expected,
});
}
} else if event.sequence != 0 {
return Err(SessionDbError::ProjectionStale {
projection: "model_context",
checkpoint: None,
expected: event.sequence.saturating_sub(1),
});
}
let event_type = model_context_event_kind_name(&event.kind);
if !matches!(
context_history_role(&event.kind),
ContextHistoryRole::Excluded
) {
if let Some(boundary) = compaction_boundary(event)? {
db.delete("model_context_entries")
.where_lte("event_seq", seq_to_value(boundary))
.execute(db)
.await?;
db.delete("model_context_entries")
.where_in(
"event_type",
vec![
DatabaseValue::String("context_compacted".to_owned()),
DatabaseValue::String("provider_context_compacted".to_owned()),
],
)
.execute(db)
.await?;
}
db.insert("model_context_entries")
.value("event_seq", seq_to_value(event.sequence))
.value("event_type", event_type)
.value("payload", encode_session_event(event)?)
.execute(db)
.await?;
}
state.model_context_checkpoint = Some(event.sequence);
Ok(())
}
async fn finalize_migration_model_context(
db: &dyn Database,
tail: &SessionEvent,
) -> SessionDbResult<()> {
db.insert("model_context_projection_state")
.value("projection_id", MODEL_CONTEXT_PROJECTION_ID)
.value(
"schema_version",
DatabaseValue::Int64(i64::from(MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION)),
)
.value("last_event_seq", seq_to_value(tail.sequence))
.execute(db)
.await?;
Ok(())
}
async fn project_model_context_event(
db: &dyn Database,
event: &SessionEvent,
) -> SessionDbResult<()> {
let state = db
.select("model_context_projection_state")
.columns(&["schema_version", "last_event_seq"])
.where_eq("projection_id", MODEL_CONTEXT_PROJECTION_ID)
.execute_first(db)
.await?;
match state.as_ref() {
None if event.sequence == 0 => {}
None => {
return Err(SessionDbError::ProjectionStale {
projection: "model_context",
checkpoint: None,
expected: event.sequence.saturating_sub(1),
});
}
Some(row) => {
let schema_version = required_i64(row, "schema_version").map(i64_to_u64)?;
if schema_version != u64::from(MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION) {
return Err(SessionDbError::ModelContextProjectionVersion {
actual: schema_version,
expected: u64::from(MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION),
});
}
let checkpoint = required_i64(row, "last_event_seq").map(i64_to_u64)?;
let expected = event.sequence.saturating_sub(1);
if event.sequence == 0 || checkpoint != expected {
return Err(SessionDbError::ModelContextProjectionStale {
checkpoint,
expected,
});
}
}
}
let event_type = model_context_event_kind_name(&event.kind);
if !matches!(
context_history_role(&event.kind),
ContextHistoryRole::Excluded
) {
if let Some(boundary) = compaction_boundary(event)? {
db.delete("model_context_entries")
.where_lte("event_seq", seq_to_value(boundary))
.execute(db)
.await?;
db.delete("model_context_entries")
.where_in(
"event_type",
vec![
DatabaseValue::String("context_compacted".to_string()),
DatabaseValue::String("provider_context_compacted".to_string()),
],
)
.execute(db)
.await?;
}
db.insert("model_context_entries")
.value("event_seq", seq_to_value(event.sequence))
.value("event_type", event_type)
.value("payload", encode_session_event(event)?)
.execute(db)
.await?;
}
db.upsert("model_context_projection_state")
.value("projection_id", MODEL_CONTEXT_PROJECTION_ID)
.value(
"schema_version",
DatabaseValue::Int64(i64::from(MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION)),
)
.value("last_event_seq", seq_to_value(event.sequence))
.execute(db)
.await?;
Ok(())
}
async fn finalize_migration_context_occupancy(
db: &dyn Database,
tail: &SessionEvent,
state: &MigrationProjectionState,
) -> SessionDbResult<()> {
db.insert("context_occupancy_projection")
.value("projection_id", CONTEXT_OCCUPANCY_PROJECTION_ID)
.value(
"schema_version",
DatabaseValue::Int64(i64::from(CONTEXT_OCCUPANCY_PROJECTION_SCHEMA_VERSION)),
)
.value(
"context_epoch",
seq_to_value(state.authoritative.context_epoch),
)
.value(
"occupancy_json",
state
.authoritative
.context_occupancy
.as_ref()
.map(serde_json::to_string)
.transpose()?,
)
.execute(db)
.await?;
update_projection_checkpoint(db, MaterializedProjection::RequestContextOccupancy, tail).await?;
Ok(())
}
async fn project_context_occupancy_row(
db: &dyn Database,
event: &SessionEvent,
) -> SessionDbResult<()> {
use bcode_session_models::{RequestContextOccupancy, SessionEventKind};
let row = db
.select("context_occupancy_projection")
.columns(&["schema_version", "context_epoch", "occupancy_json"])
.where_eq("projection_id", CONTEXT_OCCUPANCY_PROJECTION_ID)
.execute_first(db)
.await?;
let (context_epoch, current) = if let Some(row) = row.as_ref() {
let schema_version = required_i64(row, "schema_version").map(i64_to_u64)?;
if schema_version != u64::from(CONTEXT_OCCUPANCY_PROJECTION_SCHEMA_VERSION) {
return Err(SessionDbError::ProjectionIncompatible {
projection: MaterializedProjection::RequestContextOccupancy.as_str(),
actual: schema_version,
expected: u64::from(CONTEXT_OCCUPANCY_PROJECTION_SCHEMA_VERSION),
});
}
let context_epoch = required_i64(row, "context_epoch").map(i64_to_u64)?;
let current = optional_string(row, "occupancy_json")
.map(|json| serde_json::from_str::<RequestContextOccupancy>(&json))
.transpose()?;
(context_epoch, current)
} else {
(0, None)
};
let (context_epoch, occupancy) = match &event.kind {
SessionEventKind::ModelChanged { .. }
| SessionEventKind::ContextCompacted { .. }
| SessionEventKind::ProviderContextCompacted { .. } => (event.sequence, None),
SessionEventKind::RequestContextObserved { observation } => (
context_epoch,
RequestContextOccupancy::reconcile(
current.as_ref(),
context_epoch,
event.sequence,
observation.clone(),
),
),
_ => (context_epoch, current),
};
db.upsert("context_occupancy_projection")
.value("projection_id", CONTEXT_OCCUPANCY_PROJECTION_ID)
.value(
"schema_version",
DatabaseValue::Int64(i64::from(CONTEXT_OCCUPANCY_PROJECTION_SCHEMA_VERSION)),
)
.value("context_epoch", seq_to_value(context_epoch))
.value(
"occupancy_json",
occupancy.as_ref().map(serde_json::to_string).transpose()?,
)
.execute(db)
.await?;
Ok(())
}
async fn append_event_projections(
db: &dyn Database,
event: &SessionEvent,
activity_timestamp_ms: Option<u64>,
) -> SessionDbResult<()> {
insert_event(db, event, activity_timestamp_ms).await?;
project_appended_event(db, event).await
}
async fn project_appended_event(db: &dyn Database, event: &SessionEvent) -> SessionDbResult<()> {
project_event(db, event, true).await?;
crate::child_projection::project(db, event).await?;
project_model_context_event(db, event).await?;
project_context_occupancy_row(db, event).await?;
upsert_projection_checkpoints(db, MaterializedProjection::all(), event).await?;
project_turn_receipt(db, event).await
}
async fn project_turn_evidence(db: &dyn Database, event: &SessionEvent) -> SessionDbResult<()> {
let (turn_id, kind, order) = match &event.kind {
SessionEventKind::TraceEvent { trace } if matches!(&trace.payload, bcode_session_models::SessionTracePayload::ProviderEvent { event_type, .. } if event_type == bcode_session_models::PROVIDER_RETRY_CHECKPOINT_EVENT) =>
{
let Some(turn_id) = trace.turn_id.as_ref() else {
return Ok(());
};
(turn_id, "retry", event.sequence)
}
SessionEventKind::ModelTurnFinished { turn_id, .. } => (turn_id, "terminal", 0),
SessionEventKind::AssistantResponseSegment {
turn_id,
segment_order,
..
}
| SessionEventKind::PositionedAssistantResponseSegment {
turn_id,
segment_order,
..
} => (turn_id, "output", u64::from(*segment_order)),
_ => return Ok(()),
};
db.insert("turn_evidence")
.value("turn_id", turn_id.clone())
.value("evidence_kind", kind)
.value("segment_order", seq_to_value(order))
.value("event_seq", seq_to_value(event.sequence))
.execute(db)
.await?;
Ok(())
}
async fn project_turn_receipt(db: &dyn Database, event: &SessionEvent) -> SessionDbResult<()> {
crate::submission::project(db, event).await?;
project_turn_evidence(db, event).await?;
let admission = match &event.kind {
SessionEventKind::UserMessage { admission, .. } => admission,
SessionEventKind::SubmissionAccepted { submission }
if submission.target
== (bcode_session_models::SubmissionTarget::OwnTurn {
turn_id: bcode_session_models::TurnId::from_accepted_event(
event.session_id,
event.sequence,
),
})
&& submission.id
== bcode_session_models::SubmissionId::for_queued_turn(
submission.target.turn_id(),
) =>
{
&submission.admission
}
_ => return Ok(()),
};
let Some((producer, idempotency_key)) = admission.idempotency_identity() else {
return Ok(());
};
let receipt =
bcode_session_models::TurnReceipt::from_accepted_event(event.session_id, event.sequence);
db.insert("turn_receipts")
.value("producer", producer.to_owned())
.value("idempotency_key", idempotency_key.to_owned())
.value(
"accepted_event_seq",
seq_to_value(receipt.accepted_event_sequence),
)
.value("turn_id", receipt.turn_id.to_string())
.value("work_id", receipt.work_id.to_string())
.execute(db)
.await?;
Ok(())
}
async fn project_materialized_event_without_checkpoints(
db: &dyn Database,
event: &SessionEvent,
) -> SessionDbResult<()> {
project_event(db, event, false).await?;
crate::child_projection::project(db, event).await
}
async fn finalize_migration_session_state(
db: &dyn Database,
tail: &SessionEvent,
) -> SessionDbResult<()> {
db.upsert("session_state")
.value("session_id", tail.session_id.to_string())
.value("last_event_seq", seq_to_value(tail.sequence))
.value("updated_at_ms", seq_to_value(event_created_at_ms(tail)))
.execute(db)
.await?;
Ok(())
}
async fn project_migration_materialized_checkpoints_at_tail(
db: &dyn Database,
event: &SessionEvent,
) -> SessionDbResult<()> {
upsert_projection_checkpoints(db, &BASE_MATERIALIZED_PROJECTIONS, event).await
}
async fn upsert_projection_checkpoints(
db: &dyn Database,
projections: &[MaterializedProjection],
event: &SessionEvent,
) -> SessionDbResult<()> {
let rows = projections
.iter()
.map(|projection| {
vec![
(
"projection_name",
DatabaseValue::String(projection.as_str().to_owned()),
),
("last_event_seq", seq_to_value(event.sequence)),
(
"projection_version",
DatabaseValue::Int64(i64::from(projection.schema_version())),
),
("updated_at_ms", seq_to_value(event_created_at_ms(event))),
]
})
.collect();
let mut statement = db.upsert_multi("projection_checkpoints");
statement
.values(rows)
.unique(vec![Box::new(switchy::database::query::identifier(
"projection_name",
))]);
statement.execute(db).await?;
Ok(())
}
async fn load_reasoning_memory(
db: &dyn Database,
session_id: SessionId,
) -> SessionDbResult<ReasoningMemory> {
let row = db
.select("session_state")
.columns(&["reasoning_by_model"])
.where_eq("session_id", session_id.to_string())
.execute_first(db)
.await?;
Ok(row
.as_ref()
.and_then(|row| optional_string(row, "reasoning_by_model"))
.as_deref()
.map_or_else(ReasoningMemory::default, decode_reasoning_by_model))
}
async fn active_model_scope(
db: &dyn Database,
session_id: SessionId,
) -> SessionDbResult<Option<bcode_session_models::ModelScopeKey>> {
let row = db
.select("session_state")
.columns(&["current_provider", "current_model"])
.where_eq("session_id", session_id.to_string())
.execute_first(db)
.await?;
Ok(row.as_ref().and_then(|row| {
let provider = optional_string(row, "current_provider")?;
let model = optional_string(row, "current_model")?;
Some(bcode_session_models::ModelScopeKey::new(provider, model))
}))
}
fn remember_reasoning_in_projection(
memory: &mut std::collections::BTreeMap<
bcode_session_models::ModelScopeKey,
crate::state::RememberedReasoning,
>,
order: &mut Vec<bcode_session_models::ModelScopeKey>,
scope: bcode_session_models::ModelScopeKey,
reasoning: crate::state::RememberedReasoning,
) {
order.retain(|existing| existing != &scope);
if reasoning == crate::state::RememberedReasoning::default() {
memory.remove(&scope);
return;
}
memory.insert(scope.clone(), reasoning);
order.push(scope);
while order.len() > crate::state::REASONING_MEMORY_CAPACITY {
let evicted = order.remove(0);
memory.remove(&evicted);
}
}
#[allow(clippy::too_many_lines)]
async fn project_event(
db: &dyn Database,
event: &SessionEvent,
update_state: bool,
) -> SessionDbResult<()> {
if update_state {
update_session_state(db, event).await?;
}
crate::milestone::project(db, event).await?;
match &event.kind {
SessionEventKind::SessionCreated {
name,
working_directory,
} => {
db.upsert("session_state")
.value("session_id", event.session_id.to_string())
.value("last_event_seq", seq_to_value(event.sequence))
.value("explicit_name", name.clone())
.value("title", name.clone())
.value(
"working_directory",
working_directory.to_string_lossy().to_string(),
)
.value("updated_at_ms", seq_to_value(event_created_at_ms(event)))
.execute(db)
.await?;
}
SessionEventKind::ChildSessionCreated { request } => {
if request.child_id != event.session_id
|| request.parent_id == event.session_id
|| event.sequence != 1
{
return Err(SessionDbError::InvalidRow {
column: "child_session_created".into(),
});
}
let row = db
.select("session_state")
.columns(&["parent_id"])
.where_eq("session_id", event.session_id.to_string())
.execute_first(db)
.await?
.ok_or_else(|| SessionDbError::InvalidRow {
column: "missing child session state".into(),
})?;
if optional_string(&row, "parent_id").is_some() {
return Err(SessionDbError::InvalidRow {
column: "immutable parent_id".into(),
});
}
db.update("session_state")
.value("parent_id", request.parent_id.to_string())
.where_eq("session_id", event.session_id.to_string())
.execute(db)
.await?;
}
SessionEventKind::ExecutionSessionCreated {
provenance,
visibility,
} => {
db.update("session_state")
.value(
"execution_provenance",
serde_json::to_string(provenance)
.expect("execution session provenance serializes"),
)
.value(
"visibility",
serde_json::to_string(visibility).expect("session visibility serializes"),
)
.where_eq("session_id", event.session_id.to_string())
.execute(db)
.await?;
}
SessionEventKind::SessionRenamed { name } => {
db.update("session_state")
.value("explicit_name", name.clone())
.value("title", name.clone())
.where_eq("session_id", event.session_id.to_string())
.execute(db)
.await?;
}
SessionEventKind::ModelChanged {
provider,
model,
selection_source,
} => {
let (memory, _) = load_reasoning_memory(db, event.session_id).await?;
let remembered = memory
.get(&bcode_session_models::ModelScopeKey::new(provider, model))
.cloned()
.unwrap_or_default();
db.update("session_state")
.value("current_provider", provider.clone())
.value("current_model", model.clone())
.value(
"model_selection_source",
encode_model_selection_source(*selection_source),
)
.value("reasoning_effort", remembered.effort)
.value("reasoning_summary", remembered.summary)
.where_eq("session_id", event.session_id.to_string())
.execute(db)
.await?;
}
SessionEventKind::ReasoningChanged {
effort,
summary,
model_scope,
} => {
let (mut memory, mut order) = load_reasoning_memory(db, event.session_id).await?;
let scope = match model_scope.clone() {
Some(scope) => Some(scope),
None => active_model_scope(db, event.session_id).await?,
};
if let Some(scope) = scope {
remember_reasoning_in_projection(
&mut memory,
&mut order,
scope,
crate::state::RememberedReasoning {
effort: effort.clone(),
summary: summary.clone(),
},
);
}
db.update("session_state")
.value("reasoning_effort", effort.clone())
.value("reasoning_summary", summary.clone())
.value(
"reasoning_by_model",
encode_reasoning_by_model(&memory, &order),
)
.where_eq("session_id", event.session_id.to_string())
.execute(db)
.await?;
}
SessionEventKind::UserMessage { text, .. }
| SessionEventKind::SubmissionAccepted {
submission: bcode_session_models::PromptSubmission { text, .. },
} => {
db.insert("input_messages")
.value("input_seq", seq_to_value(event.sequence))
.value("event_seq", seq_to_value(event.sequence))
.value("created_at_ms", seq_to_value(event_created_at_ms(event)))
.value("text", text.clone())
.execute(db)
.await?;
if db
.select("session_state")
.columns(&["derived_title"])
.where_eq("session_id", event.session_id.to_string())
.execute_first(db)
.await?
.as_ref()
.and_then(|row| optional_string(row, "derived_title"))
.is_none()
{
db.update("session_state")
.value("derived_title", crate::title_from_first_prompt(text))
.where_eq("session_id", event.session_id.to_string())
.execute(db)
.await?;
}
insert_transcript_item(db, event, "user", "message", "complete", Some(text.clone()))
.await?;
}
SessionEventKind::ModelUsage { usage, .. } => {
project_session_usage(db, event, usage).await?;
}
SessionEventKind::AssistantMessage { text }
| SessionEventKind::AssistantResponseSegment { text, .. }
| SessionEventKind::PositionedAssistantResponseSegment { text, .. } => {
insert_transcript_item(
db,
event,
"assistant",
"message",
"complete",
Some(text.clone()),
)
.await?;
}
SessionEventKind::PluginStatusNote { text, .. } => {
insert_transcript_item(
db,
event,
"system",
"plugin_status_note",
"complete",
Some(text.clone()),
)
.await?;
}
SessionEventKind::ToolCallRequested {
tool_call_id,
tool_name,
..
}
| SessionEventKind::PositionedToolCallRequested {
tool_call_id,
tool_name,
..
} => {
db.insert("tool_runs")
.value("tool_call_id", tool_call_id.clone())
.value("event_seq_start", seq_to_value(event.sequence))
.value("status", "running")
.value("tool_name", tool_name.clone())
.value("started_at_ms", seq_to_value(event_created_at_ms(event)))
.execute(db)
.await?;
insert_transcript_item(db, event, "tool", "invocation", "running", None).await?;
}
SessionEventKind::ToolInvocationResultRecorded { record } => {
db.update("tool_runs")
.value("event_seq_end", seq_to_value(event.sequence))
.value("status", if record.is_error { "error" } else { "complete" })
.value("completed_at_ms", seq_to_value(event_created_at_ms(event)))
.value("is_error", bool_to_value(record.is_error))
.where_eq("tool_call_id", record.invocation_id.clone())
.execute(db)
.await?;
if let Some(ToolInvocationResult::Artifact { artifact }) = &record.result {
project_artifact_references(db, event.sequence, artifact).await?;
}
finalize_tool_transcript_item(
db,
&record.invocation_id,
event.sequence,
record.is_error,
)
.await?;
}
SessionEventKind::ToolInvocationLifecycle { event: lifecycle } => {
let status = match lifecycle.stage {
bcode_session_models::ToolInvocationLifecycleStage::Started
| bcode_session_models::ToolInvocationLifecycleStage::Progress
| bcode_session_models::ToolInvocationLifecycleStage::Waiting => "running",
bcode_session_models::ToolInvocationLifecycleStage::Completed => "complete",
bcode_session_models::ToolInvocationLifecycleStage::Cancelled => "cancelled",
bcode_session_models::ToolInvocationLifecycleStage::Failed => "error",
};
if matches!(
lifecycle.stage,
bcode_session_models::ToolInvocationLifecycleStage::Completed
| bcode_session_models::ToolInvocationLifecycleStage::Cancelled
| bcode_session_models::ToolInvocationLifecycleStage::Failed
) {
let is_error = matches!(
lifecycle.stage,
bcode_session_models::ToolInvocationLifecycleStage::Failed
| bcode_session_models::ToolInvocationLifecycleStage::Cancelled
);
db.update("tool_runs")
.value("event_seq_end", seq_to_value(event.sequence))
.value("status", status)
.value("completed_at_ms", seq_to_value(event_created_at_ms(event)))
.value("is_error", bool_to_value(is_error))
.where_eq("tool_call_id", lifecycle.invocation_id.clone())
.execute(db)
.await?;
finalize_tool_transcript_item(
db,
&lifecycle.invocation_id,
event.sequence,
is_error,
)
.await?;
}
}
SessionEventKind::ToolContribution {
event: contribution,
} => {
insert_transcript_item(
db,
event,
"runtime",
"tool_contribution",
"complete",
Some(serde_json::to_string(contribution)?),
)
.await?;
}
SessionEventKind::ToolExchangeRequested { request } => {
insert_transcript_item(
db,
event,
"runtime",
"tool_exchange_requested",
"running",
Some(serde_json::to_string(request)?),
)
.await?;
}
SessionEventKind::ToolExchangeResolved { event: resolution } => {
insert_transcript_item(
db,
event,
"runtime",
"tool_exchange_resolved",
"complete",
Some(serde_json::to_string(resolution)?),
)
.await?;
}
SessionEventKind::RuntimeWorkStarted {
work_id,
kind,
label,
parent_work_id,
started_at_ms,
cancellable,
..
} => {
let existing = db
.select("runtime_work")
.columns(&["status", "kind"])
.where_eq("work_id", work_id.to_string())
.execute_first(db)
.await?;
if let Some(row) = existing {
crate::db_runtime_work::parse_runtime_work_kind(&required_string(&row, "kind")?)
.ok_or_else(|| SessionDbError::InvalidRow {
column: "kind".to_owned(),
})?;
let status = crate::db_runtime_work::parse_runtime_work_status(&required_string(
&row, "status",
)?)
.ok_or_else(|| SessionDbError::InvalidRow {
column: "status".to_owned(),
})?;
if status.is_terminal() {
return Ok(());
}
}
db.upsert("runtime_work")
.unique(&["work_id"])
.value("work_id", work_id.to_string())
.value("event_seq_start", seq_to_value(event.sequence))
.value("event_seq_end", DatabaseValue::Null)
.value("finished_at_ms", DatabaseValue::Null)
.value("message", DatabaseValue::Null)
.value("kind", runtime_work_kind_name(*kind))
.value("label", label.clone())
.value(
"status",
runtime_work_status_name(RuntimeWorkStatus::Running),
)
.value(
"parent_work_id",
parent_work_id.as_ref().map(ToString::to_string),
)
.value("started_at_ms", started_at_ms.map(seq_to_value))
.value("cancellable", bool_to_value(*cancellable))
.execute(db)
.await?;
}
SessionEventKind::RuntimeWorkCancelRequested { work_id, .. } => {
db.update("runtime_work")
.value(
"status",
runtime_work_status_name(RuntimeWorkStatus::Cancelling),
)
.where_eq("work_id", work_id.to_string())
.where_in(
"status",
[
RuntimeWorkStatus::Queued,
RuntimeWorkStatus::Running,
RuntimeWorkStatus::Cancelling,
]
.map(|status| {
DatabaseValue::String(runtime_work_status_name(status).to_owned())
})
.to_vec(),
)
.execute(db)
.await?;
}
SessionEventKind::RuntimeWorkFinished {
work_id,
status,
finished_at_ms,
message,
} => {
db.update("runtime_work")
.value("event_seq_end", seq_to_value(event.sequence))
.value("status", runtime_work_status_name(*status))
.value("finished_at_ms", finished_at_ms.map(seq_to_value))
.value("message", message.clone())
.where_eq("work_id", work_id.to_string())
.where_in(
"status",
[
RuntimeWorkStatus::Queued,
RuntimeWorkStatus::Running,
RuntimeWorkStatus::Cancelling,
RuntimeWorkStatus::Suspended,
]
.map(|status| {
DatabaseValue::String(runtime_work_status_name(status).to_owned())
})
.to_vec(),
)
.execute(db)
.await?;
}
SessionEventKind::RuntimeWorkProgress {
work_id, message, ..
} => {
db.update("runtime_work")
.value("message", message.clone())
.where_eq("work_id", work_id.to_string())
.where_in(
"status",
[
RuntimeWorkStatus::Queued,
RuntimeWorkStatus::Running,
RuntimeWorkStatus::Cancelling,
]
.map(|status| {
DatabaseValue::String(runtime_work_status_name(status).to_owned())
})
.to_vec(),
)
.execute(db)
.await?;
}
SessionEventKind::WorkingDirectoryChanged {
new_working_directory,
..
} => {
db.update("session_state")
.value(
"working_directory",
new_working_directory.to_string_lossy().to_string(),
)
.where_eq("session_id", event.session_id.to_string())
.execute(db)
.await?;
}
_ => {}
}
Ok(())
}
async fn validate_original_usage_duplicate(
db: &dyn Database,
event: &SessionEvent,
original: Option<&bcode_session_models::OriginalUsage>,
) -> SessionDbResult<()> {
let SessionEventKind::ModelUsage { usage, .. } = &event.kind else {
return Ok(());
};
let Some(request_id) = &usage.request_id else {
return Ok(());
};
let Some(row) = db
.select("session_usage_requests")
.columns(&["event_seq"])
.where_eq("request_key", request_id.clone())
.execute_first(db)
.await?
else {
return Ok(());
};
let canonical = db
.select("events")
.columns(&["payload"])
.where_eq("event_seq", required_i64(&row, "event_seq")?)
.execute_first(db)
.await?
.ok_or_else(|| SessionDbError::InvalidRow {
column: "missing usage source".into(),
})?;
let payload = required_string(&canonical, "payload")?;
let previous = decode_session_event(&payload)?;
if let SessionEventKind::ModelUsage {
usage: previous, ..
} = previous.kind
&& previous.observation_ordinal == usage.observation_ordinal
&& crate::persisted::original_usage(&payload)
.map_err(|column| SessionDbError::InvalidRow { column })?
.as_ref()
!= original
{
return Err(SessionDbError::InvalidRow {
column: "conflicting duplicate original billing evidence".into(),
});
}
Ok(())
}
pub(crate) fn validate_renormalized_usage(
previous: &SessionTokenUsage,
next: &bcode_session_models::SessionUsageValuation,
) -> SessionDbResult<()> {
if previous.catalog_family != next.usage.catalog_family
|| previous.catalog_api_surface != next.usage.catalog_api_surface
{
return Err(SessionDbError::InvalidRow {
column: "normalization changed catalog attribution".into(),
});
}
if previous.request != next.usage.request
|| previous.request_id != next.usage.request_id
|| previous.observation_id != next.usage.observation_id
|| previous.terminal != next.usage.terminal
|| previous.observation_ordinal != next.usage.observation_ordinal
|| previous.catalog_provider_id != next.usage.catalog_provider_id
|| previous.catalog_entry_id != next.usage.catalog_entry_id
|| previous.pricing_target != next.usage.pricing_target
{
return Err(SessionDbError::InvalidRow {
column: "normalization changed request attribution".into(),
});
}
let mut usage = next.usage.clone();
usage.cost = Some(next.cost.clone());
usage
.validate()
.map_err(|column| SessionDbError::InvalidRow { column })
}
async fn project_session_usage(
db: &dyn Database,
event: &SessionEvent,
usage: &SessionTokenUsage,
) -> SessionDbResult<()> {
let request_key = usage
.request_id
.clone()
.unwrap_or_else(|| format!("event:{}", event.sequence));
let existing = db
.select("session_usage_requests")
.columns(&[
"usage_json",
"cost_json",
"first_observed_at_ms",
"event_seq",
])
.where_eq("request_key", request_key.clone())
.execute_first(db)
.await?;
let current = existing
.as_ref()
.map(|row| {
serde_json::from_str::<SessionTokenUsage>(&required_string(row, "usage_json")?).map_err(
|_| SessionDbError::InvalidRow {
column: "session_usage_requests.usage_json".to_owned(),
},
)
})
.transpose()?;
let accounting_error = |message: String| SessionDbError::InvalidRow {
column: format!("session_usage: {message}"),
};
let canonical_current = if let Some(row) = &existing {
let canonical = db
.select("events")
.columns(&["payload"])
.where_eq("event_seq", required_i64(row, "event_seq")?)
.execute_first(db)
.await?
.ok_or_else(|| SessionDbError::InvalidRow {
column: "missing canonical usage".into(),
})?;
let event = decode_session_event(&required_string(&canonical, "payload")?)?;
let SessionEventKind::ModelUsage { usage, .. } = event.kind else {
return Err(SessionDbError::InvalidRow {
column: "invalid canonical usage kind".into(),
});
};
Some(usage)
} else {
None
};
if !crate::usage::accepts(canonical_current.as_ref(), usage).map_err(accounting_error)? {
return Ok(());
}
let mut summary = read_session_usage_summary(db).await?;
let mut current = current;
if let (Some(current), Some(row)) = (&mut current, &existing) {
current.cost = optional_string(row, "cost_json")
.map(|json| serde_json::from_str(&json))
.transpose()?;
}
let mut projected = usage.clone();
projected.cost = Some(usage.cost.clone().unwrap_or(
bcode_session_models::SessionCostEstimate::Unavailable {
reason: bcode_session_models::SessionCostUnavailableReason::RequestPricingUnavailable,
},
));
crate::usage::replace(&mut summary, current.as_ref(), &projected).map_err(accounting_error)?;
let first_observed = existing
.as_ref()
.map(|row| required_i64(row, "first_observed_at_ms"))
.transpose()?
.unwrap_or_else(|| i64::try_from(event.timestamp_ms).unwrap_or(i64::MAX));
db.upsert("session_usage_requests")
.unique(&["request_key"])
.value("request_key", request_key)
.value("usage_json", serde_json::to_string(usage)?)
.value("cost_json", serde_json::to_string(&projected.cost)?)
.value("first_observed_at_ms", DatabaseValue::Int64(first_observed))
.value("event_seq", seq_to_value(event.sequence))
.execute(db)
.await?;
db.upsert("session_usage_projection")
.unique(&["projection_id"])
.value("projection_id", DatabaseValue::Int32(1))
.value("summary_json", serde_json::to_string(&summary)?)
.execute(db)
.await?;
Ok(())
}
async fn set_projected_request_cost(
db: &dyn Database,
event: &SessionEvent,
cost: &bcode_session_models::SessionCostEstimate,
) -> SessionDbResult<()> {
let SessionEventKind::ModelUsage { usage, .. } = &event.kind else {
return Ok(());
};
let key = usage
.request_id
.clone()
.unwrap_or_else(|| format!("event:{}", event.sequence));
let row = db
.select("session_usage_requests")
.columns(&["usage_json", "cost_json", "event_seq"])
.where_eq("request_key", key.clone())
.execute_first(db)
.await?
.ok_or_else(|| SessionDbError::InvalidRow {
column: "session_usage_requests".to_owned(),
})?;
if required_i64(&row, "event_seq")? != i64::try_from(event.sequence).unwrap_or(i64::MAX) {
return Ok(());
}
let mut previous: SessionTokenUsage =
serde_json::from_str(&required_string(&row, "usage_json")?)?;
previous.cost = optional_string(&row, "cost_json")
.map(|json| serde_json::from_str(&json))
.transpose()?;
let mut next = previous.clone();
next.cost = Some(cost.clone());
let mut summary = read_session_usage_summary(db).await?;
crate::usage::replace(&mut summary, Some(&previous), &next)
.map_err(|message| SessionDbError::InvalidRow { column: message })?;
db.update("session_usage_requests")
.value("cost_json", serde_json::to_string(cost)?)
.where_eq("request_key", key)
.execute(db)
.await?;
db.upsert("session_usage_projection")
.unique(&["projection_id"])
.value("projection_id", DatabaseValue::Int32(1))
.value("summary_json", serde_json::to_string(&summary)?)
.execute(db)
.await?;
Ok(())
}
async fn read_session_usage_summary(db: &dyn Database) -> SessionDbResult<SessionUsageSummary> {
let row = db
.select("session_usage_projection")
.columns(&["summary_json"])
.where_eq("projection_id", DatabaseValue::Int32(1))
.execute_first(db)
.await?;
let Some(row) = row else {
if db
.select("session_usage_requests")
.columns(&["request_key"])
.limit(1)
.execute_first(db)
.await?
.is_some()
{
return Err(SessionDbError::InvalidRow {
column: "missing session_usage_projection for existing requests".to_owned(),
});
}
return Ok(SessionUsageSummary::default());
};
serde_json::from_str(&required_string(&row, "summary_json")?).map_err(|_| {
SessionDbError::InvalidRow {
column: "session_usage_projection.summary_json".to_owned(),
}
})
}
async fn update_session_state(db: &dyn Database, event: &SessionEvent) -> SessionDbResult<()> {
db.upsert("session_state")
.value("session_id", event.session_id.to_string())
.value("last_event_seq", seq_to_value(event.sequence))
.value("updated_at_ms", seq_to_value(event_created_at_ms(event)))
.execute(db)
.await?;
Ok(())
}
async fn insert_transcript_item(
db: &dyn Database,
event: &SessionEvent,
role: &str,
kind: &str,
status: &str,
content: Option<String>,
) -> SessionDbResult<()> {
insert_transcript_item_with_range(
db,
event.sequence,
event.sequence,
event.sequence,
event_created_at_ms(event),
role,
kind,
status,
content,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn insert_transcript_item_with_range(
db: &dyn Database,
transcript_seq: u64,
event_seq_start: u64,
event_seq_end: u64,
created_at_ms: u64,
role: &str,
kind: &str,
status: &str,
content: Option<String>,
) -> SessionDbResult<()> {
let mut statement = db
.insert("transcript_items")
.value("transcript_seq", seq_to_value(transcript_seq))
.value("event_seq_start", seq_to_value(event_seq_start))
.value("event_seq_end", seq_to_value(event_seq_end))
.value("role", role)
.value("kind", kind)
.value("status", status)
.value("created_at_ms", seq_to_value(created_at_ms));
if let Some(content) = content {
statement = statement.value("content", content);
}
statement.execute(db).await?;
Ok(())
}
async fn finalize_tool_transcript_item(
db: &dyn Database,
tool_call_id: &str,
event_sequence: u64,
is_error: bool,
) -> SessionDbResult<()> {
let row = db
.select("tool_runs")
.columns(&["event_seq_start"])
.where_eq("tool_call_id", tool_call_id.to_owned())
.execute_first(db)
.await?;
let Some(event_seq_start) = row
.as_ref()
.map(|row| required_i64(row, "event_seq_start").map(i64_to_u64))
.transpose()?
else {
return Ok(());
};
db.update("transcript_items")
.value("event_seq_end", seq_to_value(event_sequence))
.value("kind", "result")
.value("status", if is_error { "error" } else { "complete" })
.where_eq("transcript_seq", seq_to_value(event_seq_start))
.execute(db)
.await?;
Ok(())
}
const fn inspection_event_types(category: SessionInspectionCategory) -> &'static [&'static str] {
match category {
SessionInspectionCategory::FailedToolCalls => &[
"tool_invocation_result_recorded",
"tool_invocation_lifecycle",
],
SessionInspectionCategory::Permissions => &["permission_requested", "permission_resolved"],
SessionInspectionCategory::SelectionChanges => &[
"model_changed",
"reasoning_changed",
"agent_changed",
"working_directory_changed",
],
SessionInspectionCategory::RuntimeWork => &[
"runtime_work_started",
"runtime_work_cancel_requested",
"runtime_work_finished",
"runtime_work_progress",
],
SessionInspectionCategory::Compactions => {
&["context_compacted", "provider_context_compacted"]
}
SessionInspectionCategory::TerminalOutcomes => &[
"model_turn_finished",
"tool_invocation_result_recorded",
"tool_invocation_lifecycle",
"runtime_work_finished",
],
}
}
fn inspection_event_matches(category: SessionInspectionCategory, kind: &SessionEventKind) -> bool {
match category {
SessionInspectionCategory::FailedToolCalls => {
matches!(
kind,
SessionEventKind::ToolInvocationResultRecorded { record } if record.is_error
) || matches!(
kind,
SessionEventKind::ToolInvocationLifecycle { event }
if event.stage == bcode_session_models::ToolInvocationLifecycleStage::Failed
)
}
SessionInspectionCategory::Permissions => matches!(
kind,
SessionEventKind::PermissionRequested { .. }
| SessionEventKind::PermissionResolved { .. }
),
SessionInspectionCategory::SelectionChanges => matches!(
kind,
SessionEventKind::ModelChanged { .. }
| SessionEventKind::ReasoningChanged { .. }
| SessionEventKind::AgentChanged { .. }
| SessionEventKind::WorkingDirectoryChanged { .. }
),
SessionInspectionCategory::RuntimeWork => matches!(
kind,
SessionEventKind::RuntimeWorkStarted { .. }
| SessionEventKind::RuntimeWorkCancelRequested { .. }
| SessionEventKind::RuntimeWorkFinished { .. }
| SessionEventKind::RuntimeWorkProgress { .. }
),
SessionInspectionCategory::Compactions => matches!(
kind,
SessionEventKind::ContextCompacted { .. }
| SessionEventKind::ProviderContextCompacted { .. }
),
SessionInspectionCategory::TerminalOutcomes => {
matches!(
kind,
SessionEventKind::ModelTurnFinished { .. }
| SessionEventKind::ToolInvocationResultRecorded { .. }
| SessionEventKind::RuntimeWorkFinished { .. }
) || matches!(
kind,
SessionEventKind::ToolInvocationLifecycle { event }
if matches!(
event.stage,
bcode_session_models::ToolInvocationLifecycleStage::Completed
| bcode_session_models::ToolInvocationLifecycleStage::Cancelled
| bcode_session_models::ToolInvocationLifecycleStage::Failed
)
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db_projection::ProjectionCheckpointState;
use bcode_session_models::{
CURRENT_SESSION_EVENT_SCHEMA_VERSION, ClientId, RequestContextObservation,
RequestContextTokenCount,
};
use sha2::Digest;
use std::collections::BTreeSet;
use std::time::Duration;
fn session_storage_files(root: &Path, session_id: SessionId) -> BTreeMap<String, Vec<u8>> {
let session_dir = root.join(session_id.to_string());
std::fs::read_dir(session_dir)
.expect("session directory")
.filter_map(Result::ok)
.filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_file()))
.map(|entry| {
let name = entry.file_name().to_string_lossy().into_owned();
let bytes = std::fs::read(entry.path()).expect("read session storage file");
(name, bytes)
})
.collect()
}
#[tokio::test]
async fn epoch_eleven_upgrade_preserves_user_receipt_and_empty_submission_projection() {
let root = tempfile::tempdir().unwrap();
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.unwrap();
let created = event(
id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: root.path().to_path_buf(),
},
);
db.append_event(&created).await.unwrap();
let admission = bcode_session_models::TurnAdmissionMetadata {
origin: Some(bcode_session_models::TurnOrigin {
producer: "test".into(),
correlation_id: None,
display_label: None,
}),
idempotency_key: Some("old".into()),
..Default::default()
};
let prompt = event(
id,
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "old prompt".into(),
admission,
},
);
db.append_event(&prompt).await.unwrap();
let receipt = db.turn_receipt("test", "old").await.unwrap();
assert!(matches!(
db.storage_compatibility().await.unwrap(),
SessionStorageCompatibility::Current { .. }
));
remove_milestone_projection_fixture(&db).await;
remove_parent_relationship_fixture(&db).await;
db.database()
.delete(SESSION_MIGRATIONS_TABLE)
.where_in(
"id",
vec![
"043_submissions_table",
"044_submissions_pending_index",
"045_submissions_target_index",
"046_session_state_parent_column",
"047_session_children",
"048_session_children_checkpoint",
"049_session_children_pending_index",
],
)
.execute(db.database())
.await
.unwrap();
let facts = db.storage_compatibility_facts().await.unwrap();
assert!(matches!(
bcode_session_migration_target::classify_storage_facts(&facts).unwrap(),
bcode_session_migration_target::StorageCompatibility::MigrationRequired { .. }
));
db.database()
.update("session_storage_contract")
.value("writer_epoch", 11)
.execute(db.database())
.await
.unwrap();
drop(db);
let maintenance = crate::lease::acquire_session_maintenance_guard(root.path(), id).unwrap();
let write =
crate::lease::acquire_maintenance_session_write_lock(&maintenance, root.path(), id)
.unwrap();
let migrated = SessionDb::migrate_turso_in_root(id, root.path(), &maintenance, &write)
.await
.unwrap();
assert_eq!(migrated.turn_receipt("test", "old").await.unwrap(), receipt);
assert!(
migrated
.pending_submissions(None, 256)
.await
.unwrap()
.is_empty()
);
assert_eq!(migrated.model_context_events().await.unwrap(), vec![prompt]);
assert_eq!(
migrated
.materialized_projection_checkpoint(MaterializedProjection::Submissions)
.await
.unwrap(),
Some(1)
);
}
#[tokio::test]
async fn catalog_pages_are_bounded_and_visit_equal_timestamp_rows_once() {
let root = tempfile::tempdir().unwrap();
let catalog = GlobalSessionDb::initialize_turso_in_root(root.path())
.await
.unwrap();
let mut expected = BTreeSet::new();
for _ in 0..259 {
let id = SessionId::new();
expected.insert(id);
catalog
.database()
.insert("sessions")
.value("session_id", id.to_string())
.value("db_path", "unused")
.value("title", "catalog only")
.value("working_directory", "/project")
.value("created_at_ms", 1_i64)
.value("updated_at_ms", 1_i64)
.value("state", "active")
.value("projection_status", "fresh")
.execute(catalog.database())
.await
.unwrap();
}
assert!(
catalog
.list_sessions_page(None, 0)
.await
.unwrap()
.is_empty()
);
let first = catalog.list_sessions_page(None, usize::MAX).await.unwrap();
assert_eq!(first.len(), 256);
let mut after = None;
let mut seen = BTreeSet::new();
loop {
let page = catalog.list_sessions_page(after, 17).await.unwrap();
assert!(page.len() <= 17);
if page.is_empty() {
break;
}
for row in &page {
assert!(seen.insert(row.id));
}
after = page.last().map(|row| row.id);
}
assert_eq!(seen, expected);
catalog.close().await.unwrap();
}
#[tokio::test]
async fn global_catalog_close_reaches_abstract_terminal_state() {
let root = tempfile::tempdir().expect("temp root");
let catalog = GlobalSessionDb::initialize_turso_in_root(root.path())
.await
.expect("catalog initializes");
assert!(!catalog.is_closed().await.expect("open probe"));
catalog.close().await.expect("catalog closes");
assert!(catalog.is_closed().await.expect("closed probe"));
}
async fn insert_raw_history_event_with_schema(
db: &SessionDb,
session_id: SessionId,
sequence: u64,
event_type: &str,
schema_version: u16,
kind: serde_json::Value,
) {
db.database()
.insert("events")
.value("event_seq", seq_to_value(sequence))
.value("event_type", event_type)
.value(
"schema_version",
DatabaseValue::Int32(i32::from(schema_version)),
)
.value("created_at_ms", seq_to_value(sequence))
.value(
"payload",
serde_json::json!({
"schema_version": schema_version,
"sequence": sequence,
"session_id": session_id,
"kind": kind,
})
.to_string(),
)
.execute(db.database())
.await
.expect("insert raw history event");
}
async fn insert_raw_history_event(
db: &SessionDb,
session_id: SessionId,
sequence: u64,
event_type: &str,
kind: serde_json::Value,
) {
insert_raw_history_event_with_schema(
db,
session_id,
sequence,
event_type,
CURRENT_SESSION_EVENT_SCHEMA_VERSION,
kind,
)
.await;
}
async fn reindex_model_context_for_test(
db: &SessionDb,
root: &Path,
session_id: SessionId,
) -> usize {
let maintenance = crate::lease::acquire_session_maintenance_guard(root, session_id)
.expect("maintenance guard");
let write =
crate::lease::acquire_maintenance_session_write_lock(&maintenance, root, session_id)
.expect("write guard");
db.reindex_model_context(&maintenance, &write)
.await
.expect("explicit reindex")
}
fn request_context_event(session_id: SessionId, sequence: u64) -> SessionEvent {
event(
session_id,
sequence,
SessionEventKind::RequestContextObserved {
observation: RequestContextObservation {
request: bcode_session_models::ModelRequestIdentity {
provider_plugin_id: "provider".to_string(),
requested_model_id: Some("alias".to_string()),
effective_model_id: "model".to_string(),
request_id: format!("request-{sequence}"),
model_turn_id: format!("turn-{sequence}"),
round: 0,
request_fingerprint: format!("fingerprint-{sequence}"),
effective_auth_profile: None,
context_format_version: None,
compatibility_key: None,
context_epoch: 0,
},
context_through_sequence: sequence.saturating_sub(1),
context_tokens: RequestContextTokenCount::Estimated(sequence),
local_estimate: bcode_session_models::LocalContextEstimate {
tokens: sequence,
algorithm_version: 1,
},
},
},
)
}
fn event(session_id: SessionId, sequence: u64, kind: SessionEventKind) -> SessionEvent {
SessionEvent {
schema_version: CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence,
timestamp_ms: 1,
session_id,
provenance: None,
kind,
}
}
fn complete_checkpoint_snapshot(expected: u64) -> BTreeMap<String, ProjectionCheckpointState> {
MaterializedProjection::all()
.iter()
.map(|projection| {
(
projection.as_str().to_owned(),
ProjectionCheckpointState {
version: u64::from(projection.schema_version()),
checkpoint: expected,
},
)
})
.collect()
}
#[tokio::test]
async fn first_unknown_event_envelope_is_bounded_and_fails_closed_for_future_schema() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open db");
insert_raw_history_event_with_schema(
&db,
session_id,
0,
"future_event_kind",
CURRENT_SESSION_EVENT_SCHEMA_VERSION + 1,
serde_json::json!({"future_event_kind": {}}),
)
.await;
assert_eq!(
db.first_unknown_event_envelope()
.await
.expect("bounded classification"),
Some((
CURRENT_SESSION_EVENT_SCHEMA_VERSION + 1,
"future_event_kind".to_owned()
))
);
}
#[tokio::test]
async fn first_non_current_event_schema_uses_bounded_scalar_detection() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session DB");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: std::path::PathBuf::from("."),
},
))
.await
.expect("append current event");
assert_eq!(
db.first_non_current_event_schema()
.await
.expect("schema query"),
None
);
db.database()
.update("events")
.value("schema_version", DatabaseValue::Int64(40))
.execute(db.database())
.await
.expect("install historical schema marker");
assert_eq!(
db.first_non_current_event_schema()
.await
.expect("schema query"),
Some(40)
);
}
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn current_migration_target_implementation_exercises_every_operation() {
use bcode_session_migration_target::MigrationTarget as _;
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session DB");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("target API".to_owned()),
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append event");
db.database()
.delete("session_migration_receipts")
.execute(db.database())
.await
.expect("clear receipts");
let metrics = MetricsRegistry::in_memory();
let mut target = SessionMigrationTarget::new(db.database(), &metrics);
target
.materialize_current_schema()
.await
.expect("materialize schema");
let mut rows = target.canonical_page(0, 1).await.expect("canonical page");
assert_eq!(rows.len(), 1);
let row = rows.pop().expect("canonical row");
target
.replace_canonical_row(row)
.await
.expect("replace canonical row");
let current_event = event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("target API".to_owned()),
working_directory: temp_dir.path().to_path_buf(),
},
);
target
.ingest_projectors(¤t_event)
.await
.expect("ingest projectors");
target
.write_authoritative_state(0, None)
.await
.expect("write authoritative state");
target
.finalize_projectors(Some(0))
.await
.expect("finalize projectors");
let validation = target
.validate_strict_current()
.await
.expect("strict validation facts");
validate_strict_target_facts(&validation).expect("strict current target");
let operation_id = bcode_session_models::SessionOpenOperationId::new();
target
.persist_migration_receipt(&bcode_session_migration_target::MigrationReceipt {
operation_id: operation_id.to_string(),
session_id,
source_writer_epoch: 4,
target_writer_epoch: CURRENT_SESSION_STORAGE_WRITER_EPOCH,
migration_step_ids: vec!["fixture-step".to_owned()],
source_event_count: 1,
source_event_tail: Some(0),
source_payload_digest_sha256: "source".to_owned(),
target_event_count: 1,
target_event_tail: Some(0),
target_payload_digest_sha256: "target".to_owned(),
converted_events: BTreeMap::new(),
retired_known_events: BTreeMap::new(),
completed_at_ms: 1,
})
.await
.expect("persist receipt");
target
.finalize_writer_contract()
.await
.expect("finalize writer");
let receipt = db
.database()
.select("session_migration_receipts")
.columns(&["operation_id"])
.where_eq("operation_id", operation_id.to_string())
.execute_first(db.database())
.await
.expect("receipt query");
assert!(receipt.is_some());
db.validate_write_readiness()
.await
.expect("target remains writable");
}
#[test]
fn current_migration_target_capability_surface_is_complete_and_policy_free() {
use bcode_session_migration_target::CurrentMigrationTargetCapability as Capability;
assert_eq!(
bcode_session_migration_target::current_migration_target_capabilities(),
BTreeSet::from([
Capability::MaterializeCurrentSchema,
Capability::ReadBoundedCanonicalPage,
Capability::ReplaceCanonicalRow,
Capability::WriteAuthoritativeState,
Capability::IngestProjectors,
Capability::FinalizeProjectors,
Capability::ValidateStrictCurrent,
Capability::PersistMigrationReceipt,
Capability::FinalizeWriterContract,
])
);
}
#[tokio::test]
async fn storage_compatibility_rejects_future_writer_epoch() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let future = u64::from(CURRENT_SESSION_STORAGE_WRITER_EPOCH).saturating_add(1);
db.database()
.update("session_storage_contract")
.value(
"writer_epoch",
DatabaseValue::Int64(i64::try_from(future).expect("future epoch fits")),
)
.where_eq("contract_id", SESSION_STORAGE_CONTRACT_ID)
.execute(db.database())
.await
.expect("set future writer epoch");
assert!(matches!(
db.storage_compatibility().await,
Err(SessionDbError::WriterIncompatible {
actual: Some(actual),
expected
}) if actual == future
&& expected == u64::from(CURRENT_SESSION_STORAGE_WRITER_EPOCH)
));
}
#[tokio::test]
async fn storage_compatibility_rejects_contract_missing_from_completed_history() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
switchy::database::schema::drop_table("session_storage_contract")
.execute(db.database())
.await
.expect("drop contract table");
assert!(matches!(
db.storage_compatibility().await,
Err(SessionDbError::MigrationHistoryIncompatible { .. })
));
}
#[tokio::test]
async fn storage_compatibility_classifies_clean_legacy_prefix() {
const FIRST_CONTRACT_MIGRATION: &str = "026_session_storage_contract_table";
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let post_legacy_migration_ids = session_migrations()
.migrations()
.await
.expect("current session migrations")
.into_iter()
.map(|migration| migration.id().to_owned())
.skip_while(|id| id != FIRST_CONTRACT_MIGRATION)
.map(DatabaseValue::String)
.collect::<Vec<_>>();
assert!(
post_legacy_migration_ids.len() > 1,
"the contract migration must exist and be followed by later migrations"
);
db.database()
.delete(SESSION_MIGRATIONS_TABLE)
.where_in("id", post_legacy_migration_ids)
.execute(db.database())
.await
.expect("remove contract migrations");
switchy::database::schema::drop_table("session_storage_contract")
.execute(db.database())
.await
.expect("drop contract table");
assert_eq!(
db.storage_compatibility().await.expect("compatibility"),
SessionStorageCompatibility::KnownLegacy {
writer_epoch: u64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH),
}
);
}
#[test]
fn complete_checkpoint_snapshot_at_expected_tail_passes() {
validate_projection_checkpoint_snapshot(&complete_checkpoint_snapshot(7), 7)
.expect("complete checkpoint snapshot");
}
#[test]
fn missing_checkpoint_snapshot_row_is_stale() {
let mut snapshot = complete_checkpoint_snapshot(7);
snapshot.remove(MaterializedProjection::ToolRuns.as_str());
assert!(matches!(
validate_projection_checkpoint_snapshot(&snapshot, 7),
Err(SessionDbError::ProjectionStale {
projection: "tool_runs",
checkpoint: None,
expected: 7
})
));
}
#[test]
fn incompatible_checkpoint_snapshot_version_is_rejected() {
let mut snapshot = complete_checkpoint_snapshot(7);
let state = snapshot
.get_mut(MaterializedProjection::ToolRuns.as_str())
.expect("tool runs checkpoint");
state.version = 3;
state.checkpoint = 6;
assert!(matches!(
validate_projection_checkpoint_snapshot(&snapshot, 7),
Err(SessionDbError::ProjectionIncompatible {
projection: "tool_runs",
actual: 3,
expected: 2
})
));
}
#[test]
fn stale_checkpoint_snapshot_row_is_rejected() {
let mut snapshot = complete_checkpoint_snapshot(7);
snapshot
.get_mut(MaterializedProjection::ToolRuns.as_str())
.expect("tool runs checkpoint")
.checkpoint = 6;
assert!(matches!(
validate_projection_checkpoint_snapshot(&snapshot, 7),
Err(SessionDbError::ProjectionStale {
projection: "tool_runs",
checkpoint: Some(6),
expected: 7
})
));
}
fn local_marker(session_id: SessionId, sequence: u64, boundary: u64) -> SessionEvent {
event(
session_id,
sequence,
SessionEventKind::ContextCompacted {
summary: format!("local-{sequence}"),
compacted_through_sequence: boundary,
},
)
}
fn provider_marker(session_id: SessionId, sequence: u64, boundary: u64) -> SessionEvent {
event(
session_id,
sequence,
SessionEventKind::ProviderContextCompacted {
snapshot: bcode_session_models::ProviderContextSnapshot {
format_version: 1,
request_fingerprint: None,
request_id: None,
provider_plugin_id: "provider".to_string(),
model_id: "model".to_string(),
compatibility_key: "surface".to_string(),
auth_profile: None,
origin: bcode_session_models::ProviderContextSnapshotOrigin::Explicit,
messages_json: "[]".to_string(),
portable_summary: format!("provider-{sequence}"),
},
compacted_through_sequence: boundary,
},
)
}
fn message(session_id: SessionId, sequence: u64, text: &str) -> SessionEvent {
event(
session_id,
sequence,
SessionEventKind::AssistantMessage {
text: text.to_string(),
},
)
}
fn assert_marker_transition(first: SessionEvent, second: SessionEvent) {
let session_id = first.session_id;
let events = canonical_model_context_from_events(vec![
message(session_id, 1, "old"),
first,
message(session_id, 4, "retained-before-marker"),
second,
message(session_id, 7, "retained-after-marker"),
]);
assert_eq!(
events
.iter()
.map(|event| event.sequence)
.collect::<Vec<_>>(),
vec![6, 4, 7]
);
}
#[test]
fn local_to_local_uses_newest_marker_boundary() {
let id = SessionId::new();
assert_marker_transition(local_marker(id, 3, 1), local_marker(id, 6, 2));
}
#[test]
fn local_to_provider_uses_newest_marker_boundary() {
let id = SessionId::new();
assert_marker_transition(local_marker(id, 3, 1), provider_marker(id, 6, 2));
}
#[test]
fn provider_to_local_uses_newest_marker_boundary() {
let id = SessionId::new();
assert_marker_transition(provider_marker(id, 3, 1), local_marker(id, 6, 2));
}
#[test]
fn provider_to_provider_uses_newest_marker_boundary() {
let id = SessionId::new();
assert_marker_transition(provider_marker(id, 3, 1), provider_marker(id, 6, 2));
}
#[test]
fn retained_event_before_marker_sequence_survives_boundary() {
let id = SessionId::new();
let events = canonical_model_context_from_events(vec![
message(id, 4, "retained"),
local_marker(id, 10, 2),
]);
assert_eq!(
events
.iter()
.map(|event| event.sequence)
.collect::<Vec<_>>(),
vec![10, 4]
);
}
#[test]
fn retained_event_after_marker_insertion_survives_boundary() {
let id = SessionId::new();
let events = canonical_model_context_from_events(vec![
local_marker(id, 3, 2),
message(id, 4, "after"),
]);
assert_eq!(
events
.iter()
.map(|event| event.sequence)
.collect::<Vec<_>>(),
vec![3, 4]
);
}
#[test]
fn older_markers_are_excluded_from_canonical_context() {
let id = SessionId::new();
let events = canonical_model_context_from_events(vec![
local_marker(id, 3, 1),
provider_marker(id, 5, 2),
local_marker(id, 7, 4),
]);
assert_eq!(events.len(), 1);
assert_eq!(events[0].sequence, 7);
}
#[tokio::test]
async fn db_projection_matches_in_memory_boundary_projection() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, temp_dir.path())
.await
.expect("db");
let source = vec![
message(id, 0, "old"),
local_marker(id, 1, 0),
message(id, 2, "retained"),
provider_marker(id, 3, 1),
message(id, 4, "after"),
];
for item in &source {
db.append_event(item).await.expect("append");
}
assert_eq!(
db.model_context_events().await.expect("db context"),
canonical_model_context_from_events(source)
);
}
#[tokio::test]
async fn observed_session_open_records_initialization_and_migrations() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let metrics = MetricsRegistry::in_memory();
let session_id = SessionId::new();
let db =
SessionDb::open_turso_in_root_observed(session_id, temp_dir.path(), metrics.clone())
.await
.expect("observed session database should open");
drop(db);
let report = metrics.report();
assert!(
report
.snapshot
.counters
.get("database.operation.total")
.is_some_and(|count| *count > 1)
);
assert!(
report.descriptors["database.operation.total"]
.label_keys
.contains(&"database_role".to_owned())
);
}
#[test]
fn identifies_database_lock_errors() {
assert!(is_database_lock_error_message(
"Locking error: Failed locking file '/tmp/session.db-wal'"
));
assert!(is_database_lock_error_message("database is locked"));
assert!(is_database_lock_error_message("database busy"));
}
#[test]
fn ignores_non_lock_database_errors() {
assert!(!is_database_lock_error_message("permission denied"));
}
#[tokio::test]
async fn structural_identity_and_sequence_gap_fixtures_fail_closed() {
let identity_root = tempfile::tempdir().expect("identity temp dir");
let identity_session = SessionId::new();
let identity_db = SessionDb::open_turso_in_root(identity_session, identity_root.path())
.await
.expect("open identity DB");
let identity_payload =
include_str!("../fixtures/migrations/mismatched-session-id-v39.json");
identity_db
.database()
.insert("events")
.value("event_seq", seq_to_value(0))
.value("event_type", "assistant_message")
.value("schema_version", DatabaseValue::Int32(38))
.value("created_at_ms", seq_to_value(0))
.value("payload", identity_payload)
.execute(identity_db.database())
.await
.expect("insert identity fixture");
assert!(matches!(
identity_db.all_events().await,
Err(SessionDbError::PersistedEvent(
PersistedSessionEventError::UnsupportedSchemaVersion { actual: 39, .. }
))
));
let gap_root = tempfile::tempdir().expect("gap temp dir");
let fixture_session: SessionId = "00000000-0000-0000-0000-000000000001"
.parse()
.expect("fixture session id");
let gap_db = SessionDb::open_turso_in_root(fixture_session, gap_root.path())
.await
.expect("open gap DB");
for payload in include_str!("../fixtures/migrations/sequence-gap-v39.jsonl").lines() {
let value: serde_json::Value = serde_json::from_str(payload).expect("gap fixture JSON");
let sequence = value["sequence"].as_u64().expect("fixture sequence");
let event_type = value["kind"]
.as_object()
.and_then(|kind| kind.keys().next())
.expect("fixture event kind")
.clone();
gap_db
.database()
.insert("events")
.value("event_seq", seq_to_value(sequence))
.value("event_type", event_type)
.value("schema_version", DatabaseValue::Int32(38))
.value("created_at_ms", seq_to_value(sequence))
.value("payload", payload)
.execute(gap_db.database())
.await
.expect("insert gap fixture");
}
let maintenance =
crate::lease::acquire_session_maintenance_guard(gap_root.path(), fixture_session)
.expect("maintenance guard");
let write = crate::lease::acquire_maintenance_session_write_lock(
&maintenance,
gap_root.path(),
fixture_session,
)
.expect("write guard");
assert!(matches!(
gap_db
.reindex_model_context(&maintenance, &write)
.await
.expect_err("non-current history must fail closed"),
SessionDbError::PersistedEvent(PersistedSessionEventError::UnsupportedSchemaVersion {
actual: 39,
..
})
));
}
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn epoch_three_unknown_history_fails_writable_migration_atomically() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("opaque migration".to_owned()),
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append session created");
insert_raw_history_event(
&db,
session_id,
1,
"future_event_kind",
serde_json::json!({ "future_event_kind": { "value": true } }),
)
.await;
db.database()
.update("session_storage_contract")
.value(
"writer_epoch",
DatabaseValue::Int64(i64::from(PREVIOUS_SESSION_STORAGE_WRITER_EPOCH)),
)
.where_eq(
"contract_id",
DatabaseValue::Int32(SESSION_STORAGE_CONTRACT_ID),
)
.execute(db.database())
.await
.expect("mark epoch three");
db.database()
.delete(SESSION_MIGRATIONS_TABLE)
.where_in(
"id",
vec![
DatabaseValue::String("028_session_compatibility_state".to_owned()),
DatabaseValue::String("029_session_compatibility_issues".to_owned()),
],
)
.execute(db.database())
.await
.expect("remove epoch four migrations");
drop(db);
let maintenance =
crate::lease::acquire_session_maintenance_guard(temp_dir.path(), session_id)
.expect("maintenance guard");
let write = crate::lease::acquire_maintenance_session_write_lock(
&maintenance,
temp_dir.path(),
session_id,
)
.expect("write guard");
let error =
SessionDb::migrate_turso_in_root(session_id, temp_dir.path(), &maintenance, &write)
.await
.expect_err("unknown history must fail writable migration");
assert!(matches!(
error,
SessionDbError::MigrationHistoryIncompatible { .. }
));
drop(write);
drop(maintenance);
let unchanged = SessionDb::open_existing_turso_in_root(session_id, temp_dir.path())
.await
.expect("source store should remain openable after rollback");
assert_eq!(
unchanged
.storage_writer_epoch()
.await
.expect("writer epoch"),
u64::from(PREVIOUS_SESSION_STORAGE_WRITER_EPOCH)
);
assert_eq!(
unchanged.last_event_sequence().await.expect("tail"),
Some(1)
);
drop(unchanged);
let retry_db = SessionDb::open_existing_turso_in_root(session_id, temp_dir.path())
.await
.expect("source store should reopen for retry");
retry_db
.database()
.delete("events")
.where_eq("event_seq", seq_to_value(1))
.execute(retry_db.database())
.await
.expect("remove unsupported fixture event before retry");
drop(retry_db);
let retry_maintenance =
crate::lease::acquire_session_maintenance_guard(temp_dir.path(), session_id)
.expect("retry maintenance guard");
let retry_write = crate::lease::acquire_maintenance_session_write_lock(
&retry_maintenance,
temp_dir.path(),
session_id,
)
.expect("retry write guard");
let retried = SessionDb::migrate_turso_in_root(
session_id,
temp_dir.path(),
&retry_maintenance,
&retry_write,
)
.await
.expect("retry after pre-commit rollback should succeed");
assert_eq!(
retried.storage_writer_epoch().await.expect("writer epoch"),
u64::from(CURRENT_SESSION_STORAGE_WRITER_EPOCH)
);
retried
.validate_write_readiness()
.await
.expect("retried migration should be writable");
}
#[tokio::test]
async fn normal_history_reads_reject_future_events_without_mutation() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("test".to_string()),
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append valid event");
insert_raw_history_event(
&db,
session_id,
1,
"future_event_kind",
serde_json::json!({ "future_event_kind": { "value": true } }),
)
.await;
insert_raw_history_event(
&db,
session_id,
2,
"tool_invocation_result_recorded",
serde_json::json!({ "tool_invocation_result_recorded": { "record": { "model_output": "missing invocation id" } } }),
)
.await;
insert_event(
db.database(),
&event(
session_id,
3,
SessionEventKind::AssistantMessage {
text: "still readable".to_string(),
},
),
None,
)
.await
.expect("insert second valid event");
let history_error = db
.all_events()
.await
.expect_err("structurally corrupt history must be reported");
assert!(matches!(history_error, SessionDbError::PersistedEvent(_)));
let page_error = db
.history_page(SessionHistoryQuery {
cursor: None,
direction: SessionHistoryDirection::Forward,
limit: 8,
})
.await
.expect_err("structurally corrupt page must be reported");
assert!(matches!(page_error, SessionDbError::PersistedEvent(_)));
db.database()
.delete("events")
.where_eq("event_seq", seq_to_value(2))
.execute(db.database())
.await
.expect("remove deliberately corrupt test row");
assert!(matches!(
db.all_events().await,
Err(SessionDbError::PersistedEvent(
PersistedSessionEventError::UnsupportedEventKind { .. }
))
));
assert!(matches!(
db.history_page(SessionHistoryQuery {
cursor: None,
direction: SessionHistoryDirection::Forward,
limit: 8,
})
.await,
Err(SessionDbError::PersistedEvent(
PersistedSessionEventError::UnsupportedEventKind { .. }
))
));
let raw_rows = db
.database()
.select("events")
.columns(&["event_seq"])
.sort("event_seq", SortDirection::Asc)
.execute(db.database())
.await
.expect("raw rows should remain");
assert_eq!(raw_rows.len(), 3);
}
#[tokio::test]
async fn append_event_batch_is_atomic_and_updates_projections() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let events = vec![
(
event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("batch".to_owned()),
working_directory: temp_dir.path().to_path_buf(),
},
),
Some(10),
),
(
event(
session_id,
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "hello".to_owned(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
Some(11),
),
(
event(
session_id,
2,
SessionEventKind::AssistantMessage {
text: "hi".to_owned(),
},
),
Some(12),
),
];
db.append_event_batch(&events).await.expect("batch append");
assert_eq!(db.all_events().await.expect("history").len(), 3);
assert_eq!(db.input_history().await.expect("input history").len(), 1);
assert_eq!(
db.materialized_projection_checkpoint(MaterializedProjection::Transcript)
.await
.expect("checkpoint"),
Some(2)
);
let invalid = vec![
(
event(
session_id,
3,
SessionEventKind::AssistantMessage {
text: "rolled back".to_owned(),
},
),
None,
),
(
event(
session_id,
5,
SessionEventKind::AssistantMessage {
text: "gap".to_owned(),
},
),
None,
),
];
assert!(db.append_event_batch(&invalid).await.is_err());
assert_eq!(
db.all_events().await.expect("history after rollback").len(),
3
);
}
#[tokio::test]
async fn append_event_updates_input_history_and_transcript_projections() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("test".to_string()),
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append session created");
db.append_event(&event(
session_id,
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "hello".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
))
.await
.expect("append user message");
db.append_event(&event(
session_id,
2,
SessionEventKind::AssistantMessage {
text: "hi there".to_string(),
},
))
.await
.expect("append assistant message");
let input_history = db.input_history().await.expect("input history");
assert_eq!(
input_history,
vec![SessionInputHistoryEntry {
sequence: 1,
timestamp_ms: 1,
text: "hello".to_string(),
}]
);
let transcript = db
.latest_transcript_items(10)
.await
.expect("transcript items");
assert_eq!(transcript.len(), 2);
assert_eq!(transcript[0].role, "user");
assert_eq!(transcript[0].content.as_deref(), Some("hello"));
assert_eq!(transcript[1].role, "assistant");
assert_eq!(transcript[1].content.as_deref(), Some("hi there"));
assert_eq!(
db.materialized_projection_checkpoint(MaterializedProjection::InputHistory)
.await
.expect("input checkpoint"),
Some(2)
);
assert_eq!(
db.materialized_projection_checkpoint(MaterializedProjection::Transcript)
.await
.expect("transcript checkpoint"),
Some(2)
);
for projection in MaterializedProjection::all() {
assert_eq!(
db.materialized_projection_checkpoint(*projection)
.await
.expect("materialized projection checkpoint"),
Some(2),
"projection {} should be checkpointed",
projection.as_str()
);
}
assert_eq!(
db.projection_checkpoint("model_context")
.await
.expect("model context should not be checkpointed"),
None
);
}
#[tokio::test]
async fn active_tool_runs_reflect_running_projection_rows() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::ToolCallRequested {
tool_call_id: "tool-1".to_string(),
producer_plugin_id: None,
tool_name: "shell".to_string(),
arguments_json: "{}".to_string(),
working_directory: None,
},
))
.await
.expect("append tool request");
let active = db.active_tool_runs().await.expect("active tool runs");
assert_eq!(active.len(), 1);
assert_eq!(active[0].tool_call_id, "tool-1");
assert_eq!(active[0].tool_name.as_deref(), Some("shell"));
assert_eq!(active[0].status, "running");
db.append_event(&event(
session_id,
1,
SessionEventKind::ToolInvocationResultRecorded {
record: bcode_session_models::ToolInvocationResultRecord {
invocation_id: "tool-1".to_string(),
model_output: "done".to_string(),
is_error: false,
presentation: None,
result: None,
content: Vec::new(),
},
},
))
.await
.expect("append tool finish");
assert!(
db.active_tool_runs()
.await
.expect("active after finish")
.is_empty()
);
let completed = db
.tool_runs_by_status("complete")
.await
.expect("complete tool runs");
assert_eq!(completed.len(), 1);
assert_eq!(completed[0].event_seq_end, Some(1));
assert_eq!(completed[0].is_error, Some(false));
}
#[tokio::test]
async fn generic_result_record_finishes_bounded_tool_run_projection() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::ToolCallRequested {
tool_call_id: "tool-1".to_owned(),
producer_plugin_id: Some("example.plugin".to_owned()),
tool_name: "example.tool".to_owned(),
arguments_json: "{}".to_owned(),
working_directory: None,
},
))
.await
.expect("append generic tool request");
db.append_event(&event(
session_id,
1,
SessionEventKind::ToolInvocationResultRecorded {
record: bcode_session_models::ToolInvocationResultRecord {
invocation_id: "tool-1".to_owned(),
model_output: "done".to_owned(),
is_error: false,
presentation: None,
result: None,
content: Vec::new(),
},
},
))
.await
.expect("append generic tool result");
assert!(
db.active_tool_runs()
.await
.expect("active tool runs")
.is_empty()
);
let completed = db
.tool_runs_by_status("complete")
.await
.expect("completed tool runs");
assert_eq!(completed.len(), 1);
assert_eq!(completed[0].event_seq_end, Some(1));
let transcript = db
.latest_transcript_items(10)
.await
.expect("bounded transcript");
assert_eq!(transcript.len(), 1);
assert_eq!(transcript[0].event_seq_end, 1);
assert_eq!(transcript[0].status, "complete");
}
#[tokio::test]
async fn generic_exchange_records_enter_bounded_transcript_index_opaquely() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let request = bcode_session_models::ToolExchangeRequest {
invocation_id: "call-1".to_owned(),
exchange_id: "exchange-1".to_owned(),
producer_id: "example.plugin".to_owned(),
schema: "example.request".to_owned(),
schema_version: 7,
payload: serde_json::json!({"opaque": [1, 2]}),
response_policy: bcode_session_models::ToolExchangeResponsePolicy::Required,
};
db.append_event(&event(
session_id,
0,
SessionEventKind::ToolExchangeRequested {
request: request.clone(),
},
))
.await
.expect("append exchange request");
db.append_event(&event(
session_id,
1,
SessionEventKind::ToolExchangeResolved {
event: bcode_session_models::ToolExchangeResolutionEvent {
invocation_id: request.invocation_id,
exchange_id: request.exchange_id,
resolution: bcode_session_models::ToolExchangeResolution::Responded {
payload: serde_json::json!({"opaque_response": true}),
},
},
},
))
.await
.expect("append exchange resolution");
let transcript = db
.latest_transcript_items(10)
.await
.expect("bounded transcript");
assert_eq!(transcript.len(), 2);
assert_eq!(transcript[0].kind, "tool_exchange_requested");
assert_eq!(transcript[0].status, "running");
assert!(
transcript[0]
.content
.as_deref()
.is_some_and(|content| content.contains("\"opaque\":[1,2]"))
);
assert_eq!(transcript[1].kind, "tool_exchange_resolved");
assert_eq!(transcript[1].status, "complete");
}
#[tokio::test]
async fn presentation_note_is_in_bounded_transcript_projection() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::PluginStatusNote {
plugin_id: "bcode.host".to_owned(),
note_id: "notice-1".to_owned(),
text: "durable notice".to_owned(),
metadata: BTreeMap::from([(
"presentation_only".to_owned(),
serde_json::Value::Bool(true),
)]),
},
))
.await
.expect("append presentation note");
let transcript = db
.latest_transcript_items(10)
.await
.expect("bounded transcript");
assert_eq!(transcript.len(), 1);
assert_eq!(transcript[0].role, "system");
assert_eq!(transcript[0].kind, "plugin_status_note");
assert_eq!(transcript[0].content.as_deref(), Some("durable notice"));
}
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn generic_records_reopen_to_identical_canonical_and_bounded_projections() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let events = vec![
event(
session_id,
0,
SessionEventKind::ToolCallRequested {
tool_call_id: "call-1".to_owned(),
producer_plugin_id: Some("future.plugin".to_owned()),
tool_name: "future.tool".to_owned(),
arguments_json: "{}".to_owned(),
working_directory: None,
},
),
event(
session_id,
1,
SessionEventKind::ToolInvocationLifecycle {
event: bcode_session_models::ToolInvocationLifecycleEvent {
invocation_id: "call-1".to_owned(),
sequence: 1,
stage: bcode_session_models::ToolInvocationLifecycleStage::Waiting,
message: Some("waiting".to_owned()),
metadata: serde_json::json!({"opaque": true}),
},
},
),
event(
session_id,
2,
SessionEventKind::ToolContribution {
event: bcode_session_models::ToolContributionEvent {
invocation_id: "call-1".to_owned(),
contribution_id: "surface".to_owned(),
sequence: 1,
producer_id: "future.plugin".to_owned(),
schema: "future.schema".to_owned(),
schema_version: 9,
operation: bcode_session_models::ToolContributionOperation::Upsert,
persistence: bcode_session_models::ToolContributionPersistence::Durable,
artifact: None,
payload: serde_json::json!({"opaque": [1, 2]}),
},
},
),
event(
session_id,
3,
SessionEventKind::ToolExchangeRequested {
request: bcode_session_models::ToolExchangeRequest {
invocation_id: "call-1".to_owned(),
exchange_id: "exchange-1".to_owned(),
producer_id: "future.plugin".to_owned(),
schema: "future.exchange".to_owned(),
schema_version: 3,
payload: serde_json::json!({"question": true}),
response_policy: bcode_session_models::ToolExchangeResponsePolicy::Required,
},
},
),
event(
session_id,
4,
SessionEventKind::ToolExchangeResolved {
event: bcode_session_models::ToolExchangeResolutionEvent {
invocation_id: "call-1".to_owned(),
exchange_id: "exchange-1".to_owned(),
resolution: bcode_session_models::ToolExchangeResolution::Responded {
payload: serde_json::json!({"answer": 42}),
},
},
},
),
event(
session_id,
5,
SessionEventKind::ToolInvocationResultRecorded {
record: bcode_session_models::ToolInvocationResultRecord {
invocation_id: "call-1".to_owned(),
model_output: "done".to_owned(),
is_error: false,
presentation: None,
result: None,
content: Vec::new(),
},
},
),
];
for event in &events {
db.append_event(event).await.expect("append generic event");
}
let before_events = db.all_events_strict().await.expect("canonical events");
let before_transcript = db
.latest_transcript_items(32)
.await
.expect("bounded transcript");
let before_tools = db.tool_runs_by_status("complete").await.expect("tool runs");
let before_context = db.model_context_events().await.expect("model context");
drop(db);
let reopened = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("reopen session db");
assert_eq!(
reopened.all_events_strict().await.expect("reopened events"),
before_events
);
assert_eq!(
reopened
.latest_transcript_items(32)
.await
.expect("reopened transcript"),
before_transcript
);
assert_eq!(
reopened
.tool_runs_by_status("complete")
.await
.expect("reopened tools"),
before_tools
);
assert_eq!(
reopened
.model_context_events()
.await
.expect("reopened context"),
before_context
);
}
#[tokio::test]
#[allow(clippy::too_many_lines)] async fn epoch_four_migration_repairs_terminal_tool_projection() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let tool_call_id = "call-legacy-terminal".to_owned();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::ToolCallRequested {
tool_call_id: tool_call_id.clone(),
producer_plugin_id: Some("bcode.test".to_owned()),
tool_name: "test.tool".to_owned(),
arguments_json: "{}".to_owned(),
working_directory: None,
},
))
.await
.expect("append tool request");
db.append_event(&event(
session_id,
1,
SessionEventKind::ToolInvocationLifecycle {
event: bcode_session_models::ToolInvocationLifecycleEvent {
invocation_id: tool_call_id.clone(),
sequence: u64::MAX,
stage: bcode_session_models::ToolInvocationLifecycleStage::Completed,
message: None,
metadata: serde_json::Value::Null,
},
},
))
.await
.expect("append terminal lifecycle");
db.database()
.update("tool_runs")
.value("event_seq_end", DatabaseValue::Null)
.value("status", "running")
.value("completed_at_ms", DatabaseValue::Null)
.value("is_error", DatabaseValue::Null)
.where_eq("tool_call_id", tool_call_id.clone())
.execute(db.database())
.await
.expect("restore legacy stale tool projection");
db.database()
.update("transcript_items")
.value("event_seq_end", DatabaseValue::Int64(0))
.value("kind", "invocation")
.value("status", "running")
.where_eq("transcript_seq", DatabaseValue::Int64(0))
.execute(db.database())
.await
.expect("restore legacy stale transcript projection");
db.database()
.update("projection_checkpoints")
.value("projection_version", DatabaseValue::Int64(1))
.where_in(
"projection_name",
vec![
DatabaseValue::String("transcript".to_owned()),
DatabaseValue::String("tool_runs".to_owned()),
],
)
.execute(db.database())
.await
.expect("restore legacy projection versions");
db.database()
.update("session_storage_contract")
.value(
"writer_epoch",
DatabaseValue::Int64(i64::from(PREVIOUS_SESSION_STORAGE_WRITER_EPOCH)),
)
.where_eq(
"contract_id",
DatabaseValue::Int32(SESSION_STORAGE_CONTRACT_ID),
)
.execute(db.database())
.await
.expect("restore epoch four contract");
db.database()
.delete(SESSION_MIGRATIONS_TABLE)
.where_eq("id", "032_terminal_tool_lifecycle_projection")
.execute(db.database())
.await
.expect("remove epoch five migration");
drop(db);
let maintenance =
crate::lease::acquire_session_maintenance_guard(temp_dir.path(), session_id)
.expect("maintenance guard");
let write = crate::lease::acquire_maintenance_session_write_lock(
&maintenance,
temp_dir.path(),
session_id,
)
.expect("write guard");
let migrated =
SessionDb::migrate_turso_in_root(session_id, temp_dir.path(), &maintenance, &write)
.await
.expect("migrate epoch four session");
assert_eq!(
migrated.storage_writer_epoch().await.expect("writer epoch"),
u64::from(CURRENT_SESSION_STORAGE_WRITER_EPOCH)
);
assert!(
migrated
.tool_runs_by_status("running")
.await
.expect("running tools")
.is_empty()
);
let complete = migrated
.tool_runs_by_status("complete")
.await
.expect("complete tools");
assert_eq!(complete.len(), 1);
assert_eq!(complete[0].tool_call_id, tool_call_id);
assert_eq!(complete[0].event_seq_end, Some(1));
assert_eq!(complete[0].started_at_ms, Some(1));
assert_eq!(complete[0].completed_at_ms, Some(1));
}
#[tokio::test]
async fn terminal_tool_lifecycle_closes_running_tool_projection() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let tool_call_id = "call-terminal".to_owned();
db.append_event(&event(
session_id,
0,
SessionEventKind::ToolCallRequested {
tool_call_id: tool_call_id.clone(),
producer_plugin_id: Some("bcode.test".to_owned()),
tool_name: "test.tool".to_owned(),
arguments_json: "{}".to_owned(),
working_directory: None,
},
))
.await
.expect("append tool request");
db.append_event(&event(
session_id,
1,
SessionEventKind::ToolInvocationLifecycle {
event: bcode_session_models::ToolInvocationLifecycleEvent {
invocation_id: tool_call_id.clone(),
sequence: u64::MAX,
stage: bcode_session_models::ToolInvocationLifecycleStage::Completed,
message: None,
metadata: serde_json::Value::Null,
},
},
))
.await
.expect("append terminal lifecycle");
assert!(
db.tool_runs_by_status("running")
.await
.expect("running")
.is_empty()
);
let complete = db
.tool_runs_by_status("complete")
.await
.expect("complete tools");
assert_eq!(complete.len(), 1);
assert_eq!(complete[0].tool_call_id, tool_call_id);
assert_eq!(complete[0].event_seq_end, Some(1));
assert_eq!(complete[0].started_at_ms, Some(1));
assert_eq!(complete[0].completed_at_ms, Some(1));
assert_eq!(complete[0].is_error, Some(false));
let timing = db
.database()
.select("tool_runs")
.columns(&["started_at_ms", "completed_at_ms"])
.where_eq("tool_call_id", tool_call_id.clone())
.execute_first(db.database())
.await
.expect("tool timing query")
.expect("tool timing row");
assert_eq!(required_i64(&timing, "started_at_ms").expect("started"), 1);
assert_eq!(
required_i64(&timing, "completed_at_ms").expect("completed"),
1
);
let transcript = db.latest_transcript_items(16).await.expect("transcript");
let invocation = transcript
.iter()
.find(|item| item.role == "tool" && item.kind == "result")
.expect("tool result transcript item");
assert_eq!(invocation.status, "complete");
assert_eq!(invocation.event_seq_end, 1);
}
#[tokio::test]
async fn resumed_runtime_work_clears_suspended_completion_fields() {
let root = tempfile::tempdir().expect("session root");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, root.path())
.await
.expect("session database");
let start = SessionEventKind::RuntimeWorkStarted {
work_id: WorkId::new("resumable"),
kind: RuntimeWorkKind::Workflow,
label: "resumable work".to_owned(),
tool_call_id: None,
plugin_id: None,
service_interface: None,
operation: None,
parent_work_id: None,
started_at_ms: Some(10),
cancellable: true,
};
db.append_event(&event(session_id, 0, start.clone()))
.await
.expect("start");
db.append_event(&event(
session_id,
1,
SessionEventKind::RuntimeWorkFinished {
work_id: WorkId::new("resumable"),
status: RuntimeWorkStatus::Suspended,
finished_at_ms: Some(20),
message: Some("waiting for resume".to_owned()),
},
))
.await
.expect("suspend");
assert!(db.active_runtime_work().await.expect("inactive").is_empty());
db.append_event(&event(session_id, 2, start))
.await
.expect("resume");
let active = db.active_runtime_work().await.expect("active resumed work");
assert_eq!(active.len(), 1);
assert_eq!(active[0].status, RuntimeWorkStatus::Running);
assert_eq!(active[0].event_seq_start, 2);
assert_eq!(active[0].event_seq_end, None);
assert_eq!(active[0].finished_at_ms, None);
assert_eq!(active[0].message, None);
assert_eq!(db.runtime_work_history(1).await.expect("history"), active);
}
#[tokio::test]
async fn runtime_work_lifecycles_remain_isolated_in_projection() {
for status in [
RuntimeWorkStatus::Completed,
RuntimeWorkStatus::Failed,
RuntimeWorkStatus::TimedOut,
RuntimeWorkStatus::Cancelled,
] {
assert_runtime_terminal_isolation(status).await;
}
}
async fn assert_runtime_terminal_isolation(status: RuntimeWorkStatus) {
let root = tempfile::tempdir().expect("session root");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, root.path())
.await
.expect("session database");
for (sequence, work_id) in [(0, "first"), (2, "second")] {
db.append_event(&event(
session_id,
sequence,
SessionEventKind::RuntimeWorkStarted {
work_id: WorkId::new(work_id),
kind: RuntimeWorkKind::Tool,
label: work_id.to_owned(),
tool_call_id: None,
plugin_id: None,
service_interface: None,
operation: None,
parent_work_id: None,
started_at_ms: Some(sequence),
cancellable: true,
},
))
.await
.expect("start work");
if sequence == 0 {
db.append_event(&event(
session_id,
1,
SessionEventKind::RuntimeWorkFinished {
work_id: WorkId::new("first"),
status,
finished_at_ms: Some(1),
message: Some("first completed".to_owned()),
},
))
.await
.expect("complete first work");
}
}
let history = db.runtime_work_history(10).await.expect("history");
assert_eq!(history.len(), 2);
assert_eq!(history[0].work_id, WorkId::new("first"));
assert_eq!(history[0].status, status);
assert_eq!(history[0].event_seq_end, Some(1));
assert_eq!(history[0].message.as_deref(), Some("first completed"));
assert_eq!(history[1].work_id, WorkId::new("second"));
assert_eq!(history[1].status, RuntimeWorkStatus::Running);
assert_eq!(history[1].event_seq_end, None);
assert_eq!(history[1].message, None);
db.append_event(&event(
session_id,
3,
SessionEventKind::RuntimeWorkCancelRequested {
work_id: WorkId::new("second"),
requested_at_ms: Some(3),
client_id: None,
},
))
.await
.expect("cancel second work");
let after = db.runtime_work_history(10).await.expect("updated history");
assert_eq!(after[0], history[0]);
assert_eq!(after[1].status, RuntimeWorkStatus::Cancelling);
let active = db.active_runtime_work().await.expect("active work");
assert_eq!(active.len(), 1);
assert_eq!(active[0], after[1]);
assert_late_runtime_updates(&db, session_id, &after, &active).await;
}
async fn assert_late_runtime_updates(
db: &SessionDb,
session_id: SessionId,
after: &[RuntimeWorkProjection],
active: &[RuntimeWorkProjection],
) {
db.append_event(&event(
session_id,
4,
SessionEventKind::RuntimeWorkCancelRequested {
work_id: WorkId::new("first"),
requested_at_ms: Some(4),
client_id: None,
},
))
.await
.expect("persist stale cancellation");
db.append_event(&event(
session_id,
5,
SessionEventKind::RuntimeWorkFinished {
work_id: WorkId::new("first"),
status: RuntimeWorkStatus::Failed,
finished_at_ms: Some(5),
message: Some("stale conflicting finish".to_owned()),
},
))
.await
.expect("persist conflicting finish");
db.append_event(&event(
session_id,
6,
SessionEventKind::RuntimeWorkProgress {
work_id: WorkId::new("first"),
message: "stale progress".to_owned(),
progress_at_ms: Some(6),
completed_units: None,
total_units: None,
},
))
.await
.expect("persist stale progress");
assert_eq!(
db.runtime_work_history(10).await.expect("stable history"),
after
);
assert_eq!(
db.active_runtime_work().await.expect("stable active work"),
active
);
db.append_event(&event(
session_id,
7,
SessionEventKind::RuntimeWorkProgress {
work_id: WorkId::new("second"),
message: "cleaning up".to_owned(),
progress_at_ms: Some(7),
completed_units: None,
total_units: None,
},
))
.await
.expect("active progress");
let updated = db.runtime_work_history(10).await.expect("progress history");
assert_eq!(updated[0], after[0]);
assert_eq!(updated[1].message.as_deref(), Some("cleaning up"));
assert_terminal_restart_ignored(db, session_id, &updated).await;
}
async fn assert_terminal_restart_ignored(
db: &SessionDb,
session_id: SessionId,
expected: &[RuntimeWorkProjection],
) {
db.append_event(&event(
session_id,
8,
SessionEventKind::RuntimeWorkStarted {
work_id: WorkId::new("first"),
kind: RuntimeWorkKind::Tool,
label: "stale restart".to_owned(),
tool_call_id: None,
plugin_id: None,
service_interface: None,
operation: None,
parent_work_id: None,
started_at_ms: Some(8),
cancellable: true,
},
))
.await
.expect("persist stale restart");
assert_eq!(
db.runtime_work_history(10).await.expect("terminal history"),
expected
);
}
#[tokio::test]
async fn runtime_work_history_rejects_unknown_projection_values_without_repair() {
let root = tempfile::tempdir().expect("session root");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, root.path())
.await
.expect("database");
db.append_event(&event(
session_id,
0,
SessionEventKind::RuntimeWorkStarted {
work_id: WorkId::new("unknown-projection"),
kind: RuntimeWorkKind::Tool,
label: "work".to_owned(),
tool_call_id: None,
plugin_id: None,
service_interface: None,
operation: None,
parent_work_id: None,
started_at_ms: Some(0),
cancellable: true,
},
))
.await
.expect("start");
for (column, valid) in [("kind", "tool"), ("status", "running")] {
db.database()
.update("runtime_work")
.value(column, "future_variant")
.where_eq("work_id", "unknown-projection")
.execute(db.database())
.await
.expect("damage test projection");
assert!(matches!(db.runtime_work_history(1).await,
Err(SessionDbError::InvalidRow { column: invalid }) if invalid == column));
let restart = event(
session_id,
1,
SessionEventKind::RuntimeWorkStarted {
work_id: WorkId::new("unknown-projection"),
kind: RuntimeWorkKind::Tool,
label: "restart".to_owned(),
tool_call_id: None,
plugin_id: None,
service_interface: None,
operation: None,
parent_work_id: None,
started_at_ms: Some(1),
cancellable: true,
},
);
assert!(matches!(db.append_event(&restart).await,
Err(SessionDbError::InvalidRow { column: invalid }) if invalid == column));
assert_eq!(
db.last_event_sequence()
.await
.expect("canonical tail after rejection"),
Some(0)
);
let row = db
.database()
.select("runtime_work")
.columns(&[column])
.where_eq("work_id", "unknown-projection")
.execute_first(db.database())
.await
.expect("inspect projection")
.expect("row");
assert_eq!(
required_string(&row, column).expect("unchanged value"),
"future_variant"
);
db.database()
.update("runtime_work")
.value(column, valid)
.where_eq("work_id", "unknown-projection")
.execute(db.database())
.await
.expect("restore test fixture");
}
assert_runtime_timestamp_damage_rejected(&db).await;
assert_eq!(
db.runtime_work_history(1)
.await
.expect("valid history")
.len(),
1
);
}
async fn assert_runtime_timestamp_damage_rejected(db: &SessionDb) {
for column in ["started_at_ms", "finished_at_ms"] {
db.database()
.update("runtime_work")
.value(column, -1_i64)
.where_eq("work_id", "unknown-projection")
.execute(db.database())
.await
.expect("damage timestamp fixture");
assert!(matches!(db.runtime_work_history(1).await,
Err(SessionDbError::InvalidRow { column: invalid }) if invalid == column));
assert!(matches!(db.active_runtime_work().await,
Err(SessionDbError::InvalidRow { column: invalid }) if invalid == column));
let row = db
.database()
.select("runtime_work")
.columns(&[column])
.where_eq("work_id", "unknown-projection")
.execute_first(db.database())
.await
.expect("inspect timestamp")
.expect("row");
assert_eq!(required_i64(&row, column).expect("unchanged timestamp"), -1);
db.database()
.update("runtime_work")
.value(column, DatabaseValue::Null)
.where_eq("work_id", "unknown-projection")
.execute(db.database())
.await
.expect("restore timestamp fixture");
}
}
#[tokio::test]
async fn active_runtime_work_uses_projection_rows() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let work_id = WorkId::new("work-1");
db.append_event(&event(
session_id,
0,
SessionEventKind::RuntimeWorkStarted {
work_id: work_id.clone(),
kind: RuntimeWorkKind::Tool,
label: "shell".to_string(),
tool_call_id: None,
plugin_id: None,
service_interface: None,
operation: None,
parent_work_id: None,
started_at_ms: Some(123),
cancellable: true,
},
))
.await
.expect("append runtime work start");
let active = db.active_runtime_work().await.expect("active runtime work");
assert_eq!(active.len(), 1);
assert_eq!(active[0].work_id, work_id);
assert_eq!(active[0].label, "shell");
assert_eq!(active[0].status, RuntimeWorkStatus::Running);
assert!(active[0].cancellable);
db.append_event(&event(
session_id,
1,
SessionEventKind::RuntimeWorkFinished {
work_id: work_id.clone(),
status: RuntimeWorkStatus::Failed,
finished_at_ms: Some(456),
message: Some("daemon stopped".to_string()),
},
))
.await
.expect("append runtime work finish");
assert!(
db.active_runtime_work()
.await
.expect("active after finish")
.is_empty()
);
}
#[tokio::test]
async fn provider_snapshot_opaque_json_survives_exact_db_persistence_and_replay() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open db");
let opaque = r#"[{"type":"provider_state","nested":{"escaped":"a\\nb","order":[3,2,1]},"unicode":"λ"}]"#;
db.append_event(&event(
session_id,
0,
SessionEventKind::ProviderContextCompacted {
snapshot: bcode_session_models::ProviderContextSnapshot {
format_version: 7,
request_fingerprint: None,
request_id: None,
provider_plugin_id: "provider".to_string(),
model_id: "model".to_string(),
compatibility_key: "surface".to_string(),
auth_profile: Some("profile".to_string()),
origin: bcode_session_models::ProviderContextSnapshotOrigin::Explicit,
messages_json: opaque.to_string(),
portable_summary: "portable".to_string(),
},
compacted_through_sequence: 0,
},
))
.await
.expect("append snapshot");
drop(db);
let reopened = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("reopen db");
let context = reopened.model_context_events().await.expect("context");
let SessionEventKind::ProviderContextCompacted { snapshot, .. } = &context[0].kind else {
panic!("expected provider snapshot");
};
assert_eq!(snapshot.messages_json.as_bytes(), opaque.as_bytes());
assert_eq!(snapshot.format_version, 7);
assert_eq!(snapshot.compatibility_key, "surface");
assert_eq!(snapshot.auth_profile.as_deref(), Some("profile"));
}
#[tokio::test]
async fn normal_model_context_read_is_non_mutating_and_does_not_replay_replaced_prefix() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open db");
db.append_event(&message(session_id, 0, "old"))
.await
.expect("old");
db.append_event(&local_marker(session_id, 1, 0))
.await
.expect("marker");
db.append_event(&message(session_id, 2, "tail"))
.await
.expect("tail");
db.database()
.update("events")
.value("payload", "not valid json")
.where_eq("event_seq", seq_to_value(0))
.execute(db.database())
.await
.expect("corrupt replaced prefix");
let rows_before = db
.database()
.select("events")
.columns(&["event_seq", "payload"])
.sort("event_seq", SortDirection::Asc)
.execute(db.database())
.await
.expect("before");
let context = db
.model_context_events()
.await
.expect("bounded context read");
assert_eq!(
context
.iter()
.map(|event| event.sequence)
.collect::<Vec<_>>(),
vec![1, 2]
);
let rows_after = db
.database()
.select("events")
.columns(&["event_seq", "payload"])
.sort("event_seq", SortDirection::Asc)
.execute(db.database())
.await
.expect("after");
assert_eq!(rows_before, rows_after);
assert_eq!(db.last_event_sequence().await.expect("last"), Some(2));
}
#[test]
fn context_history_roles_separate_model_visible_structural_and_excluded_events() {
assert!(is_model_context_event_type("model_turn_started"));
assert!(is_model_context_event_type("model_turn_finished"));
assert!(!is_model_context_event_type("request_context_observed"));
}
#[tokio::test]
#[ignore = "requires BCODE_MODEL_CONTEXT_BENCHMARK_DB and BCODE_MODEL_CONTEXT_BENCHMARK_SESSION_ID"]
async fn benchmark_model_context_events_from_database_copy() {
let path = std::env::var_os("BCODE_MODEL_CONTEXT_BENCHMARK_DB")
.map(std::path::PathBuf::from)
.expect("BCODE_MODEL_CONTEXT_BENCHMARK_DB must name a disposable database copy");
let session_id = std::env::var("BCODE_MODEL_CONTEXT_BENCHMARK_SESSION_ID")
.expect("BCODE_MODEL_CONTEXT_BENCHMARK_SESSION_ID must be set")
.parse::<SessionId>()
.expect("benchmark session ID must be valid");
let db = SessionDb::open_turso(session_id, &path)
.await
.expect("open benchmark database copy");
if matches!(
db.model_context_projection_status()
.await
.expect("projection status"),
ModelContextProjectionStatus::Missing
) {
let root = path
.parent()
.and_then(Path::parent)
.expect("benchmark DB must use sessions/<id>/session.db layout");
reindex_model_context_for_test(&db, root, session_id).await;
}
let _ = db
.model_context_events()
.await
.expect("warm projected context");
let mut projected_samples_ms = Vec::with_capacity(20);
let mut projected = Vec::new();
for _ in 0..20 {
let projected_started_at = std::time::Instant::now();
projected = db.model_context_events().await.expect("projected context");
projected_samples_ms.push(projected_started_at.elapsed().as_millis());
}
projected_samples_ms.sort_unstable();
let projected_p95_ms = projected_samples_ms[18];
eprintln!(
"model_context_events: events={}, projected_samples_ms={:?}, projected_p95_ms={}",
projected.len(),
projected_samples_ms,
projected_p95_ms
);
}
#[tokio::test]
#[ignore = "release-mode append-path benchmark; run explicitly with --ignored --nocapture"]
async fn benchmark_canonical_append_path() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let metrics = MetricsRegistry::in_memory();
let (session_id, db) = if let (Some(path), Ok(session_id)) = (
std::env::var_os("BCODE_APPEND_BENCHMARK_DB").map(std::path::PathBuf::from),
std::env::var("BCODE_APPEND_BENCHMARK_SESSION_ID"),
) {
let session_id = session_id
.parse::<SessionId>()
.expect("benchmark session ID must be valid");
let db = SessionDb::open_existing_turso_observed(session_id, &path, metrics.clone())
.await
.expect("open benchmark database copy");
(session_id, db)
} else {
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root_observed(
session_id,
temp_dir.path(),
metrics.clone(),
)
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: std::path::PathBuf::from("."),
},
))
.await
.expect("append created event");
(session_id, db)
};
let first_sequence = db
.last_event_sequence()
.await
.expect("tail")
.map_or(1, |sequence| sequence + 1);
let iterations = std::env::var("BCODE_APPEND_BENCHMARK_ITERATIONS")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.unwrap_or(200);
let operations_before = metrics
.snapshot()
.counters
.get("database.operation.total")
.copied()
.unwrap_or_default();
let mut samples_us = Vec::with_capacity(usize::try_from(iterations).unwrap_or(200));
for sequence in first_sequence..first_sequence + iterations {
let kind = if sequence % 4 == 0 {
SessionEventKind::AssistantMessage {
text: "x".repeat(2_048),
}
} else {
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "benchmark prompt text".to_owned(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
}
};
let started_at = std::time::Instant::now();
db.append_event(&event(session_id, sequence, kind))
.await
.expect("append benchmark event");
samples_us.push(started_at.elapsed().as_micros());
}
samples_us.sort_unstable();
let percentile = |p: usize| samples_us[(samples_us.len() - 1) * p / 100];
let operations_after = metrics
.snapshot()
.counters
.get("database.operation.total")
.copied()
.unwrap_or_default();
let mut begin_commit_us = Vec::with_capacity(100);
for _ in 0..100 {
let started_at = std::time::Instant::now();
let tx = db.database().begin_transaction().await.expect("begin");
tx.commit().await.expect("commit");
begin_commit_us.push(started_at.elapsed().as_micros());
}
begin_commit_us.sort_unstable();
eprintln!(
"canonical_append: n={} p50_us={} p95_us={} p99_us={} max_us={} db_ops_per_append={} empty_tx_p50_us={}",
samples_us.len(),
percentile(50),
percentile(95),
percentile(99),
samples_us[samples_us.len() - 1],
operations_after.saturating_sub(operations_before) / iterations,
begin_commit_us[50]
);
}
#[tokio::test]
async fn reopening_legacy_database_keeps_projection_missing_until_explicit_reindex() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
{
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
insert_event(
db.database(),
&event(
session_id,
0,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "legacy".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
None,
)
.await
.expect("insert legacy canonical event");
}
let reopened = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("reopen migrated session db");
assert_eq!(
reopened
.model_context_projection_status()
.await
.expect("projection status"),
ModelContextProjectionStatus::Missing
);
assert!(matches!(
reopened
.model_context_events()
.await
.expect_err("normal reads must require explicit reindex"),
SessionDbError::ProjectionStale {
projection: "model_context",
checkpoint: None,
expected: 0
}
));
}
#[tokio::test]
async fn explicit_reindex_projects_canonical_compaction_transitions() {
let scenarios = [
(
"uncompacted",
vec![
message(SessionId::new(), 0, "first"),
message(SessionId::new(), 1, "second"),
],
vec![0, 1],
),
(
"local-to-local",
vec![
message(SessionId::new(), 0, "old"),
local_marker(SessionId::new(), 1, 0),
message(SessionId::new(), 2, "middle"),
local_marker(SessionId::new(), 3, 2),
message(SessionId::new(), 4, "new"),
],
vec![3, 4],
),
(
"local-to-provider",
vec![
message(SessionId::new(), 0, "old"),
local_marker(SessionId::new(), 1, 0),
message(SessionId::new(), 2, "middle"),
provider_marker(SessionId::new(), 3, 2),
message(SessionId::new(), 4, "new"),
],
vec![3, 4],
),
(
"provider-to-local",
vec![
message(SessionId::new(), 0, "old"),
provider_marker(SessionId::new(), 1, 0),
message(SessionId::new(), 2, "middle"),
local_marker(SessionId::new(), 3, 2),
message(SessionId::new(), 4, "new"),
],
vec![3, 4],
),
(
"provider-to-provider",
vec![
message(SessionId::new(), 0, "old"),
provider_marker(SessionId::new(), 1, 0),
message(SessionId::new(), 2, "middle"),
provider_marker(SessionId::new(), 3, 2),
message(SessionId::new(), 4, "new"),
],
vec![3, 4],
),
];
for (name, template, expected_sequences) in scenarios {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let events = template
.into_iter()
.map(|mut event| {
event.session_id = session_id;
event
})
.collect::<Vec<_>>();
for event in &events {
insert_event(db.database(), event, None)
.await
.unwrap_or_else(|error| panic!("insert {name} canonical event: {error}"));
}
assert!(matches!(
db.model_context_events().await,
Err(SessionDbError::ProjectionStale {
projection: "model_context",
checkpoint: None,
..
})
));
reindex_model_context_for_test(&db, temp_dir.path(), session_id).await;
let projected = db
.model_context_events()
.await
.unwrap_or_else(|error| panic!("load {name} projected context: {error}"));
assert_eq!(
projected
.iter()
.map(|event| event.sequence)
.collect::<Vec<_>>(),
expected_sequences,
"scenario {name}"
);
}
}
#[tokio::test]
async fn reindex_rejects_canonical_sequence_gaps_without_replacing_projection() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "projected".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
))
.await
.expect("append projected event");
insert_event(
db.database(),
&event(
session_id,
2,
SessionEventKind::AssistantMessage {
text: "gap".to_string(),
},
),
None,
)
.await
.expect("insert gapped canonical event");
assert!(matches!(
{
let maintenance =
crate::lease::acquire_session_maintenance_guard(temp_dir.path(), session_id)
.expect("maintenance guard");
let write = crate::lease::acquire_maintenance_session_write_lock(
&maintenance,
temp_dir.path(),
session_id,
)
.expect("write guard");
db.reindex_model_context(&maintenance, &write)
.await
.expect_err("gap must reject reindex")
},
SessionDbError::InvalidCanonicalSequence {
expected: 1,
actual: 2
}
));
let rows = db
.database()
.select("model_context_entries")
.columns(&["event_seq"])
.execute(db.database())
.await
.expect("existing projection entries");
assert_eq!(rows.len(), 1);
assert_eq!(required_i64(&rows[0], "event_seq").expect("event seq"), 0);
}
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn bounded_history_has_exact_outcomes_for_every_migration_fixture() {
enum ExpectedHistory {
Sequences(&'static [u64]),
PersistedEventError,
}
let cases = [
(
"future-schema-v40.json",
include_str!("../fixtures/migrations/future-schema-v40.json"),
ExpectedHistory::PersistedEventError,
),
(
"future-schema-v41.json",
include_str!("../fixtures/migrations/future-schema-v41.json"),
ExpectedHistory::PersistedEventError,
),
(
"current-schema-v42.json",
include_str!("../fixtures/migrations/current-schema-v42.json"),
ExpectedHistory::PersistedEventError,
),
(
"current-schema-v43.json",
include_str!("../fixtures/migrations/current-schema-v43.json"),
ExpectedHistory::PersistedEventError,
),
(
"current-schema-v44.json",
include_str!("../fixtures/migrations/current-schema-v44.json"),
ExpectedHistory::PersistedEventError,
),
(
"current-schema-v45.json",
include_str!("../fixtures/migrations/current-schema-v45.json"),
ExpectedHistory::PersistedEventError,
),
(
"current-schema-v46.json",
include_str!("../fixtures/migrations/current-schema-v46.json"),
ExpectedHistory::PersistedEventError,
),
(
"future-schema-v44.json",
include_str!("../fixtures/migrations/future-schema-v44.json"),
ExpectedHistory::PersistedEventError,
),
(
"future-schema-v47.json",
&include_str!("../fixtures/migrations/future-schema-v47.json").replace(
"\"schema_version\":47",
&format!("\"schema_version\":{CURRENT_SESSION_EVENT_SCHEMA_VERSION}"),
),
ExpectedHistory::Sequences(&[0]),
),
(
"malformed-json-v39.json",
include_str!("../fixtures/migrations/malformed-json-v39.json"),
ExpectedHistory::PersistedEventError,
),
(
"mismatched-session-id-v39.json",
include_str!("../fixtures/migrations/mismatched-session-id-v39.json"),
ExpectedHistory::PersistedEventError,
),
(
"plugin-status-note-v39.json",
include_str!("../fixtures/migrations/plugin-status-note-v39.json"),
ExpectedHistory::PersistedEventError,
),
(
"unknown-future-event-kind-v39.json",
include_str!("../fixtures/migrations/unknown-future-event-kind-v39.json"),
ExpectedHistory::PersistedEventError,
),
(
"sequence-gap-v39.jsonl",
include_str!("../fixtures/migrations/sequence-gap-v39.jsonl"),
ExpectedHistory::PersistedEventError,
),
];
let fixture_session: SessionId = "00000000-0000-0000-0000-000000000001"
.parse()
.expect("fixture session ID");
for (fixture_name, fixture, expected) in cases {
let temp_dir = tempfile::tempdir().expect("temp dir");
let db = SessionDb::open_turso_in_root(fixture_session, temp_dir.path())
.await
.expect("fixture DB should initialize");
let expected_sequences = match &expected {
ExpectedHistory::Sequences(sequences) => *sequences,
ExpectedHistory::PersistedEventError => &[0],
};
for (payload, sequence) in fixture.lines().zip(expected_sequences.iter().copied()) {
let parsed = serde_json::from_str::<serde_json::Value>(payload).ok();
let event_type = parsed
.as_ref()
.and_then(|value| value["kind"].as_object())
.and_then(|kind| kind.keys().next())
.map_or_else(|| "malformed_fixture".to_owned(), Clone::clone);
let schema_version = parsed
.as_ref()
.and_then(|value| value["schema_version"].as_i64())
.and_then(|version| i32::try_from(version).ok())
.unwrap_or_else(|| i32::from(CURRENT_SESSION_EVENT_SCHEMA_VERSION));
db.database()
.insert("events")
.value("event_seq", seq_to_value(sequence))
.value("event_type", event_type)
.value("schema_version", DatabaseValue::Int32(schema_version))
.value("created_at_ms", seq_to_value(sequence))
.value("payload", payload)
.execute(db.database())
.await
.unwrap_or_else(|error| panic!("insert {fixture_name}: {error}"));
}
match expected {
ExpectedHistory::Sequences(expected_sequences) => {
let page = db
.history_page(SessionHistoryQuery {
cursor: None,
direction: SessionHistoryDirection::Forward,
limit: 1,
})
.await
.unwrap_or_else(|error| panic!("history {fixture_name}: {error}"));
assert_eq!(
page.events
.iter()
.map(|event| event.sequence)
.collect::<Vec<_>>(),
expected_sequences,
"fixture {fixture_name}"
);
}
ExpectedHistory::PersistedEventError => {
let result = db
.history_page(SessionHistoryQuery {
cursor: None,
direction: SessionHistoryDirection::Forward,
limit: 1,
})
.await;
assert!(
matches!(result, Err(SessionDbError::PersistedEvent(_))),
"fixture {fixture_name} returned {result:?}"
);
}
}
}
}
#[tokio::test]
async fn explicit_reindex_builds_missing_projection_from_canonical_history() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
for (sequence, kind) in [
(
0,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "old".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
(
1,
SessionEventKind::ContextCompacted {
summary: "summary".to_string(),
compacted_through_sequence: 0,
},
),
(
2,
SessionEventKind::AssistantMessage {
text: "new".to_string(),
},
),
] {
insert_event(db.database(), &event(session_id, sequence, kind), None)
.await
.expect("insert canonical event");
}
assert_eq!(
db.model_context_projection_status()
.await
.expect("missing status"),
ModelContextProjectionStatus::Missing
);
assert!(matches!(
db.model_context_events()
.await
.expect_err("missing projection requires explicit reindex"),
SessionDbError::ProjectionStale {
projection: "model_context",
checkpoint: None,
expected: 2
}
));
assert_eq!(
reindex_model_context_for_test(&db, temp_dir.path(), session_id).await,
3
);
assert_eq!(
db.model_context_projection_status()
.await
.expect("fresh status"),
ModelContextProjectionStatus::Fresh { checkpoint: 2 }
);
let projected = db.model_context_events().await.expect("projected context");
assert_eq!(
projected
.iter()
.map(|event| event.sequence)
.collect::<Vec<_>>(),
vec![1, 2]
);
}
#[tokio::test]
async fn fresh_model_context_projection_tracks_semantic_entries_and_compaction() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
for (sequence, kind) in [
(
0,
SessionEventKind::SessionCreated {
name: Some("projection".to_string()),
working_directory: temp_dir.path().to_path_buf(),
},
),
(
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "old".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
(
2,
SessionEventKind::AssistantMessage {
text: "old response".to_string(),
},
),
(
3,
SessionEventKind::ContextCompacted {
summary: "summary".to_string(),
compacted_through_sequence: 2,
},
),
(
4,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "new".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
(
5,
SessionEventKind::ModelTurnStarted {
turn_id: "turn-1".to_string(),
},
),
(
6,
SessionEventKind::ModelTurnFinished {
turn_id: "turn-1".to_string(),
outcome: bcode_session_models::ModelTurnOutcome::Completed,
message: None,
},
),
] {
db.append_event(&event(session_id, sequence, kind))
.await
.expect("append projected event");
}
db.append_event(&request_context_event(session_id, 7))
.await
.expect("append excluded occupancy event");
let state = db
.database()
.select("model_context_projection_state")
.columns(&["schema_version", "last_event_seq"])
.execute_first(db.database())
.await
.expect("projection state")
.expect("fresh projection state");
assert_eq!(
required_i64(&state, "schema_version").expect("version"),
i64::from(MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION)
);
assert_eq!(required_i64(&state, "last_event_seq").expect("last"), 7);
let context = db.model_context_events().await.expect("projected context");
assert_eq!(context.len(), 4);
assert_eq!(context[0].sequence, 3);
assert_eq!(context[1].sequence, 4);
assert!(matches!(
context[2].kind,
SessionEventKind::ModelTurnStarted { .. }
));
assert!(matches!(
context[3].kind,
SessionEventKind::ModelTurnFinished { .. }
));
}
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn mixed_version_incident_is_blocked_before_canonical_divergence() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("initialize session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append canonical session creation");
db.append_event(&event(
session_id,
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "legacy history through N".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
))
.await
.expect("append history through N");
db.database()
.update("session_storage_contract")
.value(
"writer_epoch",
DatabaseValue::Int64(i64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)),
)
.where_eq("contract_id", SESSION_STORAGE_CONTRACT_ID)
.execute(db.database())
.await
.expect("set legacy writer epoch");
db.database()
.update("model_context_projection_state")
.value("schema_version", DatabaseValue::Int64(1))
.execute(db.database())
.await
.expect("set legacy model-context schema");
drop(db);
let maintenance =
crate::lease::acquire_session_maintenance_guard(temp_dir.path(), session_id)
.expect("maintenance guard");
let write = crate::lease::acquire_maintenance_session_write_lock(
&maintenance,
temp_dir.path(),
session_id,
)
.expect("write guard");
let migrated =
SessionDb::migrate_turso_in_root(session_id, temp_dir.path(), &maintenance, &write)
.await
.expect("exclusive storage migration");
assert_eq!(
migrated.storage_writer_epoch().await.expect("writer epoch"),
u64::from(CURRENT_SESSION_STORAGE_WRITER_EPOCH)
);
assert_eq!(
migrated
.model_context_projection_status()
.await
.expect("model-context status"),
ModelContextProjectionStatus::Fresh { checkpoint: 1 }
);
drop(write);
drop(maintenance);
let rejected = event(
session_id,
2,
SessionEventKind::AssistantMessage {
text: "incompatible writer must not commit".to_string(),
},
);
let error = migrated
.append_event_for_writer_epoch(&rejected, None, LEGACY_SESSION_STORAGE_WRITER_EPOCH)
.await
.expect_err("legacy writer must be fenced before canonical insert");
assert!(matches!(
error,
SessionDbError::WriterIncompatible {
actual: Some(actual),
expected
} if actual == u64::from(CURRENT_SESSION_STORAGE_WRITER_EPOCH)
&& expected == u64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)
));
assert_eq!(
migrated
.last_event_sequence()
.await
.expect("canonical tail"),
Some(1)
);
for projection in MaterializedProjection::all() {
assert_eq!(
migrated
.materialized_projection_checkpoint(*projection)
.await
.expect("projection checkpoint"),
Some(1),
"projection {} must remain fresh at N",
projection.as_str()
);
}
assert_eq!(
migrated
.model_context_events()
.await
.expect("model context")
.len(),
1
);
migrated
.append_event(&event(
session_id,
2,
SessionEventKind::AssistantMessage {
text: "compatible append".to_string(),
},
))
.await
.expect("compatible writer should append N+1");
assert_eq!(
migrated
.last_event_sequence()
.await
.expect("canonical tail"),
Some(2)
);
for projection in MaterializedProjection::all() {
assert_eq!(
migrated
.materialized_projection_checkpoint(*projection)
.await
.expect("projection checkpoint"),
Some(2),
"projection {} must advance atomically to N+1",
projection.as_str()
);
}
assert_eq!(
migrated
.model_context_projection_status()
.await
.expect("model-context status"),
ModelContextProjectionStatus::Fresh { checkpoint: 2 }
);
assert_eq!(
migrated
.model_context_events()
.await
.expect("model context")
.len(),
2
);
}
#[tokio::test]
async fn incompatible_storage_writer_contract_rejects_mutations_after_reopen() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("writer contract".to_string()),
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append initial event");
let future_epoch = u64::from(CURRENT_SESSION_STORAGE_WRITER_EPOCH).saturating_add(1);
db.database()
.update("session_storage_contract")
.value("writer_epoch", seq_to_value(future_epoch))
.where_eq("contract_id", SESSION_STORAGE_CONTRACT_ID)
.execute(db.database())
.await
.expect("set future writer epoch");
drop(db);
let reopened = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("runtime open may inspect incompatible contract");
assert_eq!(
reopened.storage_writer_epoch().await.expect("writer epoch"),
future_epoch
);
let error = reopened
.append_event(&event(
session_id,
1,
SessionEventKind::AssistantMessage {
text: "must not commit".to_string(),
},
))
.await
.expect_err("future writer contract must reject append");
assert!(matches!(
error,
SessionDbError::WriterIncompatible {
actual: Some(actual),
expected
} if actual == future_epoch
&& expected == u64::from(CURRENT_SESSION_STORAGE_WRITER_EPOCH)
));
assert!(matches!(
reopened.set_session_composer_draft("blocked", 1).await,
Err(SessionDbError::WriterIncompatible { .. })
));
assert_eq!(
reopened
.last_event_sequence()
.await
.expect("canonical tail"),
Some(0)
);
assert_eq!(
reopened.session_composer_draft().await.expect("draft read"),
None
);
}
#[tokio::test]
async fn missing_storage_writer_contract_fails_closed_for_mutation() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.database()
.delete("session_storage_contract")
.execute(db.database())
.await
.expect("remove writer contract");
let error = db
.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("missing contract".to_string()),
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect_err("missing contract must reject append");
assert!(matches!(
error,
SessionDbError::WriterIncompatible {
actual: Some(actual),
expected
} if actual == u64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)
&& expected == u64::from(CURRENT_SESSION_STORAGE_WRITER_EPOCH)
));
assert_eq!(
db.storage_writer_epoch()
.await
.expect("legacy writer epoch"),
u64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)
);
assert_eq!(
db.last_event_sequence().await.expect("canonical tail"),
None
);
}
#[tokio::test]
async fn stale_model_context_projection_rejects_append_atomically() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "projected".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
))
.await
.expect("append projected event");
db.database()
.update("model_context_projection_state")
.value("last_event_seq", seq_to_value(1))
.execute(db.database())
.await
.expect("make model context checkpoint invalid");
let error = db
.append_event(&event(
session_id,
1,
SessionEventKind::AssistantMessage {
text: "must roll back".to_string(),
},
))
.await
.expect_err("stale projection must reject append");
assert!(matches!(
error,
SessionDbError::ModelContextProjectionStale {
checkpoint: 1,
expected: 0
}
));
assert_eq!(
db.last_event_sequence().await.expect("canonical tail"),
Some(0)
);
for projection in MaterializedProjection::all() {
assert_eq!(
db.materialized_projection_checkpoint(*projection)
.await
.expect("projection checkpoint"),
Some(0)
);
}
}
#[tokio::test]
async fn incompatible_context_occupancy_rejects_append_atomically() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("projection".to_string()),
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append initial event");
db.database()
.update("context_occupancy_projection")
.value("schema_version", DatabaseValue::Int64(3))
.execute(db.database())
.await
.expect("make occupancy schema incompatible");
let error = db
.append_event(&event(
session_id,
1,
SessionEventKind::AssistantMessage {
text: "must roll back".to_string(),
},
))
.await
.expect_err("incompatible projection must reject append");
assert!(matches!(
error,
SessionDbError::ProjectionIncompatible {
projection: "context_occupancy",
actual: 3,
expected: 4
}
));
assert_eq!(
db.last_event_sequence().await.expect("canonical tail"),
Some(0)
);
}
#[tokio::test]
async fn readiness_on_second_connection_does_not_stale_writer_snapshot() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let writer = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("writer connection should open");
writer
.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("snapshot".to_string()),
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("initial event should append");
let validator = SessionDb::open_existing_turso_in_root(session_id, temp_dir.path())
.await
.expect("validation connection should open");
validator
.validate_write_readiness()
.await
.expect("second connection should validate readiness");
writer
.append_event(&event(
session_id,
1,
SessionEventKind::AssistantMessage {
text: "followup".to_string(),
},
))
.await
.expect("writer snapshot should refresh after second-connection read");
assert_eq!(
writer.last_event_sequence().await.expect("canonical tail"),
Some(1)
);
}
async fn usage_fixture_payload(db: &SessionDb) -> String {
required_string(
&db.database()
.select("events")
.columns(&["payload"])
.where_eq("event_seq", 1)
.execute_first(db.database())
.await
.unwrap()
.unwrap(),
"payload",
)
.unwrap()
}
fn corrected_usage_normalizer() -> crate::actor::UsageNormalizer {
Arc::new(|source| {
Box::pin(async move {
let raw = &source.original.as_ref().unwrap().reports[0].usage_json;
assert!(raw.contains("123456789012345678901234567890"));
let parsed: serde_json::Value = serde_json::from_str(raw).unwrap();
let mut usage = source.usage;
usage.cache_write_input_tokens = parsed
.pointer("/input_tokens_details/cache_write_tokens")
.and_then(serde_json::Value::as_u64)
.map(|tokens| u32::try_from(tokens).unwrap());
usage.input_tokens = Some(120);
Ok(bcode_session_models::SessionUsageValuation {
usage,
cost: bcode_session_models::SessionCostEstimate::Estimated {
currency: "USD".into(),
total_micros: 42,
source: "corrected".into(),
revision: None,
components: vec![bcode_session_models::SessionCostComponent {
bucket: "input".into(),
modality: None,
tokens: 42,
price_micros: 1_000_000,
cost_micros: 42,
}],
},
})
})
})
}
#[test]
fn original_usage_evidence_is_preserved_by_current_migration_codec() {
let id = SessionId::new();
let usage = SessionTokenUsage {request_id:Some("r".into()),observation_id:Some("r:usage".into()),request:Some(Box::new(serde_json::from_value(serde_json::json!({"provider_plugin_id":"provider","effective_model_id":"model","request_id":"r","model_turn_id":"t","round":0,"request_fingerprint":"f"})).unwrap())),..Default::default()};
let event = event(
id,
1,
SessionEventKind::ModelUsage {
turn_id: "t".into(),
usage,
},
);
let evidence = bcode_session_models::OriginalUsage {
provider_id: "provider".into(),
api_shape: "responses".into(),
reports: vec![bcode_session_models::OriginalUsageReport {
source: "usage".into(),
usage_json: r#"{"future":123456789012345678901234567890}"#.into(),
confirmed: BTreeMap::new(),
}],
..Default::default()
};
let encoded =
crate::persisted::encode_session_event_with_original(&event, Some(&evidence)).unwrap();
let decoded = decode_session_event(&encoded).unwrap();
let retained = crate::persisted::original_usage(&encoded).unwrap();
assert_eq!(
crate::persisted::encode_session_event_with_original(&decoded, retained.as_ref())
.unwrap(),
encoded
);
assert!(!serde_json::to_string(&decoded).unwrap().contains("future"));
}
async fn assert_usage_evidence_is_private(db: &SessionDb) {
let page = db
.usage_page(&bcode_session_models::SessionUsageQuery {
range: bcode_session_models::SessionCostRange {
from_timestamp_ms: 0,
to_timestamp_ms: 101,
},
after: None,
limit: 1,
generation: None,
})
.await
.unwrap();
let evidence = page.entries[0].evidence.as_ref().unwrap();
assert!(evidence.complete);
assert_eq!(evidence.report_count, 1);
assert_eq!(evidence.capture_issue, None);
assert!(!serde_json::to_string(&page).unwrap().contains("future"));
}
#[tokio::test]
async fn global_catalog_missing_parent_column_is_rejected_without_repair() {
let root = tempfile::tempdir().expect("catalog root");
let catalog = GlobalSessionDb::initialize_turso_in_root(root.path())
.await
.expect("initialize");
switchy::database::schema::drop_index("idx_sessions_parent_id", "sessions")
.execute(&**catalog.db)
.await
.expect("drop derived index");
switchy::database::schema::alter_table("sessions")
.drop_column("parent_id".into())
.execute(&**catalog.db)
.await
.expect("legacy catalog fixture");
catalog.close().await.expect("close");
for _ in 0..2 {
assert!(matches!(
GlobalSessionDb::open_existing_turso_in_root(root.path()).await,
Err(SessionDbError::MigrationHistoryIncompatible { .. })
));
}
}
#[tokio::test]
async fn original_usage_survives_reopen_and_corrected_normalization() {
let root = tempfile::tempdir().unwrap();
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.unwrap();
db.append_event(&event(
id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: root.path().into(),
},
))
.await
.unwrap();
let original = bcode_session_models::OriginalUsage {
provider_id:"provider".into(),api_shape:"responses".into(),complete:true,
reports:vec![bcode_session_models::OriginalUsageReport { source:"response.completed".into(),usage_json:r#"{"input_tokens":100,"output_tokens":10,"input_tokens_details":{"cache_write_tokens":80},"future":123456789012345678901234567890}"#.into(),confirmed:BTreeMap::new() }],..Default::default()
};
let mut usage_event = event(
id,
1,
SessionEventKind::ModelUsage {
turn_id: "turn".into(),
usage: SessionTokenUsage {
request: Some(Box::new(serde_json::from_value(serde_json::json!({"provider_plugin_id":"provider","effective_model_id":"model","request_id":"r","model_turn_id":"turn","round":0,"request_fingerprint":"f"})).unwrap())),
request_id: Some("r".into()),
observation_id: Some("r:final".into()),
terminal: true,
input_tokens: Some(100),
output_tokens: Some(10),
..Default::default()
},
},
);
usage_event.timestamp_ms = 100;
db.append_event_with_original(&usage_event, None, Some(&original), None)
.await
.unwrap();
let before = usage_fixture_payload(&db).await;
assert!(
!serde_json::to_string(&db.events_range(0, 1, 2).await.unwrap())
.unwrap()
.contains("future")
);
drop(db);
let db = SessionDb::open_existing_turso_in_root(id, root.path())
.await
.unwrap();
let prior_summary = db.session_usage_summary().await.unwrap();
assert_usage_evidence_is_private(&db).await;
let rejected: crate::actor::UsageNormalizer =
Arc::new(|_| Box::pin(async { Err("normalizer unavailable".into()) }));
assert!(
db.renormalize_usage(
bcode_session_models::SessionCostRange {
from_timestamp_ms: 100,
to_timestamp_ms: 101
},
rejected
)
.await
.is_err()
);
assert_eq!(db.session_usage_summary().await.unwrap(), prior_summary);
let normalizer = corrected_usage_normalizer();
let (count, summary) = db
.renormalize_usage(
bcode_session_models::SessionCostRange {
from_timestamp_ms: 100,
to_timestamp_ms: 101,
},
normalizer,
)
.await
.unwrap();
assert_eq!(count, 1);
assert_eq!(summary.cumulative_metered_tokens, 130);
assert_eq!(summary.totals_micros["USD"], 42);
assert_eq!(
summary.latest_usage.unwrap().cache_write_input_tokens,
Some(80)
);
assert_eq!(usage_fixture_payload(&db).await, before);
assert_eq!(
crate::persisted::original_usage(&before).unwrap(),
Some(original.clone())
);
let mut duplicate = usage_event;
duplicate.sequence = 2;
db.append_event_with_original(&duplicate, None, Some(&original), None)
.await
.unwrap();
assert_eq!(
db.session_usage_summary()
.await
.unwrap()
.cumulative_metered_tokens,
130
);
}
async fn remove_milestone_projection_fixture(db: &SessionDb) {
for table in [
"milestone_evaluations",
"session_milestones",
"milestone_scheduling",
"milestone_turns",
] {
switchy::database::schema::drop_table(table)
.if_exists(true)
.execute(db.database())
.await
.unwrap();
}
for column in [
"automatic_title",
"automatic_title_source_sequence",
"explicit_name",
"derived_title",
] {
switchy::database::schema::alter_table("session_state")
.drop_column(column.into())
.execute(db.database())
.await
.unwrap();
}
switchy::database::schema::drop_table("submission_recovery_operations")
.if_exists(true)
.execute(db.database())
.await
.unwrap();
switchy::database::schema::drop_index("idx_submission_queue", "submissions")
.execute(db.database())
.await
.unwrap();
switchy::database::schema::alter_table("submissions")
.drop_column("queue_visible".into())
.execute(db.database())
.await
.unwrap();
db.database()
.delete(SESSION_MIGRATIONS_TABLE)
.where_gte("id", "050")
.execute(db.database())
.await
.unwrap();
}
async fn remove_parent_relationship_fixture(db: &SessionDb) {
for table in ["session_children", "session_children_checkpoint"] {
switchy::database::schema::drop_table(table)
.execute(db.database())
.await
.unwrap();
}
switchy::database::schema::alter_table("session_state")
.drop_column("parent_id".into())
.execute(db.database())
.await
.unwrap();
}
async fn remove_usage_staging_fixture(db: &SessionDb) {
switchy::database::schema::drop_table("turn_evidence")
.execute(db.database())
.await
.unwrap();
switchy::database::schema::drop_table("usage_valuation_staging")
.execute(db.database())
.await
.unwrap();
}
#[tokio::test]
async fn epoch_seven_upgrade_preserves_usage_and_seeds_separate_cost() {
let dir = tempfile::tempdir().unwrap();
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, dir.path()).await.unwrap();
db.append_event(&event(
id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: dir.path().into(),
},
))
.await
.unwrap();
let legacy = event(
id,
1,
SessionEventKind::ModelUsage {
turn_id: "t".into(),
usage: SessionTokenUsage {
request_id: Some("r".into()),
observation_id: Some("r:usage".into()),
terminal: true,
input_tokens: Some(10),
output_tokens: Some(0),
cost: Some(bcode_session_models::SessionCostEstimate::Estimated {
currency: "USD".into(),
total_micros: 10,
source: "original".into(),
revision: None,
components: vec![bcode_session_models::SessionCostComponent {
bucket: "input".into(),
modality: None,
tokens: 10,
price_micros: 1_000_000,
cost_micros: 10,
}],
}),
..Default::default()
},
},
);
db.append_event(&legacy).await.unwrap();
let bytes = usage_fixture_payload(&db).await;
switchy::database::schema::drop_index(
"idx_session_usage_timestamp",
"session_usage_requests",
)
.execute(db.database())
.await
.unwrap();
for column in ["cost_json", "first_observed_at_ms"] {
switchy::database::schema::alter_table("session_usage_requests")
.drop_column(column.to_owned())
.execute(db.database())
.await
.unwrap();
}
remove_usage_staging_fixture(&db).await;
remove_milestone_projection_fixture(&db).await;
remove_parent_relationship_fixture(&db).await;
db.database()
.delete(SESSION_MIGRATIONS_TABLE)
.where_gte("id", "038")
.execute(db.database())
.await
.unwrap();
db.database()
.update("session_storage_contract")
.value("writer_epoch", 7)
.execute(db.database())
.await
.unwrap();
drop(db);
let maintenance = crate::lease::acquire_session_maintenance_guard(dir.path(), id).unwrap();
let write =
crate::lease::acquire_maintenance_session_write_lock(&maintenance, dir.path(), id)
.unwrap();
let migrated = SessionDb::migrate_turso_in_root(id, dir.path(), &maintenance, &write)
.await
.unwrap();
let restored = migrated.session_usage_summary().await.unwrap();
assert_eq!(restored.totals_micros["USD"], 10);
assert_eq!(usage_fixture_payload(&migrated).await, bytes);
let row = migrated
.database()
.select("session_usage_requests")
.columns(&["cost_json", "first_observed_at_ms"])
.where_eq("request_key", "r")
.execute_first(migrated.database())
.await
.unwrap()
.unwrap();
let cost_json = required_string(&row, "cost_json").unwrap();
assert!(cost_json.contains("original"));
assert_eq!(
required_i64(&row, "first_observed_at_ms")
.unwrap()
.cast_unsigned(),
legacy.timestamp_ms
);
}
#[tokio::test]
async fn usage_pages_reject_stale_projection_without_repairing_it() {
use bcode_session_models::{SessionCostRange, SessionUsageQuery};
let dir = tempfile::tempdir().unwrap();
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, dir.path()).await.unwrap();
db.append_event(&event(
id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: dir.path().into(),
},
))
.await
.unwrap();
db.db
.delete("projection_checkpoints")
.where_eq(
"projection_name",
MaterializedProjection::SessionUsage.as_str(),
)
.execute(&**db.db)
.await
.unwrap();
let query = SessionUsageQuery {
range: SessionCostRange {
from_timestamp_ms: 0,
to_timestamp_ms: 100,
},
after: None,
limit: 1,
generation: None,
};
assert!(matches!(
db.usage_page(&query).await,
Err(SessionDbError::ProjectionStale { .. })
));
assert_eq!(
db.materialized_projection_checkpoint(MaterializedProjection::SessionUsage)
.await
.unwrap(),
None
);
assert_eq!(db.last_event_sequence().await.unwrap(), Some(0));
}
#[tokio::test]
async fn usage_pages_are_bounded_filtered_and_generation_fenced() {
use bcode_session_models::{SessionCostRange, SessionUsageQuery};
let dir = tempfile::tempdir().unwrap();
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, dir.path()).await.unwrap();
db.append_event(&event(
id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: dir.path().into(),
},
))
.await
.unwrap();
for sequence in 1..=3 {
let mut observation = event(
id,
sequence,
SessionEventKind::ModelUsage {
turn_id: "turn".into(),
usage: SessionTokenUsage {
request_id: Some(format!("r{sequence}")),
observation_id: Some(format!("r{sequence}:usage")),
terminal: true,
input_tokens: Some(10),
..Default::default()
},
},
);
observation.timestamp_ms = sequence * 100;
db.append_event(&observation).await.unwrap();
}
let history = db.events_range(0, 3, 4).await.unwrap();
let summary = db.session_usage_summary().await.unwrap();
let mut query = SessionUsageQuery {
range: SessionCostRange {
from_timestamp_ms: 200,
to_timestamp_ms: 300,
},
after: None,
limit: 1,
generation: None,
};
let first = db.usage_page(&query).await.unwrap();
assert!(first.entries.is_empty());
assert_eq!(first.scanned, 1);
assert_eq!(first.next_after.as_deref(), Some("r1"));
query.after = first.next_after;
assert!(db.usage_page(&query).await.is_err());
query.generation = Some(first.generation);
let second = db.usage_page(&query).await.unwrap();
assert_eq!(second.entries.len(), 1);
assert_eq!(second.entries[0].first_observed_at_ms, 200);
assert_eq!(second.entries[0].usage.input_tokens, Some(10));
assert!(matches!(
second.entries[0].usage.cost,
Some(bcode_session_models::SessionCostEstimate::Unavailable { .. })
));
query.after = second.next_after;
let third = db.usage_page(&query).await.unwrap();
assert!(third.entries.is_empty());
query.after = third.next_after;
let end = db.usage_page(&query).await.unwrap();
assert_eq!(end.scanned, 0);
assert!(end.next_after.is_none());
assert_eq!(db.session_usage_summary().await.unwrap(), summary);
assert_eq!(db.events_range(0, 3, 4).await.unwrap(), history);
db.reprice_usage(query.range, &|_| {
bcode_session_models::SessionCostEstimate::Unavailable {
reason: bcode_session_models::SessionCostUnavailableReason::ProviderUsageIncomplete,
}
})
.await
.unwrap();
assert!(db.usage_page(&query).await.is_err());
query.after = None;
query.generation = None;
let updated = db.usage_page(&query).await.unwrap();
assert_eq!(
updated.generation.cost_revision,
first.generation.cost_revision + 1
);
query.limit = 257;
assert!(db.usage_page(&query).await.is_err());
}
#[tokio::test]
async fn repricing_is_atomic_range_scoped_and_leaves_events_unchanged() {
let dir = tempfile::tempdir().unwrap();
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, dir.path()).await.unwrap();
db.append_event(&event(
id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: dir.path().into(),
},
))
.await
.unwrap();
let cost = |micros| bcode_session_models::SessionCostEstimate::Estimated {
currency: "USD".into(),
total_micros: micros,
source: "snapshot".into(),
revision: Some("revision".into()),
components: vec![bcode_session_models::SessionCostComponent {
bucket: "input".into(),
modality: None,
tokens: 10,
price_micros: micros * 100_000,
cost_micros: micros,
}],
};
for seq in 1..=3 {
let mut e = event(
id,
seq,
SessionEventKind::ModelUsage {
turn_id: "turn".into(),
usage: SessionTokenUsage {
request_id: Some(format!("r{seq}")),
observation_id: Some(format!("r{seq}:usage")),
terminal: true,
input_tokens: Some(10),
output_tokens: Some(0),
..Default::default()
},
},
);
e.timestamp_ms = seq * 100;
db.append_event_with_cost(&e, Some(&cost(100)), None)
.await
.unwrap();
}
let before = db.events_range(0, 3, 4).await.unwrap();
assert!(before.iter().all(|event| !matches!(&event.kind, SessionEventKind::ModelUsage { usage, .. } if usage.cost.is_some())));
let range = bcode_session_models::SessionCostRange {
from_timestamp_ms: 100,
to_timestamp_ms: 300,
};
let (count, summary) = db
.reprice_usage(range, &|facts| {
assert!(facts.cost.is_none());
cost(20)
})
.await
.unwrap();
assert_eq!(count, 2);
assert_eq!(summary.totals_micros["USD"], 140);
assert_eq!(summary.cost_revision, 1);
assert_eq!(summary.through_sequence, Some(3));
assert_eq!(db.events_range(0, 3, 4).await.unwrap(), before);
let invalid =
|_: &SessionTokenUsage| bcode_session_models::SessionCostEstimate::Estimated {
currency: "USD".into(),
total_micros: 1,
components: Vec::new(),
source: "bad".into(),
revision: None,
};
assert!(db.reprice_usage(range, &invalid).await.is_err());
assert_eq!(db.session_usage_summary().await.unwrap(), summary);
let (_, partial) = db
.reprice_usage(range, &|_| {
bcode_session_models::SessionCostEstimate::Unavailable {
reason: bcode_session_models::SessionCostUnavailableReason::RequestPricingUnavailable,
}
})
.await
.unwrap();
assert_eq!(partial.totals_micros["USD"], 100);
assert_eq!(partial.unavailable_usage_count, 2);
assert_eq!(db.events_range(0, 3, 4).await.unwrap(), before);
let mut duplicate = before[1].clone();
duplicate.sequence = 4;
db.append_event_with_cost(&duplicate, Some(&cost(999)), None)
.await
.unwrap();
let total = db.session_usage_summary().await.unwrap();
assert_eq!(total.totals_micros["USD"], 100);
assert_eq!(total.unavailable_usage_count, 2);
}
#[tokio::test]
async fn session_usage_rejects_conflicts_atomically_and_missing_projection() {
let temp_dir = tempfile::tempdir().unwrap();
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.unwrap();
let pending = SessionTokenUsage {
request_id: Some("attempt".into()),
observation_id: Some("attempt:pending".into()),
cost: Some(bcode_session_models::SessionCostEstimate::Unavailable {
reason: bcode_session_models::SessionCostUnavailableReason::ProviderUsageIncomplete,
}),
..Default::default()
};
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.unwrap();
db.append_event(&event(
session_id,
1,
SessionEventKind::ModelUsage {
turn_id: "turn".into(),
usage: pending.clone(),
},
))
.await
.unwrap();
let mut conflict = pending.clone();
conflict.input_tokens = Some(100);
assert!(
db.append_event(&event(
session_id,
2,
SessionEventKind::ModelUsage {
turn_id: "turn".into(),
usage: conflict
}
))
.await
.is_err()
);
assert_eq!(db.last_event_sequence().await.unwrap(), Some(1));
db.append_event(&event(
session_id,
2,
SessionEventKind::ModelUsage {
turn_id: "turn".into(),
usage: pending,
},
))
.await
.unwrap();
let summary = db.session_usage_summary().await.unwrap();
assert_eq!(summary.through_sequence, Some(2));
assert_eq!(summary.observed_usage_count, 1);
assert_eq!(summary.unavailable_usage_count, 1);
db.database()
.delete("session_usage_projection")
.execute(db.database())
.await
.unwrap();
assert!(db.session_usage_summary().await.is_err());
}
#[tokio::test]
async fn session_usage_projection_restores_complete_deduplicated_accounting() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append session");
let usage = |request: &str, ordinal, terminal, tokens, cost| SessionTokenUsage {
request_id: Some(request.to_owned()),
observation_id: Some(format!("{request}:{ordinal}")),
observation_ordinal: ordinal,
terminal,
total_tokens: Some(tokens),
cost: Some(bcode_session_models::SessionCostEstimate::Estimated {
currency: "USD".to_owned(),
total_micros: cost,
components: vec![bcode_session_models::SessionCostComponent {
bucket: "input".to_owned(),
modality: None,
tokens,
price_micros: 1_000_000,
cost_micros: cost,
}],
source: "fixture".to_owned(),
revision: None,
}),
..SessionTokenUsage::default()
};
for (sequence, request_usage) in [
(1, usage("request-1", 0, false, 10, 10)),
(2, usage("request-1", 1, true, 15, 15)),
(3, usage("request-2", 0, true, 20, 20)),
] {
db.append_event(&event(
session_id,
sequence,
SessionEventKind::ModelUsage {
turn_id: "turn".to_owned(),
usage: request_usage,
},
))
.await
.expect("append usage");
}
let summary = db.session_usage_summary().await.expect("usage summary");
assert_eq!(summary.cumulative_metered_tokens, 35);
assert_eq!(summary.totals_micros["USD"], 35);
assert_eq!(summary.observed_usage_count, 2);
assert_eq!(summary.estimated_usage_count, 2);
assert_eq!(
summary.latest_usage.and_then(|usage| usage.request_id),
Some("request-2".to_owned())
);
}
#[tokio::test]
async fn checkpoint_upsert_inserts_missing_row() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let projection = MaterializedProjection::ToolRuns;
db.database()
.delete("projection_checkpoints")
.where_eq("projection_name", projection.as_str())
.execute(db.database())
.await
.expect("remove checkpoint");
update_projection_checkpoint(
db.database(),
projection,
&event(
session_id,
7,
SessionEventKind::AssistantMessage {
text: "checkpoint".to_owned(),
},
),
)
.await
.expect("insert checkpoint");
assert_eq!(
projection_checkpoint_snapshot(db.database())
.await
.expect("checkpoint snapshot")
.get(projection.as_str()),
Some(&ProjectionCheckpointState {
version: u64::from(projection.schema_version()),
checkpoint: 7,
})
);
}
#[tokio::test]
async fn checkpoint_upsert_updates_existing_row() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let projection = MaterializedProjection::ToolRuns;
let first = event(
session_id,
6,
SessionEventKind::AssistantMessage {
text: "first checkpoint".to_owned(),
},
);
let second = event(
session_id,
7,
SessionEventKind::AssistantMessage {
text: "second checkpoint".to_owned(),
},
);
update_projection_checkpoint(db.database(), projection, &first)
.await
.expect("insert checkpoint");
update_projection_checkpoint(db.database(), projection, &second)
.await
.expect("update checkpoint");
assert_eq!(
projection_checkpoint_snapshot(db.database())
.await
.expect("checkpoint snapshot")
.get(projection.as_str()),
Some(&ProjectionCheckpointState {
version: u64::from(projection.schema_version()),
checkpoint: 7,
})
);
}
#[tokio::test]
async fn incompatible_checkpoint_is_not_overwritten_and_append_rolls_back() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("projection".to_owned()),
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append initial event");
db.database()
.update("projection_checkpoints")
.value("projection_version", DatabaseValue::Int64(3))
.where_eq("projection_name", MaterializedProjection::ToolRuns.as_str())
.execute(db.database())
.await
.expect("make checkpoint incompatible");
let before = projection_checkpoint_snapshot(db.database())
.await
.expect("checkpoint snapshot before append");
let error = db
.append_event(&event(
session_id,
1,
SessionEventKind::AssistantMessage {
text: "must roll back".to_owned(),
},
))
.await
.expect_err("incompatible checkpoint must reject append");
assert!(matches!(
error,
SessionDbError::ProjectionIncompatible {
projection: "tool_runs",
actual: 3,
expected: 2
}
));
assert_eq!(
db.last_event_sequence().await.expect("canonical tail"),
Some(0)
);
assert_eq!(
projection_checkpoint_snapshot(db.database())
.await
.expect("checkpoint snapshot after append"),
before
);
}
#[tokio::test]
async fn exact_turn_evidence_survives_unrelated_history_and_restart() {
let temp = tempfile::tempdir().expect("temp");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, temp.path())
.await
.expect("open");
db.append_event(&event(
id,
0,
SessionEventKind::AssistantResponseSegment {
turn_id: "target".into(),
segment_id: "answer".into(),
segment_order: 1,
text: "exact output".into(),
},
))
.await
.expect("output");
db.append_event(&event(
id,
1,
SessionEventKind::ModelTurnFinished {
turn_id: "target".into(),
outcome: bcode_session_models::ModelTurnOutcome::Completed,
message: None,
},
))
.await
.expect("terminal");
for sequence in 2..302 {
db.append_event(&event(
id,
sequence,
SessionEventKind::ModelTurnFinished {
turn_id: format!("other-{sequence}"),
outcome: bcode_session_models::ModelTurnOutcome::Cancelled,
message: None,
},
))
.await
.expect("unrelated terminal");
}
drop(db);
let db = SessionDb::open_existing_turso_in_root(id, temp.path())
.await
.expect("reopen");
let evidence = db.turn_evidence("target").await.expect("exact lookup");
assert_eq!(
evidence
.iter()
.map(|event| event.sequence)
.collect::<Vec<_>>(),
vec![0, 1]
);
assert!(db.turn_evidence("absent").await.expect("absent").is_empty());
switchy::database::schema::drop_index("idx_turn_evidence_sequence", "turn_evidence")
.execute(db.database())
.await
.expect("remove index");
assert!(matches!(
db.turn_evidence("target").await,
Err(SessionDbError::InvalidData(_))
));
switchy::database::schema::create_index("idx_turn_evidence_sequence")
.table("turn_evidence")
.columns(vec!["turn_id", "evidence_kind", "event_seq"])
.execute(db.database())
.await
.expect("restore fixture index");
db.database()
.delete("projection_checkpoints")
.where_eq("projection_name", "turn_evidence")
.execute(db.database())
.await
.expect("damage checkpoint");
assert!(matches!(
db.turn_evidence("target").await,
Err(SessionDbError::ProjectionStale { .. })
));
}
#[tokio::test]
async fn duplicate_turn_receipt_rolls_back_canonical_and_projection_updates() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let admission = bcode_session_models::TurnAdmissionMetadata {
origin: Some(bcode_session_models::TurnOrigin {
producer: "test.producer".to_string(),
correlation_id: None,
display_label: None,
}),
idempotency_key: Some("same-key".to_string()),
..bcode_session_models::TurnAdmissionMetadata::default()
};
db.append_event(&event(
session_id,
0,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "accepted".to_string(),
admission: admission.clone(),
},
))
.await
.expect("append accepted turn");
db.append_event(&event(
session_id,
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "duplicate".to_string(),
admission,
},
))
.await
.expect_err("duplicate receipt must roll back append");
assert_eq!(
db.last_event_sequence().await.expect("canonical tail"),
Some(0)
);
assert_eq!(db.input_history().await.expect("input history").len(), 1);
for projection in MaterializedProjection::all() {
assert_eq!(
db.materialized_projection_checkpoint(*projection)
.await
.expect("projection checkpoint"),
Some(0)
);
}
}
#[tokio::test]
async fn invalid_compaction_does_not_advance_canonical_or_projected_state() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("projection".to_string()),
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append projected event");
let error = db
.append_event(&event(
session_id,
1,
SessionEventKind::ContextCompacted {
summary: "invalid".to_string(),
compacted_through_sequence: 2,
},
))
.await
.expect_err("invalid compaction must roll back");
assert!(matches!(
error,
SessionDbError::InvalidCompactionMarker { sequence: 1, .. }
));
assert_eq!(db.last_event_sequence().await.expect("last"), Some(0));
let state = db
.database()
.select("model_context_projection_state")
.columns(&["last_event_seq"])
.execute_first(db.database())
.await
.expect("projection state")
.expect("projection state row");
assert_eq!(required_i64(&state, "last_event_seq").expect("last"), 0);
}
#[tokio::test]
async fn existing_open_and_reads_leave_database_and_sidecars_byte_identical() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("initialize session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("read immutability".to_string()),
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append event");
drop(db);
let before = session_storage_files(temp_dir.path(), session_id);
let reopened = SessionDb::open_existing_turso_in_root(session_id, temp_dir.path())
.await
.expect("open existing session");
assert_eq!(reopened.last_event_sequence().await.expect("tail"), Some(0));
assert_eq!(reopened.all_events().await.expect("history").len(), 1);
assert_eq!(
reopened
.model_context_projection_status()
.await
.expect("projection status"),
ModelContextProjectionStatus::Fresh { checkpoint: 0 }
);
drop(reopened);
let after = session_storage_files(temp_dir.path(), session_id);
assert_eq!(
after, before,
"existing open/read paths must not mutate DB or sidecars"
);
}
#[tokio::test]
async fn normal_existing_open_does_not_run_pending_projection_migrations() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("initialize session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "projected".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
))
.await
.expect("append projected event");
db.database()
.update("model_context_projection_state")
.value("schema_version", DatabaseValue::Int64(1))
.execute(db.database())
.await
.expect("set legacy projection version");
drop(db);
let reopened = SessionDb::open_existing_turso_in_root(session_id, temp_dir.path())
.await
.expect("open existing session without migration");
assert_eq!(
reopened
.model_context_projection_status()
.await
.expect("projection status"),
ModelContextProjectionStatus::Incompatible {
actual: 1,
expected: 3,
}
);
let state = reopened
.database()
.select("model_context_projection_state")
.columns(&["schema_version", "last_event_seq"])
.where_eq("projection_id", MODEL_CONTEXT_PROJECTION_ID)
.execute_first(reopened.database())
.await
.expect("projection query")
.expect("projection row");
assert_eq!(required_i64(&state, "schema_version").expect("schema"), 1);
assert_eq!(required_i64(&state, "last_event_seq").expect("tail"), 0);
}
async fn create_legacy_crash_fixture(root: &Path) -> SessionId {
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, root)
.await
.expect("open crash fixture");
db.append_event(&event(
session_id,
0,
SessionEventKind::AssistantMessage {
text: "survives migration crash".to_owned(),
},
))
.await
.expect("append crash fixture event");
db.database()
.update("session_storage_contract")
.value(
"writer_epoch",
DatabaseValue::Int64(i64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)),
)
.execute(db.database())
.await
.expect("downgrade crash fixture writer epoch");
drop(db);
session_id
}
fn run_migration_crash_child(root: &Path, session_id: SessionId, phase: &str) {
let status =
std::process::Command::new(std::env::current_exe().expect("current test binary"))
.args([
"--exact",
"db::tests::migration_crash_helper",
"--ignored",
"--nocapture",
])
.env("BCODE_MIGRATION_CRASH_ROOT", root)
.env("BCODE_MIGRATION_CRASH_SESSION_ID", session_id.to_string())
.env("BCODE_MIGRATION_CRASH_PHASE", phase)
.status()
.expect("run migration crash child");
assert!(!status.success(), "crash helper must terminate abnormally");
}
#[tokio::test]
#[ignore = "subprocess helper for migration_crash_boundaries_reclassify_durably"]
async fn migration_crash_helper() {
let root = std::env::var_os("BCODE_MIGRATION_CRASH_ROOT")
.map(std::path::PathBuf::from)
.expect("crash root");
let session_id = std::env::var("BCODE_MIGRATION_CRASH_SESSION_ID")
.expect("crash session id")
.parse::<SessionId>()
.expect("valid crash session id");
let _phase = std::env::var("BCODE_MIGRATION_CRASH_PHASE").expect("crash phase");
let maintenance = crate::lease::acquire_session_maintenance_guard(&root, session_id)
.expect("maintenance guard");
let write =
crate::lease::acquire_maintenance_session_write_lock(&maintenance, &root, session_id)
.expect("write guard");
SessionDb::migrate_turso_in_root_observed(
session_id,
&root,
&maintenance,
&write,
MetricsRegistry::disabled(),
None,
)
.await
.expect("migration crash phase must abort first");
}
#[tokio::test]
async fn migration_crash_boundaries_reclassify_durably() {
for phase in [
"before_transaction",
"transaction_start",
"normalization",
"projection_rebuild",
"final_validation",
"before_epoch_update",
"after_epoch_update_before_commit",
"during_transaction",
"after_commit",
"post_commit_checkpoint",
] {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = create_legacy_crash_fixture(temp_dir.path()).await;
run_migration_crash_child(temp_dir.path(), session_id, phase);
let reopened = SessionDb::open_existing_turso_in_root(session_id, temp_dir.path())
.await
.expect("reopen after migration crash");
match phase {
"before_transaction"
| "transaction_start"
| "normalization"
| "projection_rebuild"
| "final_validation"
| "before_epoch_update"
| "after_epoch_update_before_commit"
| "during_transaction" => {
assert!(matches!(
reopened
.storage_compatibility()
.await
.expect("classify interrupted migration"),
SessionStorageCompatibility::KnownLegacy { .. }
));
assert_eq!(
reopened.storage_writer_epoch().await.expect("writer epoch"),
u64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)
);
drop(reopened);
let maintenance = crate::lease::acquire_session_maintenance_guard(
temp_dir.path(),
session_id,
)
.expect("retry maintenance guard");
let write = crate::lease::acquire_maintenance_session_write_lock(
&maintenance,
temp_dir.path(),
session_id,
)
.expect("retry write guard");
let retried = SessionDb::migrate_turso_in_root(
session_id,
temp_dir.path(),
&maintenance,
&write,
)
.await
.expect("retry interrupted migration");
retried
.validate_write_readiness()
.await
.expect("retried migration is write ready");
}
"after_commit" | "post_commit_checkpoint" => {
assert!(matches!(
reopened
.storage_compatibility()
.await
.expect("classify committed migration"),
SessionStorageCompatibility::Current { .. }
));
reopened
.validate_write_readiness()
.await
.expect("committed migration remains write ready");
}
_ => unreachable!("fixed crash phase"),
}
}
}
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn explicit_legacy_model_context_migration_is_atomic() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append canonical session creation");
db.append_event(&event(
session_id,
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "projected".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
))
.await
.expect("append projected event");
db.database()
.update("session_storage_contract")
.value(
"writer_epoch",
DatabaseValue::Int64(i64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)),
)
.where_eq("contract_id", SESSION_STORAGE_CONTRACT_ID)
.execute(db.database())
.await
.expect("set legacy writer epoch");
db.database()
.update("model_context_projection_state")
.value("schema_version", DatabaseValue::Int64(1))
.execute(db.database())
.await
.expect("set legacy version");
insert_event(
db.database(),
&event(
session_id,
2,
SessionEventKind::AssistantMessage {
text: "canonical tail".to_string(),
},
),
None,
)
.await
.expect("append canonical tail without model-context projection");
let tail_event = event(
session_id,
2,
SessionEventKind::AssistantMessage {
text: "canonical tail".to_string(),
},
);
project_event(db.database(), &tail_event, true)
.await
.expect("project non-model read models");
project_context_occupancy_row(db.database(), &tail_event)
.await
.expect("project occupancy read model");
upsert_projection_checkpoints(db.database(), MaterializedProjection::all(), &tail_event)
.await
.expect("checkpoint non-model read models");
let canonical_columns = [
"event_seq",
"event_type",
"schema_version",
"created_at_ms",
"causation_id",
"correlation_id",
"payload",
];
let canonical_before = db
.database()
.select("events")
.columns(&canonical_columns)
.execute(db.database())
.await
.expect("canonical rows before migration");
let payload_hashes_before = canonical_before
.iter()
.map(|row| {
let payload = required_string(row, "payload").expect("canonical payload");
hex::encode(sha2::Sha256::digest(payload.as_bytes()))
})
.collect::<Vec<_>>();
drop(db);
let maintenance =
crate::lease::acquire_session_maintenance_guard(temp_dir.path(), session_id)
.expect("maintenance guard");
let write = crate::lease::acquire_maintenance_session_write_lock(
&maintenance,
temp_dir.path(),
session_id,
)
.expect("write guard");
let migrated =
SessionDb::migrate_turso_in_root(session_id, temp_dir.path(), &maintenance, &write)
.await
.expect("explicitly migrate session db");
assert_eq!(
migrated
.storage_writer_epoch()
.await
.expect("migrated writer epoch"),
u64::from(CURRENT_SESSION_STORAGE_WRITER_EPOCH)
);
assert_eq!(
migrated
.model_context_projection_status()
.await
.expect("projection status"),
ModelContextProjectionStatus::Fresh { checkpoint: 2 }
);
let context = migrated
.model_context_events()
.await
.expect("migrated context");
assert_eq!(context.len(), 2);
assert_eq!(context[1].sequence, 2);
let canonical_after = migrated
.database()
.select("events")
.columns(&canonical_columns)
.execute(migrated.database())
.await
.expect("canonical rows after migration");
assert_eq!(canonical_after, canonical_before);
let payload_hashes_after = canonical_after
.iter()
.map(|row| {
let payload = required_string(row, "payload").expect("canonical payload");
hex::encode(sha2::Sha256::digest(payload.as_bytes()))
})
.collect::<Vec<_>>();
assert_eq!(payload_hashes_after, payload_hashes_before);
validate_all_projection_checkpoints_at_tail(migrated.database(), Some(2))
.await
.expect("all projection checkpoints at canonical tail");
drop(migrated);
let reopened = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("reopen migrated session db");
assert_eq!(
reopened
.model_context_projection_status()
.await
.expect("projection status after reopen"),
ModelContextProjectionStatus::Fresh { checkpoint: 2 }
);
}
#[tokio::test]
async fn explicit_migration_refuses_malformed_canonical_history_without_mutation() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "canonical".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
))
.await
.expect("append canonical event");
db.database()
.update("session_storage_contract")
.value(
"writer_epoch",
DatabaseValue::Int64(i64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)),
)
.where_eq("contract_id", SESSION_STORAGE_CONTRACT_ID)
.execute(db.database())
.await
.expect("set legacy writer epoch");
db.database()
.update("model_context_projection_state")
.value("schema_version", DatabaseValue::Int64(1))
.execute(db.database())
.await
.expect("set legacy projection schema");
db.database()
.update("events")
.value("payload", "{not-valid-json")
.where_eq("event_seq", seq_to_value(0))
.execute(db.database())
.await
.expect("corrupt canonical payload");
drop(db);
let maintenance =
crate::lease::acquire_session_maintenance_guard(temp_dir.path(), session_id)
.expect("maintenance guard");
let write = crate::lease::acquire_maintenance_session_write_lock(
&maintenance,
temp_dir.path(),
session_id,
)
.expect("write guard");
SessionDb::migrate_turso_in_root(session_id, temp_dir.path(), &maintenance, &write)
.await
.expect_err("malformed canonical history must reject migration");
drop(write);
drop(maintenance);
let reopened = SessionDb::open_existing_turso_in_root(session_id, temp_dir.path())
.await
.expect("reopen rejected migration");
assert_eq!(
reopened.storage_writer_epoch().await.expect("writer epoch"),
u64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)
);
assert!(matches!(
reopened
.model_context_projection_status()
.await
.expect("projection status"),
ModelContextProjectionStatus::Incompatible { actual: 1, .. }
));
reopened
.all_events_strict()
.await
.expect_err("malformed canonical payload must remain visible");
}
#[tokio::test]
async fn deterministic_migration_faults_roll_back_every_transaction_phase() {
for fault in [
MigrationFaultPhase::CanonicalDecode,
MigrationFaultPhase::Projection,
MigrationFaultPhase::FinalValidation,
MigrationFaultPhase::Receipt,
MigrationFaultPhase::WriterEpochFinalization,
] {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::AssistantMessage {
text: "fault fixture".to_owned(),
},
))
.await
.expect("append fixture event");
db.database()
.update("session_storage_contract")
.value(
"writer_epoch",
DatabaseValue::Int64(i64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)),
)
.where_eq("contract_id", SESSION_STORAGE_CONTRACT_ID)
.execute(db.database())
.await
.expect("set legacy writer epoch");
drop(db);
tokio::time::sleep(Duration::from_millis(50)).await;
let storage_before = session_storage_files(temp_dir.path(), session_id);
let maintenance =
crate::lease::acquire_session_maintenance_guard(temp_dir.path(), session_id)
.expect("maintenance guard");
let write = crate::lease::acquire_maintenance_session_write_lock(
&maintenance,
temp_dir.path(),
session_id,
)
.expect("write guard");
assert!(matches!(
SessionDb::migrate_turso_in_root_observed_with_fault(
session_id,
temp_dir.path(),
&maintenance,
&write,
MetricsRegistry::disabled(),
None,
None,
test_migration_policy_callbacks(),
fault,
)
.await,
Err(SessionDbError::MigrationHistoryIncompatible { ref reason })
if reason.contains("injected migration fault")
));
drop(write);
drop(maintenance);
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(
session_storage_files(temp_dir.path(), session_id),
storage_before,
"fault {fault:?} must roll back every storage byte"
);
let reopened = SessionDb::open_existing_turso_in_root(session_id, temp_dir.path())
.await
.expect("reopen rolled-back store");
assert_eq!(
reopened.storage_writer_epoch().await.expect("writer epoch"),
u64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH),
"fault {fault:?} must not finalize writer epoch"
);
assert!(
reopened
.database()
.select("session_migration_receipts")
.columns(&["operation_id"])
.execute_first(reopened.database())
.await
.expect("receipt query")
.is_none(),
"fault {fault:?} must not commit a receipt"
);
}
}
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn failed_explicit_migration_preserves_projection_and_writer_contract() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append canonical session creation");
db.append_event(&event(
session_id,
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "projected".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
))
.await
.expect("append projected event");
db.database()
.update("session_storage_contract")
.value(
"writer_epoch",
DatabaseValue::Int64(i64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)),
)
.where_eq("contract_id", SESSION_STORAGE_CONTRACT_ID)
.execute(db.database())
.await
.expect("set legacy writer epoch");
db.database()
.update("model_context_projection_state")
.value("schema_version", DatabaseValue::Int64(1))
.execute(db.database())
.await
.expect("set legacy projection schema");
insert_event(
db.database(),
&event(
session_id,
3,
SessionEventKind::AssistantMessage {
text: "gapped tail".to_string(),
},
),
None,
)
.await
.expect("insert gapped canonical tail");
drop(db);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let storage_before = session_storage_files(temp_dir.path(), session_id);
let maintenance =
crate::lease::acquire_session_maintenance_guard(temp_dir.path(), session_id)
.expect("maintenance guard");
let write = crate::lease::acquire_maintenance_session_write_lock(
&maintenance,
temp_dir.path(),
session_id,
)
.expect("write guard");
assert!(matches!(
SessionDb::migrate_turso_in_root(session_id, temp_dir.path(), &maintenance, &write)
.await
.expect_err("gapped migration must fail"),
SessionDbError::InvalidCanonicalSequence {
expected: 2,
actual: 3
}
));
drop(write);
drop(maintenance);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert_eq!(
session_storage_files(temp_dir.path(), session_id),
storage_before,
"failed migration must preserve every session storage file byte-for-byte"
);
let reopened = SessionDb::open_existing_turso_in_root(session_id, temp_dir.path())
.await
.expect("reopen failed migration");
assert_eq!(
reopened.storage_writer_epoch().await.expect("writer epoch"),
u64::from(LEGACY_SESSION_STORAGE_WRITER_EPOCH)
);
let state = reopened
.database()
.select("model_context_projection_state")
.columns(&["schema_version", "last_event_seq"])
.where_eq("projection_id", MODEL_CONTEXT_PROJECTION_ID)
.execute_first(reopened.database())
.await
.expect("projection query")
.expect("projection state");
assert_eq!(required_i64(&state, "schema_version").expect("schema"), 1);
assert_eq!(required_i64(&state, "last_event_seq").expect("tail"), 1);
assert_eq!(
reopened
.all_events_strict()
.await
.expect("canonical history")
.iter()
.map(|event| event.sequence)
.collect::<Vec<_>>(),
vec![0, 1, 3]
);
assert!(
reopened
.database()
.select(SESSION_MIGRATIONS_TABLE)
.columns(&["id"])
.where_eq("id", "029_session_compatibility_issues")
.execute_first(reopened.database())
.await
.expect("migration ledger query")
.is_some(),
"failed migration must preserve the prior migration ledger"
);
}
#[tokio::test]
async fn incompatible_or_corrupt_model_context_projection_is_visible() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "projected".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
))
.await
.expect("append projected event");
db.database()
.update("model_context_projection_state")
.value("schema_version", DatabaseValue::Int64(1))
.execute(db.database())
.await
.expect("set incompatible version");
assert!(matches!(
db.model_context_events()
.await
.expect_err("incompatible version must surface"),
SessionDbError::ModelContextProjectionVersion {
actual: 1,
expected: 3,
}
));
db.database()
.update("model_context_projection_state")
.value(
"schema_version",
DatabaseValue::Int64(i64::from(MODEL_CONTEXT_PROJECTION_SCHEMA_VERSION)),
)
.execute(db.database())
.await
.expect("restore version");
db.database()
.update("model_context_entries")
.value("event_type", "assistant_message")
.where_eq("event_seq", seq_to_value(0))
.execute(db.database())
.await
.expect("corrupt entry identity");
assert!(matches!(
db.model_context_events()
.await
.expect_err("corrupt entry must surface"),
SessionDbError::InvalidRow { .. }
));
}
#[tokio::test]
async fn stale_model_context_projection_is_visible_and_never_falls_back() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("projection".to_string()),
working_directory: temp_dir.path().to_path_buf(),
},
))
.await
.expect("append projected event");
insert_event(
db.database(),
&event(
session_id,
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "unprojected tail".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
None,
)
.await
.expect("append canonical tail only");
let error = db
.model_context_events()
.await
.expect_err("stale projection must be visible");
assert!(matches!(
error,
SessionDbError::ModelContextProjectionStale {
checkpoint: 0,
expected: 1
}
));
}
#[tokio::test]
async fn uncompacted_model_context_is_not_limited_by_event_count_or_irrelevant_history() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let tx = db.database().begin_transaction().await.expect("begin");
for sequence in 0..513 {
insert_event(
&*tx,
&event(
session_id,
sequence,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: format!("semantic-{sequence}"),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
None,
)
.await
.expect("insert semantic event");
}
for sequence in 513..8_706 {
insert_event(
&*tx,
&event(
session_id,
sequence,
SessionEventKind::SessionCreated {
name: Some(format!("irrelevant-{sequence}")),
working_directory: temp_dir.path().to_path_buf(),
},
),
None,
)
.await
.expect("insert irrelevant event");
}
insert_event(
&*tx,
&event(
session_id,
8_706,
SessionEventKind::AssistantMessage {
text: "newest semantic event".to_string(),
},
),
None,
)
.await
.expect("insert newest semantic event");
tx.commit().await.expect("commit");
assert!(matches!(
db.model_context_events()
.await
.expect_err("normal context read must not scan canonical events"),
SessionDbError::ProjectionStale {
projection: "model_context",
checkpoint: None,
expected: 8_706
}
));
assert_eq!(
reindex_model_context_for_test(&db, temp_dir.path(), session_id).await,
8_707
);
let events = db.model_context_events().await.expect("projected context");
assert_eq!(events.len(), 514);
assert_eq!(events.first().map(|event| event.sequence), Some(0));
assert_eq!(events.last().map(|event| event.sequence), Some(8_706));
}
#[tokio::test]
async fn context_occupancy_projection_reconciles_requests_and_boundaries() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::ModelChanged {
provider: "provider".to_string(),
model: "model".to_string(),
selection_source: bcode_session_models::ModelSelectionSource::UserExplicit,
},
))
.await
.expect("append model boundary");
db.append_event(&request_context_event(session_id, 1))
.await
.expect("append estimate");
let occupancy = db
.current_context_occupancy()
.await
.expect("occupancy")
.expect("estimate should project");
assert_eq!(occupancy.observation_sequence, 1);
db.append_event(&event(
session_id,
2,
SessionEventKind::ContextCompacted {
summary: "summary".to_string(),
compacted_through_sequence: 1,
},
))
.await
.expect("append compaction boundary");
assert_eq!(
db.current_context_occupancy().await.expect("occupancy"),
None
);
}
#[tokio::test]
async fn explicit_schema_v3_context_occupancy_migration_resets_incompatible_derived_state() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::ModelChanged {
provider: "provider".to_string(),
model: "model".to_string(),
selection_source: bcode_session_models::ModelSelectionSource::UserExplicit,
},
))
.await
.expect("append model boundary");
db.database()
.update("context_occupancy_projection")
.value("schema_version", DatabaseValue::Int32(3))
.value(
"occupancy_json",
r#"{"context_epoch":0,"observation_sequence":1,"snapshot":{"invocation":{},"context_through_sequence":0}}"#,
)
.where_eq("projection_id", CONTEXT_OCCUPANCY_PROJECTION_ID)
.execute(db.database())
.await
.expect("install schema v3 occupancy");
db.database()
.delete(SESSION_MIGRATIONS_TABLE)
.where_eq("id", "024_reset_request_context_occupancy_projection")
.execute(db.database())
.await
.expect("mark compatibility migration pending");
drop(db);
let maintenance =
crate::lease::acquire_session_maintenance_guard(temp_dir.path(), session_id)
.expect("maintenance guard");
let write = crate::lease::acquire_maintenance_session_write_lock(
&maintenance,
temp_dir.path(),
session_id,
)
.expect("write guard");
let db =
SessionDb::migrate_turso_in_root(session_id, temp_dir.path(), &maintenance, &write)
.await
.expect("explicitly migrate legacy session db");
assert_eq!(db.current_context_epoch().await.expect("context epoch"), 0);
assert_eq!(
db.current_context_occupancy().await.expect("occupancy"),
None
);
db.append_event(&request_context_event(session_id, 1))
.await
.expect("append current usage");
assert!(
db.current_context_occupancy()
.await
.expect("current occupancy")
.is_some()
);
}
#[tokio::test]
async fn malformed_compaction_marker_surfaces_an_error_without_mutation() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.database()
.insert("events")
.value("event_seq", seq_to_value(0))
.value("event_type", "context_compacted")
.value(
"schema_version",
DatabaseValue::Int32(i32::from(CURRENT_SESSION_EVENT_SCHEMA_VERSION)),
)
.value("created_at_ms", seq_to_value(1))
.value("payload", "{not valid json")
.execute(db.database())
.await
.expect("insert malformed marker");
let error = db
.model_context_events()
.await
.expect_err("malformed marker must not be ignored");
assert!(matches!(
error,
SessionDbError::ProjectionStale {
projection: "model_context",
checkpoint: None,
expected: 0
}
));
assert_eq!(
db.last_event_sequence().await.expect("last sequence"),
Some(0)
);
}
#[tokio::test]
async fn model_context_read_uses_projection_without_replay_or_mutation() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "projected context".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
))
.await
.expect("append projected event");
db.database()
.update("events")
.value("payload", "corrupt canonical payload")
.where_eq("event_seq", seq_to_value(0))
.execute(db.database())
.await
.expect("corrupt canonical payload after projection");
let projection_state_before = db
.database()
.select("model_context_projection_state")
.columns(&["schema_version", "last_event_seq"])
.where_eq("projection_id", MODEL_CONTEXT_PROJECTION_ID)
.execute_first(db.database())
.await
.expect("projection state query")
.expect("projection state");
let canonical_payload_before = db
.database()
.select("events")
.columns(&["payload"])
.where_eq("event_seq", seq_to_value(0))
.execute_first(db.database())
.await
.expect("canonical event query")
.expect("canonical event");
let context = db.model_context_events().await.expect("projected context");
assert!(context.iter().any(|event| matches!(
&event.kind,
SessionEventKind::UserMessage { text, .. } if text == "projected context"
)));
let projection_state_after = db
.database()
.select("model_context_projection_state")
.columns(&["schema_version", "last_event_seq"])
.where_eq("projection_id", MODEL_CONTEXT_PROJECTION_ID)
.execute_first(db.database())
.await
.expect("projection state query")
.expect("projection state");
let canonical_payload_after = db
.database()
.select("events")
.columns(&["payload"])
.where_eq("event_seq", seq_to_value(0))
.execute_first(db.database())
.await
.expect("canonical event query")
.expect("canonical event");
assert_eq!(projection_state_after, projection_state_before);
assert_eq!(canonical_payload_after, canonical_payload_before);
}
#[tokio::test]
async fn model_context_events_start_at_latest_compaction() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "old".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
))
.await
.expect("append old event");
db.append_event(&event(
session_id,
1,
SessionEventKind::ContextCompacted {
summary: "summary".to_string(),
compacted_through_sequence: 1,
},
))
.await
.expect("append compaction");
db.append_event(&event(
session_id,
2,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "new".to_string(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
))
.await
.expect("append new event");
let events = db.model_context_events().await.expect("model context");
assert_eq!(events.len(), 2);
assert_eq!(events[0].sequence, 1);
assert_eq!(events[1].sequence, 2);
}
#[tokio::test]
async fn transcript_window_query_uses_bounded_latest_projection_rows() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
for sequence in 0..5 {
db.append_event(&event(
session_id,
sequence,
SessionEventKind::AssistantMessage {
text: format!("message {sequence}"),
},
))
.await
.expect("append event");
}
let items = db
.transcript_items_for_latest_window(2, 3, usize::MAX)
.await
.expect("transcript window items");
assert_eq!(items.len(), 3);
assert_eq!(items[0].event_seq_start, 2);
assert_eq!(items[1].event_seq_start, 3);
assert_eq!(items[2].event_seq_start, 4);
assert_eq!(
db.first_event_sequence().await.expect("first sequence"),
Some(0)
);
assert_eq!(
db.last_event_sequence().await.expect("last sequence"),
Some(4)
);
}
#[tokio::test]
async fn artifact_reference_updates_preserve_other_artifacts_and_keys() {
let temp = tempfile::tempdir().expect("temp");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, temp.path())
.await
.expect("open");
let mut artifact = bcode_session_models::ToolArtifact {
artifact_id: "first".to_owned(),
producer_plugin_id: "fixture".to_owned(),
schema: "fixture".to_owned(),
schema_version: 1,
tool_call_id: None,
title: None,
metadata: serde_json::Value::Null,
refs: vec![bcode_session_models::ToolArtifactRef {
key: "recording".to_owned(),
storage_uri: Some("file:///first".to_owned()),
content_type: None,
byte_len: Some(10),
metadata: None,
}],
};
project_artifact_references(db.database(), 1, &artifact)
.await
.expect("first");
artifact.refs[0].key = "clean".to_owned();
project_artifact_references(db.database(), 2, &artifact)
.await
.expect("second key");
artifact.artifact_id = "second".to_owned();
project_artifact_references(db.database(), 3, &artifact)
.await
.expect("second artifact");
artifact.refs[0].byte_len = Some(20);
project_artifact_references(db.database(), 4, &artifact)
.await
.expect("update");
let rows = db
.database()
.select("artifact_references")
.execute(db.database())
.await
.expect("rows");
assert_eq!(rows.len(), 3);
drop(db);
let reopened = SessionDb::open_turso_in_root(id, temp.path())
.await
.expect("reopen");
let rows = reopened
.database()
.select("artifact_references")
.execute(reopened.database())
.await
.expect("rows");
assert_eq!(rows.len(), 3);
}
#[tokio::test]
async fn artifact_sweep_high_water_excludes_new_finalizations() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("db");
db.append_event(&event(
id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: root.path().to_path_buf(),
},
))
.await
.expect("created");
for sequence in 0..2 {
if sequence == 1 {
db.append_event(&event(
id,
1,
SessionEventKind::SystemMessage {
text: "later".into(),
},
))
.await
.expect("later");
}
db.database()
.insert("artifact_references")
.value("artifact_id", format!("artifact-{sequence}"))
.value("reference_key", "recording")
.value("producer_plugin_id", "fixture")
.value("schema", "fixture")
.value("schema_version", 1)
.value("complete", true)
.value("availability", "complete")
.value("finalized_event_seq", sequence)
.execute(db.database())
.await
.expect("reference");
}
let first = db
.artifact_maintenance_page_through(None, 0)
.await
.expect("captured sweep");
assert_eq!(first, vec![("artifact-0".into(), "recording".into())]);
assert!(
db.artifact_maintenance_page_through(Some(("artifact-0", "recording")), 0)
.await
.expect("sweep done")
.is_empty()
);
assert_eq!(
db.artifact_maintenance_page_through(None, 1)
.await
.expect("next sweep")
.len(),
2
);
}
#[tokio::test]
async fn artifact_maintenance_pagination_covers_shared_ids_without_duplicates() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("database");
db.append_event(&event(
id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: root.path().to_path_buf(),
},
))
.await
.expect("created");
for index in 0..40 {
db.database()
.insert("artifact_references")
.value("artifact_id", format!("artifact-{}", index / 20))
.value("reference_key", format!("ref-{index:03}"))
.value("producer_plugin_id", "fixture")
.value("schema", "fixture")
.value("schema_version", 1)
.value("complete", true)
.value("availability", "complete")
.value("finalized_event_seq", 0)
.execute(db.database())
.await
.expect("reference");
}
let mut found = Vec::new();
loop {
let page = db
.artifact_maintenance_page(
found
.last()
.map(|(a, r): &(String, String)| (a.as_str(), r.as_str())),
)
.await
.expect("page");
assert!(page.len() <= 16);
if page.is_empty() {
break;
}
found.extend(page);
}
assert_eq!(found.len(), 40);
assert!(found.windows(2).all(|pair| pair[0] < pair[1]));
}
#[tokio::test]
async fn finalized_artifact_lookup_rejects_unsupported_storage_before_projection_access() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("database");
for epoch in [
0,
CURRENT_SESSION_STORAGE_WRITER_EPOCH - 1,
CURRENT_SESSION_STORAGE_WRITER_EPOCH + 1,
] {
db.database()
.update("session_storage_contract")
.value("writer_epoch", i64::from(epoch))
.where_eq("contract_id", SESSION_STORAGE_CONTRACT_ID)
.execute(db.database())
.await
.expect("fixture epoch");
let error = db
.finalized_artifact_reference("missing", "recording")
.await
.expect_err("unsupported");
assert!(
matches!(error, SessionDbError::WriterIncompatible { actual: Some(actual), .. } if actual == u64::from(epoch))
);
assert_eq!(
db.storage_writer_epoch().await.expect("unchanged"),
u64::from(epoch)
);
}
db.database()
.delete("session_storage_contract")
.execute(db.database())
.await
.expect("remove fixture contract");
assert!(matches!(
db.finalized_artifact_reference("missing", "recording")
.await,
Err(SessionDbError::WriterIncompatible { .. })
));
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[tokio::test]
async fn capability_artifact_compresses_through_verified_maintenance() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("database");
let invocation = "a".repeat(64);
let artifact = "b".repeat(64);
let artifact_root = root.path().join("session-artifacts").join(id.to_string());
let parent = artifact_root.join("invocation-artifacts").join(&invocation);
std::fs::create_dir_all(&parent).expect("directory");
let path = parent.join(format!("{artifact}.bin"));
let bytes = b"terminal output\n".repeat(100_000);
std::fs::write(&path, &bytes).expect("raw");
db.append_event(&event(id, 0, SessionEventKind::ToolInvocationResultRecorded {
record: bcode_session_models::ToolInvocationResultRecord {
invocation_id: "fixture".into(), model_output: "done".into(), is_error: false,
presentation: None, content: vec![], result: Some(bcode_session_models::ToolInvocationResult::Artifact {
artifact: Box::new(bcode_session_models::ToolArtifact {
artifact_id: "artifact".into(), producer_plugin_id: "fixture".into(), schema: "fixture".into(),
schema_version: 1, tool_call_id: None, title: None, metadata: serde_json::Value::Null,
refs: vec![bcode_session_models::ToolArtifactRef {
key: "recording".into(), content_type: None,
storage_uri: Some(format!("bcode-artifact://invocation/{invocation}/{artifact}")),
byte_len: Some(bytes.len() as u64),
metadata: Some(serde_json::json!({"complete": true, "availability": "complete", "checksum_sha256": "0".repeat(64)})),
}],
}),
}),
},
})).await.expect("canonical artifact");
db.database()
.update("artifact_references")
.value("checksum_sha256", "0".repeat(64))
.execute(db.database())
.await
.expect("old projection");
db.database()
.update("events")
.value("created_at_ms", 1_000_000_i64)
.where_eq("event_seq", 0)
.execute(db.database())
.await
.expect("finalization timestamp");
db.database().close().await.expect("close");
drop(db);
let mut tracking = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(root.path().join(id.to_string()).join("storage-access.bin"))
.expect("tracking");
crate::storage_access::record_access(
&mut tracking,
crate::storage_access::StorageAccessKind::History,
1,
)
.expect("old access");
drop(tracking);
let young = crate::artifact_storage::compress_finalized_artifact_with_age(
root.path(),
id,
"artifact",
"recording",
crate::artifact_compression::ArtifactCompression::Light,
4096,
Some((1_000_010, 100)),
)
.await
.expect("young artifact");
assert_eq!(
young,
crate::artifact_storage::ArtifactStorageOutcome::Unchanged
);
assert!(path.is_file());
let outcome = crate::artifact_storage::compress_finalized_artifact(
root.path(),
id,
"artifact",
"recording",
crate::artifact_compression::ArtifactCompression::Light,
4096,
)
.await
.expect("compress");
assert!(matches!(
outcome,
crate::artifact_storage::ArtifactStorageOutcome::Compressed { .. }
));
assert!(path.is_dir());
assert_eq!(
crate::artifact_storage::read_artifact_range(&artifact_root, &path, 262_140, 80)
.expect("read")
.1,
bytes[262_140..262_220]
);
}
#[tokio::test]
async fn history_compression_crash_child() {
let Ok(root) = std::env::var("BCODE_HISTORY_CRASH_ROOT") else {
return;
};
let id = std::env::var("BCODE_HISTORY_CRASH_ID")
.expect("id")
.parse()
.expect("session id");
let db = SessionDb::open_existing_turso_in_root(id, std::path::Path::new(&root))
.await
.expect("open");
let mut checks = 0;
db.compress_history_payload_page_checked(0, 1, None, || {
checks += 1;
if checks == 3 {
std::process::exit(94);
}
Ok(())
})
.await
.expect("compression");
panic!("crash boundary not reached");
}
#[tokio::test]
async fn process_crash_before_history_commit_preserves_history_and_retry() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("db");
let original = event(
id,
0,
SessionEventKind::SessionCreated {
name: Some("crash history 世界".repeat(20_000)),
working_directory: root.path().to_path_buf(),
},
);
db.append_event(&original).await.expect("append");
let before = db.canonical_rows_page(0, 1).await.expect("before")[0]
.payload
.clone();
db.database().close().await.expect("close");
drop(db);
let status = std::process::Command::new(std::env::current_exe().expect("executable"))
.args(["--exact", "db::tests::history_compression_crash_child"])
.env("BCODE_HISTORY_CRASH_ROOT", root.path())
.env("BCODE_HISTORY_CRASH_ID", id.to_string())
.status()
.expect("child");
assert_eq!(status.code(), Some(94));
let db = SessionDb::open_existing_turso_in_root(id, root.path())
.await
.expect("recover");
assert_eq!(
db.canonical_rows_page(0, 1).await.expect("recovered")[0].payload,
before
);
assert_eq!(
db.all_events_strict().await.expect("history"),
vec![original.clone()]
);
assert!(
db.compress_history_payload_page(0, 1)
.await
.expect("retry")
.compressed
> 0
);
assert_eq!(
db.all_events_strict().await.expect("compressed history"),
vec![original]
);
db.database().close().await.expect("close");
}
#[tokio::test]
async fn cancellation_before_history_commit_rolls_back_compressed_payloads() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("db");
let original = event(
id,
0,
SessionEventKind::SessionCreated {
name: Some("cancel".repeat(20_000)),
working_directory: root.path().to_path_buf(),
},
);
db.append_event(&original).await.expect("event");
let before = db.canonical_rows_page(0, 1).await.expect("before")[0]
.payload
.clone();
let mut checks = 0;
let error = db
.compress_history_payload_page_checked(0, 1, None, || {
checks += 1;
if checks == 3 {
Err(std::io::Error::new(
std::io::ErrorKind::Interrupted,
"cancel before commit",
))
} else {
Ok(())
}
})
.await
.expect_err("cancelled");
assert!(
matches!(error, SessionDbError::Io(ref error) if error.kind() == std::io::ErrorKind::Interrupted)
);
let row = db
.database()
.select("events")
.columns(&["payload"])
.where_eq("event_seq", 0)
.execute_first(db.database())
.await
.expect("row")
.expect("exists");
assert_eq!(
required_string(&row, "payload").expect("unchanged physical bytes"),
before
);
assert_eq!(
db.all_events_strict().await.expect("history"),
vec![original]
);
}
#[tokio::test]
async fn history_sweep_rejects_sequence_gaps_and_missing_captured_tail() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("db");
db.append_event(&event(
id,
0,
SessionEventKind::SessionCreated {
name: Some("compressible".repeat(2000)),
working_directory: root.path().to_path_buf(),
},
))
.await
.expect("create");
for sequence in 1..=2 {
db.append_event(&event(
id,
sequence,
SessionEventKind::SystemMessage {
text: "history".repeat(2000),
},
))
.await
.expect("append");
}
let row = db
.database()
.select("events")
.columns(&["payload"])
.where_eq("event_seq", 0)
.execute_first(db.database())
.await
.expect("query")
.expect("row");
let original = required_string(&row, "payload").expect("payload");
db.database()
.delete("events")
.where_eq("event_seq", 1)
.execute(db.database())
.await
.expect("fixture gap");
assert!(matches!(
db.compress_history_payload_page_through(0, 1, None, Some(2), || Ok(()))
.await,
Err(SessionDbError::InvalidCanonicalSequence {
expected: 1,
actual: 2
})
));
let row = db
.database()
.select("events")
.columns(&["payload"])
.where_eq("event_seq", 0)
.execute_first(db.database())
.await
.expect("query")
.expect("row");
assert_eq!(
required_string(&row, "payload").expect("rollback"),
original
);
assert!(
db.compress_history_payload_page_through(0, 1, None, Some(3), || Ok(()))
.await
.is_err()
);
db.database()
.delete("events")
.where_eq("event_seq", 2)
.execute(db.database())
.await
.expect("fixture lost tail");
assert!(
db.compress_history_payload_page_through(1, 1, None, Some(2), || Ok(()))
.await
.is_err()
);
}
#[tokio::test]
async fn history_maintenance_rolls_back_page_on_corrupt_event() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("db");
db.append_event(&event(
id,
0,
SessionEventKind::SessionCreated {
name: Some("rollback".repeat(10_000)),
working_directory: root.path().to_path_buf(),
},
))
.await
.expect("created");
let before = db
.database()
.select("events")
.columns(&["payload"])
.where_eq("event_seq", 0)
.execute_first(db.database())
.await
.expect("row")
.expect("exists");
let before = required_string(&before, "payload").expect("payload");
db.database()
.insert("events")
.value("event_seq", 1)
.value("event_type", "session_created")
.value("schema_version", 47)
.value("payload", "{}")
.execute(db.database())
.await
.expect("corrupt fixture");
assert!(db.compress_history_payload_page(0, 1).await.is_err());
let after = db
.database()
.select("events")
.columns(&["payload"])
.where_eq("event_seq", 0)
.execute_first(db.database())
.await
.expect("row")
.expect("exists");
assert_eq!(required_string(&after, "payload").expect("payload"), before);
}
#[tokio::test]
async fn age_checked_history_preserves_recent_events_and_rechecks_access() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("db");
let mut old = event(
id,
0,
SessionEventKind::SessionCreated {
name: Some("old".repeat(20_000)),
working_directory: root.path().to_path_buf(),
},
);
old.timestamp_ms = 1;
db.append_event(&old).await.expect("old");
let mut recent = event(
id,
1,
SessionEventKind::SessionRenamed {
name: Some("recent".repeat(20_000)),
},
);
recent.timestamp_ms = 950;
db.append_event(&recent).await.expect("recent");
db.database().close().await.expect("close");
drop(db);
let path = root.path().join(id.to_string()).join("storage-access.bin");
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(path)
.expect("tracking");
crate::storage_access::record_access(
&mut file,
crate::storage_access::StorageAccessKind::History,
1,
)
.expect("old access");
let page = crate::history_compression::compress_history_page_with_age(
root.path(),
id,
0,
1,
Some((1000, 100)),
)
.await
.expect("compress");
assert_eq!(
(page.inspected, page.compressed, page.next_sequence),
(2, 1, Some(2))
);
crate::storage_access::record_access(
&mut file,
crate::storage_access::StorageAccessKind::History,
2000,
)
.expect("new access");
let page = crate::history_compression::compress_history_page_with_age(
root.path(),
id,
0,
12,
Some((2050, 100)),
)
.await
.expect("defer");
assert_eq!(page.inspected, 0);
let db = SessionDb::open_existing_turso_in_root(id, root.path())
.await
.expect("reopen");
assert_eq!(
db.all_events_strict().await.expect("unchanged history"),
vec![old, recent]
);
}
#[tokio::test]
async fn history_maintenance_writes_lossless_compression_and_is_idempotent() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("db");
let created = event(
id,
0,
SessionEventKind::SessionCreated {
name: Some("history".repeat(10_000)),
working_directory: root.path().to_path_buf(),
},
);
db.append_event(&created).await.expect("created");
let logical = db.canonical_rows_page(0, 1).await.expect("logical")[0]
.payload
.clone();
db.database().close().await.expect("close");
drop(db);
let report = crate::history_compression::compress_history_page(root.path(), id, 0, 1)
.await
.expect("compress");
assert_eq!(report.compressed, 1);
assert!(report.saved_bytes > 1000);
assert_eq!(report.next_sequence, Some(1));
let repeated = crate::history_compression::compress_history_page(root.path(), id, 0, 1)
.await
.expect("repeat");
assert_eq!(repeated.compressed, 0);
let end = crate::history_compression::compress_history_page(root.path(), id, 1, 1)
.await
.expect("end");
assert_eq!(end.next_sequence, None);
let db = SessionDb::open_existing_turso_in_root(id, root.path())
.await
.expect("reopen");
assert_eq!(
db.all_events_strict().await.expect("history"),
vec![created]
);
assert_eq!(
db.canonical_rows_page(0, 1).await.expect("json")[0].payload,
logical
);
let next = event(
id,
1,
SessionEventKind::SessionRenamed {
name: Some("continued".into()),
},
);
db.append_event(&next).await.expect("continue writing");
assert_eq!(db.all_events_strict().await.expect("continued")[1], next);
}
#[tokio::test]
async fn envelope_inventory_groups_orders_and_reports_truncation() {
let root = tempfile::tempdir().expect("root");
let db = SessionDb::open_turso_in_root(SessionId::new(), root.path())
.await
.expect("database");
assert_eq!(
db.event_envelope_inventory().await.expect("empty"),
(vec![], false)
);
for sequence in 0..258_i64 {
let kind = format!("fixture-{}", 256 - sequence.min(256));
db.database()
.insert("events")
.value("event_seq", sequence)
.value("event_type", kind)
.value("schema_version", 1)
.value("payload", "{}")
.execute(db.database())
.await
.expect("fixture");
if sequence == 255 {
let (inventory, truncated) = db.event_envelope_inventory().await.expect("full");
assert_eq!(inventory.len(), 256);
assert!(!truncated);
}
}
let (inventory, truncated) = db.event_envelope_inventory().await.expect("truncated");
assert!(truncated);
assert_eq!(inventory.len(), 256);
for (index, (version, kind)) in inventory.iter().enumerate() {
assert_eq!(*version, 1);
assert_eq!(*kind, format!("fixture-{}", 256 - index));
}
}
#[tokio::test]
async fn canonical_physical_budget_counts_utf8_and_embedded_nul() {
let root = tempfile::tempdir().expect("root");
let db = SessionDb::open_turso_in_root(SessionId::new(), root.path())
.await
.expect("database");
let payload = format!("\0{}", "🦀".repeat(CANONICAL_PAGE_DECODED_BYTES / 8));
for sequence in 0..2_i64 {
db.database()
.insert("events")
.value("event_seq", sequence)
.value("event_type", "fixture")
.value("schema_version", 1)
.value("payload", payload.clone())
.execute(db.database())
.await
.expect("fixture");
}
assert_eq!(
canonical_payload_fetch_limit(db.database(), 0, 2)
.await
.expect("bounded"),
1
);
assert_eq!(
canonical_payload_fetch_limit(db.database(), 1, 2)
.await
.expect("resume"),
1
);
assert_eq!(
canonical_payload_fetch_limit(db.database(), 0, 0)
.await
.expect("zero"),
0
);
assert_eq!(
canonical_payload_fetch_limit(db.database(), 2, 2)
.await
.expect("end"),
0
);
}
#[tokio::test]
async fn canonical_page_cursor_past_sql_integer_range_does_not_repeat_tail() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("db");
db.database()
.insert("events")
.value("event_seq", i64::MAX)
.value("event_type", "fixture")
.value("schema_version", 1)
.value("payload", "{}")
.execute(db.database())
.await
.expect("fixture");
let page = db
.canonical_rows_page(i64::MAX as u64, 3)
.await
.expect("tail");
assert_eq!(page.len(), 1);
assert!(
db.canonical_rows_page(i64::MAX as u64 + 1, 3)
.await
.expect("past tail")
.is_empty()
);
assert!(
db.canonical_rows_page(0, 0)
.await
.expect("zero page")
.is_empty()
);
}
#[tokio::test]
async fn canonical_pages_stop_before_exceeding_decoded_budget_and_resume() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("database");
let logical = serde_json::json!({"text": "x".repeat(12 * 1024 * 1024)}).to_string();
let compressed =
crate::event_compression::compress_event_payload(&logical, 1).expect("compress");
for sequence in 0..3 {
db.database()
.insert("events")
.value("event_seq", sequence)
.value("event_type", "fixture")
.value("schema_version", 1)
.value("payload", compressed.clone())
.execute(db.database())
.await
.expect("fixture");
}
let first = db.canonical_rows_page(0, 3).await.expect("first page");
assert_eq!(first.len(), 2);
assert!(
first.iter().map(|row| row.payload.len()).sum::<usize>()
<= CANONICAL_PAGE_DECODED_BYTES
);
let next = first.last().expect("last").sequence + 1;
drop(first);
let second = db.canonical_rows_page(next, 3).await.expect("second page");
assert_eq!(second.len(), 1);
assert_eq!(second[0].sequence, 2);
assert_eq!(second[0].payload, logical);
assert!(db.canonical_rows_page(3, 3).await.expect("end").is_empty());
}
#[tokio::test]
async fn migration_digest_is_storage_neutral_and_rejects_sequence_damage() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("db");
let mut expected = Sha256::new();
for sequence in 0..u64::try_from(MIGRATION_EVENT_PAGE_SIZE + 3).expect("bound") {
let logical = serde_json::json!({"sequence": sequence, "private_evidence": "preserve all fields ".repeat(100)}).to_string();
expected.update((logical.len() as u64).to_le_bytes());
expected.update(logical.as_bytes());
let payload = if sequence % 2 == 0 {
crate::event_compression::compress_event_payload(&logical, 1).expect("compress")
} else {
logical
};
db.database()
.insert("events")
.value("event_seq", seq_to_value(sequence))
.value("event_type", "fixture")
.value("schema_version", 1)
.value("payload", payload)
.execute(db.database())
.await
.expect("fixture");
}
let (count, tail, actual) = migration_payload_digest(db.database())
.await
.expect("digest");
assert_eq!(count, (MIGRATION_EVENT_PAGE_SIZE + 3) as u64);
assert_eq!(tail, Some(count - 1));
assert_eq!(actual, hex::encode(expected.finalize()));
db.database()
.delete("events")
.where_eq("event_seq", 1)
.execute(db.database())
.await
.expect("damage");
assert!(matches!(
migration_payload_digest(db.database()).await,
Err(SessionDbError::InvalidCanonicalSequence {
expected: 1,
actual: 2
})
));
}
#[tokio::test]
async fn epoch_nine_upgrade_then_compression_preserves_history() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("db");
let created = event(
id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: root.path().to_path_buf(),
},
);
db.append_event(&created).await.expect("created");
let message = event(
id,
1,
SessionEventKind::SystemMessage {
text: "migrated canonical history 世界 ".repeat(150_000),
},
);
db.append_event(&message).await.expect("message");
let expected = db.all_events_strict().await.expect("history");
let json = db.canonical_rows_page(0, 16).await.expect("logical rows");
db.database()
.update("session_storage_contract")
.value("writer_epoch", 9)
.execute(db.database())
.await
.expect("epoch nine fixture");
db.database().close().await.expect("close source");
drop(db);
let maintenance =
crate::lease::acquire_session_maintenance_guard(root.path(), id).expect("maintenance");
let write =
crate::lease::acquire_maintenance_session_write_lock(&maintenance, root.path(), id)
.expect("write");
let migrated = SessionDb::migrate_turso_in_root(id, root.path(), &maintenance, &write)
.await
.expect("upgrade");
assert_eq!(
migrated.storage_writer_epoch().await.expect("writer"),
u64::from(CURRENT_SESSION_STORAGE_WRITER_EPOCH)
);
assert_eq!(
migrated
.all_events_strict()
.await
.expect("migrated history"),
expected
);
assert_eq!(
migrated
.canonical_rows_page(0, 16)
.await
.expect("migrated JSON")
.iter()
.map(|row| &row.payload)
.collect::<Vec<_>>(),
json.iter().map(|row| &row.payload).collect::<Vec<_>>()
);
migrated.database().close().await.expect("close migrated");
drop(migrated);
drop(write);
drop(maintenance);
let compressed = crate::history_compression::compress_history_page(root.path(), id, 0, 12)
.await
.expect("compress");
assert!(compressed.saved_bytes > 1024 * 1024);
let db = SessionDb::open_existing_turso_in_root(id, root.path())
.await
.expect("reopen");
assert_eq!(
db.all_events_strict().await.expect("final history"),
expected
);
let next = event(
id,
2,
SessionEventKind::SystemMessage {
text: "continued".into(),
},
);
db.append_event(&next)
.await
.expect("append after upgrade and compression");
assert_eq!(
db.all_events_strict().await.expect("continued").last(),
Some(&next)
);
}
#[tokio::test]
async fn future_compressed_payload_is_rejected_without_rewriting_history() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("database");
let created = event(
id,
0,
SessionEventKind::SessionCreated {
name: Some("future representation".repeat(1000)),
working_directory: root.path().to_path_buf(),
},
);
db.append_event(&created).await.expect("event");
let logical = encode_session_event(&created).expect("json");
let compressed =
crate::event_compression::compress_event_payload(&logical, 1).expect("compressed");
let future = compressed.replacen("bcode-event-zstd:1:", "bcode-event-zstd:2:", 1);
assert_ne!(future, compressed);
db.database()
.update("events")
.value("payload", future.clone())
.where_eq("event_seq", 0)
.execute(db.database())
.await
.expect("fixture");
assert!(
db.history_page(SessionHistoryQuery {
cursor: None,
limit: 1,
direction: SessionHistoryDirection::Backward
})
.await
.is_err()
);
assert!(db.all_events().await.is_err());
assert!(db.canonical_rows_page(0, 1).await.is_err());
let row = db
.database()
.select("events")
.columns(&["payload"])
.where_eq("event_seq", 0)
.execute_first(db.database())
.await
.expect("query")
.expect("row");
assert_eq!(required_string(&row, "payload").expect("preserved"), future);
}
#[tokio::test]
async fn compressed_canonical_payloads_preserve_bounded_history_and_migration_json() {
let root = tempfile::tempdir().expect("root");
let id = SessionId::new();
let db = SessionDb::open_turso_in_root(id, root.path())
.await
.expect("database");
let created = event(
id,
0,
SessionEventKind::SessionCreated {
name: None,
working_directory: root.path().to_path_buf(),
},
);
db.append_event(&created).await.expect("created");
let message = event(
id,
1,
SessionEventKind::UserMessage {
text: "history payload 世界 ".repeat(10_000),
client_id: ClientId::new(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
);
db.append_event(&message).await.expect("message");
let logical = encode_session_event(&message).expect("logical JSON");
let compressed =
crate::event_compression::compress_event_payload(&logical, 12).expect("compressed");
assert!(compressed.len() < logical.len());
db.database()
.update("events")
.value("payload", compressed.clone())
.where_eq("event_seq", 1)
.execute(db.database())
.await
.expect("fixture payload");
let page = db
.history_page(SessionHistoryQuery {
cursor: None,
limit: 1,
direction: SessionHistoryDirection::Backward,
})
.await
.expect("history");
assert_eq!(page.events, vec![message.clone()]);
let window = db
.history_around(bcode_session_models::SessionHistoryAroundQuery {
sequence: 1,
before: 1,
after: 0,
})
.await
.expect("around");
assert!(window.events.contains(&message));
let rows = db
.canonical_rows_page(1, 1)
.await
.expect("logical canonical page");
assert_eq!(rows[0].payload, logical);
assert_eq!(rows[0].sequence, 1);
let stored = db
.database()
.select("events")
.columns(&["payload"])
.where_eq("event_seq", 1)
.execute_first(db.database())
.await
.expect("query")
.expect("row");
assert_eq!(
required_string(&stored, "payload").expect("physical payload"),
compressed
);
}
#[tokio::test]
async fn finalized_artifact_references_are_projected_for_bounded_lookup() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("artifact projection".to_owned()),
working_directory: std::path::PathBuf::from("/tmp"),
},
))
.await
.expect("append session creation");
db.append_event(&event(
session_id,
1,
SessionEventKind::ToolInvocationResultRecorded {
record: bcode_session_models::ToolInvocationResultRecord {
invocation_id: "call-1".to_owned(),
model_output: "done".to_owned(),
is_error: false,
presentation: None,
result: Some(ToolInvocationResult::Artifact {
artifact: Box::new(bcode_session_models::ToolArtifact {
artifact_id: "artifact-1".to_owned(),
producer_plugin_id: "fixture.plugin".to_owned(),
schema: "fixture.recording".to_owned(),
schema_version: 3,
tool_call_id: Some("call-1".to_owned()),
title: None,
metadata: serde_json::Value::Null,
refs: vec![bcode_session_models::ToolArtifactRef {
key: "recording".to_owned(),
content_type: Some("application/octet-stream".to_owned()),
storage_uri: Some("file:///tmp/recording".to_owned()),
byte_len: Some(42),
metadata: Some(serde_json::json!({
"availability": "complete",
"complete": true,
"checksum_sha256": "abc123",
"plugin_only": "must not be projected"
})),
}],
}),
}),
content: Vec::new(),
},
},
))
.await
.expect("append finalized artifact");
let reference = db
.finalized_artifact_reference("artifact-1", "recording")
.await
.expect("projected lookup")
.expect("reference");
assert_eq!(reference.producer_plugin_id, "fixture.plugin");
assert_eq!(reference.schema, "fixture.recording");
assert_eq!(reference.schema_version, 3);
assert_eq!(reference.byte_len, Some(42));
assert_eq!(reference.finalized_event_seq, 1);
assert_eq!(reference.availability.as_deref(), Some("complete"));
assert_eq!(reference.complete, Some(true));
assert_eq!(reference.checksum_sha256, None);
assert!(
db.finalized_artifact_reference("artifact-1", "missing")
.await
.expect("missing lookup")
.is_none()
);
}
#[tokio::test]
async fn corrupt_finalized_artifact_projection_row_is_rejected() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.append_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("artifact projection".to_owned()),
working_directory: std::path::PathBuf::from("/tmp"),
},
))
.await
.expect("append session creation");
db.database()
.insert("artifact_references")
.value("artifact_id", "artifact")
.value("reference_key", "recording")
.value("producer_plugin_id", "plugin")
.value("schema", "schema")
.value(
"schema_version",
DatabaseValue::String("invalid".to_owned()),
)
.value("finalized_event_seq", seq_to_value(0))
.execute(db.database())
.await
.expect("insert corrupt projection row");
let error = db
.finalized_artifact_reference("artifact", "recording")
.await
.expect_err("corrupt projection row must be rejected");
assert!(matches!(error, SessionDbError::InvalidRow { .. }));
}
#[tokio::test]
async fn legacy_session_rejects_append_before_creating_partial_artifact_projection() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
db.database()
.insert("events")
.value("event_seq", seq_to_value(0))
.value("event_type", "session_created")
.value(
"schema_version",
DatabaseValue::Int32(i32::from(CURRENT_SESSION_EVENT_SCHEMA_VERSION)),
)
.value("created_at_ms", seq_to_value(0))
.value(
"payload",
encode_session_event(&event(
session_id,
0,
SessionEventKind::SessionCreated {
name: Some("legacy".to_owned()),
working_directory: std::path::PathBuf::from("/tmp"),
},
))
.expect("encode legacy event"),
)
.execute(db.database())
.await
.expect("insert legacy canonical event");
let error = db
.append_event(&event(
session_id,
1,
SessionEventKind::AssistantMessage {
text: "new tail".to_owned(),
},
))
.await
.expect_err("append must reject incomplete legacy projections");
assert!(matches!(
error,
SessionDbError::ProjectionStale {
projection: "model_context",
checkpoint: None,
expected: 0
}
));
assert_eq!(
db.last_event_sequence().await.expect("canonical tail"),
Some(0)
);
assert_eq!(
db.materialized_projection_checkpoint(MaterializedProjection::ArtifactReferences)
.await
.expect("artifact checkpoint"),
None
);
}
#[tokio::test]
async fn structured_inspection_filters_failed_tools_with_bounded_candidates() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
for (sequence, is_error) in [(0, false), (1, true), (2, true)] {
db.append_event(&event(
session_id,
sequence,
SessionEventKind::ToolInvocationResultRecorded {
record: bcode_session_models::ToolInvocationResultRecord {
invocation_id: format!("call-{sequence}"),
model_output: format!("output {sequence}"),
is_error,
presentation: None,
result: None,
content: Vec::new(),
},
},
))
.await
.expect("append tool result");
}
let page = db
.inspection_page(SessionInspectionQuery {
category: SessionInspectionCategory::FailedToolCalls,
cursor: None,
limit: 1,
direction: SessionHistoryDirection::Forward,
})
.await
.expect("inspection page");
assert_eq!(page.events.len(), 1);
assert_eq!(page.events[0].sequence, 1);
assert_eq!(page.scanned_events, 2);
assert_eq!(page.next_cursor, Some(SessionHistoryCursor { sequence: 2 }));
assert!(page.has_more);
let next = db
.inspection_page(SessionInspectionQuery {
category: SessionInspectionCategory::FailedToolCalls,
cursor: page.next_cursor,
limit: 1,
direction: SessionHistoryDirection::Forward,
})
.await
.expect("next inspection page");
assert_eq!(next.events.len(), 1);
assert_eq!(next.events[0].sequence, 2);
assert!(!next.has_more);
}
#[tokio::test]
async fn structured_inspection_rejects_oversized_result_limit() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let error = db
.inspection_page(SessionInspectionQuery {
category: SessionInspectionCategory::Permissions,
cursor: None,
limit: MAX_SESSION_INSPECTION_EVENTS + 1,
direction: SessionHistoryDirection::Forward,
})
.await
.expect_err("oversized inspection must fail");
assert!(matches!(
error,
SessionDbError::HistoryReadLimitExceeded {
maximum: MAX_SESSION_INSPECTION_EVENTS,
..
}
));
}
#[tokio::test]
async fn history_around_reads_a_bounded_canonical_window() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
for sequence in 0..5 {
db.append_event(&event(
session_id,
sequence,
SessionEventKind::AssistantMessage {
text: format!("message {sequence}"),
},
))
.await
.expect("append event");
}
let window = db
.history_around(SessionHistoryAroundQuery {
sequence: 2,
before: 1,
after: 1,
})
.await
.expect("history window");
assert_eq!(
window
.events
.iter()
.map(|event| event.sequence)
.collect::<Vec<_>>(),
vec![1, 2, 3]
);
assert!(window.anchor_present);
assert_eq!(window.first_available_sequence, Some(0));
assert_eq!(window.last_available_sequence, Some(4));
}
#[tokio::test]
async fn bounded_history_reads_reject_oversized_limits() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
let page_error = db
.history_page(SessionHistoryQuery {
cursor: None,
direction: SessionHistoryDirection::Forward,
limit: MAX_SESSION_HISTORY_READ_EVENTS + 1,
})
.await
.expect_err("oversized page must fail");
assert!(matches!(
page_error,
SessionDbError::HistoryReadLimitExceeded { .. }
));
let window_error = db
.history_around(SessionHistoryAroundQuery {
sequence: 0,
before: MAX_SESSION_HISTORY_READ_EVENTS,
after: 0,
})
.await
.expect_err("oversized window must fail");
assert!(matches!(
window_error,
SessionDbError::HistoryReadLimitExceeded { .. }
));
}
#[tokio::test]
async fn events_range_reads_canonical_db_events() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let session_id = SessionId::new();
let db = SessionDb::open_turso_in_root(session_id, temp_dir.path())
.await
.expect("open session db");
for sequence in 0..3 {
db.append_event(&event(
session_id,
sequence,
SessionEventKind::AssistantMessage {
text: format!("message {sequence}"),
},
))
.await
.expect("append event");
}
let events = db.events_range(1, 2, 10).await.expect("events range");
assert_eq!(events.len(), 2);
assert_eq!(events[0].sequence, 1);
assert_eq!(events[1].sequence, 2);
}
}