Memory docs: facts with two clocks

Memory, a Context Mode Gateway feature, keeps facts from your turns and adds the ones that match to the first request of a conversation, in Claude Code and Codex, on any machine. When a fact changes, the old row is closed, not deleted: in a store eval of 24 facts, 0 of 24 memory blocks held an old value, against 23 of 24 for an append-only store.

Key facts

What it is
Bi-temporal memory: each fact has two clocks, when it was true and when the gateway learned it.
Who it is for
Developers who want Claude Code and Codex to keep facts across sessions and machines.
Clients
Claude Code and Codex on one account share one store. Part of Pro.
Measured result
Store eval, 2026-09-30, answers from Claude Haiku 4.5 and Claude Opus 5.5: 0 of 24 memory blocks held an old value, against 23 of 24 for an append-only store, and 38 of 38 "what was true then" questions were answered. Evaluation.
Limits
Small evals. Recall in real use and in Codex is not measured. On Claude Haiku 4.5 the memory block added 534 to 548 uncached input tokens to a new session's first request.

Summary

Memory is the part of the Context Mode gateway that remembers across sessions. It takes short facts from your turns and your tools, stores each one with two clocks, and adds the ones that match to the first request of a conversation. When a fact changes, the old row is closed at the moment the new one starts. It stays as history and is never deleted. Claude Code and Codex on one account share one store.

What closing old rows changes, measured on one store of 24 facts that changed 38 times, with the gateway's own code (method):

Store eval, 2026-09-30: the gateway's write, read and block code at commit a12ecce8, on SQLite, with no meaning vector. Answers from Claude Haiku 4.5 and Claude Opus 5.5 through plain Claude Code. A controlled test of the mechanism, not a benchmark of recall in real use. Every row, block, answer and grade is in the evidence file. A live eval in Claude Code through the gateway is further down.

The problem

A coding agent starts each session with what its client loads, and nothing more. Four things go wrong.

How it works

Each fact is one row: subject, predicate, object. For example production deployed_to "Render, Oregon region". The subject and predicate together are the fact's slot. A row also records its kind, its trust tier, its writer, where it applies, and two clocks.

Bi-temporal facts: two clocks

Every row carries two time ranges.

ClockColumnsMeaning
Valid time (world)valid_at, invalid_atWhen the fact was true. An empty invalid_at means "true now". A write can name its own valid_at; captured facts use the time of the write.
Recorded time (system)created_at, expired_atWhen the gateway learned the row, and when it closed it.

The two clocks answer different questions. Valid time answers "what was true then". Recorded time answers "what did the store know then". Keeping both is what lets a fact change without losing the old value. The model is not new: Zep's Graphiti uses the same bi-temporal design. What differs is where it runs (see Compared).

What happens when a fact arrives:

One slot, production deployed_to. The Hetzner row was valid from 21:14:38 to 21:15:09 and is kept as history. The Render row is valid from 21:15:09 until now. The gateway recorded each row at the moment it learned it. Slot: production · deployed_to valid time Hetzner Cloud, Falkenstein · closed, kept Render, Oregon · current recorded time Hetzner row created Render row created; Hetzner row closed 21:14:38 21:15:09 now 2026-09-29, UTC. Times from the eval's store export.
One slot from the eval. The old value was never overwritten: its valid time ends where the new value's starts.

Worked example: a changed fact

From the eval's run of record. In session 1 the user said: "Our production deploy target is Hetzner Cloud, Falkenstein region. Our on-call engineer is Selin Aydin. Our CI runs on CircleCI." In session 2, 30 seconds later: "We moved production: our deploy target is now Render, Oregon region. Our on-call engineer is now Baran Kaya. Our CI now runs on Buildkite." Session 2 does not name the old values.

The store after session 2:

SlotValueTier, writervalid_atinvalid_at
production · deployed_toHetzner Cloud, Falkenstein regioninferred, turn extraction21:14:38.71621:15:09.150
production · deployed_toRender, Oregon regionasserted, remember tool21:15:09.150(current)
on-call engineer · isSelin Aydininferred, turn extraction21:14:38.71621:15:09.150
on-call engineer · isBaran Kayaasserted, remember tool21:15:09.150(current)
ci · runs_onCircleCIinferred, turn extraction21:14:38.71621:15:09.150
ci · runs_onBuildkiteasserted, remember tool21:15:09.150(current)
production · on_call_engineerBaran Kayainferred, turn extraction21:15:14.830(current)

All three old rows were closed and kept. The last row shows slot names drifting in this run too: turn extraction wrote Baran Kaya again under a second slot, production · on_call_engineer, so that value is current in two slots. They agree, so no answer went wrong. The new rows came from the model's own context-mode-remember call in session 2, so they carry the asserted tier. The old rows came from turn extraction, so they carry inferred. Both sessions' own words are also stored as dated event rows, which never replace each other. The export in the evidence file holds fact rows only, not these event rows. Asked "what was our deploy target before we moved it?", the model answered from the session 1 event row: "Before the move, the production deploy target was Hetzner Cloud in the Falkenstein region."

Supersession needs the same slot. In a second run (run B), the extractor wrote session 1 and session 2 under different names, such as ci_provider and then ci_platform. Only 2 of 6 old rows were closed, and 4 old values stayed current. The model still gave no old value as current, because each row it gets carries its valid_at, and the memory block tells it that the newest one wins. The extractor is shown your 40 most used predicates so names stay the same, but that does not always hold. See Limits.

Reading the history

There is no single "as of" query yet that returns the store as it stood at a past time. The timeline with until is the nearest tool.

Trust tiers

Every row names who could witness it. The gateway sets the tier from the writer, not from the text.

asserteda person stated it: a signed-in write, your git config and account, or a remember call that declares asserted
observeda tool or the runtime returned it, with no model in the loop: commits, PRs, edited files, tool errors
inferreda model picked it out of a turn

Scope: where a fact applies

Each fact belongs to one of six classes. The class sets its default place on a ladder of four: everywhere, this repo, this project, this worktree.

ClassHoldsDefault place
About youYour name, language, how you like to workEverywhere, any agent
ToolkitThe tools and skills you useEverywhere, per agent
CodebaseDecisions about a project's codeThis project
TaskWhat you are doing nowThis worktree
SessionHow your agent app behavesThis worktree, per agent
EnvironmentMachine paths and anything secretThis worktree

Capture

Facts get in three ways. You write no notes and no code.

Nothing is stored from:

With Memory off, turn extraction is queued, not run.

Injection on the first turn

The gateway adds memory in two blocks. Only the model sees them; your chat does not.

01
Standing rules
On every request, a [[CM-CORE]] block at the head of the first message: at most 8 current facts, all tier asserted, oldest first. It sits in the cached part of the prompt, so after the first request it is read from cache.
02
Search
On the first request of a conversation (before the model has answered), a subagent's task, or the first request after the client replaces the conversation with a summary: the gateway reads at most 6 facts for your message and this workspace. Keyword, meaning, related-name, subject and one-hop graph searches run, and their ranks are merged. It waits at most 450 ms for the meaning vector.
03
Rank
Closed facts, stale session state (older than 72 hours) and held-back facts drop out. Newer facts rank higher, with a 30-day half-life. Often-used facts get a boost, capped at 60%.
04
Add
One [[CM-MEM]] block on your message: at most 5 items. Over 1,200 characters, a reranker keeps the best items and shortens long ones.

The [[CM-MEM]] block gives the model these rules:

  1. Your current message overrides every fact.
  2. When facts conflict, the newest valid_at wins.
  3. ✗ INVALID lines are no longer true and are given only as history.
  4. The lines are the whole result of this read. The model must not cite memory for anything not on a line.

Each read also states its result: rows, empty, or unavailable. So "I have no record of that" is a measured answer, not a guess. Later turns of a conversation get no fact block. They keep reading the prompt from cache, and the agent asks for more with a tool.

Recall tools

context-mode-recallfacts by subject, predicate or text, newest valid value first, with recent closed rows
context-mode-rememberstore a fact with a declared tier; retract by id or by slot
context-mode-searchyour stored prompts and archived tool output; part of Search, not Memory
in scriptsctx.recall, ctx.remember, ctx.timeline, ctx.prompts, ctx.predicates, ctx.export

A recall can pass a scope, and scope_strict to read only that workspace.

Try it in 5 minutes

Two moments from a normal week. The next morning, you ask where you left off. Later, you ask Codex about a choice you made in Claude Code. You never repeat yourself.

Setup

npx @context-mode/cli      # sign in, connect Claude Code and Codex, then restart them
git clone https://github.com/expressjs/express.git && cd express && npm install
git checkout -b add-nvmrc
claude

The next morning: where was I?

Day 1, in Claude Code:

Add an .nvmrc file that pins Node 20 and commit it.
Our commits start with the Jira key of the ticket I'm on, like `PAY-1432: short summary`. I'm on PAY-1432 this week. Amend that commit.

Day 2. Type /exit, then start a new session with claude (not --resume):

Morning. Where did I leave off yesterday, and which ticket am I on?

What you will see:

⏺ context-mode · memory +1 record about user

You're on PAY-1432 — add `.nvmrc` to pin Node 20. You already made the commit yesterday:
7e2964f7 PAY-1432: add .nvmrc to pin Node 20
The branch is `add-nvmrc` and your git status is clean, so the work is done.

Decide in Claude Code, ask in Codex

In Claude Code, in the same folder:

Separate thing, for the router-cache branch: we'll use a plain Map with a size cap, not lru-cache, because we don't want a new dependency in express core. No code yet, just agree.

Later, open Codex in the same folder and ask:

Why did we pick a Map over lru-cache for the router cache?

What you will see:

We chose a plain `Map` with a size cap to avoid adding a new dependency to Express core. That was the agreed plan for the router-cache branch.

⏺ context-mode · memory +1 record about user, router-cache branch

Rules you state once

We ran this on 2026-10-04 in real Claude Code sessions (claude -p 2.1.283, Claude Haiku 4.5) and one Codex run (gpt-6-luna) through the gateway, each on a new test account. The morning question was answered from memory in 3 runs of these prompts, and the Codex question in its run. The rules ran again after the "Rules you set" change: 3 times on Claude Opus 5.5 and once on Claude Haiku 4.5.

Why it matters. You pick up where you left off, in either client, without retyping yesterday's context.

Evaluation: what closing old rows changes

The question: does closing old rows change what the model is told, and what it answers? We ran the same store three ways and changed only what happens to an old value.

Method

Results

MeasureBi-temporalAppend-onlyOverwrite
Rows in the store, open62, 24 open62, 62 open24, 24 open
"Now" questions with an old value in the block0 of 2423 of 240 of 24
Three-fact questions: current value in the block18 of 1813 of 1818 of 18
Three-fact questions: only an old value in the block0 of 182 of 180 of 18
Haiku 4.5 answers: current, old, neither41, 0, 136, 2, 4same block as bi-temporal
Opus 5.5 answers: current, old, neither40, 0, 236, 2, 4same block as bi-temporal
"What was true on this date"38 of 3838 of 380 of 38

42 graded facts per model and arm: 24 single questions and 18 facts from the three-fact questions. Source: evidence file, with every row, block, answer and grade, and both clocks on every row.

What it shows:

What it does not show: recall in real sessions, recall with the meaning vector on, or Codex. One "now" question, about CI, matched no row in any arm, because the keyword read does not match the short word "CI". One Opus 5.5 answer, "Fly.io in Frankfurt", gave the current value in other words and counts as "neither" under the exact check.

Live eval in Claude Code

An earlier, smaller check ran real claude -p sessions through the deployed gateway. It tests the whole path, and it is too small to show an effect of closing rows.

Method

Results

ArmCorrectOld value given as currentSearch callsMedian timeClient-reported cost, 8 probes
Memory on8 of 8004.9 s$0.184
Memory off8 of 8096.1 s$0.069
Memory off, not entitled8 of 80116.3 s$0.100
Claude Code, no gateway0 of 80n/an/a$0.170
Run B, memory on8 of 8006.0 sn/a
Run B, memory off7 of 80107.4 sn/a

Run C is the run of record; run B repeated the design on new accounts. "Search calls" are context-mode-search calls the model made through the gateway. Time is the wall time of each claude -p run. Source: evidence file.

Compared with other approaches

From each vendor's own pages, read on 2026-09-30. "Not stated" means the page we read does not say.

ApproachWhen a fact changesHow facts get inWhere, and which clientsWhat it does not do
Context ModeBi-temporal. Old rows closed at the new value's time, kept as history.Picked from turns and tools; the remember toolThe gateway, per account. Claude Code and Codex share one store.No single "as of" query yet. Needs a stable slot name to close the old value.
Claude Code CLAUDE.md and auto memoryYou or Claude edit the file. No fact history is stated.CLAUDE.md you write; notes Claude writes as it worksLocal files. Auto memory is per repository, shared across worktrees. Claude Code.Loads the first 200 lines or 25KB of auto memory. Anthropic calls both "context, not enforced configuration".
Codex memoriesNot statedGenerated from past chats: summaries, durable entries, recent inputsLocal files under ~/.codex/memories/. Codex. Off by default.Memories may not update right away when a chat ends.
Anthropic memory toolYour code decidesClaude asks for file operations; your application runs themStorage you run. Apps on the Claude API.A building block, not a feature of Claude Code or Codex.
Graphiti and ZepBi-temporal. Facts have validity windows; old facts are invalidated, not deleted. Queries for what was true at any point in time.Episodes you send by APIZep's cloud, or Graphiti on your own Neo4j, FalkorDB or Neptune, with an LLM. Any client you wire up.Its MCP server lets an agent add episodes; automatic capture of every Claude Code or Codex turn is not built in.
mem0Memories are added, searched and updated. A validity window is not stated.Messages you send by API, or its Claude Code pluginHosted, or self-hosted. Plugins for Claude Code, Codex, Cursor and others.The Claude Code plugin needs Python 3.10+ and Git on each machine.
supermemoryFacts "update, connect, and forget"; a change should not "leave both preferences equally true". A validity window is not stated.Its plugin saves conversations when a session ends, and on requestsupermemory's service. Claude Code, Codex and OpenCode share one store per repository. Team memory apart from personal.The hooks need Node.js 18+ on each machine.
Letta CodeThe agent commits each update to a git-backed memory file system (MemFS)The agent decides what to keepIts own agent: CLI, desktop app, cloud agentsA separate agent, not a layer under Claude Code or Codex.
claude-memNot stated; a timeline tool lists context in time orderHooks save observationsSQLite with FTS5 and Chroma. Claude Code, with installers for OpenCode and others.Codex is not listed in its README.

Where Context Mode differs. The bi-temporal model itself is not ours: Graphiti and Zep use it too. The difference is where it runs. With Graphiti and Zep, you send the data, by API or by an agent calling their MCP server, and you run or rent a graph database and a model pipeline. Context Mode fills a bi-temporal store from traffic Claude Code and Codex already send through the gateway, with trust tiers on every row, and nothing to install on each machine.

Where others are stronger.

Safety and privacy

Your controls

Memory switchIn Settings. Off: no standing rules, no fact block, and no resume bundle enter the request. Turn extraction is queued. Search stays on.
Memory screenIn Memory: current and replaced facts, "Ask what it knows", held-back facts with Restore and Delete
Place buttonsper class: Everywhere, This repo, This project, This worktree. New facts follow the button.
Retractask the agent to retract a fact by its id or its slot; it is closed and kept as history

Limits

FAQ for CTOs

Isn't bi-temporal memory already done?

Yes, and we say so: Graphiti and Zep use the same model. What we add is where it runs. The store fills itself from Claude Code and Codex traffic at the gateway, with no pipeline to build and no graph database to run.

Can the model give an old value as the current one?

It can, like any model. The design works against it: closed rows are not in the block, every row carries its valid_at, and the block says the newest wins. In the store eval, 0 of 84 answers gave an old value when old rows were closed, against 4 of 84 when they stayed open.

Can a remembered fact override what I say now?

The block's first rule says your current message overrides every fact. A fact a model picked out of a turn is tier inferred and never enters the standing rules.

If memory off still answered 8 of 8, what does memory add?

In that live run: answers on the first pass, with no search call, at a median 4.9 s against 6.1 s. With memory off, the model searched your stored turns, which Search keeps on. So on a fact this recent, memory saved a round, not an answer.

How do I make it forget?

Retract a fact, or switch Memory off. A retracted fact is closed and kept as history, so you can see what changed. It is no longer sent to the model.

Who sees my data?

Your account's own store on the gateway, and Llama 3.3 70B on Cloudflare Workers AI for at most 4,000 characters of each answered turn. Credential-shaped facts are refused.

Does it work in Codex?

Yes. npx @context-mode/cli ties your Claude and Codex accounts to one Context Mode account, so both read and write one store. This eval used Claude Code only.

Can I check the eval?

Yes. The store eval file has every row with both clocks, every memory block, answer and grade. The live eval file has the scenario, every probe, answer and grade, and the fact rows after session 2 for runs B and C.

Compared with other tools: see the landscape.