diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 679aa87..c39dfce 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,8 +11,8 @@ { "name": "memmesh", "source": "./integrations/memmesh-plugin", - "description": "MemMesh — a memory + calibrated-prediction + behavior-discovery engine for AI apps. Persistent hierarchical memory, a bi-temporal knowledge graph, and forward predictions with provenance and abstention. The engine decides what to save; your agent's own model does any extraction (zero engine-side LLM cost).", - "version": "0.1.0" + "description": "MemMesh — persistent, local-first memory for AI agents. Bi-temporal knowledge graph, typed/scoped recall, and local↔server sync, over MCP; runs from one binary on SQLite with no mandatory LLM calls (your agent's own model does any extraction). Calibrated prediction & behavior discovery available in hosted mode.", + "version": "0.1.1" } ] } diff --git a/Cargo.lock b/Cargo.lock index 2366ad5..87bb811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2065,7 +2065,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memory-audit" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "async-trait", @@ -2080,7 +2080,7 @@ dependencies = [ [[package]] name = "memory-cli" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "chrono", @@ -2104,7 +2104,7 @@ dependencies = [ [[package]] name = "memory-core" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "chrono", @@ -2121,7 +2121,7 @@ dependencies = [ [[package]] name = "memory-embed" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "async-trait", @@ -2135,7 +2135,7 @@ dependencies = [ [[package]] name = "memory-embed-server" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "axum", @@ -2149,7 +2149,7 @@ dependencies = [ [[package]] name = "memory-eval" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "async-trait", @@ -2166,7 +2166,7 @@ dependencies = [ [[package]] name = "memory-license" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -2183,7 +2183,7 @@ dependencies = [ [[package]] name = "memory-mcp" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "async-trait", @@ -2201,7 +2201,7 @@ dependencies = [ [[package]] name = "memory-server" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "axum", @@ -2224,7 +2224,7 @@ dependencies = [ [[package]] name = "memory-storage" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "async-trait", @@ -2244,7 +2244,7 @@ dependencies = [ [[package]] name = "memory-sync" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 5c1d260..82c5689 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ members = [ ] [workspace.package] -version = "0.1.0" +version = "0.1.1" edition = "2021" authors = ["MemMesh contributors"] license = "Apache-2.0" diff --git a/crates/core/src/extraction.rs b/crates/core/src/extraction.rs index 2055e30..1bce779 100644 --- a/crates/core/src/extraction.rs +++ b/crates/core/src/extraction.rs @@ -73,9 +73,52 @@ pub fn extract(text: &str, ctx: &ObserveContext) -> Vec { out.push(m); } } + + // Fallback: nothing matched a structured rule, but the input is + // substantive prose. Keep it verbatim as a raw `observation` rather than + // silently dropping it — an agent memory that discards what the user tells + // it is worse than one that over-captures. The structured rules are + // first-person-anchored ("I prefer…", "we decided…"); most real input + // (third-person, questions aside, arbitrary facts) matches nothing, so + // without this the store stays empty. Hosted LLM extraction refines these + // into typed facts; locally we at least never lose them. + if out.is_empty() { + if let Some(m) = fallback_observation(text) { + out.push(m); + } + } + out } +/// Keep substantive input that matched no structured rule as a raw +/// `observation`. Filters out questions, code/commands, and short fragments so +/// the store fills with statements rather than chatter or one-word acks. +fn fallback_observation(text: &str) -> Option { + let trimmed = text.trim(); + if is_code_or_command(trimmed) { + return None; + } + // Questions ask, they don't assert — skip them. + if trimmed.ends_with('?') { + return None; + } + // Require some substance so acks that slipped past the filler filter + // ("sounds good to me") don't become memories. + let word_count = trimmed.split_whitespace().count(); + if trimmed.chars().count() < 24 || word_count < 4 { + return None; + } + Some(ExtractedMemory { + content: normalize_subject(trimmed), + kind: "observation", + scope: MemoryScope::Project, + importance: 3.0, + impact: MemoryImpact::Low, + reason: "raw-observation", + }) +} + /// Split a message into sentence-ish lines for per-clause extraction. Real /// NLP-grade sentence splitting is overkill; we just want to keep distinct /// statements distinct. @@ -546,4 +589,36 @@ mod tests { let out = extract("What do you think we should do here?", &ctx()); assert!(out.is_empty()); } + + #[test] + fn third_person_statement_kept_as_raw_observation() { + // Matches no first-person rule, but must not be dropped. + let out = extract("Ryan prefers pnpm over npm for all projects.", &ctx()); + assert_eq!(out.len(), 1); + assert_eq!(out[0].kind, "observation"); + assert_eq!(out[0].reason, "raw-observation"); + assert!(out[0].content.to_lowercase().contains("pnpm")); + } + + #[test] + fn arbitrary_fact_captured_via_fallback() { + let out = extract("Ryan's email is ryan@thinkfleet.ai for work.", &ctx()); + assert_eq!(out.len(), 1); + assert_eq!(out[0].kind, "observation"); + } + + #[test] + fn structured_rule_still_wins_over_fallback() { + // A first-person preference should classify as `preference`, not the + // generic `observation` fallback. + let out = extract("I prefer Vitest over Jest for testing.", &ctx()); + assert_eq!(out.len(), 1); + assert_eq!(out[0].kind, "preference"); + } + + #[test] + fn fallback_skips_questions_and_short_fragments() { + assert!(extract("What should we do about the migration here?", &ctx()).is_empty()); + assert!(extract("Sounds good to me", &ctx()).is_empty()); + } } diff --git a/crates/mcp/src/lib.rs b/crates/mcp/src/lib.rs index 0069a00..ba1535d 100644 --- a/crates/mcp/src/lib.rs +++ b/crates/mcp/src/lib.rs @@ -324,6 +324,38 @@ fn tool_definitions() -> serde_json::Value { } } } + }, + { + "name": "memory_delete", + "description": "Delete a memory by id. Soft-delete by default (marks it deleted, recoverable); pass hard=true to remove the row permanently. Use to correct a mistake or honor a 'forget this' request.", + "inputSchema": { + "type": "object", + "required": ["id"], + "properties": { + "id": { "type": "string", "description": "id of the memory to delete." }, + "hard": { "type": "boolean", "default": false, "description": "true = permanent hard delete; false = recoverable soft delete." } + } + } + }, + { + "name": "memory_supersede", + "description": "Mark one memory as superseded by another — the old memory is kept for audit/history but is no longer the current truth. Use when a fact changes ('actually we moved to Postgres'): save the new memory, then supersede the old one by the new id.", + "inputSchema": { + "type": "object", + "required": ["id", "byId"], + "properties": { + "id": { "type": "string", "description": "id of the memory being superseded (the old / outdated one)." }, + "byId": { "type": "string", "description": "id of the memory that replaces it (the new current one)." } + } + } + }, + { + "name": "memory_stats", + "description": "Return counts about the memory store — currently the total number of memories held. Useful for a health check or a 'how much do you remember' summary.", + "inputSchema": { + "type": "object", + "properties": {} + } } ]) } @@ -421,6 +453,30 @@ async fn handle_tool_call( "memory_commit_extraction" => commit_extraction(storage, args).await, + "memory_delete" => { + let id = arg_str(args, "id")?; + let hard = args.get("hard").and_then(|v| v.as_bool()).unwrap_or(false); + storage.delete(id, hard).await?; + Ok(text_result(&format!( + "{} memory {id}", + if hard { "hard-deleted" } else { "deleted" } + ))) + } + + "memory_supersede" => { + let id = arg_str(args, "id")?; + let by_id = arg_str(args, "byId")?; + storage.supersede(id, by_id).await?; + Ok(text_result(&format!("superseded {id} by {by_id}"))) + } + + "memory_stats" => { + let total = storage.count_items().await?; + Ok(text_result(&serde_json::to_string_pretty( + &serde_json::json!({ "totalMemories": total }), + )?)) + } + other => anyhow::bail!("unknown tool: {other}"), } } diff --git a/integrations/memmesh-plugin/.claude-plugin/plugin.json b/integrations/memmesh-plugin/.claude-plugin/plugin.json index 49ee1e6..7871fc2 100644 --- a/integrations/memmesh-plugin/.claude-plugin/plugin.json +++ b/integrations/memmesh-plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "memmesh", - "version": "0.1.0", - "description": "Persistent memory + calibrated prediction for AI agents. Remembers decisions, preferences, and patterns across sessions; predicts what a subject will do next — with provenance and honest abstention.", + "version": "0.1.1", + "description": "Persistent, local-first memory for AI agents. Remembers facts, preferences, and decisions across sessions with a bi-temporal knowledge graph, over MCP — runs fully local on SQLite with no mandatory LLM calls. Calibrated prediction & behavior discovery available in hosted mode.", "author": { "name": "ThinkFleet", "email": "support@memmesh.ai" diff --git a/integrations/memmesh-plugin/skills/behaviors/SKILL.md b/integrations/memmesh-plugin/skills/behaviors/SKILL.md index 1a019ca..8c60a37 100644 --- a/integrations/memmesh-plugin/skills/behaviors/SKILL.md +++ b/integrations/memmesh-plugin/skills/behaviors/SKILL.md @@ -9,6 +9,9 @@ description: > # behaviors +> **⚙️ Requires MemMesh hosted mode.** Calibrated prediction and behavior discovery run on the hosted engine — set your `mm-` API key. On a local / open-source install these tools (`memory_predict`, `memory_build_context`) are not registered; if a call returns "unknown tool", tell the user this is a hosted capability and fall back to `search` / `recall` for what's already known. + + Show the patterns MemMesh discovered on its own. These `behavior_pattern` memories are what `predict` projects forward — inspecting them explains the forecasts. diff --git a/integrations/memmesh-plugin/skills/context-loader/SKILL.md b/integrations/memmesh-plugin/skills/context-loader/SKILL.md index dcf122a..1d28bdf 100644 --- a/integrations/memmesh-plugin/skills/context-loader/SKILL.md +++ b/integrations/memmesh-plugin/skills/context-loader/SKILL.md @@ -10,6 +10,9 @@ description: > # context-loader +> **⚙️ Requires MemMesh hosted mode.** Calibrated prediction and behavior discovery run on the hosted engine — set your `mm-` API key. On a local / open-source install these tools (`memory_predict`, `memory_build_context`) are not registered; if a call returns "unknown tool", tell the user this is a hosted capability and fall back to `search` / `recall` for what's already known. + + Prime the session with the right memory before you act. ## General project/session context diff --git a/integrations/memmesh-plugin/skills/graph/SKILL.md b/integrations/memmesh-plugin/skills/graph/SKILL.md index 809a02f..5c804a8 100644 --- a/integrations/memmesh-plugin/skills/graph/SKILL.md +++ b/integrations/memmesh-plugin/skills/graph/SKILL.md @@ -9,6 +9,9 @@ description: > # graph +> **⚙️ Graph reasoning requires MemMesh hosted mode.** Building the graph works locally: `memory_extract_pending` → `memory_commit_extraction` populate typed entities/edges. Multi-hop **reasoning/traversal** (`memory_graph_reason`, `memory_query_graph`, `memory_prefetch_related`) runs on the hosted engine — set your `mm-` API key. If those return "unknown tool" on a local install, say so and use `search` over the extracted entities instead. + + MemMesh links memories into a knowledge graph whose edges are **bi-temporal** (each has `valid_from` / `valid_to`). That enables answers a flat store can't give. diff --git a/integrations/memmesh-plugin/skills/predict/SKILL.md b/integrations/memmesh-plugin/skills/predict/SKILL.md index f2a59e1..50cc8c3 100644 --- a/integrations/memmesh-plugin/skills/predict/SKILL.md +++ b/integrations/memmesh-plugin/skills/predict/SKILL.md @@ -10,6 +10,9 @@ description: > # predict +> **⚙️ Requires MemMesh hosted mode.** Calibrated prediction and behavior discovery run on the hosted engine — set your `mm-` API key. On a local / open-source install these tools (`memory_predict`, `memory_build_context`) are not registered; if a call returns "unknown tool", tell the user this is a hosted capability and fall back to `search` / `recall` for what's already known. + + Turn accumulated memory into a forward forecast. Unlike `search` ("what do we know"), `predict` answers "what happens next" — and it tells you how confident it honestly is, or abstains. diff --git a/integrations/memmesh-plugin/skills/why/SKILL.md b/integrations/memmesh-plugin/skills/why/SKILL.md index 9e8b4a4..2e0b0d6 100644 --- a/integrations/memmesh-plugin/skills/why/SKILL.md +++ b/integrations/memmesh-plugin/skills/why/SKILL.md @@ -10,6 +10,9 @@ description: > # why +> **⚙️ Requires MemMesh hosted mode.** Calibrated prediction and behavior discovery run on the hosted engine — set your `mm-` API key. On a local / open-source install these tools (`memory_predict`, `memory_build_context`) are not registered; if a call returns "unknown tool", tell the user this is a hosted capability and fall back to `search` / `recall` for what's already known. + + Make MemMesh's outputs auditable. Every prediction and consolidated fact carries provenance and a calibrated confidence — this skill exposes them so a human can check the reasoning.