Skip to content

Architecture

Memoturn runs entirely on Cloudflare Workers. There is no application server; everything below the load balancer is V8 isolates and edge-resident state. That’s what makes per-project write serialization, sub-second WebSocket fan-out, and global low latency tractable in the same system.

Every agent on a project — whichever tool it runs in — reaches the same Durable Object. That is what makes memory shared rather than per-tool.

flowchart TB
  accTitle: Memoturn memory delivery path
  accDescr: Coding agents reach the edge worker over MCP and the CLI hooks. The edge worker forwards every project-scoped request into one ProjectDO, which serves reads from hot storage, fans broadcast events back out to subscribed sockets, and enqueues work for the ingest worker that writes Postgres and Vectorize.
  subgraph Surfaces["Agent surfaces"]
      direction LR
      CC["Claude Code"]
      CUR["Cursor, VS Code, Codex"]
      CLI["memoturn CLI"]
  end
  subgraph EdgeZone["Edge — api.memoturn.ai"]
      API["Hono worker: auth, rate limit, route to project"]
  end
  subgraph DOZone["ProjectDO — one instance per project"]
      direction LR
      HUB["WebSocket hub, hibernated"]
      HOT[("Hot storage: turns, memories, broadcasts, claims")]
      SEARCH["Hybrid search + 60s query cache"]
  end
  subgraph ColdZone["Cold storage"]
      direction LR
      PG[("Postgres via Hyperdrive")]
      VEC[("Vectorize")]
  end
  CC ==>|"MCP JSON-RPC"| API
  CUR ==>|"MCP JSON-RPC"| API
  CLI -->|"hooks, observe"| API
  API ==>|"one DO per project"| HOT
  API -->|"WebSocket upgrade"| HUB
  API ==>|"search_memory"| SEARCH
  HUB ==>|"broadcast events"| Surfaces
  HOT -.->|"enqueue"| Q(["INGEST_QUEUE"])
  Q -.-> ING["Ingest worker: synopsis, embed, extract"]
  ING -.-> PG
  ING -.-> VEC
  SEARCH -->|"dense leg"| VEC
  SEARCH -->|"lexical leg"| PG
  class CC,CUR,CLI agent
  class API edge
  class HOT,HUB,SEARCH core
  class PG,VEC cold
  class Q,ING async
Thick edges are the synchronous request path. Dotted edges are async: a write is readable from the hot cache before ingest has finished embedding it.
  1. The edge worker authenticates the request and routes it to a project-scoped Durable Object.
  2. ProjectDO is the single point of write serialization for that project. It deduplicates on a content hash, hot-caches recent turns, fans broadcasts out over hibernating WebSockets, and enqueues durable work onto the ingest queue.
  3. The ingest worker drains the queue: generates a contextual synopsis via LLM (prepended to the embedding input for disambiguation), embeds the content, upserts the vector, writes the canonical row, extracts entities, and runs fact + candidate extraction via LLM (structured triples and durable insights staged for review).
  4. Rolling summaries run on a timer per session: every ~20 turns or 10 minutes the DO consolidates the session into a summary memory and broadcasts a consolidation_completed event.
  5. Daily cron (03:00 UTC) prunes old events, enforces retention, and runs memory consolidation — clustering old session summaries into durable semantic-tier reflections.

Delivery works three ways at once. An agent pulls memory when it calls search_memory or get_project_state; it receives memory as typed broadcast events on /subscribe; and it contributes memory without calling anything, through memoturn hooks and memoturn observe. The sequence below is a turn written in one tool and read in another.

sequenceDiagram
  accTitle: A turn recorded in Cursor reaching Claude Code
  accDescr: Cursor records a turn through the edge worker into ProjectDO, which writes hot storage, broadcasts turn_recorded to a subscribed Claude Code session, and enqueues embedding work. A later search from Claude Code fuses hot, lexical and dense results.
  autonumber
  participant CUR as Cursor
  participant DO as ProjectDO
  participant ING as Ingest worker
  participant CC as Claude Code
  CUR->>DO: record_turn
  DO->>DO: hash, redact secrets, write hot storage
  DO-->>CC: turn_recorded over WebSocket
  DO->>ING: enqueue embedding work
  Note over CC,DO: Readable from the hot leg immediately
  ING->>ING: synopsis, embed, extract facts
  ING-->>DO: Postgres and Vectorize now current
  CC->>DO: search_memory
  DO-->>CC: fused, reranked results
Steps 3 and 5 are why a subscriber never polls. Step 8 works either way — the hot leg covers the gap while ingest is still running.

search_memory runs five retrieval legs in parallel and fuses with reciprocal rank fusion:

legwins on
dense (vector ANN)semantic match: paraphrases, related concepts
lexical (Postgres FTS)keyword and boolean queries
hot (DO storage scan)very fresh writes, before ingest catches up
entity (structured lookup)exact identifier / file path / error code matches
graph (entity co-occurrence BFS)related concepts connected in the knowledge graph
flowchart LR
  accTitle: The hybrid search pipeline
  accDescr: A query fans out to five parallel retrieval legs — dense, lexical, hot, entity and graph — which are fused by reciprocal rank, weighted by salience decay, reranked by a cross-encoder with a timeout fallback, and returned.
  QRY(["Query"]) --> FAN{"Fan out"}
  FAN --> DENSE["Dense — Vectorize"]
  FAN --> LEX["Lexical — Postgres FTS"]
  FAN --> HOTL["Hot — DO storage"]
  FAN --> ENT["Entity match"]
  FAN --> GRA["Graph walk"]
  DENSE --> FUSE["RRF fusion, k=60"]
  LEX --> FUSE
  HOTL --> FUSE
  ENT --> FUSE
  GRA --> FUSE
  FUSE --> SAL["Salience weighting"]
  SAL --> RER{"Reranker ready?"}
  RER -->|"yes"| CE["Cross-encoder rerank, 60/40 blend"]
  RER -->|"timeout or too few"| KEEP["Keep RRF order"]
  CE --> OUT(["Top k results"])
  KEEP --> OUT
  class DENSE,ENT,GRA edge
  class HOTL async
  class LEX agent
  class FUSE,SAL,CE core
Any leg can fail or time out without failing the query — fusion runs on whatever returned.

Post-fusion, results are weighted by salience (0.0–1.0 per memory, decaying exponentially by kind-specific half-life, boosted on recall) and then rescored by a cross-encoder (@cf/baai/bge-reranker-v2-m3) that blends 60% reranker score with 40% normalized RRF. An in-memory query cache (60s TTL, 100 entries) on the ProjectDO collapses repeated identical queries from agent loops.

Search modes (auto / chunks / summaries / entities / code / skills) push a kind filter into each leg so retrieval stays focused. code mode enables the entity leg, boosting turns that mention symbols or files extracted from the conversation.

The facts table stores structured subject-predicate-object triples with valid_from / valid_to temporal windows. record_fact auto-supersedes active facts with the same subject+predicate. query_facts supports point-in-time queries (“what was true on date X?”). find_contradictions scans for conflicting active facts via LLM judge.

Facts are auto-extracted during ingest (LLM extracts up to 5 triples per turn) and can be manually recorded via record_fact. The execute_edge tool creates typed relationships (supersedes/contradicts/derives_from/same_as) with cascading side effects.

LLM-extracted memory proposals land in the candidates table as pending before promotion to durable memory. list_candidates shows pending proposals; review_candidate(accept) promotes to a full pinned memory with embedding and search indexing. This prevents noise from low-confidence extractions polluting the semantic tier.

search_memory and list_recent_turns accept optional actor / tool / since / until filters. Every turn carries a tool provenance column (cursor, claude-code, cli/observe, …) so retrieval can be scoped to “everything Cursor wrote in the last 4 hours”. That’s the kind of cross-tool slice single-vendor memory can’t express.