TTemporalStore.AI GitHub

Tech & infrastructure deep dive

How TemporalStore serves temporal context at scale.

TemporalStore is an append-structured, model-aware serving engine: a metaserver plus datanodes, a multi-layer cache, replicated WAL, and tiered storage from memory to SSD to shared store — with pluggable backends up to disaggregated, five-nines deployments.

Architecture

All modules: serving core, dependencies, tiering, and storage backends.

Clients talk to a stateless proxy that routes by namespace and table. A metaserver owns placement; datanodes run the temporal models, back their state with an append-structured storage engine, and lean on two open-source libraries — MatrixCache for tiered caching and MatrixRaft for replicated durability. Context data tiers from memory to SSD to a shared store, and the shared store is pluggable up to the enterprise backends.

Clients
AI agents & coding systemsSDK writes · online reads · stream consumers · replay jobs
↓  context ingest & retrieval  ↓
TemporalStore serving core — open source
Proxy / gatewaynamespace + table routing, batching, backpressure
Metaservershards, bucket ranges, placement & membership
Datanodestemporal model executors, query planner, WAL
Dependency libraries — open source
MatrixCachemulti-layer cache — L1 memory + L2 SSD, admission & eviction
MatrixRaftconsensus & replicated WAL for high availability
↓  Context data moves hot → cold  ↓
Context data tiering
Memoryrecent events, hot Context nodes, working set
SSDwarm blocks, bands, snapshot & page index
Shared storecold blocks, replay history, durable slabs
↓  pluggable storage backend  ↓
Enterprise storage backends Enterprise only

One request path, top to bottom: route → place → execute over cached, replicated, tiered temporal state. MatrixObject, MatrixKV, and MatrixDB are enterprise backends; local disk and MatrixRaft are open source.

Node layout

The actual cluster, and what lives inside one datanode.

Zooming in on the topology: a stateless proxy tier fields client connections and caches the routing map; a single logical metaserver owns placement and membership as a control plane off the hot path; and a fleet of datanodes each own a set of shards. Inside every datanode is a memory-to-SSD-to-durable hierarchy that keeps the hot working set in RAM while nothing ever leaves the queryable surface.

Proxy tier — stateless
Proxy / gateway (N replicas)caches the shard map · routes by namespace + table + bucket · batching & backpressure · holds no durable state
↓  routing lookups (cached, refreshed on placement change)  ↓
Control plane — off the hot path
Metaservershard map · bucket ranges & placement · membership · leases & epochs — consulted only when the map changes, never per read
↓  owns which datanode serves which shard  ↓
Data plane — datanodes own shards
Datanode Ashards 0–3 · model executors + WAL
Datanode Bshards 4–7 · model executors + WAL
Datanode Cshards 8–11 · model executors + WAL
↓  inside one datanode: memory hierarchy & eviction  ↓
Datanode internals — hot → cold with cache fill on miss
Memory (L1)hot entity working set · recent events · live page index — served with no I/O
SSD (MatrixCache L2)warm blocks & bands evicted from RAM · admission on read, LRU eviction under pressure
Shared storecold blocks/bands & durable slabs · source of truth for cache fill

Least-recently-used entity state is evicted from memory down to SSD and, eventually, to the durable store — but it never leaves the queryable surface. A read that misses in RAM triggers a cache fill from L2 or the shared store, so cold history stays reachable at a modest latency cost rather than being lost. Admission keeps a burst of cold replay traffic from flushing the hot set.

Deep dive

How each layer works.

Serving core: metaserver + datanodes

The metaserver is the control plane. It owns namespace, table, shard, and bucket-range metadata, decides which datanode owns which shard, and tracks membership and leases. It is not on the hot data path: the proxy caches routing and only consults the metaserver on placement changes. Datanodes are the data plane. Each runs model-aware executors — Context, Long Sequence Feature, Aggregated Feature, Control State — over entity-local state, so a read carries temporal semantics (window, filter, distinct, tail) instead of returning an opaque blob for the client to post-process.

Storage engine: append-structured blocks

Underneath every model is one storage engine. Values are packed into blocks; blocks are grouped into append-only bands; bands are written into slab files on disk. Nothing is updated in place — new state is always appended, and obsolete state is reclaimed later by background garbage collection rather than by rewriting live data. Three indexes make reads cheap: a logical page index maps entity/time keys to the pages that hold them, a physical block index locates blocks within slabs, and an object index tracks larger materialized objects. Because appends are sequential and indexes are small, tail reads and windowed slices stay fast even as an entity's history grows long.

Multi-layer cache: MatrixCache

MatrixCache fronts the engine with two tiers. L1 is in-memory and holds the hot working set — recent events and frequently touched Context nodes. L2 is on SSD and absorbs the warm set that no longer fits in memory. Admission decides what is worth caching and eviction reclaims space under pressure, so a burst of cold replay traffic does not flush the hot set. Under high concurrency this is what keeps p99 tail reads flat.

Replication & recovery: MatrixRaft + WAL replay

Durability starts with the WAL: every mutation is appended to the write-ahead log before it is acknowledged. Periodically the engine writes a snapshot and checkpoints the index, which bounds how far recovery has to go. On restart or failover a replica loads the last snapshot and durable pages, then replays the WAL after the checkpoint to reach the current state. MatrixRaft replicates that WAL by consensus, so a shard survives node loss without shared storage; primaries and secondaries fence with leases and epochs so a stale writer cannot corrupt state after a failover.

Data tiering: memory → SSD → shared store

Context data ages automatically. Recent events and hot nodes live in memory; warm blocks, bands, and the page-index snapshot settle onto SSD; cold blocks and replay history land in the shared store as durable slabs. Reads pull from the highest tier that has the data, and the boundaries move with access, so hot workloads stay in RAM while long tails remain cheap and fully replayable.

WAL replication

How the write-ahead log moves between nodes.

A write for a shard lands on that shard's primary datanode, which appends it to the WAL and hands the record to MatrixRaft. Consensus replicates it to the secondary replicas; only once a quorum acknowledges does the write become commit-visible. Secondaries apply the committed WAL to rebuild their own queryable state, so any of them can serve reads or step up on failover.

Primary datanode — owns the shard
Primary datanodewrite lands here → appended to the WAL · not yet commit-visible
↓  replicate WAL record  ↓
Consensus — MatrixRaft
MatrixRaftreplicates the WAL record · waits for quorum ack before commit-visible · lease/epoch fencing
↓  committed WAL fans out  ↓
Secondary replicas — apply & stay hot
Secondary Aapplies committed WAL → rebuilds queryable state · failover candidate
Secondary Bapplies committed WAL → rebuilds queryable state · failover candidate

Quorum-acknowledged commit: a write is durable across the replica set before it is visible to readers. On primary loss, a replica holding the latest committed WAL is promoted; lease and epoch fencing reject a stale ex-primary that comes back, so it cannot corrupt state after failover.

1

Write

Request lands on the shard's primary datanode and is appended to the WAL.

2

Replicate

MatrixRaft ships the WAL record to the secondary replicas.

3

Quorum ack

A majority acknowledges; only then does the write become commit-visible.

4

Apply

Secondaries apply the committed WAL to rebuild queryable state, ready to serve or be promoted.

Shared-storage deployments read WAL directly

When the shared store is MatrixObject Enterprise, replication does not have to flow node-to-node. Because committed WAL and blocks are already durable in shared storage, secondaries can read them directly from the shared store instead of pulling from the primary. Compute and storage stay disaggregated: a replica catching up, or a freshly added datanode, hydrates from the object store rather than draining the primary's bandwidth, and the same committed-WAL contract still governs what is visible.

Two replication modes

Raft replication is not compute/storage disaggregation — shared storage is.

Both modes keep your context durable and highly available, but they get there in fundamentally different ways. It matters which one you pick, because only one lets compute and storage scale independently.

Raft replication — co-located
Datanode (primary) + local storagefull copy of the shard: WAL, blocks, bands
↓  MatrixRaft quorum  ↓
Replica + local storagefull copy
Replica + local storagefull copy

Every replica is a whole datanode that stores its own complete copy. Compute and storage travel together — this is replication, not disaggregation.

Shared-store — disaggregated Enterprise
Datanodestateless compute
Datanodestateless compute
Datanodestateless compute
↓  read/write one durable copy  ↓

Datanodes hold no durable data; the shared store owns durability. Compute and storage scale on independent axes — disaggregation.

Raft replication (open source)Shared-store replication Enterprise
Durable copiesOne full copy per replica (N copies)One copy in the shared store (it handles its own redundancy)
Compute & storageCo-located on each nodeDisaggregated — separate, independent tiers
Scale byAdding full replicas (each stores everything)Adding stateless datanodes (no data copied)
Recover a node byPromoting a replica; new nodes stream a full copyReopening blocks from the shared store — no state transfer
Best forHA without external storage; fixed-size clustersLarge scale, elastic capacity, concurrent read/write at five-nines
DisaggregationNoYes

Why no vector DB, why not RocksDB

Append-structured, model-aware, and temporal by design.

No separate vector store for most queries

Agent-memory retrieval is filter-first over a temporal tree. Scope hashes, time windows, and typed filters narrow the candidate set before any similarity math runs, which inverts the usual vector-database pipeline where an approximate-nearest-neighbor (ANN) index is the primary lookup and metadata filtering is a post-filter applied to whatever the index happened to return. That ordering matters: the questions agents actually ask — “what happened in this session,” “which tool calls in the last hour,” “what did I decide and when” — are answered exactly by scope and time, not by embedding proximity. Because the candidate set is already small and correct by construction, similarity ranking, when you want it, runs over tens of rows rather than the whole corpus.

Crucially, time-validity and supersession are first-class in the temporal model: a fact carries a valid-time window, and a later write can supersede an earlier one, so retrieval returns what was true at the asked-about time and drops what has been overwritten. A pure ANN index has no notion of this — it returns whatever is geometrically nearest, which routinely means stale, superseded, or out-of-scope chunks that are merely “near” in embedding space. Semantic vector recall is therefore an optional add-on layered on top of the temporal filter for genuinely fuzzy recall, not the primary index. For most queries you deploy, sync, and pay for no standalone vector store, you skip re-embedding and index-rebuild churn, and you stop leaking chunks from other sessions or from a state that no longer holds.

No update-in-place write amplification

RocksDB-style LSM trees rewrite SSTables during compaction: every logical write is re-written several times as data is merged down the levels, so a single append can cost many physical writes, and background compaction periodically competes with foreground traffic for I/O, producing compaction stalls that show up as tail-latency spikes. TemporalStore is append-structured instead — values are packed into blocks, blocks are appended into bands, and bands are written into slab files; nothing is ever rewritten in place. Obsolete data is reclaimed by background garbage collection that drops whole obsolete regions once they fall below a liveness ratio, rather than by merging and re-sorting live data.

High-write temporal workloads — event streams, tool traces, velocity counters, session logs — are exactly the pattern that punishes LSM compaction, and exactly what append-structured storage absorbs cleanly: writes stay sequential, the WAL is the durable path, and reclamation happens off the write path. On top of that, the engine is model-aware: windows, filters, distinct, and sequence logic run at the shard, next to the data, so a read carries temporal semantics rather than returning an opaque KV blob for the client to re-process.

Model-aware, not opaque blobs

Because the executors run next to the data, callers do not re-implement temporal math on top of a KV value, and the store does not ship whole histories over the wire just to have the client throw most of it away. One serving path replaces the usual stack of a stream job, a bespoke cache layout, and a repair pipeline per feature. See the data modelsContext, Long Sequence Features, Aggregated Features, and Control State — for what each executor computes at read time.

Storage backends

Pick the durability tier for the deployment.

The shared store is pluggable. Two backends are open source; the disaggregated backend is enterprise.

BackendBest forNotes
Local disk
open source
Single-node, dev, edge, self-hosted.One owner per shard; simplest operations, no external dependencies.
MatrixRaft
open source
Replicated HA without shared storage.Consensus-replicated WAL across a fixed replica set; survives node loss. Compute and storage stay co-located — replication, not disaggregation.
MatrixObject EnterpriseDisaggregated, concurrent read/write at scale.Shared durable object storage; compute and storage scale independently. Details

Deployment

From one container to a distributed cluster.

Start local: a single datanode on local disk needs nothing else. Scale out by adding a metaserver, more datanodes, and MatrixRaft replication. Bring your own OSS models for embeddings and reading — TemporalStore does not require a hosted API.

Local: single node in a container

The standalone mode runs a datanode serving a local shard with no metaserver. Point it at a data directory and go.

Docker — single standalone node
# run one self-contained datanode on local disk
docker run -d --name temporalstore \
  -p 17102:17102 \
  -e TS_STANDALONE=1 \
  -e TS_META_ADDR=local \
  -e TS_STORAGE_BACKEND=local \
  -v $PWD/ts-data:/var/lib/temporalstore \
  ghcr.io/bjmeetsfo/temporalstore:latest

Distributed: metaserver + datanodes + replication

A minimal cluster is one metaserver plus a few datanodes, with MatrixRaft replicating each shard's WAL.

docker-compose.yml — metaserver + datanodes
services:
  metaserver:
    image: ghcr.io/bjmeetsfo/temporalstore:latest
    command: metaserver --listen 0.0.0.0:17101
    ports: ["17101:17101"]

  datanode:
    image: ghcr.io/bjmeetsfo/temporalstore:latest
    command: datanode --meta metaserver:17101
    environment:
      TS_STORAGE_BACKEND: raft      # MatrixRaft replicated WAL
    deploy:
      replicas: 3
    depends_on: [metaserver]

Bring your own OSS models

Embeddings and any reader model are pluggable. A common self-hosted setup pairs a local embedding model for retrieval with an open-weights model served by Ollama.

Local models with Ollama
# pull an open-weights reader and an embedding model
ollama pull qwen2.5
ollama pull nomic-embed-text

# point TemporalStore at the local endpoints
export TS_EMBED_MODEL=nomic-embed-text
export TS_EMBED_ENDPOINT=http://localhost:11434
export TS_READER_MODEL=qwen2.5

A minimal client

Connect, append a few context records, then ask a temporal question at read time.

Python client — write then read
from temporalstore import Client

store = Client("localhost:17102")

# append time-indexed context events
store.append(session_id="sess-4817", kind="tool_trace",
             payload={"tool": "search", "ok": True})

# read time: build a budget-bounded ContextPack
pack = store.build_context(session_id="sess-4817",
                           window="24h", budget_tokens=2048)
print([(i.kind, i.ts) for i in pack.items])

Keep reading

Go deeper.

Deep essayServing engine internalsStorage layout, replication, and recovery. Data modelsWhat the engine servesContext, sequences, aggregates, control state. BenchmarksScale & quality evidenceLatency, parity, and context quality.