use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(not(target_arch = "wasm32"))]
use std::time::{SystemTime, UNIX_EPOCH};
fn now_nanos() -> u128 {
#[cfg(target_arch = "wasm32")]
{
0
}
#[cfg(not(target_arch = "wasm32"))]
{
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|elapsed| elapsed.as_nanos())
.unwrap_or(0)
}
}
fn now_unix_secs() -> u64 {
#[cfg(target_arch = "wasm32")]
{
0
}
#[cfg(not(target_arch = "wasm32"))]
{
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|elapsed| elapsed.as_secs())
.unwrap_or(0)
}
}
use serde_json::{Map, Number, Value};
use super::{positive_ttl, MemoryService, Metadata, HUB_FIELD};
use crate::context::model::{
CompilePolicy, CompileRequest, CompiledContext, ContextDecision, ContextFragment,
ContextSavings, ContextSource, ImportanceWeights, LoadedWorkingContext, MediaRef, MemoryScope,
WorkingContext, WorkingContextIndex, WorkingContextSession,
};
use crate::context::{media, provenance, ContextCompiler};
use crate::embedder::Embedder;
use crate::error::MemoryError;
use crate::id::stable_id;
use crate::model::FusionOptions;
use crate::storage::{FactStore, GraphStore, RecallStore};
const SOURCE_ID_SALT: &str = "veles-ctx-source:";
const EVENT_ID_SALT: &str = "veles-ctx-event:";
const WORKING_ID_SALT: &str = "veles-ctx-working:";
const WORKING_INDEX_ID_SALT: &str = "veles-ctx-working-index:";
const EVENT_ANCHOR: &str = "veles context compilation event";
use crate::storage::{
CTX_EVENT_FIELD, CTX_SOURCE_FIELD, CTX_WORKING_FIELD, CTX_WORKING_INDEX_FIELD,
};
const CTX_PROJECT_FIELD: &str = "_veles_ctx_project";
const CTX_MODEL_FIELD: &str = "_veles_ctx_model";
const CTX_SOURCE_MEDIA_FIELD: &str = "_veles_ctx_source_media";
const EXPIRES_AT_FIELD: &str = "_veles_expires_at";
const CTX_SESSION_FIELD: &str = "_veles_ctx_session";
const CTX_TOKENS_IN_FIELD: &str = "_veles_ctx_tokens_in";
const CTX_TOKENS_OUT_FIELD: &str = "_veles_ctx_tokens_out";
const CTX_TOKENS_SAVED_FIELD: &str = "_veles_ctx_tokens_saved";
const CTX_COST_FIELD: &str = "_veles_ctx_cost_micros";
const CTX_CURRENCY_FIELD: &str = "_veles_ctx_currency";
const CTX_AT_FIELD: &str = "_veles_ctx_at";
const CTX_SAVED_AT_FIELD: &str = "_veles_ctx_saved_at";
static EVENT_SEQ: AtomicU64 = AtomicU64::new(0);
static WORKING_INDEX_WRITE: parking_lot::Mutex<()> = parking_lot::Mutex::new(());
fn bound_sessions(sessions: &mut Vec<WorkingContextSession>, just_saved: &str) {
use crate::limits::MAX_WORKING_SESSIONS_PER_PROJECT as CAP;
if sessions.len() <= CAP {
return;
}
let unpinned_from = usize::from(move_to_front(sessions, just_saved));
sessions[unpinned_from..].sort_by_key(|s| std::cmp::Reverse(s.saved_at));
sessions.truncate(CAP);
}
fn move_to_front(sessions: &mut [WorkingContextSession], session: &str) -> bool {
let Some(at) = sessions.iter().position(|s| s.session == session) else {
return false;
};
sessions[..=at].rotate_right(1);
true
}
#[path = "memory_bridge_compile.rs"]
mod compile;
impl<E: Embedder, S: FactStore> MemoryService<E, S> {
pub fn save_working_context(
&self,
project: &str,
session: &str,
working: &WorkingContext,
) -> Result<u64, MemoryError> {
let _generation = self.enter_generation();
for (key, value) in [("project", project), ("session", session)] {
if value.trim().is_empty() {
return Err(MemoryError::EmptyWorkingContextKey { key });
}
if value.len() > crate::limits::MAX_WORKING_CONTEXT_KEY_BYTES {
return Err(MemoryError::WorkingContextKeyTooLong {
key,
len: value.len(),
max: crate::limits::MAX_WORKING_CONTEXT_KEY_BYTES,
});
}
}
if working.is_empty() {
return Err(MemoryError::EmptyWorkingContext);
}
let content =
serde_json::to_string(working).map_err(|err| MemoryError::WorkingContextCodec {
detail: "encoding the working context for storage".to_owned(),
source: Some(Box::new(err)),
})?;
if content.len() > crate::limits::MAX_FACT_BYTES {
return Err(MemoryError::ContextOverLimit(format!(
"working context of {} bytes exceeds the cap of {} bytes",
content.len(),
crate::limits::MAX_FACT_BYTES
)));
}
let id = working_id(project, session);
let embedding = self
.embedder
.embed(&format!("working context {project} {session}"))?;
let saved_at = now_unix_secs();
let meta = system_meta(&[
(CTX_WORKING_FIELD, Value::Bool(true)),
(CTX_PROJECT_FIELD, Value::String(project.to_owned())),
(CTX_SESSION_FIELD, Value::String(session.to_owned())),
(CTX_SAVED_AT_FIELD, Value::from(saved_at)),
]);
self.store_fact(id, &content, &embedding, Some(&meta), None)?;
self.update_working_index(project, session, saved_at)?;
Ok(id)
}
pub fn load_working_context(
&self,
project: &str,
session: &str,
) -> Result<Option<WorkingContext>, MemoryError> {
let _generation = self.enter_generation();
self.load_working_context_inner(project, session)
}
fn load_working_context_inner(
&self,
project: &str,
session: &str,
) -> Result<Option<WorkingContext>, MemoryError> {
let slot = working_id(project, session);
let payloads = self.store.get_metadata_batch(&[slot])?;
let marked = payloads
.into_iter()
.next()
.flatten()
.is_some_and(|meta| meta.get(CTX_WORKING_FIELD) == Some(&Value::Bool(true)));
if !marked {
return Ok(None);
}
let Some((content, _)) = self.store.get(slot)? else {
return Err(MemoryError::WorkingContextCodec {
detail: format!(
"working context for project '{project}', session '{session}' is corrupt: \
the reserved marker is present but the stored body is gone"
),
source: None,
});
};
serde_json::from_str(&content)
.map(Some)
.map_err(|err| MemoryError::WorkingContextCodec {
detail: format!(
"decoding the stored working context for project '{project}', \
session '{session}'"
),
source: Some(Box::new(err)),
})
}
pub fn resume_working_context(
&self,
project: &str,
session: &str,
) -> Result<LoadedWorkingContext, MemoryError> {
let _generation = self.enter_generation();
let working = self.load_working_context_inner(project, session)?;
let other_sessions = self.other_sessions_for(project, session, working.is_some())?;
Ok(LoadedWorkingContext {
found: working.is_some(),
working,
other_sessions,
})
}
fn other_sessions_for(
&self,
project: &str,
session: &str,
found: bool,
) -> Result<Vec<String>, MemoryError> {
let listed = match self.list_working_contexts_inner(project) {
Ok(listed) => listed,
Err(_) if found => return Ok(Vec::new()),
Err(err) => return Err(err),
};
Ok(listed
.into_iter()
.map(|entry| entry.session)
.filter(|candidate| candidate != session)
.collect())
}
fn live_sessions(
&self,
project: &str,
sessions: Vec<WorkingContextSession>,
) -> Result<Vec<WorkingContextSession>, MemoryError> {
if sessions.is_empty() {
return Ok(sessions);
}
let ids: Vec<u64> = sessions
.iter()
.map(|entry| working_id(project, &entry.session))
.collect();
let payloads = self.store.get_metadata_batch(&ids)?;
if payloads.len() != ids.len() {
return Err(MemoryError::WorkingContextCodec {
detail: format!(
"storage returned {} metadata rows for {} working-context ids",
payloads.len(),
ids.len()
),
source: None,
});
}
Ok(sessions
.into_iter()
.zip(payloads)
.filter(|(_, meta)| {
meta.as_ref()
.is_some_and(|meta| meta.get(CTX_WORKING_FIELD) == Some(&Value::Bool(true)))
})
.map(|(entry, _)| entry)
.collect())
}
fn rebuild_working_index(
&self,
project: &str,
) -> Result<Option<Vec<WorkingContextSession>>, MemoryError> {
const PAGE: usize = 256;
let wanted = Value::String(project.to_owned());
let mut sessions = Vec::new();
let mut cursor = None;
loop {
let (facts, next) = match self.store.list(cursor, PAGE) {
Ok(page) => page,
Err(MemoryError::Unsupported(_)) => return Ok(None),
Err(err) => return Err(err),
};
for fact in facts {
if fact.payload.get(CTX_WORKING_FIELD) != Some(&Value::Bool(true))
|| fact.payload.get(CTX_PROJECT_FIELD) != Some(&wanted)
{
continue;
}
let Some(Value::String(session)) = fact.payload.get(CTX_SESSION_FIELD) else {
continue;
};
let saved_at = fact.payload.get(CTX_SAVED_AT_FIELD).and_then(Value::as_u64);
sessions.push(WorkingContextSession {
session: session.clone(),
saved_at: saved_at.unwrap_or(0),
});
}
match next {
Some(at) => cursor = Some(at),
None => break,
}
}
sessions.sort_by_key(|s| std::cmp::Reverse(s.saved_at));
sessions.truncate(crate::limits::MAX_WORKING_SESSIONS_PER_PROJECT);
Ok(Some(sessions))
}
pub fn list_working_contexts(
&self,
project: &str,
) -> Result<Vec<WorkingContextSession>, MemoryError> {
let _generation = self.enter_generation();
self.list_working_contexts_inner(project)
}
fn list_working_contexts_inner(
&self,
project: &str,
) -> Result<Vec<WorkingContextSession>, MemoryError> {
let Some(index) = self.working_index(project)? else {
return Ok(Vec::new());
};
let mut sessions = self.live_sessions(project, index.sessions)?;
sessions.sort_by_key(|s| std::cmp::Reverse(s.saved_at));
Ok(sessions)
}
fn working_index(&self, project: &str) -> Result<Option<WorkingContextIndex>, MemoryError> {
let slot = working_index_id(project);
let payloads = self.store.get_metadata_batch(&[slot])?;
let marked = payloads
.into_iter()
.next()
.flatten()
.is_some_and(|meta| meta.get(CTX_WORKING_INDEX_FIELD) == Some(&Value::Bool(true)));
if !marked {
return Ok(None);
}
match self.store.get(slot)? {
Some((content, _)) => serde_json::from_str(&content).map(Some).map_err(|err| {
MemoryError::WorkingContextCodec {
detail: format!("decoding the working-context index for project '{project}'"),
source: Some(Box::new(err)),
}
}),
None => Err(MemoryError::WorkingContextCodec {
detail: format!(
"working-context index for project '{project}' is corrupt: the index \
marker is present but the stored body is gone"
),
source: None,
}),
}
}
fn update_working_index(
&self,
project: &str,
session: &str,
saved_at: u64,
) -> Result<(), MemoryError> {
let embedding = self
.embedder
.embed(&format!("working context index {project}"))?;
let mut walked: Option<Vec<WorkingContextSession>> = None;
let (_guard, mut index) = loop {
let guard = WORKING_INDEX_WRITE.lock();
match self.working_index(project) {
Ok(index) => break (guard, index.unwrap_or_default()),
Err(MemoryError::WorkingContextCodec { .. }) => {
if let Some(sessions) = walked.take() {
break (guard, WorkingContextIndex { sessions });
}
drop(guard);
walked = Some(self.rebuild_working_index(project)?.unwrap_or_default());
}
Err(err) => return Err(err),
}
};
if move_to_front(&mut index.sessions, session) {
index.sessions[0].saved_at = saved_at;
} else {
index.sessions.insert(
0,
WorkingContextSession {
session: session.to_owned(),
saved_at,
},
);
}
index.sessions = self.live_sessions(project, index.sessions)?;
bound_sessions(&mut index.sessions, session);
let content =
serde_json::to_string(&index).map_err(|err| MemoryError::WorkingContextCodec {
detail: format!("encoding the working-context index for project '{project}'"),
source: Some(Box::new(err)),
})?;
self.write_working_index(project, &content, &embedding)
}
fn write_working_index(
&self,
project: &str,
content: &str,
embedding: &[f32],
) -> Result<(), MemoryError> {
let slot = working_index_id(project);
let meta = system_meta(&[
(CTX_WORKING_INDEX_FIELD, Value::Bool(true)),
(CTX_PROJECT_FIELD, Value::String(project.to_owned())),
]);
self.store_fact(slot, content, embedding, Some(&meta), None)?;
Ok(())
}
}
const DEFAULT_MEMORY_K: usize = 5;
struct PulledMemory {
fragment: ContextFragment,
memory_id: u64,
relevance: f32,
vector_norm: f64,
graph_weight: f64,
confidence: f64,
recency: f64,
ventilated: bool,
}
struct MemoryCandidate {
memory_id: u64,
base: f64,
vector_norm: f64,
graph_weight: f64,
metadata: Option<Metadata>,
content: String,
}
impl MemoryCandidate {
fn into_pulled(self) -> PulledMemory {
#[allow(clippy::cast_possible_truncation)] let relevance = self.base as f32;
PulledMemory {
fragment: ContextFragment {
id: None,
content: self.content,
path: None,
kind: Some("memory".to_owned()),
priority: None,
metadata: None,
media: None,
},
memory_id: self.memory_id,
relevance,
vector_norm: self.vector_norm,
graph_weight: self.graph_weight,
confidence: NEUTRAL_CONFIDENCE,
recency: 0.0,
ventilated: false,
}
}
}
const NEUTRAL_CONFIDENCE: f64 = 0.5;
#[cfg(feature = "persistence")]
fn payload_confidence(payload: Option<&Metadata>) -> f64 {
f64::from(payload.map_or(
super::reinforce::RL_NEUTRAL_CONFIDENCE,
super::reinforce::read_confidence,
))
}
#[cfg(not(feature = "persistence"))]
fn payload_confidence(_payload: Option<&Metadata>) -> f64 {
NEUTRAL_CONFIDENCE
}
#[allow(
clippy::float_cmp,
reason = "an exact zero weight is the documented off switch; any non-zero weight, however small, is active"
)]
#[allow(
clippy::float_cmp,
reason = "an exact zero weight is the documented off switch for the recency term"
)]
fn system_meta(extra: &[(&str, Value)]) -> Metadata {
let mut meta = Map::new();
meta.insert(HUB_FIELD.to_owned(), Value::Bool(true));
for (key, value) in extra {
meta.insert((*key).to_owned(), value.clone());
}
meta
}
fn meta_u64(meta: &Metadata, key: &str) -> u64 {
meta.get(key).and_then(Value::as_u64).unwrap_or(0)
}
fn fragment_handle_hash(fragment: &ContextFragment) -> u64 {
fragment.media.as_ref().map_or_else(
|| stable_id(&fragment.content),
|media_ref| media::analyze(media_ref).raw_hash,
)
}
fn working_id(project: &str, session: &str) -> u64 {
stable_id(&format!("{WORKING_ID_SALT}{project}\u{1f}{session}"))
}
fn working_index_id(project: &str) -> u64 {
stable_id(&format!("{WORKING_INDEX_ID_SALT}{project}"))
}
#[cfg(all(test, feature = "persistence"))]
#[path = "memory_bridge_tests.rs"]
mod tests;