2026-04-13 19:17:31 -04:00
|
|
|
// Cap'n Proto serialization and persistence
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
//
|
Replace rkyv/bincode caching with redb indices
Remove three-tier loading (rkyv snapshot, bincode cache, capnp replay)
in favor of direct capnp log replay + redb for indexed access.
- Remove all rkyv derives from types (Node, Relation, enums, etc.)
- Remove Snapshot struct, RKYV_MAGIC, CACHE_MAGIC constants
- Remove load_snapshot_mmap(), save(), save_snapshot()
- Remove MmapView, AnyView from view.rs (keep StoreView trait)
- Simplify Store::load() to just replay capnp logs
- Add db.rs with redb schema: nodes, uuid_to_key, visits, transcript_progress
- Simplify cmd_fsck to just check capnp integrity + graph health
capnp logs remain source of truth; redb indices will be rebuilt on demand.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:30:58 -04:00
|
|
|
// capnp logs are the source of truth; redb provides indexed access.
|
2026-04-13 19:17:31 -04:00
|
|
|
// This module contains:
|
|
|
|
|
// - Serialization macros (capnp_enum!, capnp_message!)
|
|
|
|
|
// - Load/replay from capnp logs
|
|
|
|
|
// - Append to capnp logs
|
|
|
|
|
// - fsck (corruption repair)
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
|
2026-04-13 19:10:08 -04:00
|
|
|
use super::{index, types::*};
|
2026-04-13 18:33:47 -04:00
|
|
|
use redb::ReadableTableMetadata;
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
|
|
|
|
|
use crate::memory_capnp;
|
2026-04-13 19:17:31 -04:00
|
|
|
use super::Store;
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
|
2026-04-13 19:17:31 -04:00
|
|
|
use anyhow::{anyhow, Context, Result};
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
use capnp::message;
|
|
|
|
|
use capnp::serialize;
|
|
|
|
|
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::fs;
|
cleanup: fix all build warnings, delete dead DMN context code
- Delete poc-daemon/src/context.rs dead code (git_context, work_state,
irc_digest, recent_commits, uncommitted_files) — replaced by
where-am-i.md and memory graph
- Remove unused imports (BufWriter, Context, similarity)
- Prefix unused variables (_store, _avg_cc, _episodic_ratio, _message)
- #[allow(dead_code)] on public API surface that's not yet wired
(Message::assistant, ConversationLog::message_count/read_all,
Config::context_message, ContextInfo fields)
- Fix to_capnp macro dead_code warning
- Rename _rewrite_store_DISABLED to snake_case
Only remaining warnings are in generated capnp code (can't fix).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 14:20:34 -04:00
|
|
|
use std::io::{BufReader, Seek};
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
use std::path::Path;
|
|
|
|
|
|
2026-04-13 19:17:31 -04:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Capnp serialization macros
|
|
|
|
|
//
|
|
|
|
|
// Declarative mapping between Rust types and capnp generated types.
|
|
|
|
|
// Adding a field to the schema means adding it in one place below;
|
|
|
|
|
// both read and write are generated from the same declaration.
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/// Generate to_capnp/from_capnp conversion methods for an enum.
|
|
|
|
|
macro_rules! capnp_enum {
|
|
|
|
|
($rust_type:ident, $capnp_type:path, [$($variant:ident),+ $(,)?]) => {
|
|
|
|
|
impl $rust_type {
|
|
|
|
|
#[allow(clippy::wrong_self_convention, dead_code)]
|
|
|
|
|
pub(crate) fn to_capnp(&self) -> $capnp_type {
|
|
|
|
|
match self {
|
|
|
|
|
$(Self::$variant => <$capnp_type>::$variant,)+
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pub(crate) fn from_capnp(v: $capnp_type) -> Self {
|
|
|
|
|
match v {
|
|
|
|
|
$(<$capnp_type>::$variant => Self::$variant,)+
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate from_capnp/to_capnp methods for a struct with capnp serialization.
|
|
|
|
|
/// Fields are grouped by serialization kind:
|
|
|
|
|
/// text - capnp Text fields (String in Rust)
|
|
|
|
|
/// uuid - capnp Data fields ([u8; 16] in Rust)
|
|
|
|
|
/// prim - copy types (u32, f32, f64, bool)
|
|
|
|
|
/// enm - enums with to_capnp/from_capnp methods
|
|
|
|
|
/// skip - Rust-only fields not in capnp (set to Default on read)
|
|
|
|
|
macro_rules! capnp_message {
|
|
|
|
|
(
|
|
|
|
|
$struct:ident,
|
|
|
|
|
reader: $reader:ty,
|
|
|
|
|
builder: $builder:ty,
|
|
|
|
|
text: [$($tf:ident),* $(,)?],
|
|
|
|
|
uuid: [$($uf:ident),* $(,)?],
|
|
|
|
|
prim: [$($pf:ident),* $(,)?],
|
|
|
|
|
enm: [$($ef:ident: $et:ident),* $(,)?],
|
|
|
|
|
skip: [$($sf:ident),* $(,)?] $(,)?
|
|
|
|
|
) => {
|
|
|
|
|
impl $struct {
|
|
|
|
|
pub fn from_capnp(r: $reader) -> Result<Self> {
|
|
|
|
|
paste::paste! {
|
|
|
|
|
Ok(Self {
|
|
|
|
|
$($tf: read_text(r.[<get_ $tf>]()),)*
|
|
|
|
|
$($uf: read_uuid(r.[<get_ $uf>]()),)*
|
|
|
|
|
$($pf: r.[<get_ $pf>](),)*
|
|
|
|
|
$($ef: $et::from_capnp(
|
|
|
|
|
r.[<get_ $ef>]().map_err(|_| anyhow!(concat!("bad ", stringify!($ef))))?
|
|
|
|
|
),)*
|
|
|
|
|
$($sf: Default::default(),)*
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn to_capnp(&self, mut b: $builder) {
|
|
|
|
|
paste::paste! {
|
|
|
|
|
$(b.[<set_ $tf>](&self.$tf);)*
|
|
|
|
|
$(b.[<set_ $uf>](&self.$uf);)*
|
|
|
|
|
$(b.[<set_ $pf>](self.$pf);)*
|
|
|
|
|
$(b.[<set_ $ef>](self.$ef.to_capnp());)*
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Capnp helpers
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/// Read a capnp text field, returning empty string on any error
|
|
|
|
|
fn read_text(result: capnp::Result<capnp::text::Reader>) -> String {
|
|
|
|
|
result.ok()
|
|
|
|
|
.and_then(|t| t.to_str().ok())
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
.to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read a capnp data field as [u8; 16], zero-padded
|
|
|
|
|
fn read_uuid(result: capnp::Result<&[u8]>) -> [u8; 16] {
|
|
|
|
|
let mut out = [0u8; 16];
|
|
|
|
|
if let Ok(data) = result
|
|
|
|
|
&& data.len() >= 16 {
|
|
|
|
|
out.copy_from_slice(&data[..16]);
|
|
|
|
|
}
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Type-to-capnp mappings
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
capnp_enum!(NodeType, memory_capnp::NodeType,
|
|
|
|
|
[EpisodicSession, EpisodicDaily, EpisodicWeekly, Semantic, EpisodicMonthly]);
|
|
|
|
|
|
|
|
|
|
capnp_enum!(RelationType, memory_capnp::RelationType,
|
|
|
|
|
[Link, Causal, Auto]);
|
|
|
|
|
|
|
|
|
|
capnp_message!(Node,
|
|
|
|
|
reader: memory_capnp::content_node::Reader<'_>,
|
|
|
|
|
builder: memory_capnp::content_node::Builder<'_>,
|
|
|
|
|
text: [key, content, source_ref, provenance],
|
|
|
|
|
uuid: [uuid],
|
|
|
|
|
prim: [version, timestamp, weight, emotion, deleted,
|
|
|
|
|
retrievals, uses, wrongs, last_replayed,
|
|
|
|
|
spaced_repetition_interval, created_at, last_scored],
|
|
|
|
|
enm: [node_type: NodeType],
|
|
|
|
|
skip: [community_id, clustering_coefficient, degree],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
capnp_message!(Relation,
|
|
|
|
|
reader: memory_capnp::relation::Reader<'_>,
|
|
|
|
|
builder: memory_capnp::relation::Builder<'_>,
|
|
|
|
|
text: [source_key, target_key, provenance],
|
|
|
|
|
uuid: [uuid, source, target],
|
|
|
|
|
prim: [version, timestamp, strength, deleted],
|
|
|
|
|
enm: [rel_type: RelationType],
|
|
|
|
|
skip: [],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Migration helpers (legacy provenance enum → string)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/// Convert legacy capnp provenance enum to string label.
|
|
|
|
|
fn legacy_provenance_label(p: memory_capnp::Provenance) -> &'static str {
|
|
|
|
|
use memory_capnp::Provenance::*;
|
|
|
|
|
match p {
|
|
|
|
|
Manual => "manual",
|
|
|
|
|
Journal => "journal",
|
|
|
|
|
Agent => "agent",
|
|
|
|
|
Dream => "dream",
|
|
|
|
|
Derived => "derived",
|
|
|
|
|
AgentExperienceMine => "agent:experience-mine",
|
|
|
|
|
AgentKnowledgeObservation => "agent:knowledge-observation",
|
|
|
|
|
AgentKnowledgePattern => "agent:knowledge-pattern",
|
|
|
|
|
AgentKnowledgeConnector => "agent:knowledge-connector",
|
|
|
|
|
AgentKnowledgeChallenger => "agent:knowledge-challenger",
|
|
|
|
|
AgentConsolidate => "agent:consolidate",
|
|
|
|
|
AgentDigest => "agent:digest",
|
|
|
|
|
AgentFactMine => "agent:fact-mine",
|
|
|
|
|
AgentDecay => "agent:decay",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Node {
|
|
|
|
|
/// Read from capnp with migration: if the new provenance text field
|
|
|
|
|
/// is empty (old record), fall back to the deprecated provenanceOld enum.
|
|
|
|
|
pub fn from_capnp_migrate(r: memory_capnp::content_node::Reader<'_>) -> Result<Self> {
|
|
|
|
|
let mut node = Self::from_capnp(r)?;
|
|
|
|
|
if node.provenance.is_empty()
|
|
|
|
|
&& let Ok(old) = r.get_provenance_old() {
|
|
|
|
|
node.provenance = legacy_provenance_label(old).to_string();
|
|
|
|
|
}
|
|
|
|
|
// Sanitize timestamps: old capnp records have raw offsets instead
|
|
|
|
|
// of unix epoch. Anything past year 2100 (~4102444800) is bogus.
|
|
|
|
|
const MAX_SANE_EPOCH: i64 = 4_102_444_800;
|
|
|
|
|
if node.timestamp > MAX_SANE_EPOCH || node.timestamp < 0 {
|
|
|
|
|
node.timestamp = node.created_at;
|
|
|
|
|
}
|
|
|
|
|
if node.created_at > MAX_SANE_EPOCH || node.created_at < 0 {
|
|
|
|
|
node.created_at = node.timestamp.min(MAX_SANE_EPOCH);
|
|
|
|
|
}
|
|
|
|
|
Ok(node)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Relation {
|
|
|
|
|
pub fn from_capnp_migrate(r: memory_capnp::relation::Reader<'_>) -> Result<Self> {
|
|
|
|
|
let mut rel = Self::from_capnp(r)?;
|
|
|
|
|
if rel.provenance.is_empty()
|
|
|
|
|
&& let Ok(old) = r.get_provenance_old() {
|
|
|
|
|
rel.provenance = legacy_provenance_label(old).to_string();
|
|
|
|
|
}
|
|
|
|
|
Ok(rel)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 19:31:28 -04:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Direct node access
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/// Read a single node at the given offset in the capnp log.
|
|
|
|
|
/// The offset must point to a valid message containing the node.
|
|
|
|
|
pub fn read_node_at_offset(offset: u64) -> Result<Node> {
|
|
|
|
|
let path = nodes_path();
|
|
|
|
|
let mut file = fs::File::open(&path)
|
|
|
|
|
.with_context(|| format!("open {}", path.display()))?;
|
|
|
|
|
|
|
|
|
|
use std::io::{Seek, SeekFrom};
|
|
|
|
|
file.seek(SeekFrom::Start(offset))?;
|
|
|
|
|
|
|
|
|
|
let mut reader = BufReader::new(file);
|
|
|
|
|
let msg = serialize::read_message(&mut reader, message::ReaderOptions::new())
|
|
|
|
|
.with_context(|| format!("read message at offset {}", offset))?;
|
|
|
|
|
|
|
|
|
|
let log = msg.get_root::<memory_capnp::node_log::Reader>()
|
|
|
|
|
.with_context(|| "read node log")?;
|
|
|
|
|
let nodes = log.get_nodes()
|
|
|
|
|
.with_context(|| "get nodes")?;
|
|
|
|
|
|
|
|
|
|
// A message at this offset should have exactly one node (from upsert),
|
|
|
|
|
// or we take the last one if there are multiple (from batch operations like rename)
|
|
|
|
|
if nodes.is_empty() {
|
|
|
|
|
anyhow::bail!("no nodes in message at offset {}", offset);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return the first non-deleted node, or the first one if all are deleted
|
|
|
|
|
for node_reader in nodes.iter() {
|
|
|
|
|
let node = Node::from_capnp_migrate(node_reader)?;
|
|
|
|
|
if !node.deleted {
|
|
|
|
|
return Ok(node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All nodes in this message are deleted - shouldn't happen if index is correct
|
|
|
|
|
Node::from_capnp_migrate(nodes.get(0))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 19:17:31 -04:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Store persistence methods
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
impl Store {
|
2026-04-13 19:31:28 -04:00
|
|
|
/// Load store by opening redb index and replaying relations.
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
pub fn load() -> Result<Store> {
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
let nodes_p = nodes_path();
|
|
|
|
|
let rels_p = relations_path();
|
|
|
|
|
|
|
|
|
|
let mut store = Store::default();
|
|
|
|
|
|
2026-04-13 19:31:28 -04:00
|
|
|
// Open redb index first (rebuilds from capnp if needed)
|
|
|
|
|
let db_p = db_path();
|
|
|
|
|
store.db = Some(store.open_or_rebuild_db(&db_p)?);
|
|
|
|
|
|
|
|
|
|
// Replay relations
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
if rels_p.exists() {
|
|
|
|
|
store.replay_relations(&rels_p)?;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 19:31:28 -04:00
|
|
|
// Record log sizes
|
2026-03-06 21:38:26 -05:00
|
|
|
store.loaded_nodes_size = fs::metadata(&nodes_p).map(|m| m.len()).unwrap_or(0);
|
|
|
|
|
store.loaded_rels_size = fs::metadata(&rels_p).map(|m| m.len()).unwrap_or(0);
|
|
|
|
|
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
// Drop edges referencing deleted/missing nodes
|
2026-04-13 19:31:28 -04:00
|
|
|
let db = store.db.as_ref().unwrap();
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
store.relations.retain(|r|
|
2026-04-13 19:31:28 -04:00
|
|
|
index::contains_key(db, &r.source_key).unwrap_or(false) &&
|
|
|
|
|
index::contains_key(db, &r.target_key).unwrap_or(false)
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
);
|
|
|
|
|
|
2026-03-11 01:42:32 -04:00
|
|
|
Ok(store)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 18:33:47 -04:00
|
|
|
/// Open redb database, rebuilding if unhealthy.
|
|
|
|
|
fn open_or_rebuild_db(&self, path: &Path) -> Result<redb::Database> {
|
|
|
|
|
// Try opening existing database
|
|
|
|
|
if path.exists() {
|
2026-04-13 19:10:08 -04:00
|
|
|
match index::open_db(path) {
|
2026-04-13 18:33:47 -04:00
|
|
|
Ok(database) => {
|
|
|
|
|
if self.db_is_healthy(&database)? {
|
|
|
|
|
return Ok(database);
|
|
|
|
|
}
|
|
|
|
|
eprintln!("redb index stale, rebuilding...");
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("redb open failed ({}), rebuilding...", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 19:10:08 -04:00
|
|
|
// Rebuild index from capnp log
|
|
|
|
|
rebuild_index(path, &nodes_path())
|
2026-04-13 18:33:47 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-13 19:31:28 -04:00
|
|
|
/// Check if redb index is healthy by verifying some offsets are valid.
|
2026-04-13 18:33:47 -04:00
|
|
|
fn db_is_healthy(&self, database: &redb::Database) -> Result<bool> {
|
2026-04-13 19:31:28 -04:00
|
|
|
use redb::{ReadableDatabase, ReadableTable};
|
2026-04-13 18:33:47 -04:00
|
|
|
|
|
|
|
|
let txn = database.begin_read()?;
|
2026-04-13 19:10:08 -04:00
|
|
|
let nodes_table = txn.open_table(index::NODES)?;
|
2026-04-13 18:33:47 -04:00
|
|
|
|
2026-04-13 19:31:28 -04:00
|
|
|
// Check that we can read the table and it has entries
|
|
|
|
|
if nodes_table.len()? == 0 {
|
|
|
|
|
// Empty database - might be stale or new
|
|
|
|
|
let capnp_size = fs::metadata(nodes_path()).map(|m| m.len()).unwrap_or(0);
|
|
|
|
|
return Ok(capnp_size == 0); // healthy only if capnp is also empty
|
2026-04-13 18:33:47 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-13 19:31:28 -04:00
|
|
|
// Spot check: verify a few offsets point to valid messages
|
|
|
|
|
let mut checked = 0;
|
|
|
|
|
for entry in nodes_table.iter()? {
|
|
|
|
|
if checked >= 5 { break; }
|
|
|
|
|
let (key, offset) = entry?;
|
|
|
|
|
let offset = offset.value();
|
|
|
|
|
|
|
|
|
|
// Try to read the node at this offset
|
|
|
|
|
if read_node_at_offset(offset).is_err() {
|
2026-04-13 18:33:47 -04:00
|
|
|
return Ok(false);
|
|
|
|
|
}
|
2026-04-13 19:31:28 -04:00
|
|
|
checked += 1;
|
|
|
|
|
let _ = key; // silence unused warning
|
2026-04-13 18:33:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(true)
|
|
|
|
|
}
|
|
|
|
|
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
/// Replay relation log, keeping latest version per UUID
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
fn replay_relations(&mut self, path: &Path) -> Result<()> {
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
let file = fs::File::open(path)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("open {}", path.display()))?;
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
let mut reader = BufReader::new(file);
|
|
|
|
|
|
|
|
|
|
// Collect all, then deduplicate by UUID keeping latest version
|
|
|
|
|
let mut by_uuid: HashMap<[u8; 16], Relation> = HashMap::new();
|
|
|
|
|
|
|
|
|
|
while let Ok(msg) = serialize::read_message(&mut reader, message::ReaderOptions::new()) {
|
|
|
|
|
let log = msg.get_root::<memory_capnp::relation_log::Reader>()
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("read relation log"))?;
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
for rel_reader in log.get_relations()
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("get relations"))? {
|
2026-03-11 01:19:52 -04:00
|
|
|
let rel = Relation::from_capnp_migrate(rel_reader)?;
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
let existing_version = by_uuid.get(&rel.uuid)
|
|
|
|
|
.map(|r| r.version)
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
if rel.version >= existing_version {
|
|
|
|
|
by_uuid.insert(rel.uuid, rel);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.relations = by_uuid.into_values()
|
|
|
|
|
.filter(|r| !r.deleted)
|
|
|
|
|
.collect();
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 14:30:21 -04:00
|
|
|
/// Find all duplicate keys: keys with multiple live UUIDs in the log.
|
|
|
|
|
/// Returns a map from key → vec of all live Node versions (one per UUID).
|
|
|
|
|
/// The "winner" in self.nodes is always one of them.
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
pub fn find_duplicates(&self) -> Result<HashMap<String, Vec<Node>>> {
|
2026-03-10 14:30:21 -04:00
|
|
|
let path = nodes_path();
|
|
|
|
|
if !path.exists() { return Ok(HashMap::new()); }
|
|
|
|
|
|
|
|
|
|
let file = fs::File::open(&path)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("open {}", path.display()))?;
|
2026-03-10 14:30:21 -04:00
|
|
|
let mut reader = BufReader::new(file);
|
|
|
|
|
|
|
|
|
|
// Track latest version of each UUID
|
|
|
|
|
let mut by_uuid: HashMap<[u8; 16], Node> = HashMap::new();
|
|
|
|
|
|
|
|
|
|
while let Ok(msg) = serialize::read_message(&mut reader, message::ReaderOptions::new()) {
|
|
|
|
|
let log = msg.get_root::<memory_capnp::node_log::Reader>()
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("read node log"))?;
|
2026-03-10 14:30:21 -04:00
|
|
|
for node_reader in log.get_nodes()
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("get nodes"))? {
|
2026-03-11 01:19:52 -04:00
|
|
|
let node = Node::from_capnp_migrate(node_reader)?;
|
2026-03-10 14:30:21 -04:00
|
|
|
let dominated = by_uuid.get(&node.uuid)
|
|
|
|
|
.map(|n| node.version >= n.version)
|
|
|
|
|
.unwrap_or(true);
|
|
|
|
|
if dominated {
|
|
|
|
|
by_uuid.insert(node.uuid, node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Group live (non-deleted) nodes by key
|
|
|
|
|
let mut by_key: HashMap<String, Vec<Node>> = HashMap::new();
|
|
|
|
|
for node in by_uuid.into_values() {
|
|
|
|
|
if !node.deleted {
|
|
|
|
|
by_key.entry(node.key.clone()).or_default().push(node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep only duplicates
|
|
|
|
|
by_key.retain(|_, nodes| nodes.len() > 1);
|
|
|
|
|
Ok(by_key)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 19:13:25 -04:00
|
|
|
/// Append nodes to the log file. Returns the offset where the message was written.
|
2026-04-13 19:10:08 -04:00
|
|
|
pub fn append_nodes(&mut self, nodes: &[Node]) -> Result<u64> {
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
let mut msg = message::Builder::new_default();
|
|
|
|
|
{
|
|
|
|
|
let log = msg.init_root::<memory_capnp::node_log::Builder>();
|
|
|
|
|
let mut list = log.init_nodes(nodes.len() as u32);
|
|
|
|
|
for (i, node) in nodes.iter().enumerate() {
|
|
|
|
|
node.to_capnp(list.reborrow().get(i as u32));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-06 21:38:26 -05:00
|
|
|
let mut buf = Vec::new();
|
|
|
|
|
serialize::write_message(&mut buf, &msg)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("serialize nodes"))?;
|
2026-03-06 21:38:26 -05:00
|
|
|
|
|
|
|
|
let path = nodes_path();
|
|
|
|
|
let file = fs::OpenOptions::new()
|
|
|
|
|
.create(true).append(true).open(&path)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("open {}", path.display()))?;
|
2026-04-13 19:10:08 -04:00
|
|
|
|
|
|
|
|
// Get offset before writing
|
|
|
|
|
let offset = file.metadata().map(|m| m.len()).unwrap_or(0);
|
|
|
|
|
|
2026-03-06 21:38:26 -05:00
|
|
|
use std::io::Write;
|
|
|
|
|
(&file).write_all(&buf)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("write nodes"))?;
|
2026-03-06 21:38:26 -05:00
|
|
|
|
|
|
|
|
self.loaded_nodes_size = file.metadata().map(|m| m.len()).unwrap_or(0);
|
2026-04-13 19:10:08 -04:00
|
|
|
Ok(offset)
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-06 21:38:26 -05:00
|
|
|
/// Append relations to the log file.
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
pub fn append_relations(&mut self, relations: &[Relation]) -> Result<()> {
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
let mut msg = message::Builder::new_default();
|
|
|
|
|
{
|
|
|
|
|
let log = msg.init_root::<memory_capnp::relation_log::Builder>();
|
|
|
|
|
let mut list = log.init_relations(relations.len() as u32);
|
|
|
|
|
for (i, rel) in relations.iter().enumerate() {
|
|
|
|
|
rel.to_capnp(list.reborrow().get(i as u32));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-06 21:38:26 -05:00
|
|
|
let mut buf = Vec::new();
|
|
|
|
|
serialize::write_message(&mut buf, &msg)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("serialize relations"))?;
|
2026-03-06 21:38:26 -05:00
|
|
|
|
|
|
|
|
let path = relations_path();
|
|
|
|
|
let file = fs::OpenOptions::new()
|
|
|
|
|
.create(true).append(true).open(&path)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("open {}", path.display()))?;
|
2026-03-06 21:38:26 -05:00
|
|
|
use std::io::Write;
|
|
|
|
|
(&file).write_all(&buf)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("write relations"))?;
|
2026-03-06 21:38:26 -05:00
|
|
|
|
|
|
|
|
self.loaded_rels_size = file.metadata().map(|m| m.len()).unwrap_or(0);
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
Replace rkyv/bincode caching with redb indices
Remove three-tier loading (rkyv snapshot, bincode cache, capnp replay)
in favor of direct capnp log replay + redb for indexed access.
- Remove all rkyv derives from types (Node, Relation, enums, etc.)
- Remove Snapshot struct, RKYV_MAGIC, CACHE_MAGIC constants
- Remove load_snapshot_mmap(), save(), save_snapshot()
- Remove MmapView, AnyView from view.rs (keep StoreView trait)
- Simplify Store::load() to just replay capnp logs
- Add db.rs with redb schema: nodes, uuid_to_key, visits, transcript_progress
- Simplify cmd_fsck to just check capnp integrity + graph health
capnp logs remain source of truth; redb indices will be rebuilt on demand.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:30:58 -04:00
|
|
|
/// Placeholder - indices will be updated on write with redb.
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
pub fn save(&self) -> Result<()> {
|
store: split mod.rs into persist.rs and ops.rs
mod.rs was 937 lines with all Store methods in one block.
Split into three files by responsibility:
- persist.rs (318 lines): load, save, replay, append, snapshot
— all disk IO and cache management
- ops.rs (300 lines): upsert, delete, modify, mark_used/wrong,
decay, fix_categories, cap_degree — all mutations
- mod.rs (356 lines): re-exports, key resolution, ingestion,
rendering, search — read-only operations
No behavioral changes; cargo check + full smoke test pass.
2026-03-03 16:40:32 -05:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-08 18:31:19 -04:00
|
|
|
|
|
|
|
|
/// Check and repair corrupt capnp log files.
|
|
|
|
|
///
|
|
|
|
|
/// Reads each message sequentially, tracking file position. On the first
|
|
|
|
|
/// corrupt message, truncates the file to the last good position. Also
|
|
|
|
|
/// removes stale caches so the next load replays from the repaired log.
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
pub fn fsck() -> Result<()> {
|
2026-03-08 18:31:19 -04:00
|
|
|
let mut any_corrupt = false;
|
|
|
|
|
|
|
|
|
|
for (path, kind) in [
|
|
|
|
|
(nodes_path(), "node"),
|
|
|
|
|
(relations_path(), "relation"),
|
|
|
|
|
] {
|
|
|
|
|
if !path.exists() { continue; }
|
|
|
|
|
|
|
|
|
|
let file = fs::File::open(&path)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("open {}", path.display()))?;
|
2026-03-08 18:31:19 -04:00
|
|
|
let file_len = file.metadata()
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("stat {}", path.display()))?.len();
|
2026-03-08 18:31:19 -04:00
|
|
|
let mut reader = BufReader::new(file);
|
|
|
|
|
|
|
|
|
|
let mut good_messages = 0u64;
|
|
|
|
|
let mut last_good_pos = 0u64;
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
let pos = reader.stream_position()
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("tell {}", path.display()))?;
|
2026-03-08 18:31:19 -04:00
|
|
|
|
|
|
|
|
let msg = match serialize::read_message(&mut reader, message::ReaderOptions::new()) {
|
|
|
|
|
Ok(m) => m,
|
|
|
|
|
Err(_) => {
|
|
|
|
|
// read_message fails at EOF (normal) or on corrupt framing
|
|
|
|
|
if pos < file_len {
|
|
|
|
|
// Not at EOF — corrupt framing
|
|
|
|
|
eprintln!("{}: corrupt message at offset {}, truncating", kind, pos);
|
|
|
|
|
any_corrupt = true;
|
|
|
|
|
drop(reader);
|
|
|
|
|
let file = fs::OpenOptions::new().write(true).open(&path)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("open for truncate"))?;
|
2026-03-08 18:31:19 -04:00
|
|
|
file.set_len(pos)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("truncate {}", path.display()))?;
|
2026-03-08 18:31:19 -04:00
|
|
|
eprintln!("{}: truncated from {} to {} bytes ({} good messages)",
|
|
|
|
|
kind, file_len, pos, good_messages);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Validate the message content too
|
|
|
|
|
let valid = if kind == "node" {
|
|
|
|
|
msg.get_root::<memory_capnp::node_log::Reader>()
|
|
|
|
|
.and_then(|l| l.get_nodes().map(|_| ()))
|
|
|
|
|
.is_ok()
|
|
|
|
|
} else {
|
|
|
|
|
msg.get_root::<memory_capnp::relation_log::Reader>()
|
|
|
|
|
.and_then(|l| l.get_relations().map(|_| ()))
|
|
|
|
|
.is_ok()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if valid {
|
|
|
|
|
good_messages += 1;
|
|
|
|
|
last_good_pos = reader.stream_position()
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("tell {}", path.display()))?;
|
2026-03-08 18:31:19 -04:00
|
|
|
} else {
|
|
|
|
|
eprintln!("{}: corrupt message content at offset {}, truncating to {}",
|
|
|
|
|
kind, pos, last_good_pos);
|
|
|
|
|
any_corrupt = true;
|
|
|
|
|
drop(reader);
|
|
|
|
|
let file = fs::OpenOptions::new().write(true).open(&path)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("open for truncate"))?;
|
2026-03-08 18:31:19 -04:00
|
|
|
file.set_len(last_good_pos)
|
Convert store and CLI to anyhow::Result for cleaner error handling
Replace Result<_, String> with anyhow::Result throughout:
- hippocampus/store module (persist, ops, types, view, mod)
- CLI modules (admin, agent, graph, journal, node)
- Run trait in main.rs
Use .context() and .with_context() instead of .map_err(|e| format!(...))
patterns. Add bail!() for early error returns.
Add access_local() helper in hippocampus/mod.rs that returns
Result<Arc<Mutex<Store>>> for direct local store access.
Fix store access patterns to properly lock Arc<Mutex<Store>> before
accessing fields in mind/unconscious.rs, mind/mod.rs, subconscious/learn.rs,
and hippocampus/memory.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-13 18:05:04 -04:00
|
|
|
.with_context(|| format!("truncate {}", path.display()))?;
|
2026-03-08 18:31:19 -04:00
|
|
|
eprintln!("{}: truncated from {} to {} bytes ({} good messages)",
|
|
|
|
|
kind, file_len, last_good_pos, good_messages);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !any_corrupt {
|
|
|
|
|
eprintln!("{}: {} messages, all clean", kind, good_messages);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if any_corrupt {
|
|
|
|
|
eprintln!("repair complete — run `poc-memory status` to verify");
|
|
|
|
|
} else {
|
|
|
|
|
eprintln!("store is clean");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-04-13 19:10:08 -04:00
|
|
|
|
|
|
|
|
/// Rebuild redb index from capnp log.
|
|
|
|
|
/// Scans the log, tracking offsets, and records latest version of each node.
|
|
|
|
|
fn rebuild_index(db_path: &Path, capnp_path: &Path) -> Result<redb::Database> {
|
|
|
|
|
// Remove old database if it exists
|
|
|
|
|
if db_path.exists() {
|
|
|
|
|
fs::remove_file(db_path)
|
|
|
|
|
.with_context(|| format!("remove old db {}", db_path.display()))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let database = index::open_db(db_path)?;
|
|
|
|
|
|
|
|
|
|
if !capnp_path.exists() {
|
|
|
|
|
return Ok(database);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Track latest (offset, uuid, version, deleted) per key
|
|
|
|
|
let mut latest: HashMap<String, (u64, [u8; 16], u32, bool)> = HashMap::new();
|
|
|
|
|
|
|
|
|
|
let file = fs::File::open(capnp_path)
|
|
|
|
|
.with_context(|| format!("open {}", capnp_path.display()))?;
|
|
|
|
|
let mut reader = BufReader::new(file);
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
let offset = reader.stream_position()?;
|
|
|
|
|
let msg = match serialize::read_message(&mut reader, message::ReaderOptions::new()) {
|
|
|
|
|
Ok(m) => m,
|
|
|
|
|
Err(_) => break,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let log = match msg.get_root::<memory_capnp::node_log::Reader>() {
|
|
|
|
|
Ok(l) => l,
|
|
|
|
|
Err(_) => continue,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let nodes = match log.get_nodes() {
|
|
|
|
|
Ok(n) => n,
|
|
|
|
|
Err(_) => continue,
|
|
|
|
|
};
|
|
|
|
|
for node_reader in nodes {
|
|
|
|
|
let key = node_reader.get_key().ok()
|
|
|
|
|
.and_then(|t| t.to_str().ok())
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
.to_string();
|
|
|
|
|
if key.is_empty() { continue; }
|
|
|
|
|
|
|
|
|
|
let version = node_reader.get_version();
|
|
|
|
|
let deleted = node_reader.get_deleted();
|
|
|
|
|
|
|
|
|
|
let mut uuid = [0u8; 16];
|
|
|
|
|
if let Ok(data) = node_reader.get_uuid() {
|
|
|
|
|
if data.len() >= 16 {
|
|
|
|
|
uuid.copy_from_slice(&data[..16]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep if newer version
|
|
|
|
|
let dominated = latest.get(&key)
|
|
|
|
|
.map(|(_, _, v, _)| version >= *v)
|
|
|
|
|
.unwrap_or(true);
|
|
|
|
|
if dominated {
|
|
|
|
|
latest.insert(key, (offset, uuid, version, deleted));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write index entries for non-deleted nodes
|
|
|
|
|
{
|
|
|
|
|
let txn = database.begin_write()?;
|
|
|
|
|
{
|
|
|
|
|
let mut nodes_table = txn.open_table(index::NODES)?;
|
|
|
|
|
let mut uuid_table = txn.open_table(index::UUID_TO_KEY)?;
|
|
|
|
|
|
|
|
|
|
for (key, (offset, uuid, _, deleted)) in latest {
|
|
|
|
|
if !deleted {
|
|
|
|
|
nodes_table.insert(key.as_str(), offset)?;
|
|
|
|
|
uuid_table.insert(uuid.as_slice(), key.as_str())?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
txn.commit()?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(database)
|
|
|
|
|
}
|