# 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](https://context-mode.com/docs/faq#access).

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](#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](#evaluation)):

[Old values in the memory block · 24 questions 0 of 24 with closed rows. 23 of 24 when old rows stay open (append-only).](#evaluation) [Current value pushed out · 18 slot answers 0 of 18 with closed rows. 5 of 18 append-only: old rows of other facts filled the six places.](#evaluation) [Model gave an old value · 84 answers 0 with closed rows, on Haiku 4.5 and Opus 5.5. Append-only: 4 old values and 8 "not in memory".](#evaluation) ["What was true on a date" · 38 questions 38 of 38 answered from history. A store that overwrites answers 0 of 38.](#evaluation)

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](https://context-mode.com/memory/data/supersession-2026-09-30.json). A [live eval](#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.

- **It forgets.** What you said in one session is not in the next one, unless someone wrote it into a file the client loads.
- **Facts change.** You move from one host to another, or a new person goes on call. A store that overwrites loses what was true before. A store that only appends keeps two "current" answers, and the model cannot tell which one is true now.
- **Two clients, two memories.** Claude Code and Codex keep separate local memories. A fact you give one is unknown to the other.
- **Notes look like your words.** A note a model wrote about you reads the same as a rule you stated, unless the store records who wrote it.

## 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.

| Clock | Columns | Meaning |
| --- | --- | --- |
| **Valid time** (world) | valid_at, invalid_at | When 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_at | When 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](#compared)).

What happens when a fact arrives:

- **One value or many.** Each predicate holds one current value or many. The writer's own declaration decides first, then a fixed registry. Events and symmetric relations hold many. Anything else holds one.
- **A new value for a one-value slot.** Every current row in the slot gets `invalid_at` set to the new row's `valid_at`, and `expired_at` set to the time of the write. Then the new row is inserted. The two windows meet with no gap and no overlap.
- **An older value that arrives late.** If the incoming `valid_at` is older than the current value, the row is stored already closed, as history. It does not replace the newer value.
- **The same fact in other words.** A restatement with close wording (character-pair overlap of 0.7 or more, and no new content) updates the existing row instead of opening a new one. A changed value, such as "3 attempts" to "5 attempts", is not a restatement.
- **Many-value slots** keep each value, and drop near-copies the same way.
- **Nothing is deleted.** Closing a row only sets its timestamps. A slot closed by mistake can be reopened.

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:

| Slot | Value | Tier, writer | valid_at | invalid_at |
| --- | --- | --- | --- | --- |
| production · deployed_to | Hetzner Cloud, Falkenstein region | inferred, turn extraction | 21:14:38.716 | 21:15:09.150 |
| production · deployed_to | Render, Oregon region | asserted, remember tool | 21:15:09.150 | (current) |
| on-call engineer · is | Selin Aydin | inferred, turn extraction | 21:14:38.716 | 21:15:09.150 |
| on-call engineer · is | Baran Kaya | asserted, remember tool | 21:15:09.150 | (current) |
| ci · runs_on | CircleCI | inferred, turn extraction | 21:14:38.716 | 21:15:09.150 |
| ci · runs_on | Buildkite | asserted, remember tool | 21:15:09.150 | (current) |
| production · on_call_engineer | Baran Kaya | inferred, turn extraction | 21: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](#limits).

### Reading the history

- **Timeline.** `ctx.timeline` lists facts in time order on either clock: `clock=world` orders by `valid_at`, `clock=system` by `created_at`. It takes `since` and `until`.
- **Recent changes.** A recall lists at most 3 rows closed in the last 48 hours next to the current ones, so the model sees what changed.
- **Everything.** The Memory screen and the export return every row, closed rows included.
- **What the model sees.** The memory block the gateway adds drops closed rows, except for session-state predicates such as `decided` or `working_on`. Those show as `✗ INVALID` lines, marked as 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.

asserted a person stated it: a signed-in write, your git config and account, or a remember call that declares `asserted`

observed a tool or the runtime returned it, with no model in the loop: commits, PRs, edited files, tool errors

inferred a model picked it out of a turn

- **Ceilings.** Each writer has a highest tier it may claim. When the source and the channel differ, the lower of the two wins.
- **Silence is not an assertion.** A remember call that declares no tier is stored with none. A row with no tier is never sent to the model.
- **Checked against your words.** A remember call that claims `asserted` drops to `inferred` when the value is not in your own message.
- **A hint, not a permission.** The memory block tells the model to treat `inferred` lines as a guess to verify, never as something you said. A tier grants and withholds nothing.

### 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.

| Class | Holds | Default place |
| --- | --- | --- |
| About you | Your name, language, how you like to work | Everywhere, any agent |
| Toolkit | The tools and skills you use | Everywhere, per agent |
| Codebase | Decisions about a project's code | This project |
| Task | What you are doing now | This worktree |
| Session | How your agent app behaves | This worktree, per agent |
| Environment | Machine paths and anything secret | This worktree |

- A fact with no class goes to **Task**, the narrowest place.
- Eleven categories, such as env, git, cwd, file and sandbox, can never go wider than one project.
- A read returns rows for this workspace and rows with no place.
- **A change closes only what it can see.** A new value closes old rows in the same workspace or with no place. A fact about you closes the old value everywhere, so you never have two current names.
- Your account is the only wall between tenants. The place decides relevance inside your account.

### Capture

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

- **From your turns.** After a turn ends normally, the gateway sends at most 2,500 characters of your text and 2,500 of the answer (4,000 in total) to Llama 3.3 70B on Cloudflare Workers AI. It keeps at most 5 facts of 10 words or less, each labelled fact or event.
- **From your tools.** Rules with no model read commits, pushes, PRs, edited files, subagent tasks and tool errors.
- **On request.** The agent calls `context-mode-remember`, or a [Thinking in Code](https://context-mode.com/docs/how-saving-works#code-mode) script calls `remember`.

Nothing is stored from:

- a turn with no new text from you, a turn cut short, or a refused turn. A remember call made during a turn waits for the turn's end, and is dropped if the model refuses;
- recall and "current work" questions, such as "what is my name";
- a fact whose text matches a credential pattern;
- a dry run, or a request the gateway cannot tie to an account.

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-recall facts by subject, predicate or text, newest valid value first, with recent closed rows

context-mode-remember store a fact with a declared tier; retract by id or by slot

context-mode-search your stored prompts and archived tool output; part of [Search](https://context-mode.com/docs/search), not Memory

in scripts `ctx.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.
```

- **Where it came from.** The ticket and the task come from memory. The commit line also has the key, so to check memory alone, ask from `main` with the agent's tools off. We did that twice. Both times it named PAY-1432 and the `.nvmrc` work.
- **In the console.** The Memory screen lists what it kept, for example "user current_ticket PAY-1432".

### 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
```

- **No tool call.** Codex read no file and ran no command. The reason came from memory.
- **A new Claude Code session** in the same folder gives the same reason, with the line `memory +2 records about user, router-cache branch, express core`.
- **Another worktree.** By default a decision stays in the worktree where you made it. In a second worktree of the same repo, the agent said it had no record. To share decisions across a project, set "Decisions about a codebase" to This project on the Memory screen.

### Rules you state once

- **Commit format.** Say once "our commits start with the Jira key of the ticket I'm on, like `PAY-1432: short summary`". In the next session the commit is `PAY-1432: ignore .env.local`, colon included: 3 of 3 runs on Claude Opus 5.5, and on Claude Haiku 4.5. The rule is sent first in each new session under "Rules you set", with your own words as the format.
- **Answer style.** Say once "keep your answers to five lines or less, no headings, no tables". In the next session the answers were 5, 4 and 5 lines, with no heading or table: 3 of 3 runs on Claude Opus 5.5. On Claude Haiku 4.5: 3 lines.

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

- **Code.** The gateway's own write route (`/fact-assert`), read route (`/fact-query`), timeline route and block builder, from commit a12ecce8, run against a real SQLite engine. The read is the one the first turn makes: the turn path's query, a limit of 6 rows, and its own filters. No meaning vector, so only the keyword legs run, as on a turn where the vector misses its deadline.
- **Store.** 24 facts about one team's setup: deploy target, on-call engineer, CI, database engine and 20 more. Each changed 1 to 3 times, about 30 days apart, from 2026-06-01. 62 writes, 38 of them changes. Each write names its own `valid_at`, so the two clocks differ on every row.
- **Arms.** **Bi-temporal**: the shipped code. **Append-only**: the shipped code with every fact declared many-valued, so nothing closes. **Overwrite**: the shipped code, then every closed row deleted, so only the current value is kept.
- **Questions.** 24 "what is it now" questions, one per fact. 6 questions that each ask about 3 facts. 38 "what was true on this date" questions, answered from the timeline by a fixed rule.
- **Answers.** Each memory block and its question went to Claude Haiku 4.5 and Claude Opus 5.5 through plain Claude Code 2.1.283, with no tools and no gateway. An exact string check graded each fact: current value, old value, or neither.

### Results

| Measure | Bi-temporal | Append-only | Overwrite |
| --- | --- | --- | --- |
| Rows in the store, open | 62, 24 open | 62, 62 open | 24, 24 open |
| "Now" questions with an old value in the block | 0 of 24 | 23 of 24 | 0 of 24 |
| Three-fact questions: current value in the block | 18 of 18 | 13 of 18 | 18 of 18 |
| Three-fact questions: only an old value in the block | 0 of 18 | 2 of 18 | 0 of 18 |
| Haiku 4.5 answers: current, old, neither | 41, 0, 1 | 36, 2, 4 | same block as bi-temporal |
| Opus 5.5 answers: current, old, neither | 40, 0, 2 | 36, 2, 4 | same block as bi-temporal |
| "What was true on this date" | 38 of 38 | 38 of 38 | 0 of 38 |

42 graded facts per model and arm: 24 single questions and 18 facts from the three-fact questions. Source: [evidence file](https://context-mode.com/memory/data/supersession-2026-09-30.json), with every row, block, answer and grade, and both clocks on every row.

What it shows:

- **Closing keeps the block clean.** With old rows open, 23 of 24 blocks carried an old value next to the new one. The model then has to pick by date.
- **Old rows take the places current facts need.** The first-turn read keeps 6 rows. In the append-only store, old rows of one fact pushed other facts out. On "who is on call, who owns payments, which package manager", the block held only the oldest on-call name, and both models gave it as current.
- **Closing keeps the history an overwrite loses.** Asked what was true on a past date, the bi-temporal store answered 38 of 38 from closed rows. The overwrite store had nothing to answer from.
- **Append-only keeps history too.** With stable slot names, it answered every date question. Its cost is in the block, not the history.

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

- **Scenario.** Session 1 states three facts. Session 2, 30 seconds later, states that all three changed and does not name the old values. Then 8 probes, each a new session: three "now" questions, three "before" questions, "is it still Hetzner?", and "list all three current values".
- **Client.** Claude Code 2.1.283 (`claude -p`), Claude Opus 5.5, effort low, a fresh config folder for every run, so no CLAUDE.md or auto memory carried over. Gateway build 9d047fb4. New, disposable accounts, one per arm.
- **Arms.** Memory on. Memory off. Memory off and not entitled. Claude Code with no gateway, which by design keeps nothing between sessions: it is a no-memory baseline, not a test of Claude Code's own memory. In no arm did the user ask the agent to remember.
- **Grading.** An exact string check on the new value (now probes) or the old value (before probes). Whether an answer gave an old value as current was read by hand.

### Results

| Arm | Correct | Old value given as current | Search calls | Median time | Client-reported cost, 8 probes |
| --- | --- | --- | --- | --- | --- |
| **Memory on** | 8 of 8 | 0 | 0 | 4.9 s | $0.184 |
| Memory off | 8 of 8 | 0 | 9 | 6.1 s | $0.069 |
| Memory off, not entitled | 8 of 8 | 0 | 11 | 6.3 s | $0.100 |
| Claude Code, no gateway | 0 of 8 | 0 | n/a | n/a | $0.170 |
| Run B, memory on | 8 of 8 | 0 | 0 | 6.0 s | n/a |
| Run B, memory off | 7 of 8 | 0 | 10 | 7.4 s | n/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](https://context-mode.com/memory/data/bitemporal-2026-09-29.json).

- **Run C closed every old value.** Run C closed 3 of 3 old values.
- **No arm gave a stale answer.** Memory off also gave none, so this run does not show an effect of memory on stale answers.
- **Memory on answered faster, and cost more.** With memory on, every probe was answered from the memory block with no search call, at a median 4.9 s against 6.1 s. The same 8 probes cost $0.184 against $0.069 with memory off. We have not found the cause of the higher cost.
- **The memory switch controls injection, not recall.** With memory off, the model still found the answers by searching your stored turns, which is part of Search.
- **One clock in the export.** In this run every row was written at the moment it became true, so `valid_at` equals `created_at`, and the export has no `expired_at`. The store eval above has both clocks on every row.

## 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.

| Approach | When a fact changes | How facts get in | Where, and which clients | What it does not do |
| --- | --- | --- | --- | --- |
| **Context Mode** | **Bi-temporal.** Old rows closed at the new value's time, kept as history. | Picked from turns and tools; the remember tool | The 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 memory](https://code.claude.com/docs/en/memory) | You or Claude edit the file. No fact history is stated. | CLAUDE.md you write; notes Claude writes as it works | Local 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 memories](https://developers.openai.com/codex/memories) | Not stated | Generated from past chats: summaries, durable entries, recent inputs | Local files under `~/.codex/memories/` . Codex. Off by default. | Memories may not update right away when a chat ends. |
| [Anthropic memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool) | Your code decides | Claude asks for file operations; your application runs them | Storage you run. Apps on the Claude API. | A building block, not a feature of Claude Code or Codex. |
| [Graphiti](https://github.com/getzep/graphiti) and [Zep](https://help.getzep.com/concepts) | **Bi-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 API | Zep'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. |
| [mem0](https://docs.mem0.ai/platform/overview) | Memories are added, searched and updated. A validity window is not stated. | Messages you send by API, or its [Claude Code plugin](https://docs.mem0.ai/integrations/claude-code) | Hosted, or self-hosted. Plugins for Claude Code, Codex, Cursor and others. | The Claude Code plugin needs Python 3.10+ and Git on each machine. |
| [supermemory](https://supermemory.ai/docs/intro) | Facts "update, connect, and forget"; a change should not "leave both preferences equally true". A validity window is not stated. | Its [plugin](https://github.com/supermemoryai/claude-supermemory) saves conversations when a session ends, and on request | supermemory'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 Code](https://docs.letta.com/letta-code/memory) | The agent commits each update to a git-backed memory file system (MemFS) | The agent decides what to keep | Its own agent: CLI, desktop app, cloud agents | A separate agent, not a layer under Claude Code or Codex. |
| [claude-mem](https://github.com/thedotmack/claude-mem) | Not stated; a timeline tool lists context in time order | Hooks save observations | SQLite 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.**

- Graphiti can query what was true at any point in time, and it searches by meaning, keyword and graph together.
- CLAUDE.md is free, local, reviewable in git, and needs no network.
- For rules that must always hold, memory is the wrong tool. Anthropic's page calls memory "context, not enforced configuration" and points to a hook for blocking. That holds for our memory too; for enforcement, see [Cage](https://context-mode.com/docs/cage).
- mem0 and supermemory cover more clients, such as Cursor and OpenCode.

## Safety and privacy

- **Where it lives.** In your account's own store on the gateway: one Durable Object per account. Your account is the wall between tenants.
- **Who reads your turns.** Turn extraction sends at most 4,000 characters of each answered turn to Llama 3.3 70B on Cloudflare Workers AI.
- **Secrets.** A fact whose subject, predicate or value matches a credential pattern (such as `sk-`, `ghp_`, `npm_` or `api_key=`) is refused at the one point every write passes. Model output is redacted before it is stored.
- **Your message wins.** The memory block tells the model that your current message overrides every fact. A model-picked fact never enters the standing rules.
- **Refusals.** A fact that rides refused turns goes under watch. It is held back only when a model wrote it, refusals in at least 2 separate turns carried it, and no turn that carried it got a normal answer, all in the last 30 days. You can restore it.
- **Unknown callers.** A request the gateway cannot tie to an account gets no memory, no skills and no tools, and nothing from it is stored.
- **Nothing is deleted by an agent.** Supersede, retract and hold-back close a row and keep it as history. Physical delete exists only as an operator cleanup; an agent's call is refused.

### Your controls

Memory switch In [Settings](https://console.context-mode.com/app/controls). Off: no standing rules, no fact block, and no resume bundle enter the request. Turn extraction is queued. Search stays on.

Memory screen In [Memory](https://console.context-mode.com/app/memory): current and replaced facts, "Ask what it knows", held-back facts with Restore and Delete

Place buttons per class: Everywhere, This repo, This project, This worktree. New facts follow the button.

Retract ask the agent to retract a fact by its id or its slot; it is closed and kept as history

## Limits

- **Small evals.** The store eval tests the mechanism on one scenario of 24 facts, without the meaning vector. The live eval has one scenario of three facts, on Claude Code only. Recall in real use, on a later day and in Codex is not measured.
- **Slot names drift.** A new value closes the old one only in the same slot. In live run B, 4 of 6 old values stayed current under different names, and in run C one value was written under a second slot. The answers were still right there, but a store with drifted names acts like the append-only arm of the store eval.
- **No "as of" query.** You can list history by time, but not ask for the whole store as it stood at a past moment.
- **History is not in the block.** Closed rows reach the model only for session-state predicates, and a recall lists closed rows from the last 48 hours only.
- **Cost.** In the live eval, the memory-on probes cost more than the memory-off probes, as the client reported: $0.184 against $0.069 for 8 probes. We have not found the cause. In earlier paired runs on Claude Haiku 4.5, memory added 534 to 548 uncached input tokens to a new session's first request.
- **No-tool requests get no memory.** The gateway treats a Claude Code request that declares no tools as a side call and adds no memory to it. On such requests we also saw the stream stop after the thinking block in some tries. That is not fixed.
- **Extraction.** The extractor has no category field, so most picked facts land in Task (this worktree) unless they are about who you are.
- **Languages.** Recall and "current work" questions are matched in English and Turkish only.
- **Secrets.** A secret with no label or known prefix can pass the credential check.
- **Other gaps.** Rules that read tool output still write when Memory is off. Place buttons for a single fact do not yet change what a turn reads. A client that replaces its own system prompt sends no folder, so its read covers the whole account. The block shows every time at UTC+3, whatever the account's time zone; a per-account time zone is not built yet.

## 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](https://context-mode.com/memory/data/supersession-2026-09-30.json) has every row with both clocks, every memory block, answer and grade. The [live eval file](https://context-mode.com/memory/data/bitemporal-2026-09-29.json) 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](https://context-mode.com/docs/landscape#memory).

[Connect your agent](https://context-mode.com/docs/quick-start) [Memory overview](https://context-mode.com/memory) [Search](https://context-mode.com/docs/search)
