Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.18"
"version": "0.62.19"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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.18",
"version": "0.62.19",
"author": {
"name": "Fabian Wilde",
"email": "docwilde@proton.me"
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.62.19 — 2026-10-09

**Pending summary completeness**

- Fix **`pending_for_sessions_v1`** reporting `complete: true` and zero pending when `pending/*.json` has an invalid or non-UTF-8 filename stem. These entries now make every requested session incomplete; valid IDs remain visible.

## 0.62.18 — 2026-10-09

**Session-scoped pending review**
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "lore",
"version": "0.62.18",
"version": "0.62.19",
"description": "Unified user, repo, and session memory for Claude Code, Codex, and DOXA",
"author": {
"name": "Dr. Fabian Wilde",
Expand Down
2 changes: 1 addition & 1 deletion rust/lore-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "lore-core"
version = "0.62.18"
version = "0.62.19"
edition = "2021"
license = "AGPL-3.0-only"
description = "Native LORE memory, beliefs, review and session index"
Expand Down
61 changes: 56 additions & 5 deletions rust/lore-core/src/pending.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,11 @@ pub fn for_sessions(cfg: &Config, req: &Value) -> Result<Value> {
}).collect::<Result<Vec<_>>>()?;
let _namespace = namespace_lock(cfg)?;
let mut complete = true;
let pending_ids = match ids(cfg) {
Ok(ids) => ids,
let pending_ids = match scan_ids(cfg) {
Ok((ids, invalid_stem)) => {
if invalid_stem { complete = false; }
ids
},
Err(Error::TooLarge) => { complete = false; Vec::new() },
Err(error) => return Err(error),
};
Expand Down Expand Up @@ -201,28 +204,36 @@ fn visible(item: &Value, slug: &str) -> bool {
gate::pending_project(item).is_none_or(|project| project == slug)
}
pub fn ids(cfg: &Config) -> Result<Vec<String>> {
Ok(scan_ids(cfg)?.0)
}
/// Keep malformed JSON names visible as uncertainty to source-scoped summaries.
/// A bounded directory scan is still shared with ordinary pending ID listings.
fn scan_ids(cfg: &Config) -> Result<(Vec<String>, bool)> {
let dir = cfg.root.join("pending");
if fs::symlink_metadata(&dir).is_err_and(|error| error.kind() == std::io::ErrorKind::NotFound) {
return Ok(Vec::new());
return Ok((Vec::new(), false));
}
let mut ids = Vec::new();
let mut invalid_stem = false;
for name in files::directory_names(&dir, 4096)? {
let path = dir.join(name);
if path
.extension()
.is_some_and(|extension| extension == "json")
.is_some_and(|extension| extension == "json") || path.file_name().is_some_and(|name| name == ".json")
{
if let Some(id) = path
.file_stem()
.and_then(|value| value.to_str())
.filter(|id| valid_id(id))
{
ids.push(id.to_owned());
} else {
invalid_stem = true;
}
}
}
ids.sort();
Ok(ids)
Ok((ids, invalid_stem))
}
fn scrub_display(value: &Value, depth: usize) -> Result<Value> {
if depth > 32 {
Expand Down Expand Up @@ -1181,6 +1192,46 @@ mod tests {
assert_eq!(result["sessions"][0]["complete"], false);
}
#[test]
fn invalid_json_stems_make_every_session_summary_incomplete() {
let temp = tempfile::tempdir().unwrap();
let cfg = Config::for_root(temp.path().join("lore"));
let cwd = temp.path().join("source");
let pid = gate::stage(&cfg, &json!({"kind":"memory","scope":"user",
"source_project":project_slug(&cwd),"session_id":"one","text":"known proposal"}), &auth()).unwrap();
let request = json!({"cwd":cwd,"session_ids":["one","two"]});
assert_eq!(for_sessions(&cfg, &request).unwrap()["complete"], true);
let dir = cfg.root.join("pending");
for name in ["bad stem.json", ".json"] {
fs::write(dir.join(name), b"{}").unwrap();
let result = for_sessions(&cfg, &request).unwrap();
assert_eq!(result["complete"], false, "{name}");
assert_eq!(result["sessions"][0]["complete"], false, "{name}");
assert_eq!(result["sessions"][1]["complete"], false, "{name}");
assert_eq!(result["sessions"][0]["pending_pids"], json!([pid]), "{name}");
assert_eq!(result["sessions"][1]["pending_pids"], json!([]), "{name}");
fs::remove_file(dir.join(name)).unwrap();
assert_eq!(for_sessions(&cfg, &request).unwrap()["complete"], true);
}
}
#[cfg(unix)]
#[test]
fn non_utf8_json_stem_makes_every_session_summary_incomplete() {
use std::os::unix::ffi::OsStringExt;
let temp = tempfile::tempdir().unwrap();
let cfg = Config::for_root(temp.path().join("lore"));
let request = json!({"cwd":temp.path(),"session_ids":["one","two"]});
assert_eq!(for_sessions(&cfg, &request).unwrap()["complete"], true);
let dir = cfg.root.join("pending");
files::private_dir(&dir).unwrap();
let name = std::ffi::OsString::from_vec(b"bad\xff.json".to_vec());
fs::write(dir.join(&name), b"{}").unwrap();
let result = for_sessions(&cfg, &request).unwrap();
assert_eq!(result["complete"], false);
assert!(result["sessions"].as_array().unwrap().iter().all(|row| row["complete"] == false));
fs::remove_file(dir.join(name)).unwrap();
assert_eq!(for_sessions(&cfg, &request).unwrap()["complete"], true);
}
#[test]
fn summary_waits_for_writer_and_refuses_an_unreadable_entry() {
use std::sync::mpsc;
use std::time::Duration;
Expand Down
Loading