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.
Data flow
Section titled “Data flow”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 - The edge worker authenticates the request and routes it to a project-scoped Durable Object.
ProjectDOis 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.- 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).
- 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_completedevent. - Daily cron (03:00 UTC) prunes old events, enforces retention, and runs memory consolidation — clustering old session summaries into durable semantic-tier reflections.
Crossing tools
Section titled “Crossing tools”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
Hybrid retrieval
Section titled “Hybrid retrieval”search_memory runs five retrieval legs in parallel and fuses with reciprocal rank fusion:
| leg | wins 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 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.
Temporal knowledge graph
Section titled “Temporal knowledge graph”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.
Candidate staging
Section titled “Candidate staging”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.