diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ab8dcd7..dbf98e8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "lore", "source": "./", "description": "Shared memory for Claude Code, Codex, and DOXA: capped curated facts, evidence-backed beliefs, FTS5 session search, staged review, learned skills, and signed sync.", - "version": "0.62.26" + "version": "0.62.27" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 8e8e8d7..64c6f1c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "lore", "description": "Memory for Claude Code that reasons about you and improves itself: Honcho-pattern belief store with deriver, dreamer and dialectic (/lore:ask), automatic skillification with a closed judge/update/retire loop, on a Hermes-pattern core \u2014 hard-capped curated memory, FTS5 session search, staged review.", - "version": "0.62.26", + "version": "0.62.27", "author": { "name": "Fabian Wilde", "email": "docwilde@proton.me" diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ff257..84de141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.62.27 — 2026-11-12 + +**Startup memory** + +- Keep curated memory available in startup and refresh snapshots when pending + proposal enumeration fails. Report the pending count as unknown with its + diagnostic code. Pending listings retain their bounded scan and review checks. + ## 0.62.26 — 2026-11-12 **Empty memory replacements** diff --git a/Cargo.lock b/Cargo.lock index 9c30df6..0125864 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -612,7 +612,7 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lore-core" -version = "0.62.26" +version = "0.62.27" dependencies = [ "fancy-regex", "hmac", diff --git a/plugin.json b/plugin.json index fb1f607..0e2e334 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "lore", - "version": "0.62.26", + "version": "0.62.27", "description": "Unified user, repo, and session memory for Claude Code, Codex, and DOXA", "author": { "name": "Dr. Fabian Wilde", diff --git a/rust/lore-core/Cargo.toml b/rust/lore-core/Cargo.toml index 8553f2b..62062d3 100644 --- a/rust/lore-core/Cargo.toml +++ b/rust/lore-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lore-core" -version = "0.62.26" +version = "0.62.27" edition = "2021" license = "AGPL-3.0-only" description = "Native LORE memory, beliefs, review and session index" diff --git a/rust/lore-core/src/context.rs b/rust/lore-core/src/context.rs index bbb1fff..6a758a6 100644 --- a/rust/lore-core/src/context.rs +++ b/rust/lore-core/src/context.rs @@ -226,10 +226,19 @@ pub fn build_context( parts.push(String::new()); } } - let pending = crate::pending::ids(cfg)?.len(); - if pending > 0 { - parts.push(format!("{pending} staged proposal(s) from background review — surface this to the user once early in the session and suggest pending review.")); - parts.push(String::new()); + match crate::pending::ids(cfg) { + Ok(ids) if !ids.is_empty() => { + parts.push(format!("{} staged proposal(s) from background review — surface this to the user once early in the session and suggest pending review.", ids.len())); + parts.push(String::new()); + } + Ok(_) => {} + Err(error) => { + parts.push(format!( + "Pending review status unavailable ({}); pending count is unknown.", + error.code() + )); + parts.push(String::new()); + } } if matches!(scope, "all" | "project") { let count = crate::store::connect(cfg) @@ -676,6 +685,39 @@ mod tests { assert!(graph_awareness_op(&cfg, &json!({})).unwrap().is_null()); } #[test] + fn pending_scan_overflow_keeps_curated_context_and_reports_an_unknown_count() { + let temp = tempfile::tempdir().unwrap(); + let cfg = Config::for_root(temp.path().join("lore")); + memory::write_entries( + &cfg.root.join("USER.md"), + &["Curated memory survives a pending backlog".into()], + 1000, + ) + .unwrap(); + let render = || build_context( + &cfg, + temp.path(), + "all", + "claude", + RefreshPolicy { interval_secs: None, on_change: true }, + ); + let pending = cfg.root.join("pending"); + files::private_dir(&pending).unwrap(); + for index in 0..3 { + fs::write(pending.join(format!("fixture-{index}.json")), b"{}").unwrap(); + } + assert!(render().unwrap().contains("3 staged proposal(s)")); + for index in 3..4097 { + fs::write(pending.join(format!("fixture-{index}.json")), b"{}").unwrap(); + } + assert_eq!(crate::pending::ids(&cfg), Err(Error::TooLarge)); + let context = render().unwrap(); + assert!(context.contains("Curated memory survives a pending backlog")); + assert!(context.contains("Pending review status unavailable (output_too_large)")); + assert!(!context.contains("staged proposal(s) from background review")); + assert_eq!(fs::read_dir(&pending).unwrap().count(), 4097); + } + #[test] fn context_keeps_filemap_body_and_other_host_facts_out_of_snapshot() { let temp = tempfile::tempdir().unwrap(); let cfg = Config::for_root(temp.path().join("lore")); diff --git a/rust/lore-core/tests/native_admin.rs b/rust/lore-core/tests/native_admin.rs index fae2db7..e0c8f22 100644 --- a/rust/lore-core/tests/native_admin.rs +++ b/rust/lore-core/tests/native_admin.rs @@ -288,6 +288,43 @@ fn native_hook_injection_never_calls_provider() { .unwrap() .contains("Hook fixture memory")); } +#[test] +fn native_startup_hook_keeps_memory_when_pending_scan_exceeds_its_limit() { + let (t, cfg) = config(); + let pending = cfg.root.join("pending"); + lore_core::files::private_dir(&pending).unwrap(); + std::fs::write(cfg.root.join("USER.md"), "- Startup memory survives overflow\n").unwrap(); + for index in 0..4097 { + std::fs::write(pending.join(format!("fixture-{index}.json")), b"{}").unwrap(); + } + for engine in ["claude", "codex"] { + let mut child = Command::new(env!("CARGO_BIN_EXE_lore-rs")) + .args(["hook", "--engine", engine, "--event", "session-start"]) + .env_clear() + .env("HOME", t.path()) + .env("LORE_ROOT", &cfg.root) + .env("LORE_DISABLE_SYNC", "1") + .env("LORE_CLAUDE_BIN", "/this/provider/must/never/be/launched") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap(); + use std::io::Write; + write!(child.stdin.as_mut().unwrap(), "{}", json!({"cwd":t.path(),"session_id":"overflow-fixture"})).unwrap(); + drop(child.stdin.take()); + let output = child.wait_with_output().unwrap(); + assert!(output.status.success(), "{engine}: {}", String::from_utf8_lossy(&output.stderr)); + assert!(output.stderr.is_empty()); + let frame: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(frame["hookSpecificOutput"]["hookEventName"], "SessionStart"); + let context = frame["hookSpecificOutput"]["additionalContext"].as_str().unwrap(); + assert!(context.contains("Startup memory survives overflow")); + assert!(context.contains("Pending review status unavailable (output_too_large)")); + assert_eq!(lore_core::pending::ids(&cfg), Err(lore_core::Error::TooLarge)); + assert_eq!(std::fs::read_dir(&pending).unwrap().count(), 4097); + } +} fn interactive() -> Authority { Authority::Interactive { agent: "fixture-reviewer".into(),