{
  "markdown": "# tether\n\n**A shared memory layer for personal agents, across devices.** `tether` is an\n[MCP](https://modelcontextprotocol.io) server backed by a local SQLite file. Any\nMCP-compatible agent can `remember`, `recall`, `link`, and `forget` durable notes\n— facts about you, your projects, your preferences — so context follows you\ninstead of dying with each session.\n\nIt runs **local-only with zero configuration**. Point it at a hosted\n[libSQL/Turso](https://turso.tech) primary and the same file becomes an embedded\nreplica that syncs your memory across every device in near-real-time.\n\n## Why\n\nThe near future is personal agents living across many devices — laptop, desktop,\nphone. For that to feel like *one* assistant rather than several amnesiac ones,\nmemory has to be a substrate that follows you: readable and writable from every\ndevice and from any agent, not siloed inside a single tool.\n\n`tether` is that substrate. It is deliberately a *convenience layer* — it makes an\nagent more useful when present, and never breaks the agent's work when degraded.\n\n## Status\n\n**v0.6.1.** The core (four memory verbs + boot index + FTS5) shipped in v0.1;\nsince then recall has grown a semantic arm, consolidation, an associative usage\ngraph, a self-organizing store, and opt-in crystallization — each additive and\neach degrading cleanly to plain keyword recall. Every feature below is\nimplemented.\n\nDesign and rationale start at\n[`docs/superpowers/specs/2026-07-03-tether-design.md`](docs/superpowers/specs/2026-07-03-tether-design.md);\nthe associative core, seed-dominant recall, self-organizing store (Tier B1), and\ncrystallization (Tier B2) each have their own design doc under\n[`docs/superpowers/specs/`](docs/superpowers/specs/), with matching plans in\n[`docs/superpowers/plans/`](docs/superpowers/plans/).\n\n## Design at a glance\n\n- **Four memory verbs**: `remember` · `recall` · `link` · `forget`. (Enabling\n  crystallization adds one reflection-control tool, `dismiss_cluster` — not a\n  memory operation.)\n- **Upsert on write** so the store doesn't rot into near-duplicates.\n- **Rich recall** (id, type, title, body, tags, `updated_at`, plus a `via`\n  receipt saying why each hit surfaced) so an agent can judge staleness and\n  cite what it updates. `body` is a query-centered excerpt, not the whole\n  memory — see [Excerpts](#excerpts).\n- **An auto-loaded boot index** — a compact one-line-per-memory list surfaced to\n  the agent each session, so memory helps even when the agent doesn't think to\n  search.\n- **Local-first, sync optional** — the local path is untouched when no backend is\n  configured; degradation never throws.\n- **Hybrid search, associative on top** — FTS5 keyword hits and local static\n  embeddings are fused, then a usage graph (explicit links, learned co-recall,\n  semantic neighbours) pulls in connected memories. Every layer is additive\n  and degrades to plain keyword recall.\n- **Safe under parallel tool calls** — an agent that fires several\n  `remember`/`recall` calls at once gets atomic, correctly-reported results;\n  see [Performance and durability](#performance-and-durability).\n\n## Install\n\nRequires Python ≥3.10 on Linux, macOS, or Windows.\n\nRegister it with Claude Code — with [uv](https://docs.astral.sh/uv/):\n\n```sh\nclaude mcp add tether -- uvx tether-memory\n```\n\n…or install it first:\n\n```sh\npip install tether-memory\nclaude mcp add tether -- tether-memory\n```\n\n(The package is named `tether-memory` on PyPI — `tether` was already reserved\nas a common brand name. `tether` in `claude mcp add tether -- ...` is just the\nlabel Claude Code uses to refer to this server; it doesn't need to match the\ninstalled command.)\n\nOr add it as a Claude Code plugin (this repo doubles as a one-plugin\nmarketplace; the plugin runs `uvx --from 'tether-memory[semantic]'`, so\nsemantic recall comes along):\n\n```sh\n/plugin marketplace add sidyellur/tether\n/plugin install tether@tether\n```\n\nIt is also listed in the [MCP Registry](https://registry.modelcontextprotocol.io)\nas `mcp-name: io.github.sidyellur/tether`.\n\nBy default memory lives in a local SQLite file at\n`~/.local/share/tether/memory.db` — on Windows,\n`%LOCALAPPDATA%\\tether\\memory.db` (override either with `TETHER_DB`, or set\n`XDG_DATA_HOME`, which is honored on every platform). No accounts, no\nnetwork — this is the whole tool for a single machine.\n\n## Project awareness\n\ntether knows which project it is serving: Claude Code sets\n`CLAUDE_PROJECT_DIR` in every MCP server's environment, and tether takes the\ndirectory's name as the project (override or disable with `TETHER_PROJECT`).\nThat drives three things, none of which need configuration:\n\n- **The boot index leads with this project.** The auto-loaded index opens\n  with a `# This project (<name>)` section, then `# Everything else`, so the\n  agent starts a session already looking at the decisions and gotchas for the\n  repo it is in rather than whatever you touched last, anywhere.\n- **Work memories are tagged automatically.** `project`, `feedback` and\n  `reference` memories get a `proj:<name>` tag unless the agent passes a\n  `proj:` tag itself. `user` memories are about you, not the work, and stay\n  global. The tag is an ordinary tag: `recall(tags=\"proj:<name>\")` lists a\n  project's memories deterministically.\n- **Recall prefers this project on a near-tie.** A hit tagged with the\n  current project ranks a few places ahead of an equally-good hit from\n  another project; it never outranks a clearly better match, and untagged\n  memories are neither boosted nor penalized.\n\n| Var | Default | Effect |\n|---|---|---|\n| `TETHER_PROJECT` | basename of `CLAUDE_PROJECT_DIR` | name the project explicitly; `off` disables project awareness |\n\nNothing falls back to the working directory: outside Claude Code (or with\n`TETHER_PROJECT=off`) tether behaves exactly as before.\n\n## Sync across devices (optional)\n\nPoint tether at a [Turso](https://turso.tech) / libSQL database and the local\nfile becomes an embedded replica — local-speed reads, writes that propagate to\nyour other devices. Install the extra and set two env vars:\n\n```sh\npip install 'tether-memory[sync]'\nexport TETHER_SYNC_URL='libsql://<your-db>.turso.io'\nexport TETHER_SYNC_TOKEN='<your-auth-token>'\n```\n\nIf the backend is unreachable, tether logs `sync offline` and keeps working\nagainst the local file. Sync is a single-primary design: every write goes to\nthe hosted primary and replicas only pull, so a device that was merely\noffline catches up (including tombstones from `forget`) on its next pull and\ncan never resurrect a memory another device forgot. The one gap: writes made\n*while degraded* land only in that device's local file and are not merged\nback later (#99 tracks reconciliation).\n\nWrites push immediately. Reads also pull, debounced to at most once every\n`TETHER_SYNC_READ_INTERVAL` seconds (default 30) — so a device that only\n*asks* things still sees what your other devices wrote, instead of staying\nfrozen at its own startup state until it happens to write something. The\nread-path pull is bounded much more tightly than the write-path one: if the\nbackend is slow, the recall returns local data and the pull lands for the\nnext read rather than making you wait.\n\n| Var | Default | Effect |\n|---|---|---|\n| `TETHER_SYNC_READ_INTERVAL` | `30` | seconds between read-path pulls; `0` = only sync on writes |\n| `TETHER_DEVICE_ID` | hostname | the device id recorded on each memory (and the default `TETHER_AUTHOR`) |\n\nOne thing to know about replicas: libSQL forwards **every write** to the\nhosted primary, and with the associative graph on (the default) `recall`\nwrites too — it records what was recalled together so memories can wire up\nover time. On a replica that makes each recall a few network round-trips on\ntop of the local search. If that matters more to you than learned\nassociations, `TETHER_ASSOC=0` makes recall read-only again.\n\n## Keyword search\n\nThe keyword arm is SQLite FTS5 over title, body and tags, ranked by bm25. Ask\nin plain language: a memory that contains *some* of the query's words is a\nhit, and one that contains more of them ranks higher, so \"how do we run the\nintegration tests?\" finds the note that says \"pytest runs the tests\" (common\nfunction words are ignored). The index stems English words, so `tests`\nmatches `test` and `deciding` matches `decided`. Stemming is English-only;\nturn it off for a store in another language and tether rebuilds the index\non the next start.\n\n| Var | Default | Effect |\n|---|---|---|\n| `TETHER_FTS_STEMMING` | on | set `0`/`false`/`off` to index words exactly as written |\n\nMeasured on the LoCoMo long-conversation benchmark (one memory per dialogue\nturn, 1,536 questions, \"did recall return the turns that answer it\" in the\ntop 10), the keyword arm alone finds the evidence for 61% of questions,\nagainst 54% for a textbook BM25 over the same text. That harness ships in\nthe repo — see [Benchmarks](#benchmarks).\n\n## Semantic search (optional)\n\nBy default `recall` is **hybrid**: keyword (FTS5) results are fused with\nsemantic (vector) results, so a query finds relevant memories even when the\nexact words differ (\"automobile\" recalls a note about your \"car\"). Semantic\nrecall runs a small **static** embedding model locally — no network, no API\nkey, nothing to hang on. Install the extra:\n\n```sh\npip install 'tether-memory[semantic]'\n```\n\nWithout the extra (or with `TETHER_SEMANTIC=0`), tether runs keyword-only\nFTS5 — semantic is a pure add-on and never a requirement. The first run embeds\nexisting memories once (a one-time backfill); after that it is incremental.\n\nEnvironment:\n\n| Var | Default | Effect |\n|---|---|---|\n| `TETHER_SEMANTIC` | on | set `0`/`false`/`off` to force keyword-only recall |\n| `TETHER_EMBEDDING_MODEL` | `minishlab/potion-base-8M` | override the local static model |\n\n## Consolidation (optional)\n\ntether keeps a superseded fact rather than overwriting it: when a memory is\nreplaced, the old one is marked no longer current (retained for history) and\nexcluded from `recall` and the boot index. Recall also gently favors more\nrecent facts. Two opt-in behaviors go further:\n\n| Var | Default | Effect |\n|---|---|---|\n| `TETHER_CONSOLIDATE` | off | on (`1`/`true`) merges a near-duplicate on write — supersedes the old fact instead of fragmenting the store (needs the `[semantic]` extra) |\n| `TETHER_DEDUP_THRESHOLD` | `0.92` | cosine similarity required to treat two facts as duplicates |\n| `TETHER_DECAY_HALF_LIFE_DAYS` | off | set a positive number to exponentially down-rank older facts in recall |\n| `TETHER_AUTHOR` | device id | attribution recorded on each memory |\n\nConsolidation never deletes — `forget` soft-deletes (see [Tools](#tools)), and\nonly the admin CLI's `purge` is permanent. All of this degrades to plain\nkeyword recall when the semantic extra is absent.\n\n## Associative recall (optional)\n\n`recall` doesn't just return keyword/semantic matches — it follows a **usage\ngraph** to related memories, so asking about one thing surfaces its connected\ncontext. The graph's edges come from three local, deterministic sources — no\nLLM, no network:\n\n- **semantic** — nearest neighbours by embedding (needs the `[semantic]` extra),\n- **explicit** — the `link()` verb,\n- **hebbian** — memories you recall *together* get wired together over time.\n\nEvery hit carries a `via` receipt saying why it surfaced (a direct match, or the\nedge it came through), and two optional `recall` args tune it:\n\n| Arg / var | Default | Effect |\n|---|---|---|\n| `budget` (per call) | `TETHER_RECALL_BUDGET` | how far to follow associations; `0` = direct matches only |\n| `session` (per call) | time-bucketed | group related recalls so they prime each other |\n| `TETHER_ASSOC` | on | set `0`/`false`/`off` for plain keyword+semantic recall |\n| `TETHER_RECALL_BUDGET` | `8` | default association breadth |\n| `TETHER_PROTECT_HEAD` | `8` | how many top direct hits are locked above associations |\n| `TETHER_SEED_FLOOR` | `0.35` | minimum cosine similarity a semantic hit needs to seed an associative walk; below it a memory is only reachable through an edge. `0` disables the floor |\n\nAssociative recall is **seed-dominant**: the top direct matches are locked in\nplace, and associations only fill the slots below them — so turning association\non never demotes a hit that keyword/semantic search already ranked highly.\n\nWith `TETHER_ASSOC=0` (or `budget=0`, or an empty graph), `recall` behaves exactly\nas before — associative recall is purely additive and never breaks a lookup.\n\n## Self-organizing store (optional)\n\nAs a store grows, tether keeps it legible using the same usage graph:\n\n- **Hub-curated boot-index.** The auto-loaded memory index is capped once it\n  passes `TETHER_BOOT_INDEX_CAP` (default 50) — the cap always applies, so a\n  large store never gets an unbounded index. With a graph, above the cap it\n  shows two labeled slices — **load-bearing** memories (highest *behavioral*\n  degree: `explicit` links + learned co-recall, never mere similarity) and the\n  most **recent** ones — so the index stays small and shows what actually\n  matters. Without a graph (`TETHER_ASSOC=0`), it falls back to a plain\n  most-recent-N list instead of the hub/recency split. Below the cap it's the\n  full newest-first list either way.\n- **Forgetting-by-disconnection** (opt-in, `TETHER_FORGET`). A bounded sweep\n  runs every `TETHER_FORGET_INTERVAL` writes and *soft-archives* memories that\n  are both **old** (`TETHER_FORGET_AGE_DAYS`, default 90) and **behaviorally\n  isolated** (no `explicit`/`hebbian` edge — semantic similarity doesn't count).\n  Archived memories drop out of recall and the boot-index but are **retained and\n  reversible** (it reuses the same mark-invalid machinery as consolidation;\n  nothing is deleted). Safety rails: never runs without a live behavioral graph,\n  below `2 × CAP` memories, or more than `TETHER_FORGET_MAX_PER_SWEEP` (default\n  10) per sweep.\n\n| var | default | effect |\n|---|---|---|\n| `TETHER_BOOT_INDEX_CAP` | `50` | curate the boot-index above this size |\n| `TETHER_FORGET` | off | enable the forgetting sweep |\n| `TETHER_FORGET_AGE_DAYS` | `90` | minimum age to be eligible to fade |\n| `TETHER_FORGET_INTERVAL` | `20` | writes between sweeps |\n| `TETHER_FORGET_MAX_PER_SWEEP` | `10` | max archived per sweep |\n\nWith `TETHER_FORGET` off (default) and a normal store size, recall and the\nboot-index behave exactly as before.\n\n## Crystallization (optional, off by default)\n\nWith `TETHER_CRYSTALLIZE=1`, tether reflects: it detects dense clusters of\nrelated memories and offers them for naming. Read `tether://crystallization`\nduring a reflection pass (it is pull-only, never auto-loaded) to get candidate\nclusters; name a real principle with `remember(..., crystallizes=[source_ids])`\n— which writes the principle and links it over its sources — or drop a candidate\nwith `dismiss_cluster(id_a, id_b)`. Clusters are seeded by explicit links +\nusage (semantic similarity fills out membership), so this finds *\"these belong\ntogether\"* structure, not mere topical similarity. tether finds the structure;\nyour agent supplies the words.\n\nA crystallized principle becomes a boot-index hub and is reachable from its\nsources in recall. Note: this makes \"named\" a third importance signal alongside\n\"used\" and \"linked\" — deliberate, since an agent judging something\nprinciple-worthy is a strong signal.\n\n## Tools\n\n| Tool | What it does |\n|---|---|\n| `remember(type, title, body, tags?, links?, crystallizes?)` | Save a memory; upserts on `type`+`title` so facts refine rather than duplicate. `crystallizes=[ids]` writes it as a principle over those sources (needs `TETHER_CRYSTALLIZE`) |\n| `recall(query?, type?, limit?, budget?, session?, tags?, id?, full?)` | Hybrid keyword + semantic search, then follows the usage graph to related memories; returns id/type/title/body/tags/updated_at + a `via` receipt. `body` is a **query-centered excerpt** — see [Excerpts](#excerpts) — with `id=N` fetching one memory whole. `tags` is an exact-match filter (a memory must carry every listed tag); combine it with `query`, or omit `query` for a guaranteed-complete tag lookup |\n| `link(id_a, id_b)` | Bidirectional link between two memories |\n| `forget(id)` | Soft-delete a memory: marks it no longer current (excluded from recall/the boot index) via the same reversible `valid_to` machinery as consolidation, rather than deleting the row. See [Export and permanent deletion](#export-and-permanent-deletion) for a real, permanent delete |\n| `dismiss_cluster(id_a, id_b)` | Reflection control (crystallization): drop the candidate cluster nucleated by peak edge `(id_a, id_b)` so it isn't re-surfaced. Not a memory operation; only relevant with `TETHER_CRYSTALLIZE` |\n\nPlus three resources: the auto-loaded `tether://memory-index` (a compact\none-line-per-memory index surfaced each session), the pull-only\n`tether://status` (runtime config: semantic/sync state, memory and edge\ncounts, DB path — for debugging what's actually active), and, with\n`TETHER_CRYSTALLIZE`, the pull-only `tether://crystallization` (candidate\nclusters for a reflection pass).\n\n## Excerpts\n\n`recall` returns a **relevance-centered excerpt** of each memory's body, not\nthe whole thing — the window is centered on the first query term that appears,\nso you see *why* the memory matched rather than just its opening lines. A hit\nthat was cut also carries `truncated: true` and `body_chars` (the real length),\nand you fetch the one memory you actually want in full with `recall(id=N)`.\n\nThis is the search-engine shape: the result list is an index of pointers with\nenough text to judge relevance, not a payload of documents. It matters because\nmemories can be large — a single 44KB journal memory made unrelated queries\ncost ~57–67KB per call while the retrieval itself took under a millisecond.\nThe response was fat, not the engine:\n\n| query | full bodies | excerpts |\n|---|---|---|\n| `seed dominance` | 57.0KB | **1.4KB** |\n| `hebbian edges` | 66.9KB | **2.0KB** |\n| `cold start latency` | 66.9KB | **2.0KB** |\n\nA memory shorter than the excerpt width is returned whole and unmarked, exactly\nas before.\n\n| Var / arg | Default | Effect |\n|---|---|---|\n| `TETHER_EXCERPT_CHARS` | `500` | excerpt width; `0` returns full bodies |\n| `id` (per call) | — | fetch just this memory, whole |\n| `full` (per call) | `false` | full bodies for every hit — costs the whole payload; prefer `id=` |\n\n## Performance and durability\n\ntether is meant to be invisible in an agent's loop, so the hot paths are\nmeasured and kept flat as the store grows. Numbers below are from a local\nSQLite store with the semantic and associative layers on, single process:\n\n| memories | `remember` | `recall` (rare term) | `recall` (term in most memories) |\n|---|---|---|---|\n| 500 | 1.6 ms | 1.8 ms | 3.7 ms |\n| 2,000 | 1.8 ms | 2.5 ms | 7.7 ms |\n| 8,000 | 3.4 ms | 1.0 ms | 19 ms |\n\nA few things that make this hold:\n\n- **Writes don't scale with the store.** The embedding matrix used for\n  semantic search and neighbour wiring is kept in memory and patched row by\n  row on every write, rather than re-read from SQLite. It is rebuilt only\n  when vectors change wholesale (a model change, a backfill) or when another\n  process has written to the file (a CLI `purge`, a second server, a sync\n  pull) — SQLite's `data_version` counter catches that.\n- **Parallel tool calls are serialized.** MCP runs each tool call on its own\n  thread, and agents issue calls in parallel. All Store operations take one\n  lock, so a `recall` and a `remember` arriving together are each atomic:\n  no interleaved transactions, no half-committed writes, and `action` is\n  always right.\n- **Commits don't fsync.** Local connections run WAL with\n  `synchronous=NORMAL`: still safe against corruption, but the last few\n  transactions can be lost if the *machine* loses power before a checkpoint\n  (an application crash loses nothing). Every `remember` and, with the graph\n  on, every `recall` commits, so this is one disk sync saved per call.\n- **Search stays cheap.** Vector search is a single numpy matmul over the\n  in-memory matrix — well under a millisecond at thousands of memories,\n  which is why there is no vector-index extension to install. Keyword cost\n  is FTS5's: proportional to how many memories match the query.\n\nCosts to expect once: the first boot after installing the `[semantic]` extra\n(or changing the model) embeds every existing memory and wires its\nneighbours, which takes a second or two per few thousand memories. The boot\nindex and tag-only lookups scan the store on each call; both are fast at\ntypical sizes (under 10 ms at 2,000 memories) and are the next things on the\nlist to cache.\n\n## Benchmarks\n\nTwo harnesses live in `bench/`, both runnable without an API key:\n\n- **`python -m bench.locomo`** — retrieval-only evaluation on\n  [LoCoMo](https://github.com/snap-research/locomo), ten long multi-session\n  conversations with ~1,500 questions each labelled with the dialogue turns\n  that answer it. Every turn becomes a memory; the score is whether\n  `recall(question)` returns those turns (recall@k, MRR), per condition:\n  keyword only, keyword + semantic, and the full associative path, next to a\n  textbook BM25 baseline. No LLM is involved, so the number measures the one\n  thing a memory layer controls — did it hand the agent the right facts — and\n  is not comparable to the LLM-judged \"accuracy\" figures memory vendors\n  publish on the same dataset. The data (~1.5 MB) is downloaded on first\n  use. Install the `[semantic]` extra to measure the semantic and associative\n  conditions with the real model.\n- **`python -m bench.run`** — tether's own associative-recall evaluation: a\n  controlled corpus of tasks whose members are used together, measuring what\n  the usage graph adds over keyword + semantic search after simulated use.\n  Needs the `[semantic]` extra.\n\n## Export and permanent deletion\n\n`forget` never deletes data — it soft-deletes, like consolidation. Two admin\noperations, deliberately kept off the MCP tool surface so an agent can't\ntrigger them, live in a small CLI instead:\n\n```sh\ntether export                    # dump all current memories to JSON (stdout)\ntether export -o backup.json     # ...or to a file\ntether import backup.json        # merge an export back into the store\ntether restore <id>              # un-forget a soft-deleted memory\ntether purge <id> --yes          # permanently delete a memory (bypasses forget)\n```\n\n`import` replays records through the normal write path, so it upserts on\n`type`+`title` like `remember` does — importing into a non-empty store merges\nrather than duplicating. Ids are not preserved (an id in the file may map to a\ndifferent one here); links are remapped accordingly, and a link pointing\noutside the file is dropped rather than pointed at the wrong memory. The\nreport tells you what happened: `{\"created\", \"updated\", \"skipped\", \"linked\",\n\"dropped_links\"}`.\n\n`restore` clears `valid_to`, reversing a `forget` (or a consolidation, or a\nforgetting sweep). It refuses if a newer memory has since claimed the same\n`type`+`title`, naming the blocker rather than failing opaquely.\n\n`purge` refuses to run without `--yes`. All commands honor the same\n`TETHER_DB`/`TETHER_SYNC_*` env vars as the server.\n\n## License\n\nMIT\n",
  "bytes": 23245,
  "sha": "244200b664086f523e4381f31fd90c1a82a701c10431d826defbb400d5711072",
  "repo_slug": "sidyellur/tether",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_sidyellur_tether_fc36ddd8/readme"
}