1. Executive Summary
Codex is OpenAI's coding agent, a Rust CLI, TUI and app server
licensed Apache-2.0. Its memory is a feature called
memories. It is marked stable and switched off by default
(codex-rs/features/src/lib.rs:1418-1421). When on, it
learns from the user's own past sessions. No tool call writes a memory.
At the start of each root session a background task picks a few finished
sessions and has a model extract a raw memory and a rollout
summary from each into SQLite. A second, sandboxed Codex agent then
rewrites a Markdown folder under ~/.codex/memories from a
git diff of what changed. The next session receives that folder's
summary in its instructions and searches the rest with ordinary file
tools.
What is notable is how forgetting works. There is no delete verb on a
memory. Deleting a session, marking it polluted or letting
it age out removes its row. The next consolidation sees the matching
summary file vanish in the diff, and its prompt tells it to "Delete
only memory supported by deleted inputs". Forgetting is a diff the
consolidator must act on, by the same route as an addition. The
consolidation agent is also tightly locked down. It runs with approvals
set to never, no MCP servers, collaboration and memory switched off,
and, under Codex's managed sandbox, one writable root and no network
(memories/write/src/phase2.rs:310-370).
The weakness is the same move seen from the other side. Every
guarantee ends at a prompt. Whether a forgotten or polluted fact
actually leaves MEMORY.md is up to a model reading a diff,
and no test asserts that it does. The git history that would show what a
consolidation changed is deliberately discarded, so nothing records it.
One memory spans every project the user works on. The codebase is very
large and heavily tested, and the memory subsystem is a small, recent
part of it with its own suite of state-machine tests.
2. Mental Model
A memory goes through four states, and only the first two are rows.
A session is a candidate. Every thread row in the
state database carries memory_mode: enabled,
disabled or polluted
(state/migrations/0018_phase2_selection_snapshot.sql:3).
New threads are disabled when
generate_memories = false. With
disable_on_external_context set, an MCP call to a server
marked as polluting, or other external context, flips the thread to
polluted (core/src/mcp_tool_call.rs:987-1003).
That setting is off by default
(config/src/types.rs:330-332).
Phase 1 makes a raw memory. At startup,
claim_stage1_jobs_for_startup claims threads that are
enabled, from an interactive source, not the current
thread, idle at least 6 hours and updated within 10 days, two per
startup by default (state/src/runtime/memories.rs:143-153,
:244, config/src/types.rs:57-62). A model call
with a strict output schema returns raw_memory,
rollout_summary and an optional slug. The rollout input and
all three outputs pass through redact_secrets
(memories/write/src/phase1.rs:424,
memories/write/src/phase1_output.rs:49-56). The row is
upserted only when its source is newer than the stored one
(state/src/runtime/memories.rs:922-928). An empty result
deletes any earlier row for that thread.
Phase 2 makes it a belief. Selection reads non-empty
rows whose thread is still enabled. It drops any not used
within 30 days, or whose session is older than 30 days if never used,
and ranks by usage_count, then recency, taking the top 256.
The result goes to the folder in stable thread-id order as
raw_memories.md and one file per session under
rollout_summaries/. Files for sessions no longer selected
are deleted. A git diff against the last successful baseline decides
whether anything changed. If it did, a consolidation agent edits
MEMORY.md, memory_summary.md and
skills/, and the baseline is reset.
Use keeps it alive. When a reply ends with an
<oai-mem-citation> block naming session ids,
record_stage1_output_usage increments
usage_count and sets last_usage
(core/src/stream_events_utils.rs:185-212). Cited memories
rank higher at selection and escape the retention prune.
Diagram source
%% caption: a past session becomes a SQLite row, then a consolidator rewrites MEMORY.md from a git diff; deletions arrive the same way and the baseline keeps no history
flowchart TD
T["Past session<br/>memory_mode on thread row<br/>enabled · disabled · polluted"]
T -->|"enabled, idle ≥6h,<br/>≤10 days, 2 per start"| P1["Phase 1 model call<br/>strict schema<br/>secrets redacted in and out"]
P1 --> R["stage1_outputs row<br/>raw_memory · rollout_summary<br/>usage_count · last_usage"]
R -->|"thread still enabled,<br/>used ≤30 days, top 256"| S["Folder sync<br/>raw_memories.md<br/>rollout_summaries/*.md"]
D["Thread deleted or polluted<br/>row unused 30 days"] -->|"row gone or<br/>filtered out"| S
S --> G{"git diff vs<br/>last baseline"}
G -->|"no change"| Z["Job succeeds,<br/>nothing rewritten"]
G -->|"changed"| A["Consolidation agent<br/>no approvals, no MCP<br/>one writable root"]
A -->|"prompt: delete what only<br/>deleted inputs supported"| M["MEMORY.md<br/>memory_summary.md<br/>skills/"]
M --> B["Baseline reset<br/>one commit,<br/>history dropped"]
M -->|"summary ≤2,500 tokens"| N["Next session's<br/>developer instructions"]
N --> C["Agent greps MEMORY.md,<br/>cites session ids"]
C -->|"usage_count + 1"| R3. Architecture
Nothing has to run beside Codex. The memory database is a second
SQLite file opened by the same StateRuntime as the thread
catalogue. A v2 database is opened on demand when the v2 pipeline or
dual-write is selected
(state/src/runtime/memory_versions.rs:10-26). The folder is
under CODEX_HOME, one per memory version. Both phases run as a Tokio
task spawned from start_memories_startup_task. It returns
at once for ephemeral sessions, sub-agent sessions or a disabled feature
(memories/write/src/start.rs:24-38).
Cost falls on model calls. Phase 1 uses a low-reasoning extraction
model with up to eight concurrent jobs. Each rollout is cut to 70% of
the model's input window, or 150,000 tokens without metadata. Phase 2
runs a full Codex agent at medium reasoning on the provider's
consolidation model. A rate-limit guard skips both phases when less than
25% of a Codex usage window remains
(memories/write/src/guard.rs:9-61). The retention prune
runs before that check because it spends no tokens.
The code lives in four places. codex-rs/memories/write
holds both phases, prompts, folder sync and extension pruning.
codex-rs/ext/memories holds read-path injection and the
optional memory tools. codex-rs/memories/read holds
citation parsing and usage telemetry.
codex-rs/state/src/runtime/memories.rs holds every query.
The crate's own README says runtime orchestration lives in
codex-core under core/src/memories/, and that
the read templates are in memories/read/templates/. Neither
path exists at this commit. They are in memories/write/src/
and ext/memories/templates/.
4. Essential Implementation Paths
- Start:
start_memories_startup_task(memories/write/src/start.rs:24-95): version selection, layout, extension seeding, prune, rate guard, phase 1, phase 2. - Phase 1 claim:
claim_stage1_jobs_for_startupandtry_claim_stage1_job(state/src/runtime/memories.rs:153,:686). Leases are 3,600 seconds and a failure retries after 3,600 seconds. - Phase 1 extraction:
phase1::job::runandsample(memories/write/src/phase1.rs:196-316); input filtering insanitize_response_item_for_memories, which drops AGENTS.md and skill fragments from user messages (memories/write/src/rollout_input.rs:328-360). - Phase 1 commit:
mark_stage1_job_succeeded, one transaction that closes the job, upserts the row and enqueues consolidation (state/src/runtime/memories.rs:873-944). - Phase 2:
phase2::run(memories/write/src/phase2.rs:50-207), thenagent::get_config(:310-370) andagent::handle, which resets the baseline only if the job lease is still owned (:441-470). - Baseline:
reset_git_repositoryreplaces any.gitwith a fresh one-commit repository (git-utils/src/baseline.rs:74-78). - Read:
build_memory_tool_developer_instructions(ext/memories/src/prompts.rs:35-64) and theContextContributorthat adds it as a developer-policy fragment (ext/memories/src/extension.rs:56-105). - Usage:
record_stage1_output_usage(state/src/runtime/memories.rs:60-91). - Deletion:
delete_threads_strictcallsdelete_versioned_thread_memoryfor every thread it deletes (state/src/runtime/threads.rs:1192-1210).
5. Memory Data Model
stage1_outputs has one row per thread, keyed by
thread_id, with source_updated_at,
raw_memory, rollout_summary,
rollout_slug, generated_at,
usage_count, last_usage,
selected_for_phase2 and
selected_for_phase2_source_updated_at
(state/memory_migrations/0001_memories.sql). A
jobs table holds leases, ownership tokens, retry counters
and watermarks for both phases. Neither table carries a project,
workspace or user key. The thread's cwd, rollout path and
git branch are joined in from the state database at selection time.
The consolidated folder is the memory the agent reads:
memory_summary.md— the injected index, with recent work grouped under project-scope headings the consolidator writes.MEMORY.md— the searchable registry the read prompt calls the primary file to query.rollout_summaries/<slug>.md— one recap per selected session.skills/<name>/— generatedSKILL.md, scripts, examples and templates.extensions/<name>/—instructions.mdplusresources/ornotes/. Codex seedsad_hoc. Achronicleextension appears in test fixtures, and its producer is not in this tree.
Nothing in either store has a status. A row is present or absent. The
only discrete state in the pipeline is the thread's
memory_mode, which sits on the source session rather than
on what was extracted from it.
6. Retrieval Mechanics
The read path has two layers.
Injection. On every thread whose config enables
memories and use_memories, the extension reads
memory_summary.md, truncates it to 2,500 tokens and renders
it into the read-path template (ext/memories/src/lib.rs:16,
ext/memories/src/prompts.rs:35-66). The v1 template tells
the agent to skip memory only for self-contained requests. It asks for a
quick pass of at most four to six searches: keywords from the summary, a
search of MEMORY.md, then one or two rollout summaries.
Answers that rely on unverified memory are to be marked as
memory-derived and possibly stale. The v2 template is shorter:
"Memory is not proof of current behavior."
Lookup. By default the agent searches with whatever
file tools it has. With dedicated_tools, the extension also
registers memories.list, read,
search and add_ad_hoc_note. Search walks the
folder and does substring or same-window line matching, capped at 200
results. It refuses symlinks and paths outside the root
(ext/memories/src/local/search.rs). Nothing is embedded and
nothing is ranked at read time. Ranking happens once, at phase-2
selection, where usage_count decides which 256 sessions
reach the folder at all.
Retrieval is not scoped. Every session on a CODEX_HOME receives the
same summary, whatever project it is in. The v2 consolidation prompt
groups recent work under ### <project scope>
headings, and the read prompt's decision rule mentions the workspace.
That is organisation by the model, not a filter. A separate CODEX_HOME
is a separate memory, which is a physical partition.
7. Write Mechanics
Writes never block the agent. Both phases run in a background task at the start of a later session, and the current session is excluded from the claim.
The lag is long by design. A session becomes a raw
memory only once it has been idle six hours and another root session
starts. It reaches the folder when that start's phase 2 runs, and
reaches the agent at the following session. Each start processes two
sessions by default, so a backlog clears slowly.
min_rollout_idle_hours is documented as "> 12h
recommended".
Phase 2 rewrites the whole consolidated folder when anything changed. The consolidation agent is given the diff file and the folder, and nothing limits what it may edit inside the root. A run with no diff skips the agent entirely.
Other writers:
add_ad_hoc_note, opt-in, writes onecreate_newfile underextensions/ad_hoc/notes/. The tool description and both read templates restrict it to an explicit remember, forget or correction request from the user (ext/memories/src/tools/ad_hoc_note.rs:55,ext/memories/src/local/ad_hoc_note.rs:12-28). Without dedicated tools, the template tells the agent to write the same note file with its own tools. Nothing checks either case against the user's words.- The external-agent importer, behind the under-development
external_agent_memory_importflag, feeds another agent's memory files into consolidation (external-agent-migration/src/memory_import.rs). - A remote
memories/trace_summarizeclient,ModelClient::summarize_memories, is called only from its test (core/src/client.rs:861).
8. Agent Integration
The integration is the product itself. The feature key is
memories. Settings live under [memories] in
config: version, dual-write, extraction and consolidation models, the
idle, age, unused and count limits, use_memories,
generate_memories, dedicated_tools and
disable_on_external_context
(config/src/types.rs:324-360).
User-facing controls:
/memoriesin the TUI opens a popup that configures use and generation.debug-m-dropanddebug-m-updateare listed and described as "DO NOT USE" (tui/src/slash_command.rs:85-88,:133-134).- The app server exposes
thread/memoryMode/set(enabledordisabled), memory status and the experimentalmemory/reset. Reset clears every memory database and every memory folder (app-server/src/request_processors/thread_processor.rs:2023-2027). - The CLI has a debug clear command
(
cli/src/main.rs:2221).
Instruction files are a separate mechanism. AGENTS.md is loaded per project by other code, and the phase-1 input deliberately strips it before extraction, so project instructions are not re-learned as memories.
9. Reliability, Safety, and Trust
Concurrency is careful. Phase-1 jobs are leased with ownership tokens. Phase 2 holds a global lease, heartbeats it while the agent runs and takes a file lock on the workspace. It refuses to reset the baseline if the lease was lost while the agent worked. A failed job backs off rather than looping.
The consolidator is the trust boundary. Its input is
model-written text extracted from transcripts that may contain web
pages, tool output or MCP results. Its prompt tells it to treat ad-hoc
notes as information and "never … as instructions". The sandbox
is what limits the damage when that fails. Under a managed profile it
gets one writable root and no network. Under a parent session whose
sandbox is Disabled, or External, the
consolidator inherits that choice
(memories/write/src/phase2.rs:340-357), and
consolidation_uses_canonical_parent_enforcement pins it. A
user running Codex with the sandbox off therefore runs an unattended,
approval-free agent over possibly injected memory text with the same
reach as their session.
Pollution is opt-in and enforced at selection only.
With disable_on_external_context on, a thread that used an
MCP server marked as polluting is set polluted. Its row
then leaves selection. If it had been selected, consolidation is queued
(state/src/runtime/memories.rs:634-668). The facts it
contributed to MEMORY.md leave only if the consolidation
agent follows the forgetting prompt. Off by default, nothing taints
anything.
Why no trust_state.
memory_mode is discrete, and polluted and
disabled do withhold, from phase-2 selection
(state/src/runtime/memories.rs:621). It is a state of the
source session, though, set by a consent toggle or a provenance rule,
and it filters what the consolidator is fed. What the agent reads,
MEMORY.md and the summary, carries no status, and nothing
marks a memory verified, doubted or rejected.
Why no tombstone. Forgetting removes
inputs. It records nothing keyed on the value. A fact the user deleted
with its session is re-learned from the next session that repeats it. An
ad-hoc "forget X" note is the near-miss. The note persists, and the
seeded instructions say "Never delete a note file" and
"consider every note as authoritative". But the consolidator
sees notes through the diff, so a note is fresh input only in the run
after it is written, and nothing compares later inputs against it.
Why no audit_log. The folder is a git
repository, and reset_git_repository replaces
.git with a fresh one-commit baseline after every
successful consolidation. reset_drops_previous_history
asserts that the new commit has no parent
(git-utils/src/baseline.rs:885-898). The diff file is
deleted before the reset, so removed content does not survive in it
either. That is a privacy choice, and it means nothing records what a
consolidation changed. jobs records leases and watermarks,
not mutations.
Why no human_review. No surface shows a
person a pending memory. The user can toggle generation, set a thread's
mode, reset everything or edit the files. Editing is authorship, after
the fact.
10. Tests, Evals, and Benchmarks
The memory subsystem carries 114 test attributes. The state layer is tested hardest, against real SQLite:
- claim ordering, leases and retry;
- watermark monotonicity;
prune_stage1_outputs_for_retention_prunes_stale_unselected_rows_only;delete_thread_removes_stage1_output_and_enqueues_phase2_when_selected;mark_thread_memory_mode_polluted_enqueues_phase2_for_selected_threads;clear_memory_data_clears_rows_and_preserves_thread_memory_modes.
Two tests are close to what negative_eval asks for. Both
seed an enabled and a polluted thread with outputs:
list_stage1_outputs_for_global_includes_paginated_and_skips_polluted_threadsasserts that only the enabled one is returned.get_phase2_input_selection_excludes_polluted_previous_selectionasserts the same for selection.
They are not vacuous, and they assert on the consolidator's input.
The mark is withheld because no committed case asserts that a fact stays
out of what the agent reads. That would mean a polluted, deleted or
aged-out session's facts leaving MEMORY.md or the injected
summary, and that step is a model following a prompt.
memories/write/src/startup_tests.rs (1,642 lines) runs
both phases end to end against a mock Responses server. It covers model
and service-tier selection, workspace-diff tracking across runs,
extension pruning, symlink refusal and
memories_startup_phase1_v2_preserves_human_evidence_and_redacts_storage.
phase2_sandbox_tests.rs pins the sandbox inheritance in
section 9.
No retrieval benchmark, extraction-quality eval or
consolidation-fidelity eval is committed. The README names a separate
harness repository for prompt snapshots,
openai/project/agent_memory/write, which is not public. No
paper is cited. I ran no tests: 192 dependency files carried a commit
date inside the seven-day cooldown in a shallow clone, which dates every
file to the tip.
11. For Your Own Build
Steal
- Forgetting by diff. Make deletion an input change and let consolidation see it beside additions. Sync derived files exactly to the selected inputs, diff against the last good baseline and hand the consolidator the diff. A removed source then forces a rewrite, with no separate delete path to forget to call.
- Redact on both sides of the model call. Codex redacts the serialized transcript before extraction, and again the three fields the model returns.
- Usage that feeds selection. Citations increment a count that ranks sessions into the consolidated set and shields them from the age prune. That is a cheap, honest signal that a memory earned its place.
- A locked-down consolidator. The memory-writing agent gets no approvals, no MCP, no collaboration, no memory of its own, no notifications and one writable root. Its own threads are ephemeral so they never become memories.
Avoid
- Inheriting the parent's sandbox choice for an unattended writer. A user who disables the sandbox for their own work has not agreed to an approval-free background agent reading model-extracted text with the same reach.
- A forget path that ends at a prompt. If a deletion must hold, the system needs a record keyed on the value that the next extraction is checked against. A diff the model is asked to honour is not that.
- Discarding the only change history. A one-commit baseline protects deleted content, at the price of never knowing what a consolidation did. A log of which inputs changed and which output sections were edited, without the text, would keep both properties.
Fit
This design suits one developer who wants Codex to pick up their habits, preferences and project facts across sessions and accepts that the result is a model's notes about them. It costs nothing to run beyond model calls, and the memory is plain Markdown the user can read and edit. It does not suit work where memory must be correctable on demand, separated between clients or repositories, or explained after the fact. Here one folder serves every project, forgetting is a request the model may honour, and nothing records what it did.
12. Open Questions
- Does the v2 consolidation, which drops the long handbook prompt, delete stale content from deleted inputs as reliably as v1? Nothing committed measures either.
- What writes
extensions/chronicle/resources/? The feature flag and test fixtures are in the tree and the producer is not. - Is a re-asserted fact after a "forget" note ever caught? The note persists, and only a run that re-reads every note could catch it.
Appendix: File Index
Write path
codex-rs/memories/write/src/start.rs— startup gating and phase ordercodex-rs/memories/write/src/phase1.rs,phase1_output.rs,rollout_input.rs— claim, extraction, filtering and redactioncodex-rs/memories/write/src/phase2.rs,workspace.rs,storage.rs— selection sync, diff, consolidation agent and baseline resetcodex-rs/memories/write/src/extensions/— ad-hoc instructions and resource pruningcodex-rs/memories/write/templates/memories/— stage-one and consolidation prompts, v1 and v2codex-rs/git-utils/src/baseline.rs— the one-commit baseline and diff
State
codex-rs/state/memory_migrations/—stage1_outputs,jobs,consolidation_progresscodex-rs/state/src/runtime/memories.rs— every memory query, with its testscodex-rs/state/src/runtime/memory_versions.rs— v1/v2 stores, reset and thread deletion across bothcodex-rs/state/src/runtime/threads.rs— thread deletion cascade
Read path
codex-rs/ext/memories/src/— injection, optional tools, local backendcodex-rs/ext/memories/templates/memories/— read-path promptscodex-rs/memories/read/src/— citation parsing and usage telemetrycodex-rs/core/src/stream_events_utils.rs— citation to usage count
Configuration and controls
codex-rs/config/src/types.rs—MemoriesToml, defaultscodex-rs/features/src/lib.rs— thememoriesandchronicleflagscodex-rs/core/src/mcp_tool_call.rs— pollution marking
Recorded searches
Run from the repository root at the pinned commit.
- No value-keyed suppression in the memory code.
git grep -n -i -E 'tombstone|suppress|forgotten|blocklist|denylist' -- codex-rs/memories codex-rs/ext/memories codex-rs/state/src/runtime/memories.rs codex-rs/state/memory_migrationsreturns nothing. - No validity time.
git grep -n -i -E 'valid_from|valid_to|valid_until|valid_at|event_time|occurred_at' -- codex-rs/memories codex-rs/ext/memories codex-rs/statereturns nothing. - No scope key in the memory schema.
git grep -n -i -E 'project|cwd|workspace' -- codex-rs/state/memory_migrationsreturns nothing. - No mutation log.
git grep -n -i -E 'INSERT INTO (audit|history|memory_events)|memory_history|audit_log' -- codex-rs/state codex-rs/memoriesreturns nothing. - No review state.
git grep -n -i -E 'approv|pending_review|verified|rejected' -- codex-rs/memories/write/src codex-rs/ext/memories/src codex-rs/state/memory_migrationsreturns seven lines: input-validation errors in tests and the consolidator'sAskForApproval::Never(phase2.rs:26,:329-330). - The remote summarize client has one caller, its
test.
git grep -n 'summarize_memories(' -- 'codex-rs/*.rs'returnscore/src/client.rs:861andcore/src/client_tests.rs:1650. - Chronicle has no producer here.
git grep -l -i 'chronicle' -- 'codex-rs/*.rs'lists four files underfeatures/andconfig/and two memory test files.
History
2026-11-02 — 0a2d5f55…,
release rust-v0.166.0 plus later commits. First reading, from a shallow
clone with history taken from the GitHub API. Screened first: one
auto-run surface (.vscode/settings.json), ten build-time
execution paths, three unpinned surfaces and every dependency file
inside the cooldown window; nothing installed, built or run. No marks.
trust_state and negative_eval are withheld on
the same boundary: polluted threads leave the
consolidator's input, tested, and what the agent reads carries no state
(section 9).