记忆 · 从历史会话里提炼经验
打开 memories 后,每开一轮新对话,Codex 都会在后台把闲置已久的旧会话分两阶段提炼成 ~/.codex/memories 下的记忆:Phase 1 用一次模型调用把单个会话抽成原始记忆存进 SQLite,Phase 2 由一个锁死权限的整合子代理把它们归并成 MEMORY.md 与 memory_summary.md。之后每个会话把摘要注入开发者指令,模型引用了哪条记忆就给它记一次使用,决定它下次还能不能留下。
记忆 · 从历史会话里提炼经验
记忆是一个默认关闭的功能开关 memories(稳定)。打开后,Codex 会把你过去的会话提炼成一个本地的记忆目录,新会话开始时先读一份摘要,需要时再翻细节。TUI 的 /memories 可以分别开关“使用记忆”和“生成记忆”,或者清空全部记忆;相关配置见手册的配置参考与斜杠命令。这一篇看记忆从哪里来、存在哪里、怎么被用上、怎么清掉。
三个 crate,读写分开
| crate | 目录 | 职责 |
|---|---|---|
codex-memories-write | codex-rs/memories/write | 后台管线:Phase 1 抽取、Phase 2 整合、记忆目录的 git 基线与清理 |
codex-memories-read | codex-rs/memories/read | 解析回复里的记忆引用、读取行为的遥测分类 |
codex-memories-extension | codex-rs/ext/memories | 扩展:把记忆摘要注入提示词,可选地提供专用记忆工具 |
数据分两处放。记录与任务在单独的 SQLite 库里:memories_1.sqlite(memories.version = "v2" 时是 memories_v2_1.sqlite),默认在 ~/.codex 下,表是 stage1_outputs(每个会话一行原始记忆)和 jobs(任务租约与重试)。文件在 ~/.codex/memories/(v2 是 memories_v2/),是给模型读的。
codex-rs/memories/README.md 有两处过时:它说 Phase 1、Phase 2 的编排“still lives in codex-core under codex-rs/core/src/memories/”,但 v0.158.0 没有这个目录,编排就在 memories/write 里;它列的读取模板 read/templates/memories/read_path.md 实际在 ext/memories/templates/memories/read_path.md。README 里对两个阶段的描述大体仍与代码一致。
什么时候跑
README 说管线在根会话启动时触发,代码里的触发点其实是 app-server 的 turn_start_inner:一次 turn/start 真的开了新一轮(不是插进进行中的轮次)、并且配置了主执行环境,就调用 start_memories_startup_task。它在临时会话、功能没开、子代理会话时直接返回;每个要生成的版本各起一个后台任务(memories.dual_write = true 时 v1、v2 都跑),状态库打不开就跳过。任务里先建好目录(记忆根目录是符号链接会拒绝),然后按这个顺序走:
// Clean memories to make preserve DB size. This does not consume tokens so can be
// done before the quota check.
phase1::prune(context.as_ref(), &config).await;
if !guard::rate_limits_ok(&auth_manager, &config).await {
context.counter(
MEMORY_STARTUP,
/*inc*/ 1,
&[("status", "skipped_rate_limit")],
);
return;
}
// Run phase 1.
phase1::run(Arc::clone(&context), Arc::clone(&config)).await;
// Run phase 2.
phase2::run(context, config, parent_permission_profile).await;(codex-rs/memories/write/src/start.rs:75)
prune 删掉当前没被 Phase 2 选用、最后一次使用(没用过就看会话更新时间)早于 memories.max_unused_days(默认 30 天)的原始记忆,每次最多 200 条。额度检查只在认证走 Codex 后端(如 ChatGPT 登录)时生效:已经撞上额度上限,或主、次额度窗口任一已用超过 100 - min_rate_limit_remaining_percent(默认要求剩余 25%),就不跑;其它认证方式查不到额度,按“允许”处理。每开一轮都会走到这里,靠数据库里的租约和锁保证不重复干活。
Phase 1:一次模型调用,抽一个会话
claim_stage1_jobs_for_startup 从状态库的 threads 表里挑会话。来源只限 INTERACTIVE_SESSION_SOURCES(CLI、VS Code 和两个自定义来源 atlas、chatgpt,所以 codex exec 的会话不算),再加上这几条:
builder.push(" AND threads.memory_mode = 'enabled'");
builder
.push(" AND threads.id != ")
.push_bind(current_thread_id.as_str());
builder
.push(" AND ")
.push("threads.updated_at_ms")
.push(" >= ")
.push_bind(max_age_cutoff);
builder
.push(" AND ")
.push("threads.updated_at_ms")
.push(" <= ")
.push_bind(idle_cutoff);(codex-rs/state/src/runtime/memories.rs:241)
即:会话的 memory_mode 是 enabled、不是当前会话、最近 max_rollout_age_days(默认 10 天)内动过、但已闲置 min_rollout_idle_hours(默认 6 小时)以上,并且在上次抽取之后又有更新。按更新时间倒序最多扫 5000 个,每次最多认领 max_rollouts_per_startup(默认 2)个,每个带 1 小时租约。
抽取不开线程,而是直接用 ModelClient 发一次请求:读出会话的 rollout,丢掉开发者消息、推理、压缩记录等,只留用户与助手消息、工具调用和输出,序列化后过一遍 redact_secrets 脱敏;系统提示是 stage_one_system.md(开头就是 “Memory Writing Agent: Phase 1”,并说明没有值得记的就什么都别写),输出用严格的 JSON schema 约束成 raw_memory、rollout_summary、rollout_slug 三个字段,结果再脱敏一次。模型默认取模型提供方给的偏好(默认实现是 gpt-5.6-luna,Bedrock 另有映射),推理强度 low,可用 memories.extract_model 改;最多 8 个会话并发。成功的写进 stage1_outputs,没产出的记为 succeeded_no_output,失败的 1 小时后重试。
Phase 2:一个锁死权限的整合子代理
Phase 2 先抢全局锁:已有人在跑、正在退避、或者上次成功不到 6 小时,就跳过。拿到锁后:
- 确保记忆根目录是一个 git 仓库,作为比对基线;
- 从
stage1_outputs里只看最近max_unused_days内用过(没用过就看会话更新时间)的记录,按usage_count从高到低、再按最近使用时间排序,取前max_raw_memories_for_consolidation(默认 256)条,同步成rollout_summaries/下的逐会话摘要和合并的raw_memories.md,顺带删掉extensions/下超过 7 天的资源文件; - 与基线做 git diff:没有变化、且成品文件都合法,就直接记成功;否则把差异写进
phase2_workspace_diff.md(上限 4 MiB),启动整合子代理。
整合子代理是一个内部线程(SessionSource::Internal(InternalSessionSource::MemoryConsolidation)),工作目录就是记忆根目录,配置被这样收紧:
agent_config.cwd = root.clone();
// Consolidation threads must never feed back into phase-1 memory generation.
agent_config.ephemeral = true;
agent_config.memories.generate_memories = false;
agent_config.memories.use_memories = false;
// Background memory work must not send user-facing completion notifications.
agent_config.notify = None;
agent_config.include_apps_instructions = false;
agent_config.mcp_servers = Constrained::allow_only(HashMap::new());
// Approval policy
agent_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
// Consolidation runs as an internal worker and must not recursively delegate.
let _ = agent_config.features.disable(Feature::Collab);
let _ = agent_config.features.disable(Feature::MemoryTool);
let _ = agent_config.features.disable(Feature::Apps);
let _ = agent_config.features.disable(Feature::Plugins);(codex-rs/memories/write/src/phase2.rs:304)
父会话用 Codex 管理的沙箱时,子代理只能写记忆根目录、不能联网;父会话关了沙箱则沿用父会话的选择。模型默认同样取提供方的偏好(默认实现是 gpt-5.6-terra,可用 memories.consolidation_model 改),推理强度 medium。提示词 consolidation.md 要它维护几样东西:必须以一行 v1 开头、会被注入提示词的 memory_summary.md,供检索的手册 MEMORY.md,以及可选的 skills/。子代理跑的时候每 90 秒给锁续一次租约;结束后校验成品,重置 git 基线,把用到的记录标成 selected_for_phase2,任务记成功。
下一个会话怎么用上
读的一侧是扩展(机制见上一篇)。codex-memories-extension 在功能开着、且 memories.use_memories 为真时,作为 ContextContributor 读出 memory_summary.md,截到 2500 token,填进模板 read_path.md,作为一段开发者指令放进上下文。模板告诉模型:摘要已经给你了;MEMORY.md 是可搜索的主索引;再往下是 skills/ 和 rollout_summaries/;动手前先做一次“快速记忆检索”,最好不超过 4 到 6 步。模型用普通的 shell 工具去读这些文件;打开 memories.dedicated_tools 则会多一组 memories 命名空间的工具:list、read、search、add_ad_hoc_note。
回路靠引用闭合。模板要求用到记忆时,在最终回复末尾附一个 <oai-mem-citation> 块,列出引用的文件行号和 rollout_ids。内核在条目完成时把这个块从可见文本里剥掉,用 codex-memories-read 解析出 id,再调 record_stage1_output_usage 给对应记录的 usage_count 加一、把 last_usage 设为当前时间(codex-rs/core/src/stream_events_utils.rs:201)。被引用得多的记忆在 Phase 2 排得靠前,久不被引用的超过 max_unused_days 就被清理掉。用户明确要求改记忆时,模板要求模型只往 extensions/ad_hoc/notes/ 写一个小笔记文件,由下一次 Phase 2 归并,而不是直接改记忆文件。
怎么关、怎么清
memories.generate_memories = false:新建的会话在状态库里记为memory_mode = "disabled",Phase 1 不会挑它;memories.use_memories = false:不再注入摘要;memories.disable_on_external_context = true:会话里出现网页搜索、工具搜索等外部内容时,把它标成polluted,同样不参与抽取;- 清空:TUI
/memories里的 “Reset all memories” 走 app-server 的实验方法memory/reset;隐藏的 CLI 子命令codex debug clear-memories做同样的事。两者都清空两个版本记忆库里的stage1_outputs与记忆相关的jobs,再清空memories、memories_v2、memories_extensions三个目录的内容(目录本身保留,是符号链接的拒绝处理)。
和《从 LLM 到 Coding Agent》对照
那本的会话历史用 append-only 的 JSONL 把一次会话落盘,解决的是“活过进程”;Codex 的记忆以这些 rollout 为原料,把许多次会话蒸馏成跨会话的经验。注入方式也对得上那本的上下文注入:项目记忆一类稳定内容前置,按相关性检索的细节按需取用。Codex 把截断后的 memory_summary.md 放进开发者指令,MEMORY.md 与逐会话摘要则让模型自己去翻。
对照 Grok Build 的记忆,两边骨架很像:grok 的 /flush 把单个会话写成日志、/dream 把多次会话合并进 MEMORY.md,对应 Codex 的 Phase 1 与 Phase 2。差别在触发与检索:grok 在压缩前、空闲时或手动触发,检索用 SQLite 的 FTS5 加向量混合搜索;Codex 只在新一轮开始时于后台处理闲置已久的旧会话,检索交给模型自己搜文件,再用回复里的引用给记忆计数。
上一篇:扩展 API · goal 与内部扩展点 · 下一篇:从别的 agent 迁移 · 导入 Claude Code 与 Cursor 的配置