Codex CLI 0.165.0, released 27 October, "enabled memory v2 by default for fresh profiles". Fresh means one thing: ~/.codex/memories does not exist. An empty folder with that name keeps a brand new install on v1, an explicit memories.version = "v2" moves a legacy profile over, and neither that key nor the dual_write opt-out is in the configuration reference.
Memories are still off by default. The moment you turn them on, Codex starts a second agent at your next launch, on GPT-5.6 Terra rather than your session model, with approvals set to never. On an empty profile it has nothing to consolidate, it runs shell commands inside the memory folder anyway, and if your session ends inside about 15 seconds it is shut down and recorded as failed_agent with a retry an hour later.
The same release note says Codex now imports memories from other agents into the v2 store. The feature flag that governs that import, external_agent_memory_import, is marked under development and off in 0.165.0, and OpenAI's own maturity page says under development means do not use.
What the release notes say
Three Codex releases in one week touched memories. 0.164.0 on 23 October stopped dual-writing to the v1 store once a profile is on v2. 0.165.0 on 27 October made v2 the default for fresh profiles and added v2 support to external-agent memory imports. 0.166.0 on 29 October lets you configure the consolidation service tier separately.
The pull request behind the 0.165 change is more precise than the note. Codex selects v2 "when memories are enabled and the memories directory is absent". Explicit memories.version settings and a memories.dual_write = false opt-out are preserved, and existing workspaces keep v1. That is the whole test. Nothing looks at what is inside the folder.
The memories guide does not mention v2, says the files live under ~/.codex/memories/, and lists eleven memories.* keys in the configuration reference. version and dual_write are not among them.
What I ran
Codex CLI 0.165.0 on macOS, signed in with a ChatGPT account, default model GPT-6.1 Sol. Each test used its own empty CODEX_HOME with a copy of the auth file and at most a two-line config.toml, and ran one or two codex exec turns in read-only mode against a throwaway three-file repo. I captured RUST_LOG=debug to a file and read the SQLite databases Codex left behind. Eleven homes, nine turns that reached the model and four that stopped at config loading, all on the subscription.
| Fresh home | What Codex created |
|---|---|
| Defaults, memories off | memories_1.sqlite, four tables, zero rows. No memories/, no memories_v2/ |
memories = true |
memories_v2/ (a git repo) and memories_v2_1.sqlite, plus an empty memories_1.sqlite. No memories/ |
memories = true, empty memories/ folder pre-created |
v1: memories/raw_memories.md saying "No raw memories yet", memories_1.sqlite. No memories_v2/ |
memories = true, version = "v2", empty memories/ pre-created |
v2 wins: memories_v2/ created next to the legacy folder |
memories = true, version = "v1" |
v1 |
memories = true, dual_write = false, no legacy folder |
v1, so the opt-out works as a version pin |
version = "2" or version = 1 |
Config refuses to load; only the strings "v1" and "v2" are accepted |
The v1 database file appears in every configuration, including the one with memories off. In a profile with the feature off, the threads table also records every session with memory_mode = enabled, which is the column default rather than a decision.
The agent you did not start
With memories on and nothing else changed, the first codex exec in a fresh home spawned a second thread. The debug log tags it model=gpt-5.6-terra, effort=Some(Medium), approval_policy=Never, sandbox_policy=WorkspaceWrite with network access off, while my own turn ran on GPT-6.1 Sol. It is the global consolidation job, and the v2 database records it under memory_consolidate_global.
Its working directory is the memory folder, judging by what its commands found. The commands it ran were pwd && rg --files, sed -n '1,240p' phase2_workspace_diff.md, a read of extensions/ad_hoc/instructions.md, find . -maxdepth 3 -type f, and git status --short && git log --oneline --all -10. The diff file it reads says "Status: none". The folder was created seconds earlier. There was nothing to consolidate.
What happened next depended only on how long my session lasted. Request counts below come from the websocket request lines in the debug log, about six per request.
| First turn | Hidden agent | Job status |
|---|---|---|
| Read two files, about 17 seconds | 2 model requests, 15 seconds, then a shutdown op when exec exited | error, last_error = failed_agent, retries left 3 to 2, retry_at exactly 3,600 seconds later |
| Same on the v1 path (legacy folder) | 2 requests, 12 seconds, shut down | error, failed_agent |
sleep 90 so exec stayed alive |
3 requests, 23 seconds, finished | done, wrote memory_summary.md |
The completed run produced a 241-byte summary: a v1 header and four sections each saying nothing is stored yet. The git repo in the folder kept only its baseline commit. On the next start in that home the agent did not run again, so this is a startup pass gated on pending work, not a tax on every launch. But every fresh profile that enables memories and then runs a quick command gets one failed job and one scheduled retry before it has a single memory.
The token usage for the hidden thread is not in the turn.completed event, which covers only your own thread, and the debug log does not print it. Once the summary existed, the same six-word prompt cost 23,019 input tokens against 22,367 with memories off, so the injection is around 650 tokens.
Extraction waits six hours
The per-thread extraction that turns a session into a raw memory is gated by memories.min_rollout_idle_hours, default 6 and clamped to a minimum of 1. I backdated my first thread by two hours in the state database and on the rollout file, set the gate to 1 hour and the rate-limit threshold to 0, and ran another turn. No memory_stage1 job appeared. Either the idle check reads something I did not change, or the pass did not run. Codex's claim that it redacts secrets from generated memories is therefore untested here; the TOKEN value my test session read is in the session file, not yet in a memory.
An existing profile
My own ~/.codex has both folders. The v1 memory_summary.md was rewritten this morning. The v2 one was last written on 23 October at 17:08, the day 0.164.0 shipped with the change that stops dual-writing. The v1 database shows 48 extractions done and 2 in error; the v2 database stopped at 35. Anyone who turned memories on before last week is on v1 and stays there until they set version = "v2", after which v1 stops being written.
The import flag
The app-server protocol in 0.165.0 has an externalAgentConfig/import method whose item types include MEMORY next to AGENTS_MD, SKILLS, HOOKS and SESSIONS. The import page says the desktop app brings "Project memories from Claude Code" into Memories, and that the CLI's /import command covers Claude Code and Cursor. codex features list in a fresh home shows external_agent_memory_import under development false. I did not enable it.
What to do
Run ls ~/.codex/memories before you reason about which store you are on. If the folder exists you are on v1 whatever the release note says; if you want v2, set memories.version = "v2" as a string. Keep in mind this is an undocumented key.
If you enable memories in a disposable home, in CI, or in a script that runs codex exec once and exits, expect a second agent on GPT-5.6 Terra and a failed_agent row. Either give the first session more than 25 seconds or accept the hourly retry. The consolidation model is overridable with memories.consolidation_model; whether memories.generate_memories = false prevents the startup pass I did not test.
Not tested: the ChatGPT desktop app, the TUI's /memories and /import commands, Codex 0.166.0, whether the extraction pass eventually processes my backdated thread, and the import flag itself. All numbers are from 1 November between 10:59 and 11:17 UTC.