#![forbid(unsafe_code)]
use std::sync::Arc;
use bytes::Bytes;
use futures::StreamExt;
use lunaris_core::storage::types::{Filter, Lsn};
use lunaris_core::{Hlc, LunarisError, Scope, StorageError};
use lunaris_retrieve::Hit;
use crate::forget::{ForgetReceipt, ForgetTarget, ScopeSpec};
use crate::handle::Lunaris;
use crate::primitives::WorkingMemory;
const HELIOS_PREFIX: &str = "helios:fs/";
#[derive(Clone)]
pub struct CodingSessionMemory {
lunaris: Arc<Lunaris>,
scope: Scope,
session_prefix: String,
wm: WorkingMemory,
}
impl CodingSessionMemory {
pub fn new(lunaris: Arc<Lunaris>, scope: Scope, session_id: &str) -> Self {
let session_prefix = format!("{HELIOS_PREFIX}{session_id}/");
let wm = WorkingMemory::new(lunaris.clone(), scope.clone(), session_prefix.clone());
Self { lunaris, scope, session_prefix, wm }
}
pub async fn write(&self, path: &str, content: impl Into<String>) -> Result<Lsn, LunarisError> {
self.wm.write(path, serde_json::Value::String(content.into())).await
}
pub async fn write_dated(
&self,
path: &str,
content: impl Into<String>,
t_ref: chrono::DateTime<chrono::Utc>,
) -> Result<Lsn, LunarisError> {
self.wm.write_dated(path, serde_json::Value::String(content.into()), t_ref).await
}
pub async fn read(&self, path: &str) -> Result<Option<String>, LunarisError> {
match self.wm.read(path).await? {
Some(serde_json::Value::String(s)) => Ok(Some(s)),
Some(_) => Err(LunarisError::Storage(StorageError::Backend(
"coding_session_memory_read_unexpected_json_shape".into(),
))),
None => Ok(None),
}
}
pub async fn edit(&self, path: &str, _old: &str, new: &str) -> Result<Lsn, LunarisError> {
self.write(path, new).await
}
pub async fn grep(&self, pattern: &str, k: usize) -> Result<Vec<Hit>, LunarisError> {
let filter =
Filter::StartsWith { field: "source".into(), prefix: self.session_prefix.clone() };
let builder = self.lunaris.recall_with_degraded_check().await?;
builder.filter(filter).top(k).execute(lunaris_retrieve::Query::text(pattern)).await
}
pub async fn ls(&self, prefix: Option<&str>) -> Result<Vec<String>, LunarisError> {
let key_prefix: &[u8] = b"episode:";
let storage = self.lunaris.storage();
let mut stream = storage
.scan_range(&self.scope, key_prefix, None)
.await
.map_err(LunarisError::Storage)?;
let target_prefix = match prefix {
Some(p) => format!("{}{}", self.session_prefix, p),
None => self.session_prefix.clone(),
};
let mut paths: Vec<String> = Vec::new();
while let Some(item) = stream.next().await {
let (_k, v): (Bytes, Bytes) = item.map_err(LunarisError::Storage)?;
let Ok(json) = serde_json::from_slice::<serde_json::Value>(&v) else {
continue;
};
let Some(source) = json.get("source").and_then(|s| s.as_str()) else {
continue;
};
if let Some(rel) = source.strip_prefix(&target_prefix) {
let mut full = String::with_capacity(target_prefix.len() + rel.len());
if let Some(tail) = source.strip_prefix(&self.session_prefix) {
full.push_str(tail);
} else {
full.push_str(rel);
}
paths.push(full);
}
}
paths.sort();
paths.dedup();
Ok(paths)
}
pub async fn forget(&self) -> Result<ForgetReceipt, LunarisError> {
#[allow(deprecated)]
self.lunaris
.forget(ForgetTarget::Scope(ScopeSpec::BySource(self.session_prefix.clone())))
.await
}
pub fn as_of(&self, ts: Hlc) -> AsOfScratchpad<'_> {
AsOfScratchpad { inner: self, ts }
}
}
#[deprecated(
since = "0.5.0",
note = "use CodingSessionMemory; HeliosScratchpad will be removed in v0.7"
)]
pub type HeliosScratchpad = CodingSessionMemory;
pub struct AsOfScratchpad<'a> {
inner: &'a CodingSessionMemory,
ts: Hlc,
}
impl AsOfScratchpad<'_> {
pub async fn read(&self, path: &str) -> Result<Option<String>, LunarisError> {
match self.inner.wm.read_at(path, Some(self.ts)).await? {
Some(serde_json::Value::String(s)) => Ok(Some(s)),
Some(_) => Err(LunarisError::Storage(StorageError::Backend(
"coding_session_memory_as_of_read_unexpected_json_shape".into(),
))),
None => Ok(None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coding_session_memory_public_surface_under_50_loc() {
let src = include_str!("./coding_session_memory.rs");
let production = src.split("#[cfg(test)]").next().unwrap_or(src);
let pub_fns = production.matches(" pub fn ").count()
+ production.matches(" pub async fn ").count();
assert!(
pub_fns <= 10,
"HELIOS-01 ≤50-LOC contract: CodingSessionMemory+AsOfScratchpad have {pub_fns} pub fns; cap is 10 (9 methods on CodingSessionMemory incl. write_dated [Mechanism B session-date grounding, 2026-07-29] + AsOfScratchpad::read)"
);
assert!(
pub_fns >= 10,
"HELIOS-01 contract: expected exactly 10 public methods (9 on CodingSessionMemory + AsOfScratchpad::read); got {pub_fns} — did the public surface shrink?"
);
}
#[test]
fn new_constructs_session_prefix_format() {
let prefix = format!("{HELIOS_PREFIX}{}/", "session-42");
assert_eq!(prefix, "helios:fs/session-42/");
}
#[test]
fn helios_prefix_constant_is_stable() {
assert_eq!(HELIOS_PREFIX, "helios:fs/");
}
#[test]
fn coding_session_memory_contains_no_sql_wildcard_fragment() {
let src = include_str!("./coding_session_memory.rs");
let production = src.split("#[cfg(test)]").next().unwrap_or(src);
let banned: String = ['L', 'I', 'K', 'E'].iter().collect();
assert!(
!production.contains(&banned),
"T-12-01-01: SQL wildcard fragment found in production portion of coding_session_memory.rs — use Filter::StartsWith instead"
);
}
}