Skip to main content

Storage Layer

ClawDesk uses SochDB as its embedded ACID-compliant vector database. SochDB provides MVCC transactions, columnar storage, and hybrid search (B-Tree + HNSW) — all within the same process, eliminating the need for external database infrastructure.

Architecture Overview

Port → Adapter Mapping

Port Trait (clawdesk-storage)Adapter (clawdesk-sochdb)SochDB Feature
SessionStoreSochSessionStoreB-Tree key-value
ConversationStoreSochConversationStoreColumnar LSCS
ConfigStoreSochConfigStoreB-Tree key-value
VectorStoreSochVectorStoreHNSW index

SochDB Fundamentals

Storage Engine: LSCS

SochDB uses a Log-structured Columnar Storage (LSCS) engine that combines the benefits of LSM trees with columnar layout:

MVCC + SSI Transactions

SochDB implements Multi-Version Concurrency Control (MVCC) with Serializable Snapshot Isolation (SSI):

// MVCC transaction model in SochDB
pub struct Transaction {
/// Unique, monotonically increasing transaction ID
tx_id: TxId,

/// Snapshot timestamp — reads see data as of this point
snapshot_ts: Timestamp,

/// Write set — tracks all modifications
write_set: HashSet<Key>,

/// Read set — tracks all reads (for SSI conflict detection)
read_set: HashSet<Key>,

/// Transaction state
state: TxState,
}

#[derive(Debug)]
pub enum TxState {
Active,
Committed,
Aborted,
}

Isolation Levels

LevelReadsWritesAnomalies Prevented
Snapshot IsolationConsistent snapshotDeferred conflict checkDirty, non-repeatable, phantom
Serializable (SSI)Consistent snapshot + read trackingWrite-write + read-write conflict detectionAll anomalies
Default Isolation

SochDB defaults to SSI for all ClawDesk transactions. This prevents all concurrency anomalies including write skew, at the cost of occasional transaction aborts under high contention.

Conflict Detection

SSI detects conflicts using read-write intersection analysis:

$$ \text{conflict}(T_1, T_2) = (\text{readSet}(T_1) \cap \text{writeSet}(T_2) \neq \emptyset) \lor (\text{writeSet}(T_1) \cap \text{readSet}(T_2) \neq \emptyset) $$

When a conflict is detected, the later transaction is aborted:

// Conflict detection during commit
impl Transaction {
pub fn commit(mut self) -> Result<(), TransactionError> {
// Check for write-write conflicts
for key in &self.write_set {
if self.db.was_written_since(key, self.snapshot_ts)? {
return Err(TransactionError::WriteConflict {
key: key.clone(),
tx_id: self.tx_id,
});
}
}

// Check for read-write conflicts (SSI)
for key in &self.read_set {
if self.db.was_written_since(key, self.snapshot_ts)? {
return Err(TransactionError::SerializationFailure {
key: key.clone(),
tx_id: self.tx_id,
});
}
}

// All checks pass — durably commit
self.db.wal.append_commit(self.tx_id, &self.write_set)?;
self.state = TxState::Committed;
Ok(())
}
}

Write-Ahead Log (WAL)

All writes are first appended to the WAL for crash recovery:

/// WAL entry format
#[derive(Debug, Serialize, Deserialize)]
pub struct WalEntry {
/// Transaction ID
pub tx_id: TxId,

/// Entry sequence number (monotonic)
pub lsn: LogSequenceNumber,

/// Checksum for integrity
pub checksum: u32,

/// Operation
pub op: WalOp,
}

#[derive(Debug, Serialize, Deserialize)]
pub enum WalOp {
Put { key: Vec<u8>, value: Vec<u8> },
Delete { key: Vec<u8> },
Commit { tx_id: TxId },
Abort { tx_id: TxId },
}
Durability

The WAL uses fsync() after each commit to ensure durability. This means committed data survives process crashes and power failures. The trade-off is increased write latency (~1-2ms per sync on SSD).

Vector Search: HNSW

SochDB includes a built-in Hierarchical Navigable Small World (HNSW) index for approximate nearest neighbor search:

/// HNSW index configuration
pub struct HnswConfig {
/// Maximum number of connections per node at layer 0
pub m: usize, // default: 16

/// Maximum number of connections per node at higher layers
pub m_max: usize, // default: 32

/// Size of the dynamic candidate list during construction
pub ef_construction: usize, // default: 200

/// Size of the dynamic candidate list during search
pub ef_search: usize, // default: 100

/// Distance metric
pub metric: DistanceMetric, // default: Cosine
}

#[derive(Debug, Clone, Copy)]
pub enum DistanceMetric {
Cosine,
Euclidean,
DotProduct,
}

Search Performance

The HNSW index provides sub-linear search time:

$$ T_{\text{search}} = O(\log n \cdot ef) $$

Where $n$ is the number of indexed vectors and $ef$ is the search beam width.

Dataset Sizeef=50ef=100ef=200Recall@10
1K vectors0.2ms0.4ms0.6ms99.5%
10K vectors0.8ms1.5ms2.8ms99.2%
100K vectors2.1ms3.8ms7.2ms98.8%
1M vectors4.5ms8.2ms15.1ms98.1%

Vector Store Implementation

// crates/clawdesk-sochdb/src/vector_store.rs

pub struct SochVectorStore {
db: Database,
hnsw: HnswIndex,
dimension: usize,
}

#[async_trait]
impl VectorStore for SochVectorStore {
async fn upsert_embedding(
&self,
id: &str,
embedding: &[f32],
metadata: serde_json::Value,
) -> Result<(), StorageError> {
assert_eq!(embedding.len(), self.dimension);

let txn = self.db.begin_write()?;

// Store the raw embedding + metadata
let mut table = txn.open_table("embeddings")?;
let record = EmbeddingRecord {
id: id.to_string(),
vector: embedding.to_vec(),
metadata,
updated_at: Utc::now(),
};
table.insert(id.as_bytes(), &bincode::serialize(&record)?)?;

// Update the HNSW index
self.hnsw.insert(id, embedding)?;

txn.commit()?;
Ok(())
}

async fn search_similar(
&self,
query_embedding: &[f32],
top_k: usize,
filter: Option<VectorFilter>,
) -> Result<Vec<VectorMatch>, StorageError> {
let candidates = self.hnsw.search(query_embedding, top_k * 2)?;

// Apply metadata filters
let txn = self.db.begin_read()?;
let table = txn.open_table("embeddings")?;

let mut results = Vec::with_capacity(top_k);
for candidate in candidates {
if let Some(bytes) = table.get(candidate.id.as_bytes())? {
let record: EmbeddingRecord = bincode::deserialize(&bytes)?;

if let Some(ref f) = filter {
if !f.matches(&record.metadata) {
continue;
}
}

results.push(VectorMatch {
id: record.id,
score: candidate.distance,
metadata: record.metadata,
});

if results.len() >= top_k {
break;
}
}
}

Ok(results)
}
}

Context Queries and TOON Format

SochDB's Path API enables structured context queries using the ContextQueryBuilder. Results can be formatted in TOON (Token-Optimized Object Notation), which uses 58–67% fewer tokens than equivalent JSON.

ContextQueryBuilder

/// Builder for context-aware queries that combine
/// conversation history, vector search, and metadata filtering.
pub struct ContextQueryBuilder<'a> {
db: &'a Database,
session_key: Option<&'a SessionKey>,
query_text: Option<String>,
query_embedding: Option<Vec<f32>>,
time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
max_results: usize,
output_format: OutputFormat,
}

impl<'a> ContextQueryBuilder<'a> {
pub fn new(db: &'a Database) -> Self {
Self {
db,
session_key: None,
query_text: None,
query_embedding: None,
time_range: None,
max_results: 10,
output_format: OutputFormat::Toon,
}
}

pub fn session(mut self, key: &'a SessionKey) -> Self {
self.session_key = Some(key);
self
}

pub fn text_query(mut self, text: impl Into<String>) -> Self {
self.query_text = Some(text.into());
self
}

pub fn vector_query(mut self, embedding: Vec<f32>) -> Self {
self.query_embedding = Some(embedding);
self
}

pub fn time_range(mut self, from: DateTime<Utc>, to: DateTime<Utc>) -> Self {
self.time_range = Some((from, to));
self
}

pub fn max_results(mut self, n: usize) -> Self {
self.max_results = n;
self
}

pub fn format(mut self, format: OutputFormat) -> Self {
self.output_format = format;
self
}

pub async fn execute(&self) -> Result<ContextResult, StorageError> {
// ... combines BM25, vector search, and metadata filters
}
}

#[derive(Debug, Clone, Copy)]
pub enum OutputFormat {
Json,
Toon,
Markdown,
}

TOON Format

TOON is a compact serialization format designed for LLM context windows:

# JSON (153 tokens)
{
"messages": [
{
"role": "user",
"content": "What is ClawDesk?",
"timestamp": "2026-01-15T10:30:00Z"
},
{
"role": "assistant",
"content": "ClawDesk is a multi-channel AI agent gateway.",
"timestamp": "2026-01-15T10:30:05Z"
}
]
}

# TOON equivalent (58 tokens — 62% reduction)
messages[
{role:user content:"What is ClawDesk?" ts:2026-01-15T10:30:00Z}
{role:assistant content:"ClawDesk is a multi-channel AI agent gateway." ts:2026-01-15T10:30:05Z}
]

Key TOON optimizations:

FeatureJSONTOONSaving
Quotes on keysRequiredOmitted~15%
Commas between itemsRequiredWhitespace-delimited~5%
Null valuesExplicitly nullOmitted entirely~10%
Datetime formatISO 8601 stringCompact notation~8%
Arrays[item, item]name[item item]~5%
Nested objects{"a": {"b": 1}}{a.b:1} (path notation)~20%
Token Savings at Scale

For a 10-message conversation with tool call history, TOON typically saves 600–1,200 tokens compared to JSON. At scale this significantly increases the amount of useful context that fits within a model's context window.

Hybrid Search (Memory Subsystem)

The clawdesk-memory crate implements hybrid search combining BM25 lexical search and vector semantic search:

Reciprocal Rank Fusion (RRF)

RRF merges results from BM25 and vector search using a rank-based scoring function:

$$ \text{RRF}(d) = \sum_{r \in R} \frac{1}{k + r(d)} $$

Where $R$ is the set of result lists, $r(d)$ is the rank of document $d$ in list $r$, and $k$ is a smoothing constant (default: 60).

// crates/clawdesk-memory/src/hybrid.rs

pub struct HybridSearcher {
bm25: Bm25Index,
vector_store: Arc<dyn VectorStore>,
embedding_model: Arc<dyn EmbeddingModel>,
reranker: Option<Arc<dyn Reranker>>,
rrf_k: f32, // default: 60.0
}

impl HybridSearcher {
pub async fn search(
&self,
query: &str,
token_budget: usize,
) -> Result<ContextChunk, MemoryError> {
// 1. BM25 lexical search
let bm25_results = self.bm25.search(query, 20);

// 2. Vector similarity search
let embedding = self.embedding_model.embed(query).await?;
let vector_results = self.vector_store
.search_similar(&embedding, 20, None)
.await?;

// 3. Reciprocal Rank Fusion
let merged = reciprocal_rank_fusion(
&bm25_results,
&vector_results,
self.rrf_k,
);

// 4. Optional reranking
let ranked = match &self.reranker {
Some(reranker) => reranker.rerank(query, merged).await?,
None => merged,
};

// 5. Truncate to token budget
let mut accumulated_tokens = 0;
let mut selected = Vec::new();
for result in ranked {
let tokens = estimate_tokens(&result.content);
if accumulated_tokens + tokens > token_budget {
break;
}
accumulated_tokens += tokens;
selected.push(result);
}

Ok(ContextChunk {
results: selected,
total_tokens: accumulated_tokens,
})
}
}

BM25 Index

// crates/clawdesk-memory/src/bm25.rs

pub struct Bm25Index {
/// Inverted index: term → (doc_id, term_frequency)
index: HashMap<String, Vec<(DocId, f32)>>,

/// Document lengths for normalization
doc_lengths: HashMap<DocId, usize>,

/// Average document length
avg_doc_length: f32,

/// Total number of documents
doc_count: usize,

/// BM25 parameters
k1: f32, // default: 1.2
b: f32, // default: 0.75
}

The BM25 scoring formula:

$$ \text{BM25}(q, d) = \sum_{t \in q} \text{IDF}(t) \cdot \frac{f(t, d) \cdot (k_1 + 1)}{f(t, d) + k_1 \cdot \left(1 - b + b \cdot \frac{|d|}{\text{avgdl}}\right)} $$

Database Compaction

SochDB periodically compacts SSTables to reclaim space and merge levels:

/// Compaction runs on spawn_blocking to avoid blocking the async runtime.
pub async fn schedule_compaction(
db: Database,
interval: Duration,
token: CancellationToken,
) {
let mut ticker = tokio::time::interval(interval);

loop {
tokio::select! {
_ = ticker.tick() => {
let db = db.clone();
match tokio::task::spawn_blocking(move || db.compact()).await {
Ok(Ok(stats)) => {
tracing::info!(
reclaimed_bytes = stats.reclaimed_bytes,
merged_tables = stats.merged_tables,
"compaction completed"
);
}
Ok(Err(e)) => {
tracing::error!("compaction failed: {e}");
}
Err(e) => {
tracing::error!("compaction task panicked: {e}");
}
}
}
_ = token.cancelled() => break,
}
}
}

Storage Metrics

MetricTypical ValueNotes
Point read latency (p50)< 0.1msB-Tree, mmap
Point read latency (p99)< 1msIncluding cache miss
Write latency (p50)< 1msWAL append + fsync
Vector search (10K, ef=100)< 2msHNSW
Compaction throughput~100 MB/sLZ4 compression
Compression ratio3–5xLZ4 on columnar data
WAL write amplification1.0xSingle write to WAL
Total write amplification2–3xWAL + compaction