clawhdf5-migrate wrote a layout of its own (/chunks, /sessions, /entities, /relations, root attributes, no /meta or schema_version) that HDF5Memory::open rejects, so a "migrated" SQLite database could not be used as agent memory — contrary to the README. It now writes through the agent's own API (HDF5Memory::create/open, save_batch, the session cache and the knowledge graph), so there is no second copy of the schema: - sessions and entities/relations carry over; deleted rows become deleted records (or are left out with --skip-deleted); - embeddings follow the library default (float16), --f32 opts out and --float16 is a hidden no-op, as in clawhdf5-cli; the `half`-based conversion is gone; - every source row is checked before the output is created: a wrong embedding length, an empty embedding, a dimension that differs from an existing store's, or a float16 value beyond +-65504 is an error naming the chunk id, and an existing store is left untouched; - --incremental opens the existing store, adds only rows it does not hold (matched by content) and follows the source's deleted flags; - a source with no memory rows needs --embedding-dim; - validation reads the result back with HDF5Memory::open_read_only, compares every field (embeddings bit for bit, round_to_f16 of the source for float16) and checks a migrated record is found by search. clawhdf5-agent gains HDF5Memory::sessions()/sessions_mut(), HDF5Memory::delete_batch (one save, all-or-nothing, no auto-compact), SessionCache::add_at, and re-exports SessionCache/SessionEntry. The old layout's per-dataset SHA-256 provenance attributes have no place in the agent schema and are gone. An adversarial review found two blockers (silent truncation of long embeddings; an --incremental dimension check that could never fire) and four majors (a failed run wiping the existing store, dim-0 stores, deleted-flag drift); all are fixed with regression tests. 42 migrate tests, incl. h5py opening a migrated store. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
94 lines
2.3 KiB
Rust
94 lines
2.3 KiB
Rust
//! Session tracking cache and data structures.
|
|
|
|
/// A single session entry.
|
|
#[derive(Debug, Clone)]
|
|
pub struct SessionEntry {
|
|
pub id: String,
|
|
pub start_idx: u64,
|
|
pub end_idx: u64,
|
|
pub channel: String,
|
|
pub ts: f64,
|
|
}
|
|
|
|
/// In-memory cache for the /sessions group.
|
|
#[derive(Debug, Clone)]
|
|
pub struct SessionCache {
|
|
pub entries: Vec<SessionEntry>,
|
|
pub summaries: Vec<String>,
|
|
}
|
|
|
|
impl SessionCache {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
entries: Vec::new(),
|
|
summaries: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.entries.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.entries.is_empty()
|
|
}
|
|
|
|
/// Add a new session with its summary, timestamped now.
|
|
pub fn add(
|
|
&mut self,
|
|
id: &str,
|
|
start_idx: usize,
|
|
end_idx: usize,
|
|
channel: &str,
|
|
summary: &str,
|
|
) {
|
|
let ts = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs_f64()
|
|
* 1_000_000.0; // microseconds
|
|
self.add_at(id, start_idx, end_idx, channel, summary, ts);
|
|
}
|
|
|
|
/// Add a session with an explicit timestamp (Unix **microseconds**, the
|
|
/// unit [`SessionEntry::ts`] uses) — for importers carrying sessions over
|
|
/// from another store, whose original time should be kept.
|
|
pub fn add_at(
|
|
&mut self,
|
|
id: &str,
|
|
start_idx: usize,
|
|
end_idx: usize,
|
|
channel: &str,
|
|
summary: &str,
|
|
ts: f64,
|
|
) {
|
|
self.entries.push(SessionEntry {
|
|
id: id.to_string(),
|
|
start_idx: start_idx as u64,
|
|
end_idx: end_idx as u64,
|
|
channel: channel.to_string(),
|
|
ts,
|
|
});
|
|
self.summaries.push(summary.to_string());
|
|
}
|
|
|
|
/// Return the ID of the most recently added session, if any.
|
|
pub fn latest_session_id(&self) -> Option<&str> {
|
|
self.entries.last().map(|e| e.id.as_str())
|
|
}
|
|
|
|
/// Find the summary for a session by ID.
|
|
pub fn find_summary(&self, session_id: &str) -> Option<&str> {
|
|
self.entries
|
|
.iter()
|
|
.position(|e| e.id == session_id)
|
|
.map(|idx| self.summaries[idx].as_str())
|
|
}
|
|
}
|
|
|
|
impl Default for SessionCache {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|