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
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
22 changes: 11 additions & 11 deletions 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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ members = [
]

[workspace.package]
version = "0.1.0"
version = "0.1.1"
edition = "2021"
authors = ["MemMesh contributors"]
license = "Apache-2.0"
Expand Down
75 changes: 75 additions & 0 deletions crates/core/src/extraction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,52 @@ pub fn extract(text: &str, ctx: &ObserveContext) -> Vec<ExtractedMemory> {
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<ExtractedMemory> {
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.
Expand Down Expand Up @@ -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());
}
}
56 changes: 56 additions & 0 deletions crates/mcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
}
])
}
Expand Down Expand Up @@ -421,6 +453,30 @@ async fn handle_tool_call<S: Storage>(

"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}"),
}
}
Expand Down
4 changes: 2 additions & 2 deletions integrations/memmesh-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
3 changes: 3 additions & 0 deletions integrations/memmesh-plugin/skills/behaviors/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions integrations/memmesh-plugin/skills/context-loader/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions integrations/memmesh-plugin/skills/graph/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions integrations/memmesh-plugin/skills/predict/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions integrations/memmesh-plugin/skills/why/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading