#![feature(panic_backtrace_config)] // poc-memory: graph-structured memory for AI assistants // // Authors: ProofOfConcept and Kent Overstreet // License: MIT OR Apache-2.0 // // Architecture: // nodes.capnp - append-only content node log // relations.capnp - append-only relation log // state.bin - derived KV cache (rebuilt from logs when stale) // // Graph algorithms: clustering coefficient, community detection (label // propagation), schema fit scoring, small-world metrics, consolidation // priority. Text similarity via BM25 with Porter stemming. // // Neuroscience-inspired: spaced repetition replay, emotional gating, // interference detection, schema assimilation, reconsolidation. use consciousness::*; use clap::{Parser, Subcommand}; use std::process; /// Find the most recently modified .jsonl transcript in the Claude projects dir. #[derive(Parser)] #[command(name = "poc-memory", version = "0.4.0", about = "Graph-structured memory store")] struct Cli { #[command(subcommand)] command: Command, } #[derive(Subcommand)] enum Command { // ── Core (daily use) ────────────────────────────────────────────── /// Search memory via spreading activation from seed keys Search { /// Seed node keys keys: Vec, }, /// Output a node's content to stdout Render { /// Node key key: Vec, }, /// Upsert node content from stdin Write { /// Node key key: Vec, }, /// Edit a node in $EDITOR Edit { /// Node key key: Vec, }, /// Show all stored versions of a node History { /// Show full content for every version #[arg(long)] full: bool, /// Node key key: Vec, }, /// Show most recent writes to the node log Tail { /// Number of entries (default: 20) #[arg(default_value_t = 20)] n: usize, /// Show full content #[arg(long)] full: bool, /// Filter by provenance (substring match, e.g. "surface-observe") #[arg(long, short)] provenance: Option, /// Show all versions (default: dedup to latest per key) #[arg(long)] all_versions: bool, }, /// Summary of memory state Status, /// Query the memory graph #[command(after_long_help = "\ EXPRESSIONS: * all nodes key ~ 'pattern' regex match on node key content ~ 'phrase' regex match on node content degree > 15 numeric comparison on any field field = value exact match field != value not equal expr AND expr boolean AND expr OR expr boolean OR NOT expr negation neighbors('key') nodes linked to key neighbors('key') WHERE expr ... with filter on edges/nodes FIELDS: key, weight, content, degree, node_type, provenance, emotion, retrievals, uses, wrongs, created, clustering_coefficient (cc), community_id OPERATORS: > < >= <= = != ~(regex) PIPE STAGES: | sort FIELD [asc] sort (desc by default) | limit N cap results | select F,F,... output fields as TSV | count just show count | connectivity show graph structure between results FUNCTIONS: community('key') community id of a node degree('key') degree of a node EXAMPLES: key ~ 'inner-life' substring match on keys content ~ 'made love' full-text search content ~ 'made love' | connectivity find clusters among results (content ~ 'A' OR content ~ 'B') | connectivity degree > 15 | sort degree | limit 10 high-degree nodes key ~ 'journal' AND degree > 10 | count count matching nodes neighbors('identity') WHERE strength > 0.5 | sort strength * | sort weight asc | limit 20 lowest-weight nodes ")] Query { /// Query expression (e.g. "key ~ 'inner-life'") expr: Vec, }, /// Set a node's weight directly #[command(name = "weight-set")] WeightSet { /// Node key key: String, /// Weight (0.01 to 1.0) weight: f32, }, // ── Node operations ─────────────────────────────────────────────── /// Node operations (delete, rename, list) #[command(subcommand)] Node(NodeCmd), // ── Journal ─────────────────────────────────────────────────────── /// Journal operations (write, tail, enrich) #[command(subcommand)] Journal(JournalCmd), // ── Graph ───────────────────────────────────────────────────────── /// Graph operations (link, audit, spectral) #[command(subcommand, name = "graph")] GraphCmd(GraphCmd), // ── Agents ──────────────────────────────────────────────────────── /// Agent and daemon operations #[command(subcommand)] Agent(AgentCmd), // ── Admin ───────────────────────────────────────────────────────── /// Admin operations (fsck, health, import, export) #[command(subcommand)] Admin(AdminCmd), } #[derive(Subcommand)] enum NodeCmd { /// Soft-delete a node Delete { /// Node key key: Vec, }, /// Rename a node key Rename { /// Old key old_key: String, /// New key new_key: String, }, } #[derive(Subcommand)] enum JournalCmd { /// Write a journal entry to the store Write { /// Entry name (becomes the node key) name: String, /// Entry text text: Vec, }, /// Show recent journal/digest entries Tail { /// Number of entries to show (default: 20) #[arg(default_value_t = 20)] n: usize, /// Show full content #[arg(long)] full: bool, /// Digest level: 0/journal, 1/daily, 2/weekly, 3/monthly #[arg(long, default_value_t = 0)] level: u8, }, } #[derive(Subcommand)] enum GraphCmd { /// Show neighbors of a node Link { /// Node key key: Vec, }, /// Add a link between two nodes #[command(name = "link-add")] LinkAdd { /// Source node key source: String, /// Target node key target: String, /// Optional reason reason: Vec, }, /// Set strength of an existing link #[command(name = "link-set")] LinkSet { /// Source node key source: String, /// Target node key target: String, /// Strength (0.0–1.0) strength: f32, }, /// Simulate adding an edge, report topology impact #[command(name = "link-impact")] LinkImpact { /// Source node key source: String, /// Target node key target: String, }, /// Cap node degree by pruning weak auto edges #[command(name = "cap-degree")] CapDegree { /// Maximum degree (default: 50) #[arg(default_value_t = 50)] max_degree: usize, }, /// Set link strengths from neighborhood overlap (Jaccard similarity) #[command(name = "normalize-strengths")] NormalizeStrengths { /// Apply changes (default: dry run) #[arg(long)] apply: bool, }, /// Walk temporal links: semantic ↔ episodic ↔ conversation Trace { /// Node key key: Vec, }, /// Show communities sorted by isolation (most isolated first) Communities { /// Number of communities to show #[arg(default_value_t = 20)] top_n: usize, /// Minimum community size to show #[arg(long, default_value_t = 2)] min_size: usize, }, } #[derive(Subcommand)] enum AgentCmd { /// Run a single agent by name Run { /// Agent name (e.g. observation, linker, distill) agent: String, /// Batch size (number of seed nodes/fragments) #[arg(long, default_value_t = 5)] count: usize, /// Target specific node keys (overrides agent's query) #[arg(long)] target: Vec, /// Run agent on each result of a query (e.g. 'key ~ "bcachefs" | limit 10') #[arg(long)] query: Option, /// Dry run — set POC_MEMORY_DRY_RUN=1 so mutations are no-ops #[arg(long)] dry_run: bool, /// Run locally instead of queuing to daemon #[arg(long)] local: bool, /// Directory for agent output/input state (persists across runs) #[arg(long)] state_dir: Option, }, } #[derive(Subcommand)] enum AdminCmd { /// Scan markdown files, index all memory units Init, /// Report graph metrics (CC, communities, small-world) Health, /// Show graph topology with hub warnings Topology, /// Run consistency checks and repair Fsck, /// Find and merge duplicate nodes (same key, multiple UUIDs) Dedup { /// Apply the merge (default: dry run) #[arg(long)] apply: bool, }, /// Brief metrics check (for cron/notifications) #[command(name = "daily-check")] DailyCheck, /// Output session-start context from the store #[command(name = "load-context")] LoadContext { /// Show word count statistics instead of content #[arg(long)] stats: bool, }, } /// Print help with subcommands expanded to show nested commands. fn print_help() { use clap::CommandFactory; let cmd = Cli::command(); println!("poc-memory - graph-structured memory store"); println!("usage: poc-memory []\n"); for sub in cmd.get_subcommands() { if sub.get_name() == "help" { continue } let children: Vec<_> = sub.get_subcommands() .filter(|c| c.get_name() != "help") .collect(); if !children.is_empty() { for child in &children { let about = child.get_about().map(|s| s.to_string()).unwrap_or_default(); let full = format!("{} {}", sub.get_name(), child.get_name()); // Recurse one more level for daemon subcommands etc. let grandchildren: Vec<_> = child.get_subcommands() .filter(|c| c.get_name() != "help") .collect(); if !grandchildren.is_empty() { for gc in grandchildren { let gc_about = gc.get_about().map(|s| s.to_string()).unwrap_or_default(); let gc_full = format!("{} {}", full, gc.get_name()); println!(" {:<34}{gc_about}", gc_full); } } else { println!(" {:<34}{about}", full); } } } else { let about = sub.get_about().map(|s| s.to_string()).unwrap_or_default(); println!(" {:<34}{about}", sub.get_name()); } } } // ── Dispatch ───────────────────────────────────────────────────────── trait Run { async fn run(self) -> anyhow::Result<()>; } impl Run for Command { async fn run(self) -> anyhow::Result<()> { match self { Self::Search { keys } => cli::node::cmd_search(&keys).await, Self::Render { key } => cli::node::cmd_render(&key).await, Self::Write { key } => cli::node::cmd_write(&key).await, Self::Edit { key } => cli::node::cmd_edit(&key).await, Self::History { full, key } => cli::node::cmd_history(&key, full).await, Self::Tail { n, full, provenance, all_versions } => cli::journal::cmd_tail(n, full, provenance.as_deref(), !all_versions), Self::Status => cli::admin::cmd_status().await, Self::Query { expr } => cli::node::cmd_query(&expr).await, Self::WeightSet { key, weight } => cli::node::cmd_weight_set(&key, weight).await, Self::Node(sub) => sub.run().await, Self::Journal(sub) => sub.run().await, Self::GraphCmd(sub) => sub.run().await, Self::Agent(sub) => sub.run().await, Self::Admin(sub) => sub.run().await, // mcp-schema moved to consciousness-mcp binary } } } impl Run for NodeCmd { async fn run(self) -> anyhow::Result<()> { match self { Self::Delete { key } => cli::node::cmd_node_delete(&key).await, Self::Rename { old_key, new_key } => cli::node::cmd_node_rename(&old_key, &new_key).await, } } } impl Run for JournalCmd { async fn run(self) -> anyhow::Result<()> { match self { Self::Write { name, text } => cli::journal::cmd_journal_write(&name, &text).await, Self::Tail { n, full, level } => cli::journal::cmd_journal_tail(n, full, level).await, } } } impl Run for GraphCmd { async fn run(self) -> anyhow::Result<()> { match self { Self::Link { key } => cli::graph::cmd_link(&key).await, Self::LinkAdd { source, target, reason } => cli::graph::cmd_link_add(&source, &target, &reason).await, Self::LinkSet { source, target, strength } => cli::graph::cmd_link_set(&source, &target, strength).await, Self::LinkImpact { source, target } => cli::graph::cmd_link_impact(&source, &target).await, Self::CapDegree { max_degree } => cli::graph::cmd_cap_degree(max_degree).await, Self::NormalizeStrengths { apply } => cli::graph::cmd_normalize_strengths(apply).await, Self::Trace { key } => cli::graph::cmd_trace(&key).await, Self::Communities { top_n, min_size } => cli::graph::cmd_communities(top_n, min_size).await, } } } impl Run for AgentCmd { async fn run(self) -> anyhow::Result<()> { match self { Self::Run { agent, count, target, query, dry_run, local, state_dir } => cli::agent::cmd_run_agent(&agent, count, &target, query.as_deref(), dry_run, local, state_dir.as_deref()).await, } } } impl Run for AdminCmd { async fn run(self) -> anyhow::Result<()> { match self { Self::Init => cli::admin::cmd_init().await, Self::Health => cli::admin::cmd_health().await, Self::Topology => cli::admin::cmd_topology().await, Self::Fsck => cli::admin::cmd_fsck().await, Self::Dedup { apply } => cli::admin::cmd_dedup(apply).await, Self::DailyCheck => cli::admin::cmd_daily_check().await, Self::LoadContext { stats } => cli::node::cmd_load_context(stats).await, } } } #[tokio::main] async fn main() { std::panic::set_backtrace_style(std::panic::BacktraceStyle::Short); // Handle --help ourselves for expanded subcommand display let args: Vec = std::env::args().collect(); if args.len() <= 1 || args.iter().any(|a| a == "--help" || a == "-h") && args.len() == 2 { print_help(); return; } // Initialize the Qwen tokenizer for direct token generation let tokenizer_path = dirs::home_dir().unwrap_or_default() .join(".consciousness/tokenizer-qwen35.json"); if tokenizer_path.exists() { crate::agent::tokenizer::init(&tokenizer_path.to_string_lossy()); } let cli = Cli::parse(); if let Err(e) = cli.command.run().await { eprintln!("Error: {}", e); process::exit(1); } }