{"id":47956292,"url":"https://github.com/dungle-scrubs/hippo","last_synced_at":"2026-04-04T09:34:01.033Z","repository":{"id":340286161,"uuid":"1165351104","full_name":"dungle-scrubs/hippo","owner":"dungle-scrubs","description":"Persistent memory for AI agents — facts, semantic search, conflict resolution, and decay. SQLite-backed, zero external services.","archived":false,"fork":false,"pushed_at":"2026-03-20T10:18:25.000Z","size":1093,"stargazers_count":1,"open_issues_count":10,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-21T02:58:03.445Z","etag":null,"topics":["ai-agents","embeddings","llm","mcp","memory","semantic-search","sqlite","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dungle-scrubs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-24T04:32:20.000Z","updated_at":"2026-03-20T10:16:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dungle-scrubs/hippo","commit_stats":null,"previous_names":["dungle-scrubs/hippo"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/dungle-scrubs/hippo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dungle-scrubs%2Fhippo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dungle-scrubs%2Fhippo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dungle-scrubs%2Fhippo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dungle-scrubs%2Fhippo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dungle-scrubs","download_url":"https://codeload.github.com/dungle-scrubs/hippo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dungle-scrubs%2Fhippo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31394804,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T09:13:02.600Z","status":"ssl_error","status_checked_at":"2026-04-04T09:13:01.683Z","response_time":60,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","embeddings","llm","mcp","memory","semantic-search","sqlite","typescript"],"created_at":"2026-04-04T09:34:00.943Z","updated_at":"2026-04-04T09:34:01.016Z","avatar_url":"https://github.com/dungle-scrubs.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/logo.png\" alt=\"Hippo\" width=\"200\" height=\"200\"\u003e\n\u003c/p\u003e\n\n# Hippo\n\nPersistent memory for AI agents. Give your agent the ability to\nlearn facts, store experiences, recall semantically, and forget\non command — backed by SQLite, with no external services.\n\n## Three ways to use it\n\n| Mode | What | When |\n|------|------|------|\n| **Library** | `createHippoTools(opts)` returns `AgentTool[]` | You're building on marrow / pi-agent-core |\n| **MCP server** | `hippo-server` binary, HTTP/SSE or STDIO | Any MCP-compatible client (Claude, Cursor, etc.) |\n| **CLI** | `hippo` binary for inspection and management | Database admin, debugging, backup/restore |\n\nAll three share the same SQLite storage, strength model, and\nconflict resolution. The library provides all 8 memory tools; the\nMCP server exposes 7 (all except `recall_conversation`, which\nrequires a client-managed messages table). The CLI provides\nread/write database access without embedding or LLM calls.\n\n## Install\n\n```bash\npnpm add @dungle-scrubs/hippo\n```\n\n### Dependencies by usage mode\n\n| Dependency | Library | MCP server | CLI |\n|------------|---------|------------|-----|\n| `better-sqlite3` | Required | Required | Required |\n| `@mariozechner/pi-agent-core` | Required | — | — |\n| `@mariozechner/pi-ai` | Required | — | — |\n\n**Library mode** returns `AgentTool` instances from pi-agent-core,\nso both pi packages are peer dependencies. **MCP server** and\n**CLI** are standalone — they have no pi dependency. If you're\nonly using hippo as an MCP server or CLI tool, you only need\n`better-sqlite3`.\n\n## Quick start — Library\n\n```typescript\nimport Database from \"better-sqlite3\";\nimport { createHippoTools } from \"@dungle-scrubs/hippo\";\n\nconst db = new Database(\"agent.db\");\n\nconst tools = createHippoTools({\n  db,\n  agentId: \"my-agent\",\n  embed: async (text) =\u003e {\n    // Your embedding function → Float32Array\n    return callEmbeddingApi(text);\n  },\n  llm: {\n    complete: async (messages, systemPrompt) =\u003e {\n      return callLlm(messages, systemPrompt);\n    },\n  },\n});\n\n// Pass to your agent framework\nagent.addTools(tools);\n```\n\n`createHippoTools` initializes the schema (idempotent) and returns\n7 tools. Pass `messagesTable: \"messages\"` to get an 8th tool that\nsearches conversation history via FTS5.\n\n### Built-in providers\n\nDon't want to wire up embedding and LLM functions yourself? Hippo\nships OpenAI-compatible providers that work with any `/v1/embeddings`\nor `/v1/chat/completions` endpoint (OpenAI, OpenRouter, Ollama,\nvLLM, etc.):\n\n```typescript\nimport Database from \"better-sqlite3\";\nimport {\n  createHippoTools,\n  createEmbeddingProvider,\n  createLlmProvider,\n} from \"@dungle-scrubs/hippo\";\n\nconst db = new Database(\"agent.db\");\n\nconst tools = createHippoTools({\n  db,\n  agentId: \"my-agent\",\n  embed: createEmbeddingProvider({\n    apiKey: process.env.OPENAI_API_KEY!,\n    baseUrl: \"https://api.openai.com/v1\",\n    model: \"text-embedding-3-small\",\n    dimensions: 1536, // optional\n  }),\n  llm: createLlmProvider({\n    apiKey: process.env.OPENROUTER_API_KEY!,\n    baseUrl: \"https://openrouter.ai/api/v1\",\n    model: \"google/gemini-flash-2.0\",\n  }),\n});\n```\n\n### Embedding model safety\n\nCall `verifyEmbeddingModel(db, \"text-embedding-3-small\")` after\n`initSchema` to lock the database to a specific embedding model.\nFirst call stores the model name. Subsequent calls throw if the\nmodel doesn't match — prevents mixing incompatible vector spaces.\n\n```typescript\nimport { initSchema, verifyEmbeddingModel } from \"@dungle-scrubs/hippo\";\n\ninitSchema(db);\nverifyEmbeddingModel(db, \"text-embedding-3-small\");\n// Later, with a different model:\nverifyEmbeddingModel(db, \"voyage-3\"); // throws!\n```\n\n## Quick start — MCP server\n\nThe MCP server handles embedding and LLM calls internally. Clients\nsend text; hippo vectorizes and stores. Every tool call takes an\n`agent_id` parameter for multi-agent support on a shared database.\n\n```bash\n# Required\nexport HIPPO_DB=./agent.db\nexport HIPPO_EMBED_KEY=sk-...\nexport HIPPO_LLM_KEY=sk-...\n\n# Start HTTP/SSE server (default)\nhippo-server\n\n# Or STDIO for single-client piping\nHIPPO_TRANSPORT=stdio hippo-server\n```\n\n### Server environment variables\n\n| Variable | Required | Default |\n|----------|----------|---------|\n| `HIPPO_DB` | Yes | — |\n| `HIPPO_EMBED_KEY` | Yes | — |\n| `HIPPO_LLM_KEY` | Yes | — |\n| `HIPPO_TRANSPORT` | No | `http` |\n| `HIPPO_PORT` | No | `3100` |\n| `HIPPO_EMBED_URL` | No | `https://api.openai.com/v1` |\n| `HIPPO_EMBED_MODEL` | No | `text-embedding-3-small` |\n| `HIPPO_EMBED_DIMENSIONS` | No | (model default) |\n| `HIPPO_LLM_URL` | No | `https://openrouter.ai/api/v1` |\n| `HIPPO_LLM_MODEL` | No | `google/gemini-flash-2.0` |\n\n### HTTP endpoints\n\n| Method | Path | Purpose |\n|--------|------|---------|\n| `GET` | `/sse` | Open SSE connection (returns `sessionId`) |\n| `POST` | `/messages?sessionId=\u003cid\u003e` | Send MCP messages |\n| `GET` | `/health` | Health check (`{\"status\": \"ok\"}`) |\n\n## Quick start — CLI\n\nThe CLI inspects and manages hippo databases without embedding or\nLLM access. For semantic operations, use the library or MCP server.\n\n```bash\n# Set once, or pass --db \u003cpath\u003e to every command\nexport HIPPO_DB=./agent.db\n\n# Initialize schema (idempotent)\nhippo init\n\n# Overview\nhippo stats\nhippo agents\n\n# Browse data\nhippo chunks my-agent\nhippo chunks my-agent --kind fact --limit 20\nhippo blocks my-agent\nhippo block my-agent persona\n\n# Text search (case-insensitive, across all agents)\nhippo search \"redux\" --kind fact\n\n# Maintenance\nhippo delete CHUNK_ID_1 CHUNK_ID_2 --force\nhippo purge --force\nhippo purge --agent my-agent --before 2025-01-01 --force\n\n# Backup and restore\nhippo export my-agent \u003e backup.json\nhippo import backup.json\n```\n\nAll commands support `--json` for machine-readable output.\n\n### CLI commands\n\n| Command | What it does |\n|---------|-------------|\n| `init` | Create tables and indexes (idempotent) |\n| `stats` | Chunk counts, block counts, agent count, file size |\n| `agents` | List all agent IDs with chunk counts |\n| `chunks \u003cagent\u003e` | List chunks with filters (`--kind`, `--superseded`, `--limit`) |\n| `blocks \u003cagent\u003e` | List memory blocks with sizes |\n| `block \u003cagent\u003e \u003ckey\u003e` | Get contents of a named block |\n| `search \u003ctext\u003e` | Case-insensitive LIKE search across chunks |\n| `delete \u003cids...\u003e` | Hard delete by ID, resurrects superseded chunks |\n| `purge` | Remove superseded chunks (`--agent`, `--before` filters) |\n| `export \u003cagent\u003e` | Export all data as JSON (embeddings as base64) |\n| `import \u003cfile\u003e` | Import from JSON, skip duplicate IDs |\n\n## Tools\n\n### Write\n\n| Tool | What it does |\n|------|-------------|\n| `remember_facts` | Extract facts from text, rate intensity, detect duplicates and contradictions, store or update |\n| `store_memory` | Store raw content (docs, decisions, experiences) with content-hash dedup |\n| `append_memory_block` | Append text to a named block (creates if missing) |\n| `replace_memory_block` | Find/replace text in a named block (replaces all occurrences) |\n| `forget_memory` | Semantic match → hard delete. No audit trail. |\n\n### Read\n\n| Tool | What it does |\n|------|-------------|\n| `recall_memories` | Semantic search across facts and memories, ranked by relevance × strength × recency |\n| `recall_memory_block` | Get contents of a named block (null if missing) |\n| `recall_conversation` | Full-text search over past messages (FTS5) |\n\n## Key concepts\n\n### Facts vs memories\n\nBoth live in the same `chunks` table, distinguished by a `kind` column.\n\n**Facts** are atomic claims that can conflict. \"User lives in\nBerlin\" can be superseded by \"User lives in Bangkok.\" They go\nthrough extraction, embedding, and conflict resolution.\n\n**Memories** are raw content — experiences, documents, decisions.\nThey can't conflict. Verbatim duplicates are strengthened, not\nre-inserted.\n\n### Strength and decay\n\nEvery memory decays over time unless actively used. Two forces\ninteract:\n\n**Running intensity** — a moving average across encounters.\nA single emotional outburst doesn't cement a memory; sustained\nintensity over multiple encounters does. Early readings have\nhigh influence, but the average stabilizes as data accumulates.\n\n**Decay resistance** — built by access frequency. A memory\nrecalled 50 times decays far slower than one recalled once.\n\n```\neffective_strength = intensity × e^(-λ / resistance × hours)\nresistance = 1 + log(1 + access_count) × 0.3\n```\n\n| Access count | Decay resistance | Half-life |\n|--------------|-----------------|-----------|\n| 0 | 1.0 | ~29 days |\n| 5 | 1.54 | ~44 days |\n| 20 | 1.91 | ~55 days |\n| 100 | 2.38 | ~69 days |\n\nMemories below 5% effective strength are excluded from search\nresults — effectively forgotten through disuse.\n\n### Conflict resolution\n\nWhen `remember_facts` stores a new fact, it checks existing\nfacts by cosine similarity (top 5 candidates):\n\n```\n\u003e 0.93  → auto-classify DUPLICATE, strengthen existing\n0.78–0.93 → LLM tiebreaker (one cheap call)\n\u003c 0.78  → auto-classify NEW, insert\n```\n\nThe LLM returns one of three verdicts: **DUPLICATE** (same info,\ndifferent words), **SUPERSEDES** (same topic, new value), or\n**DISTINCT** (related but both true).\n\nFacts extracted from the same text have intra-batch visibility —\neach fact sees the results of previously processed facts in the\nsame call, preventing duplicate insertions within a batch.\n\n### Search scoring\n\n`recall_memories` ranks results by a weighted composite:\n\n```\nscore = 0.6 × cosine_similarity\n      + 0.3 × effective_strength\n      + 0.1 × recency_score\n```\n\nRecency decays exponentially: ~0.97 at 3 days, ~0.74 at 30 days,\n~0.03 at 1 year.\n\nAccessed chunks get a small retrieval boost (+0.02 to intensity),\nso frequently recalled memories stay strong.\n\n### Forgetting\n\n`forget_memory` performs a hard delete. No soft deletes, no audit\ntrail. When a deleted chunk had superseded another chunk, the\nsuperseded chunk is resurrected (its `superseded_by` reference is\ncleared).\n\nMemory blocks are not touched by `forget_memory` — use\n`replace_memory_block` separately if needed.\n\n## Configuration\n\n### Library options\n\n```typescript\ninterface HippoOptions {\n  db: Database;                           // better-sqlite3 handle\n  agentId: string;                        // namespace for multi-agent isolation\n  embed: EmbedFn;                         // (text, signal?) =\u003e Float32Array\n  llm: LlmClient;                         // { complete(messages, systemPrompt, signal?) }\n  messagesTable?: string;                 // enables recall_conversation\n  scope?: string;                         // default write scope (optional)\n  recallScopes?: string | readonly string[]; // optional recall filter\n}\n```\n\n**`agentId`** — all chunks and blocks are scoped to this ID.\nMultiple agents can share one database without interference.\n\n**`scope`** — optional default scope for writes (`remember_facts`,\n`store_memory`, and memory block tools). Omit to write globally.\n\n**`recallScopes`** — optional scope filter for recall operations\n(`recall_memories`, `forget_memory`). Accepts one scope or many.\nWhen omitted, recall behavior is unchanged (searches all scopes).\n\n**`embed`** — you provide the embedding function. Hippo stores\nthe resulting `Float32Array` as a BLOB and does brute-force\ncosine similarity at query time. Any embedding model works;\ndimensions don't matter as long as they're consistent.\n\n**`llm`** — used only by `remember_facts` for extraction and\nconflict classification. A cheap, fast model is ideal. Most\ntools make zero LLM calls.\n\n**`messagesTable`** — if your agent writes messages to a table,\nhippo can search it with FTS5. You own the table and FTS index;\nhippo just reads from it. Omit this to exclude\n`recall_conversation` from the tool set.\n\n### Embedding provider options\n\n```typescript\ninterface EmbeddingProviderConfig {\n  apiKey: string;       // Bearer token\n  baseUrl: string;      // e.g. \"https://api.openai.com/v1\"\n  model: string;        // e.g. \"text-embedding-3-small\"\n  dimensions?: number;  // optional, model-dependent\n}\n```\n\n### LLM provider options\n\n```typescript\ninterface LlmProviderConfig {\n  apiKey: string;        // Bearer token\n  baseUrl: string;       // e.g. \"https://openrouter.ai/api/v1\"\n  model: string;         // e.g. \"google/gemini-flash-2.0\"\n  maxTokens?: number;    // default: 2048\n  temperature?: number;  // default: 0\n}\n```\n\n## LLM and embedding costs\n\n| Tool | LLM calls | Embed calls |\n|------|-----------|-------------|\n| `remember_facts` | 1–2 | N (per extracted fact) |\n| `store_memory` | 0 | 1 |\n| `recall_memories` | 0 | 1 |\n| `forget_memory` | 0 | 1 |\n| `recall_memory_block` | 0 | 0 |\n| `replace_memory_block` | 0 | 0 |\n| `append_memory_block` | 0 | 0 |\n| `recall_conversation` | 0 | 0 |\n\nThe second LLM call in `remember_facts` only fires when a\ncandidate falls in the ambiguous 0.78–0.93 similarity band.\nMost facts are clearly new or clearly duplicate and skip it.\n\n## Storage\n\nSQLite via better-sqlite3. Schema is created automatically on\nfirst call. WAL mode and 5-second busy timeout are set on every\nconnection.\n\n```\nchunks         — facts and memories with embeddings\nmemory_blocks  — key-value text blocks (persona, objectives, etc.)\nhippo_meta     — embedding model tracking\n```\n\nBrute-force cosine similarity is viable up to ~10k chunks per\nagent. Beyond that, pre-filter by recency or tags. Past 50k,\nconsider migrating to sqlite-vec.\n\n## Dashboard chunk mutations\n\nThese are library helpers for building edit/delete APIs outside the\nagent tool interface:\n\n```typescript\nimport { deleteChunk, updateChunk } from \"@dungle-scrubs/hippo\";\n\nconst updated = await updateChunk(db, embed, chunkId, \"new content\");\nconst deleted = deleteChunk(db, chunkId);\n```\n\n- `updateChunk` re-embeds content and updates chunk fields in a\n  single transaction.\n- `deleteChunk` hard-deletes a chunk and clears supersession\n  references that pointed to it.\n\n## Exports\n\nThe library exports both the tool factory and all building blocks:\n\n```typescript\n// Main API\nexport { createHippoTools } from \"@dungle-scrubs/hippo\";\n\n// Built-in providers\nexport { createEmbeddingProvider } from \"@dungle-scrubs/hippo\";\nexport { createLlmProvider } from \"@dungle-scrubs/hippo\";\n\n// Chunk mutation helpers\nexport { deleteChunk, updateChunk } from \"@dungle-scrubs/hippo\";\n\n// Schema utilities\nexport { initSchema, verifyEmbeddingModel } from \"@dungle-scrubs/hippo\";\n\n// Types\nexport type {\n  Chunk, ChunkKind, EmbedFn, HippoOptions,\n  LlmClient, MemoryBlock, RememberFactAction,\n  RememberFactsResult, ScopeFilter, SearchResult,\n  EmbeddingProviderConfig, LlmProviderConfig,\n} from \"@dungle-scrubs/hippo\";\n```\n\n## Development\n\n```bash\npnpm install\njust ci          # build + check + test\n\njust build       # tsc → dist/\njust test        # vitest run (209 tests)\njust test-watch  # vitest watch mode\njust typecheck   # tsc --noEmit\njust check       # biome lint + format\njust fix         # auto-fix lint and format\n```\n\n## Requirements\n\n- Node ≥ 22\n- pnpm\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdungle-scrubs%2Fhippo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdungle-scrubs%2Fhippo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdungle-scrubs%2Fhippo/lists"}