2026-03-24 12:27:54 -04:00
|
|
|
// memory-search CLI — thin wrapper around poc_memory::memory_search
|
poc-memory v0.4.0: graph-structured memory with consolidation pipeline
Rust core:
- Cap'n Proto append-only storage (nodes + relations)
- Graph algorithms: clustering coefficient, community detection,
schema fit, small-world metrics, interference detection
- BM25 text similarity with Porter stemming
- Spaced repetition replay queue
- Commands: search, init, health, status, graph, categorize,
link-add, link-impact, decay, consolidate-session, etc.
Python scripts:
- Episodic digest pipeline: daily/weekly/monthly-digest.py
- retroactive-digest.py for backfilling
- consolidation-agents.py: 3 parallel Sonnet agents
- apply-consolidation.py: structured action extraction + apply
- digest-link-parser.py: extract ~400 explicit links from digests
- content-promotion-agent.py: promote episodic obs to semantic files
- bulk-categorize.py: categorize all nodes via single Sonnet call
- consolidation-loop.py: multi-round automated consolidation
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-02-28 22:17:00 -05:00
|
|
|
//
|
2026-03-24 12:27:54 -04:00
|
|
|
// --hook: run hook logic (for debugging; poc-hook calls the library directly)
|
2026-03-24 20:31:21 -04:00
|
|
|
// surface/reflect: run agent, parse output, render memories to stdout
|
2026-03-24 12:27:54 -04:00
|
|
|
// no args: show seen set for current session
|
poc-memory v0.4.0: graph-structured memory with consolidation pipeline
Rust core:
- Cap'n Proto append-only storage (nodes + relations)
- Graph algorithms: clustering coefficient, community detection,
schema fit, small-world metrics, interference detection
- BM25 text similarity with Porter stemming
- Spaced repetition replay queue
- Commands: search, init, health, status, graph, categorize,
link-add, link-impact, decay, consolidate-session, etc.
Python scripts:
- Episodic digest pipeline: daily/weekly/monthly-digest.py
- retroactive-digest.py for backfilling
- consolidation-agents.py: 3 parallel Sonnet agents
- apply-consolidation.py: structured action extraction + apply
- digest-link-parser.py: extract ~400 explicit links from digests
- content-promotion-agent.py: promote episodic obs to semantic files
- bulk-categorize.py: categorize all nodes via single Sonnet call
- consolidation-loop.py: multi-round automated consolidation
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-02-28 22:17:00 -05:00
|
|
|
|
2026-03-24 20:31:21 -04:00
|
|
|
use clap::{Parser, Subcommand};
|
poc-memory v0.4.0: graph-structured memory with consolidation pipeline
Rust core:
- Cap'n Proto append-only storage (nodes + relations)
- Graph algorithms: clustering coefficient, community detection,
schema fit, small-world metrics, interference detection
- BM25 text similarity with Porter stemming
- Spaced repetition replay queue
- Commands: search, init, health, status, graph, categorize,
link-add, link-impact, decay, consolidate-session, etc.
Python scripts:
- Episodic digest pipeline: daily/weekly/monthly-digest.py
- retroactive-digest.py for backfilling
- consolidation-agents.py: 3 parallel Sonnet agents
- apply-consolidation.py: structured action extraction + apply
- digest-link-parser.py: extract ~400 explicit links from digests
- content-promotion-agent.py: promote episodic obs to semantic files
- bulk-categorize.py: categorize all nodes via single Sonnet call
- consolidation-loop.py: multi-round automated consolidation
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-02-28 22:17:00 -05:00
|
|
|
use std::fs;
|
2026-03-24 20:31:21 -04:00
|
|
|
use std::io::{self, Read};
|
|
|
|
|
use std::process::Command;
|
2026-03-24 12:27:54 -04:00
|
|
|
|
|
|
|
|
const STASH_PATH: &str = "/tmp/claude-memory-search/last-input.json";
|
poc-memory v0.4.0: graph-structured memory with consolidation pipeline
Rust core:
- Cap'n Proto append-only storage (nodes + relations)
- Graph algorithms: clustering coefficient, community detection,
schema fit, small-world metrics, interference detection
- BM25 text similarity with Porter stemming
- Spaced repetition replay queue
- Commands: search, init, health, status, graph, categorize,
link-add, link-impact, decay, consolidate-session, etc.
Python scripts:
- Episodic digest pipeline: daily/weekly/monthly-digest.py
- retroactive-digest.py for backfilling
- consolidation-agents.py: 3 parallel Sonnet agents
- apply-consolidation.py: structured action extraction + apply
- digest-link-parser.py: extract ~400 explicit links from digests
- content-promotion-agent.py: promote episodic obs to semantic files
- bulk-categorize.py: categorize all nodes via single Sonnet call
- consolidation-loop.py: multi-round automated consolidation
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-02-28 22:17:00 -05:00
|
|
|
|
2026-03-09 01:19:04 -04:00
|
|
|
#[derive(Parser)]
|
|
|
|
|
#[command(name = "memory-search")]
|
|
|
|
|
struct Args {
|
2026-03-24 12:27:54 -04:00
|
|
|
/// Run hook logic (reads JSON from stdin or stash file)
|
2026-03-09 01:19:04 -04:00
|
|
|
#[arg(long)]
|
|
|
|
|
hook: bool,
|
2026-03-24 20:31:21 -04:00
|
|
|
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: Option<Cmd>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Subcommand)]
|
|
|
|
|
enum Cmd {
|
|
|
|
|
/// Run surface agent, parse output, render memories
|
|
|
|
|
Surface,
|
|
|
|
|
/// Run reflect agent, dump output
|
|
|
|
|
Reflect,
|
2026-03-24 01:53:28 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 12:27:54 -04:00
|
|
|
fn show_seen() {
|
|
|
|
|
let input = match fs::read_to_string(STASH_PATH) {
|
2026-03-13 15:26:35 -04:00
|
|
|
Ok(s) => s,
|
2026-03-24 12:27:54 -04:00
|
|
|
Err(_) => { eprintln!("No session state available"); return; }
|
2026-03-13 15:26:35 -04:00
|
|
|
};
|
2026-03-24 12:27:54 -04:00
|
|
|
let Some(session) = poc_memory::memory_search::Session::from_json(&input) else {
|
2026-03-24 01:53:28 -04:00
|
|
|
eprintln!("No session state available");
|
|
|
|
|
return;
|
|
|
|
|
};
|
Agent identity, parallel scheduling, memory-search fixes, stemmer optimization
- Agent identity injection: prepend core-personality to all agent prompts
so agents dream as me, not as generic graph workers. Include instructions
to walk the graph and connect new nodes to core concepts.
- Parallel agent scheduling: sequential within type, parallel across types.
Different agent types (linker, organize, replay) run concurrently.
- Linker prompt: graph walking instead of keyword search for connections.
"Explore the local topology and walk the graph until you find the best
connections."
- memory-search fixes: format_results no longer truncates to 5 results,
pipeline default raised to 50, returned file cleared on compaction,
--seen and --seen-full merged, compaction timestamp in --seen output,
max_entries=3 per prompt for steady memory drip.
- Stemmer optimization: strip_suffix now works in-place on a single String
buffer instead of allocating 18 new Strings per word. Note for future:
reversed-suffix trie for O(suffix_len) instead of O(n_rules).
- Transcript: add compaction_timestamp() for --seen display.
- Agent budget configurable (default 4000 from config).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:49:10 -04:00
|
|
|
|
2026-03-24 01:53:28 -04:00
|
|
|
println!("Session: {}", session.session_id);
|
2026-03-09 01:19:04 -04:00
|
|
|
|
2026-03-24 01:53:28 -04:00
|
|
|
if let Ok(cookie) = fs::read_to_string(&session.path("cookie")) {
|
Agent identity, parallel scheduling, memory-search fixes, stemmer optimization
- Agent identity injection: prepend core-personality to all agent prompts
so agents dream as me, not as generic graph workers. Include instructions
to walk the graph and connect new nodes to core concepts.
- Parallel agent scheduling: sequential within type, parallel across types.
Different agent types (linker, organize, replay) run concurrently.
- Linker prompt: graph walking instead of keyword search for connections.
"Explore the local topology and walk the graph until you find the best
connections."
- memory-search fixes: format_results no longer truncates to 5 results,
pipeline default raised to 50, returned file cleared on compaction,
--seen and --seen-full merged, compaction timestamp in --seen output,
max_entries=3 per prompt for steady memory drip.
- Stemmer optimization: strip_suffix now works in-place on a single String
buffer instead of allocating 18 new Strings per word. Note for future:
reversed-suffix trie for O(suffix_len) instead of O(n_rules).
- Transcript: add compaction_timestamp() for --seen display.
- Agent budget configurable (default 4000 from config).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:49:10 -04:00
|
|
|
println!("Cookie: {}", cookie.trim());
|
2026-03-09 01:19:04 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 01:53:28 -04:00
|
|
|
match fs::read_to_string(&session.path("compaction")) {
|
|
|
|
|
Ok(s) => {
|
|
|
|
|
let offset: u64 = s.trim().parse().unwrap_or(0);
|
|
|
|
|
let ts = poc_memory::transcript::compaction_timestamp(&session.transcript_path, offset);
|
Agent identity, parallel scheduling, memory-search fixes, stemmer optimization
- Agent identity injection: prepend core-personality to all agent prompts
so agents dream as me, not as generic graph workers. Include instructions
to walk the graph and connect new nodes to core concepts.
- Parallel agent scheduling: sequential within type, parallel across types.
Different agent types (linker, organize, replay) run concurrently.
- Linker prompt: graph walking instead of keyword search for connections.
"Explore the local topology and walk the graph until you find the best
connections."
- memory-search fixes: format_results no longer truncates to 5 results,
pipeline default raised to 50, returned file cleared on compaction,
--seen and --seen-full merged, compaction timestamp in --seen output,
max_entries=3 per prompt for steady memory drip.
- Stemmer optimization: strip_suffix now works in-place on a single String
buffer instead of allocating 18 new Strings per word. Note for future:
reversed-suffix trie for O(suffix_len) instead of O(n_rules).
- Transcript: add compaction_timestamp() for --seen display.
- Agent budget configurable (default 4000 from config).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:49:10 -04:00
|
|
|
match ts {
|
|
|
|
|
Some(t) => println!("Last compaction: offset {} ({})", offset, t),
|
|
|
|
|
None => println!("Last compaction: offset {}", offset),
|
|
|
|
|
}
|
2026-03-09 01:19:04 -04:00
|
|
|
}
|
Agent identity, parallel scheduling, memory-search fixes, stemmer optimization
- Agent identity injection: prepend core-personality to all agent prompts
so agents dream as me, not as generic graph workers. Include instructions
to walk the graph and connect new nodes to core concepts.
- Parallel agent scheduling: sequential within type, parallel across types.
Different agent types (linker, organize, replay) run concurrently.
- Linker prompt: graph walking instead of keyword search for connections.
"Explore the local topology and walk the graph until you find the best
connections."
- memory-search fixes: format_results no longer truncates to 5 results,
pipeline default raised to 50, returned file cleared on compaction,
--seen and --seen-full merged, compaction timestamp in --seen output,
max_entries=3 per prompt for steady memory drip.
- Stemmer optimization: strip_suffix now works in-place on a single String
buffer instead of allocating 18 new Strings per word. Note for future:
reversed-suffix trie for O(suffix_len) instead of O(n_rules).
- Transcript: add compaction_timestamp() for --seen display.
- Agent budget configurable (default 4000 from config).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:49:10 -04:00
|
|
|
Err(_) => println!("Last compaction: none detected"),
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 01:53:28 -04:00
|
|
|
let pending = fs::read_dir(&session.path("chunks")).ok()
|
|
|
|
|
.map(|d| d.flatten().count()).unwrap_or(0);
|
Agent identity, parallel scheduling, memory-search fixes, stemmer optimization
- Agent identity injection: prepend core-personality to all agent prompts
so agents dream as me, not as generic graph workers. Include instructions
to walk the graph and connect new nodes to core concepts.
- Parallel agent scheduling: sequential within type, parallel across types.
Different agent types (linker, organize, replay) run concurrently.
- Linker prompt: graph walking instead of keyword search for connections.
"Explore the local topology and walk the graph until you find the best
connections."
- memory-search fixes: format_results no longer truncates to 5 results,
pipeline default raised to 50, returned file cleared on compaction,
--seen and --seen-full merged, compaction timestamp in --seen output,
max_entries=3 per prompt for steady memory drip.
- Stemmer optimization: strip_suffix now works in-place on a single String
buffer instead of allocating 18 new Strings per word. Note for future:
reversed-suffix trie for O(suffix_len) instead of O(n_rules).
- Transcript: add compaction_timestamp() for --seen display.
- Agent budget configurable (default 4000 from config).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:49:10 -04:00
|
|
|
if pending > 0 {
|
|
|
|
|
println!("Pending chunks: {}", pending);
|
2026-03-09 01:19:04 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 01:53:28 -04:00
|
|
|
for (label, suffix) in [("Current seen set", ""), ("Previous seen set (pre-compaction)", "-prev")] {
|
|
|
|
|
let path = session.state_dir.join(format!("seen{}-{}", suffix, session.session_id));
|
|
|
|
|
let content = fs::read_to_string(&path).unwrap_or_default();
|
|
|
|
|
let lines: Vec<&str> = content.lines().filter(|s| !s.is_empty()).collect();
|
|
|
|
|
if lines.is_empty() { continue; }
|
2026-03-22 16:27:52 -04:00
|
|
|
|
2026-03-24 01:53:28 -04:00
|
|
|
println!("\n{} ({}):", label, lines.len());
|
|
|
|
|
for line in &lines { println!(" {}", line); }
|
|
|
|
|
}
|
2026-03-09 01:19:04 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 20:31:21 -04:00
|
|
|
fn run_agent_and_parse(agent: &str) {
|
|
|
|
|
let session_id = std::env::var("CLAUDE_SESSION_ID")
|
|
|
|
|
.or_else(|_| {
|
|
|
|
|
fs::read_to_string(STASH_PATH).ok()
|
|
|
|
|
.and_then(|s| poc_memory::memory_search::Session::from_json(&s))
|
|
|
|
|
.map(|s| s.session_id)
|
|
|
|
|
.ok_or(std::env::VarError::NotPresent)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
if session_id.is_empty() {
|
|
|
|
|
eprintln!("No session ID available (set CLAUDE_SESSION_ID or run --hook first)");
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
eprintln!("Running {} agent (session {})...", agent, &session_id[..8.min(session_id.len())]);
|
|
|
|
|
|
|
|
|
|
let output = Command::new("poc-memory")
|
|
|
|
|
.args(["agent", "run", agent, "--count", "1", "--local"])
|
|
|
|
|
.env("POC_SESSION_ID", &session_id)
|
|
|
|
|
.output();
|
|
|
|
|
|
|
|
|
|
let output = match output {
|
|
|
|
|
Ok(o) => o,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("Failed to run agent: {}", e);
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let result = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
if !stderr.is_empty() {
|
|
|
|
|
eprintln!("{}", stderr);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 20:33:50 -04:00
|
|
|
// Extract the final response — after the last "=== RESPONSE ===" marker
|
|
|
|
|
let response = result.rsplit_once("=== RESPONSE ===")
|
|
|
|
|
.map(|(_, rest)| rest.trim())
|
|
|
|
|
.unwrap_or(result.trim());
|
|
|
|
|
|
2026-03-24 20:31:21 -04:00
|
|
|
if agent == "reflect" {
|
2026-03-24 20:33:50 -04:00
|
|
|
// Reflect: find REFLECTION marker and dump what follows
|
|
|
|
|
if let Some(pos) = response.find("REFLECTION") {
|
|
|
|
|
let after = &response[pos + "REFLECTION".len()..];
|
|
|
|
|
let text = after.trim();
|
|
|
|
|
if !text.is_empty() {
|
|
|
|
|
println!("{}", text);
|
2026-03-24 20:31:21 -04:00
|
|
|
}
|
2026-03-24 20:33:50 -04:00
|
|
|
} else if response.contains("NO OUTPUT") {
|
2026-03-24 20:31:21 -04:00
|
|
|
println!("(no reflection)");
|
|
|
|
|
} else {
|
|
|
|
|
eprintln!("Unexpected output format");
|
2026-03-24 20:33:50 -04:00
|
|
|
println!("{}", response);
|
2026-03-24 20:31:21 -04:00
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Surface: parse NEW RELEVANT MEMORIES, render them
|
2026-03-24 20:33:50 -04:00
|
|
|
let tail_lines: Vec<&str> = response.lines().rev()
|
2026-03-24 20:31:21 -04:00
|
|
|
.filter(|l| !l.trim().is_empty()).take(8).collect();
|
|
|
|
|
let has_new = tail_lines.iter().any(|l| l.starts_with("NEW RELEVANT MEMORIES:"));
|
|
|
|
|
let has_none = tail_lines.iter().any(|l| l.starts_with("NO NEW RELEVANT MEMORIES"));
|
|
|
|
|
|
|
|
|
|
if has_new {
|
2026-03-24 20:33:50 -04:00
|
|
|
let after_marker = response.rsplit_once("NEW RELEVANT MEMORIES:")
|
2026-03-24 20:31:21 -04:00
|
|
|
.map(|(_, rest)| rest).unwrap_or("");
|
|
|
|
|
let keys: Vec<String> = after_marker.lines()
|
|
|
|
|
.map(|l| l.trim().trim_start_matches("- ").trim().to_string())
|
|
|
|
|
.filter(|l| !l.is_empty() && !l.starts_with("```")).collect();
|
|
|
|
|
|
|
|
|
|
if keys.is_empty() {
|
|
|
|
|
println!("(no memories found)");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let Ok(store) = poc_memory::store::Store::load() else {
|
|
|
|
|
eprintln!("Failed to load store");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for key in &keys {
|
|
|
|
|
if let Some(content) = poc_memory::cli::node::render_node(&store, key) {
|
|
|
|
|
if !content.trim().is_empty() {
|
|
|
|
|
println!("--- {} (surfaced) ---", key);
|
|
|
|
|
print!("{}", content);
|
|
|
|
|
println!();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
eprintln!(" key not found: {}", key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if has_none {
|
|
|
|
|
println!("(no new relevant memories)");
|
|
|
|
|
} else {
|
|
|
|
|
eprintln!("Unexpected output format");
|
2026-03-24 20:33:50 -04:00
|
|
|
print!("{}", response);
|
2026-03-24 20:31:21 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 12:27:54 -04:00
|
|
|
fn main() {
|
|
|
|
|
let args = Args::parse();
|
|
|
|
|
|
2026-03-24 20:31:21 -04:00
|
|
|
if let Some(cmd) = args.command {
|
|
|
|
|
match cmd {
|
|
|
|
|
Cmd::Surface => run_agent_and_parse("surface"),
|
|
|
|
|
Cmd::Reflect => run_agent_and_parse("reflect"),
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 12:27:54 -04:00
|
|
|
if args.hook {
|
|
|
|
|
// Read from stdin if piped, otherwise from stash
|
|
|
|
|
let input = {
|
|
|
|
|
let mut buf = String::new();
|
|
|
|
|
io::stdin().read_to_string(&mut buf).ok();
|
|
|
|
|
if buf.trim().is_empty() {
|
|
|
|
|
fs::read_to_string(STASH_PATH).unwrap_or_default()
|
|
|
|
|
} else {
|
|
|
|
|
let _ = fs::create_dir_all("/tmp/claude-memory-search");
|
|
|
|
|
let _ = fs::write(STASH_PATH, &buf);
|
|
|
|
|
buf
|
2026-03-03 01:33:31 -05:00
|
|
|
}
|
2026-03-24 12:27:54 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let output = poc_memory::memory_search::run_hook(&input);
|
|
|
|
|
print!("{}", output);
|
|
|
|
|
} else {
|
|
|
|
|
show_seen()
|
2026-03-03 01:33:31 -05:00
|
|
|
}
|
|
|
|
|
}
|