codex cli 源码教程 | 第十篇:上下文管理、项目指令与自动压缩
上一篇跟踪了 Provider、认证和 HTTP/WebSocket 传输层。
模型请求最终会被组装为:
pub struct Prompt {
pub input: Vec<ResponseItem>,
pub(crate) tools: Vec<ToolSpec>,
pub(crate) parallel_tool_calls: bool,
pub base_instructions: BaseInstructions,
pub output_schema: Option<Value>,
pub output_schema_strict: bool,
}
其中最容易被低估的是:
input
base_instructions
它们不是简单的聊天记录。
在一次真实 Turn 中,模型可能同时看到:
模型基础指令
开发者指令
权限与协作模式
Skills 和插件说明
AGENTS.md
当前目录、Shell、日期和网络权限
历史用户消息
历史模型输出
Reasoning Item
Tool Call
Tool Result
当前用户输入
这些内容还会在以下事件发生时改变:
切换 Model
切换权限
修改 CWD
环境上线或离线
Resume / Fork / Rollback
自动压缩
手动 /compact
上下文窗口溢出
因此,Codex 需要的不只是:
Vec<Message>
而是一套能够:
追加历史
修复调用配对
截断大型工具结果
追踪动态世界状态
只发送上下文差量
估算 Token
压缩并替换历史
持久化压缩检查点
恢复上下文基线
的 Context Runtime。
本篇将从 ContextManager 出发,完整跟踪 AGENTS.md、WorldState、
Token 预算和三种压缩实现。
本篇目标
阅读完成后,你应该能够:
- 区分 Prompt、Conversation History、World State 和 Rollout。
- 列出一次模型请求中所有主要上下文来源。
- 解释
ContextManager::record_items与for_prompt的职责差异。 - 说明 Tool Call 和 Tool Output 为什么必须保持配对。
- 跟踪
AGENTS.md从文件发现到模型可见 User Item 的完整链路。 - 解释
AGENTS.override.md、Fallback Filename 和总字节预算。 - 理解
reference_context_item与WorldStateSnapshot两套差量基线。 - 区分服务端 Token Usage 和本地启发式估算。
- 解释
Total与BodyAfterPrefix两种自动压缩计费范围。 - 区分 Turn 前、Turn 中和手动压缩。
- 区分本地压缩、远程 v1 和远程 v2。
- 说明压缩后的替换历史如何持久化并在 Resume 时恢复。
1. 本篇源码地图
History 与上下文构建
| 文件 | 职责 |
|---|---|
| context_manager/history.rs | History、Token Usage、截断与替换 |
| context_manager/normalize.rs | Call/Output 配对与图片降级 |
| context_manager/updates.rs | Turn 间上下文差量 |
| session/mod.rs | 初始上下文、记录、替换和持久化 |
| session/turn.rs | Sampling 前后压缩触发 |
项目指令与动态状态
| 文件 | 职责 |
|---|---|
| agents_md.rs | 项目指令发现、读取和合并 |
| agents_md_manager.rs | Session 级缓存 |
| codex-home/instructions/mod.rs | $CODEX_HOME 全局指令 |
| session/world_state.rs | 构造当前 World State |
| context/world_state/mod.rs | Snapshot、Diff 与 Merge Patch |
| context/world_state/agents_md.rs | AGENTS.md World State Section |
| context/world_state/environment.rs | CWD、Shell、时间和权限状态 |
Token 与压缩
| 文件 | 职责 |
|---|---|
| session/context_window.rs | 压缩阈值计算 |
| state/auto_compact_window.rs | 压缩窗口 ID、Prefix Baseline 与提醒 |
| compact.rs | 本地摘要压缩 |
| compact_remote.rs | 远程 /responses/compact |
| compact_remote_v2.rs | CompactionTrigger 流式压缩 |
| compact_token_budget.rs | Token Budget 实验模式 |
| session/rollout_reconstruction.rs | Resume/Fork 历史重建 |
完整主线是:
Session Startup
-> Load Global Instructions
-> Discover Project AGENTS.md
-> Create ContextManager
Turn Start
-> Pre-Turn Compaction Check
-> Capture StepContext
-> Build WorldState
-> Full Context or Context Diff
-> Record User Input
-> ContextManager::for_prompt
-> Prompt
-> Model Sampling
-> Record Response / Tool Result
-> Token Status Check
-> Optional Mid-Turn Compaction
Compaction
-> Build Replacement History
-> Advance Context Window
-> Replace ContextManager Items
-> Persist CompactedItem
-> Resume Sampling or Wait Next Turn
2. 上下文不是聊天消息数组
先区分四个容易混淆的概念。
Prompt
Prompt 是一次模型请求的完整输入。
它包括:
Base Instructions
Input Items
Tool Specs
Parallel Tool Call Flag
Output Schema
它是请求级对象。
Conversation History
ContextManager 保存当前模型可继续使用的历史:
User Message
Developer Message
Assistant Message
Reasoning
Tool Call
Tool Output
Compaction Item
它是 Session 级可变状态。
World State
WorldState 表示当前仍然有效的外部事实:
AGENTS.md
Environment
Apps Instructions
Plugins Instructions
Extension State
它不是永远追加完整副本,而是可以对前一个 Snapshot 生成差量。
Rollout
Rollout 是持久化事实流:
ResponseItem
TurnContext
WorldState
Compacted
EventMsg
Resume 时通过它重建 History 和差量基线。
四者的关系是:
Rollout
-> reconstruct
-> ContextManager + Baselines
Live Environment
-> WorldState
-> Context Diff
-> ContextManager
ContextManager Clone
-> normalize
-> Prompt.input
Session Base Instructions
-> Prompt.base_instructions
3. Prompt 是最后一道请求边界
Prompt 定义位于:
核心结构为:
pub struct Prompt {
pub input: Vec<ResponseItem>,
pub(crate) tools: Vec<ToolSpec>,
pub(crate) parallel_tool_calls: bool,
pub base_instructions: BaseInstructions,
pub output_schema: Option<Value>,
pub output_schema_strict: bool,
}
普通 Sampling 通过:
run_sampling_request
-> build_prompt
-> ModelClientSession::stream
组装请求。
build_prompt 本身很薄:
pub(crate) fn build_prompt(
input: Vec<ResponseItem>,
router: &ToolRouter,
turn_context: &TurnContext,
base_instructions: BaseInstructions,
) -> Prompt
复杂性发生在它之前:
input 已经由 ContextManager 维护、截断和规范化
tools 已经由 ToolRouter 规划
base_instructions 已经在 Session 创建时解析
4. Base Instructions 与 History 分开传输
普通 Responses 请求中:
Prompt.base_instructions
-> request.instructions
而:
Prompt.input
-> request.input
因此 Base Instructions 通常不作为 History 中的 System Message 保存。
ContextManager::is_api_message 还会明确过滤:
ResponseItem::Message { role, .. }
if role != "system"
也就是说:
System Message
-> 不进入普通 ContextManager History
Base Instructions
-> 由 Session 单独保存
Responses Lite 是例外。
在 Lite 模式下,Codex 会把:
AdditionalTools
Base Instructions Developer Message
插到 Input 前缀,同时把顶层:
instructions
tools
置空或省略。
所以讨论“模型看到了什么”时,必须区分:
逻辑上下文
Wire Encoding
5. Base Instructions 的解析优先级
Session 创建时的优先级为:
1. config.base_instructions
2. Resume History 中 SessionMeta.base_instructions
3. 当前 ModelInfo 的模型指令
源码位于:
对应逻辑:
let base_instructions = config
.base_instructions
.clone()
.or_else(|| conversation_history.get_base_instructions().map(|s| s.text))
.unwrap_or_else(|| {
model_info.get_model_instructions(config.personality)
});
这个顺序有两个目的。
第一,显式配置拥有最高优先级。
第二,Resume 默认保持原 Session 的基础指令,而不是静默切换到当前模型目录中的新文本。
6. Base Instructions 是 Session 级状态
解析结果写入:
pub(crate) struct SessionConfiguration {
pub(super) developer_instructions: Option<String>,
pub(super) base_instructions: String,
pub(super) compact_prompt: Option<String>,
// ...
}
每次 Sampling 通过:
sess.get_base_instructions().await
读取。
Token 重算也使用这个 Session 值,而不是重新从 ModelInfo 读取。
这保证:
模型请求看到的 Base Instructions
==
本地 Token 估算使用的 Base Instructions
如果两者不同,压缩阈值会产生系统性偏差。
7. ContextManager 的核心状态
ContextManager 定义在:
核心字段为:
pub(crate) struct ContextManager {
items: Vec<ResponseItem>,
history_version: u64,
token_info: Option<TokenUsageInfo>,
reference_context_item: Option<TurnContextItem>,
world_state_baseline: Option<WorldStateSnapshot>,
}
每个字段解决一个不同问题。
| 字段 | 作用 |
|---|---|
items |
当前模型历史,按旧到新排列 |
history_version |
History 被重写时失效外部游标 |
token_info |
最近服务端 Token Usage 与累计 Usage |
reference_context_item |
Turn 配置差量基线 |
world_state_baseline |
动态世界状态差量基线 |
注意:
reference_context_item
!=
world_state_baseline
前者关注:
Model
Permission
Collaboration Mode
Personality
Realtime
后者关注:
AGENTS.md
Environment
Apps
Plugins
Extension State
8. History 只允许单向追加和显式重写
正常路径是:
record_items
-> append
特殊路径才会:
replace
drop_last_n_user_turns
replace_last_turn_images
这一区分很重要。
追加不会增加 history_version。
replace 以及成功改写最后一轮图片时会增加:
history_version =
history_version.saturating_add(1);
依赖 History 游标的 Guardian 增量审查可以据此判断:
旧游标是否仍然有效
remove_first_item 是本地压缩重试使用的工作副本操作,不会修改 Session
中的活跃 History,因此也不递增这个版本。
9. 记录 Item 的完整链路
大多数模型可见 Item 都经过:
Session::record_conversation_items
-> prepare_conversation_items_for_history
-> prepare_response_items
-> 补 Turn ID
-> 可选补 Item ID
-> SessionState::record_items
-> ContextManager::record_items
-> persist_rollout_response_items
-> RawResponseItem Event
这意味着一次记录同时更新:
内存 History
持久化 Rollout
客户端 Raw Item 流
不能只修改 ContextManager.items,否则 Resume 和客户端视图都会失真。
10. 图片在进入 History 前会被规范化
prepare_response_items 会处理:
User Message Image
Tool Output Image
主要规则包括:
拒绝远程图片 URL
拒绝不支持的 low detail
限制图片尺寸和 Patch 数
加载并转换为可发送 Data URL
失败时替换为文本占位符
这发生在持久化前。
因此 Rollout 保存的是:
已经准备过的模型输入
Resume 时对旧 Rollout 还会再次运行兼容性准备,但不会改写原始 Rollout 文件。
11. ContextManager 过滤非 API Item
record_items 不是无条件保存所有 ResponseItem。
is_api_message 会保留:
非 System Message
AgentMessage
Reasoning
Function Call / Output
Custom Tool Call / Output
Local Shell Call
Tool Search Call / Output
Web Search Call
Image Generation Call
Compaction
ContextCompaction
会过滤:
System Message
CompactionTrigger
Other
CompactionTrigger 只是远程 v2 请求控制信号,不是持久化对话事实。
12. 工具结果在写入时被截断
大型 Tool Output 可能一次耗尽上下文。
ContextManager::process_item 只对以下 Item 应用截断:
FunctionCallOutput
CustomToolCallOutput
截断策略来自:
ModelInfo.truncation_policy
用户配置:
tool_output_token_limit = 10000
会先覆盖 ModelInfo.truncation_policy 的 Limit,并保留模型原本选择的 Bytes
或 Tokens 模式。
类型为:
pub struct TruncationPolicyConfig {
pub mode: TruncationMode,
pub limit: i64,
}
支持:
Bytes
Tokens
13. 为什么截断预算乘以 1.2
源码中有:
let policy_with_serialization_budget = policy * 1.2;
然后才调用:
truncate_function_output_payload
这里不是把上下文限制放宽 20%。
它是在工具结果存储阶段,为序列化表示和不同截断路径留出调整空间。
最终 Context Window 是否需要压缩,仍由独立 Token Status 决定。
14. Structured Tool Output 的截断边界
Tool Output 可以是:
Text
ContentItems
对于 ContentItems:
InputText
InputImage
EncryptedContent
截断器只消费文本预算。
图片与加密内容会保留。
如果多个文本 Item 超出预算,尾部还会加入类似:
[omitted N text items ...]
这比直接截断 JSON 字符串更安全,因为它保持结构化 Content Item 合法。
15. for_prompt 不修改真实 History
Sampling 前调用:
sess.clone_history()
.await
.for_prompt(&turn_context.model_info.input_modalities)
流程是:
Clone ContextManager
-> Normalize Clone
-> Return Vec<ResponseItem>
因此:
Prompt 修复
!=
持久化 History 重写
这样可以避免仅为某次请求补出的兼容 Item 污染 Rollout。
16. Prompt 规范化维护三个不变量
normalize_history 依次执行:
ensure_call_outputs_present(&mut items);
remove_orphan_outputs(&mut items);
strip_images_when_unsupported(input_modalities, &mut items);
三个不变量是:
每个 Call 都有 Output
每个客户端 Output 都有 Call
不支持图片的模型不收到图片 Payload
这也是为什么不能把 History 当作任意 JSON 数组处理。
17. 缺失 Tool Output 会被补成 aborted
如果 History 中存在:
FunctionCall
但缺少对应:
FunctionCallOutput
规范化会在 Call 后插入:
output = "aborted"
它覆盖:
FunctionCall
ToolSearchCall
CustomToolCall
LocalShellCall
这样即使 Turn 被 Interrupt,后续请求仍满足 Responses API 的调用配对要求。
其中 Custom Tool 与 Local Shell 的缺失 Output 被视为内部不变量错误:Debug
构建会通过 error_or_panic 直接 Panic;非 Debug 构建才在报告错误后继续插入aborted Output。普通 Function Call 与客户端 Tool Search 不走这条 Panic 分支。
18. 合成 Output ID 必须稳定
当源 Call 已有非空 Item ID 时,合成 Output 的 ID 使用 UUID v5:
固定 Namespace
+ Item 类型前缀
+ 源 Call Item ID
而不是随机 UUID。
原因写在源码中:
Prompt normalization can run repeatedly
without persisting synthetic outputs.
如果每次生成不同 ID:
相同逻辑 Prompt
-> 不同 Wire Input
-> Prompt Cache 失效
稳定 ID 让 Retry 和 Resume 上的临时修复仍可复用缓存。
旧 History 中的 Call 如果没有 Item ID,合成 Output 也保持 id = None,以兼容
旧行为;这类 Item 无法获得 UUID v5 的稳定 ID。
19. Orphan Output 会被删除
如果存在:
FunctionCallOutput
但找不到对应 Call,规范化会把它视为 Orphan。
Debug 构建会对 Function、Custom Tool 和客户端 Tool Search 的 Orphan 调用error_or_panic;非 Debug 构建在报告错误后删除它。
特殊例外是:
execution = "server"
的 Tool Search Output。
因为服务端执行的 Tool Search 不一定具有客户端可见 Call。
这体现了一个原则:
只修复客户端能够证明不合法的配对
20. 模型不支持图片时不会直接删除消息
strip_images_when_unsupported 的策略是:
Message InputImage
-> 文本占位符
Tool Output InputImage
-> 文本占位符
ImageGenerationCall.result
-> 清空
占位符为:
image content omitted because you do not support image input
保留占位符比静默删除更好:
模型仍然知道这里原本存在一张图片
21. 模型可见上下文组成表
一次普通请求中的主要上下文如下。
| 内容 | 逻辑角色 | 存储位置 | 更新方式 |
|---|---|---|---|
| Base Instructions | 顶层 Instructions | SessionConfiguration | Session 级 |
| Permissions | Developer | History | Full 或 Diff |
| Developer Override | Developer | History | Initial Context |
| Collaboration Mode | Developer | History | Full 或 Diff |
| Skills Catalog | Developer | History | Initial Context |
| AGENTS.md | User Context | History + WorldState | Full 或 Diff |
| Environment | User Context | History + WorldState | Full 或 Diff |
| User Prompt | User | History | Append |
| Assistant Output | Assistant | History | Append |
| Reasoning | Reasoning Item | History | Append |
| Tool Call | Call Item | History | Append |
| Tool Result | Output Item | History | Append + Truncate |
| Compaction Summary | User/Compaction Item | Replacement History | Replace |
“逻辑角色”与 UI 展示角色不总是相同。
例如 AGENTS.md 在当前实现中是:
role = user
但它是运行时生成的 Contextual User Fragment,不应在界面上伪装成用户手工输入。
22. 初始上下文不是 Session 创建时立即写入
新 Session 初始化时:
ContextManager 为空
previous_turn_settings = None
reference_context_item = None
初始上下文会推迟到第一个真实 Turn。
原因是:
turn/start 仍可能覆盖 Model、权限、CWD、Personality 等配置
如果 Session 创建时就持久化上下文,第一 Turn 的 Override 会让它立即过期。
源码注释明确说明:
Defer initial context insertion until the first real turn starts.
23. 第一 Turn 的上下文构建链
第一 Turn 进入:
run_turn
-> run_pre_sampling_compact
-> capture_step_context
-> record_context_updates_and_set_reference_context_item
-> build_world_state_for_step
-> build_initial_context_with_world_state
-> record_conversation_items
随后才记录:
用户输入
Skill / Plugin Injection
最后:
clone_history
-> for_prompt
-> run_sampling_request
24. 初始 Developer Context 包含什么
build_initial_context_with_world_state 会聚合多个 Developer Section:
Model Switch Instructions
Permission Instructions
config.developer_instructions
Collaboration Mode
Realtime State
Personality
Available Skills
Extension Thread Context
Extension Turn Context
Token Budget Metadata
World State 中的 Developer Fragment
大多数 Section 被合并成一个 Developer Message。
Guardian Policy 等需要独立审计的内容会保留为单独顶层 Developer Message。
25. 初始 User Context 包含什么
Contextual User Section 主要包括:
Recommended Plugins
Extension User Context
AGENTS.md
Environment
它们通过:
build_contextual_user_message(text_sections)
合并为 User Message。
World State Fragment 会按自身声明的 Role 分流。当前 Apps 与 Plugins 使用
Developer Role,因此进入 Developer Message,而不是这个 User Message。
Context Fragment 本身带有明确 Marker,例如:
# AGENTS.md instructions
<INSTRUCTIONS>
...
</INSTRUCTIONS>
或者:
<environment_context>
...
</environment_context>
这些 Marker 让 Rollback、Compaction 和 UI Mapping 能识别“上下文消息”。
26. 为什么要引入 WorldState
如果每个 Sampling 都重复注入完整:
AGENTS.md
CWD
Shell
Date
Permissions
Apps
Plugins
会产生三个问题:
浪费 Token
降低 Prompt Cache 命中
旧状态与新状态同时存在
WorldState 将动态上下文拆成独立 Section,并为每个 Section 保存可比较 Snapshot。
它允许:
首次发送 Full
稳定时不发送
变化时只发送 Diff
需要撤销时发送 Removal
是否需要模型可见 Removal 由各 WorldStateSection::render_diff 决定。例如
AGENTS.md 和 Environment 会表达替换或移除;当前 Apps/Plugins Section 只在能力
变为可用时补充通用说明,变为不可用时不会追加 Removal 文本。
27. WorldStateSection 的抽象
每个动态 Section 实现:
pub(crate) trait WorldStateSection {
const ID: &'static str;
type Snapshot: DeserializeOwned + Serialize;
fn snapshot(&self) -> Self::Snapshot;
fn render_diff(
&self,
previous: PreviousSectionState<'_, Self::Snapshot>,
) -> Option<Box<dyn ContextualUserFragment>>;
}
关键约束是:
ID 必须稳定
Snapshot 只保存比较所需数据
Snapshot 不能序列化为 null
Section 自己决定如何表达变化
28. PreviousSectionState 有三种状态
pub(crate) enum PreviousSectionState<'a, T> {
Absent,
Unknown,
Known(&'a T),
}
语义分别是:
Absent
-> 确认模型没有看到旧状态
Unknown
-> History 中可能有旧 Fragment,但缺少精确 Snapshot
Known
-> 有持久化 Snapshot,可精确比较
Unknown 对兼容旧 Rollout 很重要。
它避免在没有 Snapshot 时错误地假设:
模型从未见过旧状态
29. WorldState 当前包含哪些 Section
build_world_state_for_step 当前加入:
AgentsMdState
EnvironmentsState
AppsInstructionsState
PluginsInstructionsState
Extension World State Sections
EnvironmentsState 内又包括:
Environment ID
CWD
Environment Status
Shell
Current Date
Timezone
Network Context
File System Context
Subagent State
这套结构已经超出传统“环境变量提示词”的范围。
30. WorldState Snapshot 使用 RFC 7386 Merge Patch
每次 World State 变化时:
Current Snapshot
- Previous Snapshot
-> RFC 7386 Merge Patch
持久化为:
pub struct WorldStateItem {
pub full: bool,
pub state: Value,
}
其中:
full = true
-> 建立新 Baseline
full = false
-> 对现有 Baseline 应用 Patch
对象中的 null 表示删除,因此 Snapshot 会先移除对象字段中的 Null。
31. TurnContext 与 WorldState 使用两套 Baseline
reference_context_item 保存:
TurnContextItem
其中有:
CWD
Workspace Roots
Date / Timezone
Approval Policy
Permission Profile
Network
Model
Comp Hash
Personality
Collaboration Mode
Multi-Agent Mode
Realtime
Reasoning Effort
但实际模型可见动态环境又由 WorldState 管理。
当前代码中的 TODO 也承认:
Context Update 还不是纯粹由持久化
Previous / Current TurnContextItem 决定
因此两套 Baseline 必须同时维护,不能合并成一个布尔值。
32. reference_context_item 缺失意味着发送 Full Context
核心判断是:
let should_inject_full_context =
reference_context_item.is_none();
如果为 None:
build_initial_context_with_world_state
set_world_state_baseline
persist WorldState(full)
如果存在:
build_settings_update_items
update_world_state
persist WorldState(patch)
压缩、旧 Rollout 或 Rollback 使 Baseline 不可信时,Codex 宁可重新发送完整上下文。
33. 稳态 Turn 只追加差量
当 Baseline 已建立时,Codex 比较:
Previous TurnContextItem
Current TurnContext
可能生成:
Model Switch
Permission Change
Collaboration Mode Change
Multi-Agent Mode Change
Realtime Change
Personality Change
同时比较 World State:
Previous WorldStateSnapshot
Current WorldState
可能生成:
AGENTS.md Replacement
Environment Update
Apps Availability Update
Plugins Availability Update
Extension Update
没有变化就不追加任何 Context Item。
34. 上下文持久化顺序是有意设计的
当动态状态变化时,顺序是:
1. 生成模型可见 Context Item
2. 记录到 Conversation History
3. 持久化 WorldState Full/Patch
4. 持久化 TurnContextItem
5. 更新内存 Baseline
源码注释强调:
Persist state only after any model-visible context
generated from it.
如果先持久化 Baseline,再因崩溃没有写入模型可见 Item,Resume 会误以为模型已经看到新状态。
35. StepContext 固定一次 Sampling 的动态视图
StepContext 包含:
pub(crate) struct StepContext {
pub(crate) turn: Arc<TurnContext>,
pub(crate) environments: TurnEnvironmentSnapshot,
pub(crate) selected_capability_roots:
Vec<ResolvedSelectedCapabilityRoot>,
pub(crate) mcp: Arc<McpRuntimeSnapshot>,
mcp_tool_snapshot: OnceCell<Vec<ToolInfo>>,
pub(crate) loaded_agents_md: Option<Arc<LoadedAgentsMd>>,
}
同一次 Sampling 中:
上下文
Tool Spec
Tool Execution Environment
AGENTS.md
MCP Runtime
都来自同一个 Step Snapshot。
这样可以避免:
模型看到环境 A 的工具
实际却在环境 B 执行
36. Dynamic State 何时重新捕获
每次 Sampling Loop 可以重新调用:
capture_step_context
但环境是否刷新取决于:
Feature::DeferredExecutor
未启用时:
使用 TurnContext 中冻结的 Environment Snapshot
启用时:
重新读取 Thread Environment Snapshot
刷新 AGENTS.md Cache
重新解析 Capability Roots
重新捕获 MCP Runtime
这是一条重要并发边界:
动态刷新发生在 Sampling Request 之间
不会在一次 Tool Call 执行中间替换 StepContext
37. Model Switch 通过差量消息桥接
切换 Model 时,顶层 Base Instructions 可能改变。
但旧 History 中仍然包含在前一个模型指令环境下产生的上下文。
Codex 会在 Model Slug 变化时生成:
<model_switch>
新模型指令
</model_switch>
对应函数:
build_model_instructions_update_item(
previous_turn_settings,
next,
)
这样新模型不会只依赖不可见的 Session 内部状态。
38. Prompt Cache 为什么偏爱追加
普通 Sampling History 的理想变化是:
Old Prefix
+ New User Message
+ Model Output
+ Tool Result
而不是:
每轮重写前缀
HTTP Provider 可利用稳定前缀做 Prompt Cache。
WebSocket 还可以检查:
Current Input
==
Previous Request Input
+ Previous Response Items
+ Incremental Items
成立时只发送:
previous_response_id
incremental input
Context Diff 与稳定合成 ID 都在保护这个追加模型。
39. History 重写会打断增量请求
压缩或 Rollback 后:
Current Input
不再是上一次 Input 的严格扩展。
get_incremental_items 会返回:
None
客户端随即发送完整请求,而不是错误复用:
previous_response_id
这说明:
ContextManager.history_version
不是 WebSocket 增量判断的直接输入。
WebSocket 会比较真实 Request Properties 和 Item Prefix。
40. AGENTS.md 有两层来源
Codex 将项目指令分成:
Global User Instructions
Project Instructions
全局指令由:
codex-home/instructions/mod.rs
从:
$CODEX_HOME/AGENTS.override.md
$CODEX_HOME/AGENTS.md
中选择一个。
项目指令由:
沿项目目录层级发现。
41. 全局指令优先选择 Override
CodexHomeUserInstructionsProvider 的候选顺序是:
1. AGENTS.override.md
2. AGENTS.md
找到第一个:
存在
是普通文件
Trim 后非空
的候选后立即返回。
它不会同时合并这两个全局文件。
读取失败不会直接终止 Session,而是产生 Startup Warning。
42. 项目根目录如何确定
项目指令发现从当前 CWD 向上查找:
project_root_markers
默认 Marker 为:
.git
语义是:
找到最近的带 Marker 祖先
-> 它是 Project Root
找不到 Marker
-> 只检查当前 CWD
Marker 列表为空
-> 禁止向父目录遍历
Codex 不会越过 Project Root。
43. Project Config 不能控制自己的 Root Marker
查找 project_root_markers 时,源码会合并 Config Layer,但排除:
ConfigLayerSource::Project { .. }
原因是循环依赖。
如果项目内 .codex/config.toml 可以改变“哪里是项目根”,就必须先知道项目根才能找到这份配置。
因此 Root Marker 只能来自:
默认值
用户层配置
系统层配置
CLI Override
而不是待发现项目自身的 Config Layer。
44. 项目指令按 Root 到 CWD 搜索
假设:
repo/
.git/
AGENTS.md
crates/
AGENTS.md
core/
AGENTS.md
当前 CWD 为:
repo/crates/core
搜索目录顺序是:
repo
repo/crates
repo/crates/core
最终合并顺序也是:
Root -> Child -> Current
这让更靠后的深层指令在语言模型上下文中更接近当前任务。
45. 每个目录只选择一个候选文件
候选文件优先级为:
1. AGENTS.override.md
2. AGENTS.md
3. project_doc_fallback_filenames 中的自定义文件
在同一个目录:
找到第一个普通文件后停止
所以:
AGENTS.override.md
不是在 AGENTS.md 后面追加一层,它会替换同目录的默认文件。
46. Fallback Filename 是有序列表
配置示例:
project_doc_fallback_filenames = [
"CONTRIBUTING.md",
".agent-guide.md",
]
只有当前目录缺少:
AGENTS.override.md
AGENTS.md
时才尝试 Fallback。
空字符串会被过滤。
与内置名称重复的候选也会去重。
47. AGENTS.md 的总预算默认是 32 KiB
默认值为:
pub const DEFAULT_PROJECT_DOC_MAX_BYTES: usize =
32 * 1024;
配置项:
project_doc_max_bytes = 32768
注意对单个项目 Environment 来说,它是:
从 Project Root 到 CWD 的所有层级文档共享总预算
不是每个文件 32 KiB。
多环境 Turn 会分别调用一次项目文档加载,因此每个 Environment 各自拥有这份预算。
48. 字节预算从 Root 向深层消费
读取顺序为:
Root AGENTS
Child AGENTS
Current AGENTS
预算也按这个顺序扣减。
例如:
总预算 = 7 Bytes
Root 内容 = "root" 4 Bytes
Child 内容 = "abcdef" 6 Bytes
最终是:
root
abc
这意味着大型 Root 指令可能挤占深层指令预算。
配置时应避免把大段通用文档直接复制进 Root AGENTS.md。
49. 截断按 Bytes 而不是 Token
单个 Environment 的项目文档预算使用:
Vec<u8>::truncate
随后通过:
String::from_utf8_lossy(&data)
转换。
如果截断落在多字节 UTF-8 字符中间,末尾会出现替换字符:
�
因此:
project_doc_max_bytes
不是精确 Token 限制,也不保证字符边界截断。
50. 全局指令不消费项目字节预算
加载流程先创建:
LoadedAgentsMd::from_user_instructions
然后再以:
project_doc_max_bytes
读取项目文档。
所以每个 Environment 的 32 KiB 限制只应用于它的项目文件。
全局 $CODEX_HOME/AGENTS.md 不从该预算中扣减。
51. 全局与项目指令的合并格式
单环境下:
Global Instructions
--- project-doc ---
Root Project Instructions
Child Project Instructions
--- project-doc --- 只在:
全局或内部指令
-> 第一段项目指令
的转换处出现一次。
项目层级之间只用空行连接。
52. 多环境指令会显式标记 Environment
当一个 Turn 同时关联多个项目环境时,不能只写一个外层 CWD。
格式会变为:
for `environment-a` with root /workspace/a
Environment A Instructions
for `environment-b` with root C:\workspace\b
Environment B Instructions
每个 Environment 的多个层级文档仍保持 Root 到 CWD 顺序。
路径按目标 Environment 的原生路径格式展示。
53. AGENTS.md 最终是 User Context Fragment
LoadedAgentsMd::contextual_user_fragment 生成:
UserInstructions {
directory,
text,
}
UserInstructions 的角色是:
fn role(&self) -> &'static str {
"user"
}
渲染结果类似:
# AGENTS.md instructions for /repo
<INSTRUCTIONS>
...
</INSTRUCTIONS>
这是当前源码事实。
不能因为 Base Prompt 中把 AGENTS.md 描述为“用户指令”,就推断它通过顶层 instructions 字段发送。
54. AGENTS.md Sources 保留来源路径
LoadedAgentsMd 为每个 Entry 保存:
Project Source Path
Environment ID
Selected CWD
CodexThread::instruction_sources 会返回:
Global Source
Root AGENTS Source
Nested AGENTS Source
这些来源适合用于:
UI 展示
调试
审计
配置诊断
模型可见文本本身不会暴露每个项目文件的精确来源路径。
55. Session 创建时会加载并缓存 AGENTS.md
Session 初始化会:
ThreadEnvironments::snapshot
-> AgentsMdManager::new
-> AgentsMdManager::refresh
-> cache.loaded
AgentsMdManager 缓存:
struct AgentsMdCache {
selections: Option<Vec<TurnEnvironmentSelection>>,
loaded: Option<Arc<LoadedAgentsMd>>,
}
如果 Environment Selection 没变:
refresh 直接返回
它不会通过文件修改时间自动失效。
56. 默认模式保留创建时指令快照
普通模式下,capture_step_context 使用:
TurnContext.environments
AgentsMdManager 当前缓存
不会每个 Turn 重新扫描磁盘。
因此在 Session 创建后修改:
$CODEX_HOME/AGENTS.md
项目 AGENTS.md
当前 Session 通常仍继续使用创建时快照。
这同样适用于:
手动压缩
Turn 中自动压缩
远程 v2 压缩
57. DeferredExecutor 会改变刷新语义
启用:
Feature::DeferredExecutor
时,capture_step_context 会:
重新获取 Environment Snapshot
调用 AgentsMdManager::refresh
但 refresh 仍先比较:
TurnEnvironmentSelection
如果选择完全相同,仅修改同一路径文件内容不一定触发重新读取。
因此它更准确的语义是:
环境选择感知的刷新
而不是通用文件 Watcher。
58. Cold Resume 会重新加载当前全局与项目指令
Resume 创建新的 Session。
新的:
UserInstructionsProvider
AgentsMdManager
会从当前文件系统重新加载 $CODEX_HOME 全局指令和当前项目环境的层级指令。
同时 Rollout 可能包含旧的 World State Baseline 和旧模型可见 Fragment。
如果新旧内容不同,AgentsMdState::render_diff 会生成:
These AGENTS.md instructions replace all previously
provided AGENTS.md instructions.
<new instructions>
如果新指令消失,则发送:
The previously provided AGENTS.md instructions
no longer apply.
59. AGENTS.md Diff 不能只发送新正文
模型 History 中旧指令不会被物理删除。
如果变化时只追加:
New Instructions
模型可能同时遵循新旧两份。
因此 AgentsMdState 显式说明:
replace all previously provided instructions
或:
previous instructions no longer apply
这是一种面向 LLM 的逻辑 Tombstone。
60. 项目指令优先级由两部分共同实现
Codex 的 AGENTS 规则不是由 Rust 代码强制解析冲突。
Rust 负责:
Root 到 CWD 的稳定排序
Override Filename 选择
清晰的 Context Wrapper
变更时 Replacement Notice
Base Instructions 负责告诉模型:
更深层 AGENTS.md 优先
直接 System / Developer / User 指令优先
也就是说:
加载顺序由 Runtime 保证
语义优先级由 Model Instructions 声明
61. Token 统计有两个来源
Codex 同时使用:
服务端返回的 Token Usage
本地启发式估算
服务端数据更接近真实计费和模型上下文。
本地估算用于:
压缩后重算
Resume 时缺少新请求
本地新增 Item 尚未被服务端统计
远程压缩前预估
不能把两者当成同一精度。
62. ContextManager 如何记录服务端 Usage
每次 response.completed 可能包含:
TokenUsage {
input_tokens,
cached_input_tokens,
output_tokens,
reasoning_output_tokens,
total_tokens,
}
update_token_info 会构造或追加:
TokenUsageInfo.total_token_usage
TokenUsageInfo.last_token_usage
TokenUsageInfo.model_context_window
其中自动压缩主要关心当前活跃上下文,而不是历史累计账单。
63. 活跃上下文 Token 不是简单 last.total_tokens
get_total_token_usage 从:
last_token_usage.total_tokens
开始,再补入:
最近模型生成 Item 之后新增的本地 Item
例如:
User Steer Input
Tool Output
Hook Prompt
Context Diff
这些 Item 尚未出现在上一次服务端 Usage 中。
64. 旧 Reasoning 是否补算取决于服务端能力
如果:
server_reasoning_included = true
Codex 相信服务端已计入历史 Reasoning。
否则还会本地估算:
最后一个 User Turn 之前的加密 Reasoning Items
再加到当前 Usage。
这是为了适配不同 Provider 对 Reasoning Token 的统计差异。
65. 本地 Token 估算是字节启发式
estimate_token_count_with_base_instructions 计算:
approx_token_count(Base Instructions)
+
sum(estimate_item_token_count(Item))
核心换算近似为:
4 Bytes ~= 1 Token
源码明确把它称为:
coarse lower bound
not tokenizer-accurate
因此它适合做保护性决策,不适合展示精确账单。
66. 加密 Reasoning 使用特殊估算
对:
Reasoning.encrypted_content
Compaction.encrypted_content
ContextCompaction.encrypted_content
不能直接按 Base64 字符串长度计费。
估算先近似还原:
encoded_len * 3 / 4 - 650
再换算 Token。
这避免把传输编码膨胀全部算成模型上下文。
67. 图片使用 Patch 成本估算
普通 High/Auto 图片使用固定模型可见成本:
7,373 Bytes
≈ 1,844 Tokens
detail = original 时:
解码图片
计算 32px Patch 数
最多 10,000 Patches
结果缓存在一个容量为 32 的 Blocking LRU Cache 中。
这样不会每次 Token 检查都重复解码相同 Data URL。
68. Effective Context Window 会预留空间
TurnContext::model_context_window 不直接返回 Catalog 原值。
它计算:
resolved_context_window
* effective_context_window_percent
/ 100
effective_context_window_percent 用于为:
System Prompt
Tool Overhead
模型输出
预留 Headroom。
因此:
Model Catalog Context Window
!=
Codex 可用 Context Window
69. 自动压缩阈值默认是原始窗口的 90%
ModelInfo::auto_compact_token_limit 计算:
context_limit = resolved_context_window * 90%
如果 Catalog 还显式提供:
auto_compact_token_limit
则选择:
min(config_limit, context_limit)
所以显式值可以更保守,但不能超过默认 90% 上限。
70. Config Override 与 ModelInfo Limit 的边界
Core Config 还有:
model_context_window = 200000
model_auto_compact_token_limit = 150000
model_auto_compact_token_limit_scope = "total"
其中:
model_context_window
会参与 ModelInfo Override。
而 context_window_token_status 在 Total 模式中读取:
turn_context.model_info.auto_compact_token_limit()
在 BodyAfterPrefix 模式中,Scope Limit 则优先读取:
config.model_auto_compact_token_limit
再回退到 ModelInfo。
71. Total Scope 统计完整活跃上下文
默认:
AutoCompactTokenLimitScope::Total
计算:
auto_compact_scope_tokens
= active_context_tokens
auto_compact_scope_limit
= ModelInfo.auto_compact_token_limit()
适合传统压缩模型:
Prefix 和新对话都消耗同一个阈值
72. BodyAfterPrefix 只统计窗口增长
BodyAfterPrefix 计算:
baseline = 当前 Compaction Window 的 Prefill Input Tokens
scope_tokens =
active_context_tokens - baseline
Scope Limit 只约束:
压缩后新增的正文增长
但它仍然额外检查:
完整活跃上下文
>=
Effective Context Window
所以大 Prefix 不会绕过物理上下文上限。
73. AutoCompactWindow 保存什么
每个 Session 维护:
pub(super) struct AutoCompactWindow {
window_number: u64,
ids: AutoCompactWindowIds,
new_context_window_requested: bool,
prefill_input_tokens: Option<AutoCompactWindowPrefill>,
token_budget_reminder_delivered: bool,
}
每次压缩:
window_number + 1
previous_window_id = old window_id
window_id = new UUIDv7
reminder flag reset
first_window_id 在整个 Thread Window Chain 中保持不变。
74. Prefix Baseline 有估算值和服务端值
enum AutoCompactWindowPrefill {
ServerObserved(i64),
Estimated(i64),
}
Resume 或压缩后立即重算时,先记录:
Estimated
收到当前窗口第一份服务端 Usage 后,替换成:
ServerObserved(input_tokens)
一旦有 ServerObserved,后续估算不能覆盖它。
这保证精度单向提升。
75. Token Status 可以同时看两个上限
ContextWindowTokenStatus 返回:
pub(crate) struct ContextWindowTokenStatus {
pub(crate) active_context_tokens: i64,
pub(crate) auto_compact_scope_tokens: i64,
pub(crate) auto_compact_scope_limit: Option<i64>,
pub(crate) full_context_window_limit: Option<i64>,
pub(crate) tokens_until_compaction: Option<i64>,
pub(crate) auto_compact_window_prefill_tokens: Option<i64>,
pub(crate) full_context_window_limit_reached: bool,
pub(crate) token_limit_reached: bool,
}
最终:
token_limit_reached =
scope limit reached
OR
full context limit reached
tokens_until_compaction 取两个剩余额度的较小值。
在默认 Total 模式中,full_context_window_limit 为 None,主要依赖原始窗口
90% 的 Auto-Compact Limit。BodyAfterPrefix 才会同时填入 Effective Context
Window,防止一个很大的 Prefix 绕过物理边界。
76. ContextWindowExceeded 会把 Usage 标记为满
如果服务端直接返回:
ContextWindowExceeded
run_sampling_request 会调用:
sess.set_total_tokens_full
把当前 Token Usage 填充到 Context Window。
随后当前 Sampling 返回错误。
在普通 run_turn 中,这个错误会结束当前 Turn;它不会在同一个错误分支中自动压缩并重试。
“填满 Usage”的意义是:
下一次 Turn 的 Pre-Turn Check
会立即触发压缩
77. 自动压缩有两个触发位置
Turn 开始前
-> run_pre_sampling_compact
Sampling 完成后且仍需 Follow-Up
-> run_auto_compact MidTurn
两者的 History 形状不同。
Turn 前
压缩旧 History
下一步再注入当前 Context 和新用户输入
Turn 中
当前用户输入和 Tool Result 已在 History
压缩后必须立即继续同一 Turn
78. Turn 前压缩发生在新输入写入之前
run_turn 一开始就调用:
run_pre_sampling_compact(
&sess,
&turn_context,
&mut client_session,
).await
此时尚未执行:
capture_step_context
record_context_updates
record user input
因此当前实现的 Turn 前压缩请求不包含:
即将到来的用户消息
本 Turn 新 Context Diff
源码留有 TODO,要在未来估算这些 Pending Incoming Items。
79. Turn 前压缩的第一个原因:ContextLimit
流程是:
context_window_token_status
-> token_limit_reached
-> capture_step_context
-> run_auto_compact
reason = ContextLimit
phase = PreTurn
injection = DoNotInject
压缩完成后:
reference_context_item = None
随后当前真实 Turn 会重新注入完整 Context。
80. Turn 前压缩的第二个原因:CompHashChanged
ModelInfo 可以声明:
comp_hash
它是:
压缩兼容配置的不透明标识
触发条件非常严格:
Previous Comp Hash 存在
Current Comp Hash 存在
两者不同
如果任意一侧缺失:
不因 Comp Hash 触发压缩
缺失不等于不兼容,只表示信息不足。
81. Turn 前压缩的第三个原因:ModelDownshift
切换到更小 Context Window 的模型时,旧 History 可能无法装入新模型。
Codex 检查:
Previous Model Window > New Model Window
Previous Model != New Model
当前活跃 Token 已超过新模型安全边界
满足后:
reason = ModelDownshift
phase = PreTurn
82. Model Switch 压缩优先使用旧模型
Comp Hash 或 Downshift 触发时,Codex 构造:
previous_model_turn_context
并先用旧模型压缩旧 History。
这是合理的:
旧模型最了解自己产生的加密 Reasoning
旧模型的 Compaction Contract 与旧 History 匹配
如果旧模型在 OpenAI Codex Backend 上已退役并返回 InvalidRequest,才可能回退当前模型。
83. Previous Model Fallback 不是通用 Retry
构造 Fallback StepContext 需要:
当前认证使用 Codex Backend
Provider 是 OpenAI
Previous Model != Current Model
并且仅在第一次压缩返回:
InvalidRequest
时尝试当前模型。
网络错误、认证错误和 Fatal Error 不会借此切换模型。
84. Turn 中压缩只在还需要继续时触发
Sampling 返回后计算:
model_needs_follow_up
has_pending_input
needs_follow_up =
model_needs_follow_up || has_pending_input
只有:
needs_follow_up
AND
(
new context window explicitly requested
OR token_limit_reached
)
才执行 Mid-Turn Compaction。
如果模型已经完成 Final Answer:
即使刚刚跨过阈值,也不会为了当前 Turn 再压缩
下一 Turn 会在开始前处理。
85. Mid-Turn 压缩必须重新注入 Context
调用参数是:
InitialContextInjection::BeforeLastUserMessage(
Arc::clone(&world_state),
)
原因是压缩后还要立即继续:
Tool Call
-> Tool Result
-> Compact
-> Continue Sampling
不能等下一 Turn 才恢复:
Permissions
AGENTS.md
Environment
Skills
86. Mid-Turn Context 的插入位置
insert_initial_context_before_last_real_user_or_summary 使用优先级:
1. 最后一个真实 User Message 之前
2. 最后一个 Compaction Summary 之前
3. 最后一个 Compaction Item 之前
4. 没有边界时追加到末尾
目标是同时满足:
Canonical Context 位于当前用户任务之前
Compaction Summary / Item 保持最后
这是模型训练形状的一部分,不是随意排序。
87. Mid-Turn 压缩后延迟 Steer Drain
压缩完成后:
can_drain_pending_input = !model_needs_follow_up;
如果模型本身还需要 Tool Continuation:
先让模型基于压缩后的 Tool State 继续
再吸收 Steer Input
如果只有 Pending User Input 触发 Follow-Up:
压缩后可以立即 Drain
这避免 Steer 破坏尚未完成的 Tool Call Continuation。
88. 手动压缩是独立 Non-Steerable Turn
入口可以是:
Core Op::Compact
App Server thread/compact/start
TUI /compact
调用链:
Op::Compact
-> handlers::compact
-> spawn_task(CompactTask)
-> CompactTask::run
-> Local / Remote / TokenBudget Compaction
CompactTask 的 Task Kind 是:
TaskKind::Compact
App Server 会把它视为:
NonSteerableCompact
89. 三种压缩实现如何选择
普通模式的选择为:
Feature::TokenBudget?
|
|-- true -> 新 Context Window,不做摘要
|
`-- false
|
| Provider supports remote compaction?
| |
| |-- false -> Local Summary
| |
| `-- true
| |
| | RemoteCompactionV2?
| | |-- true -> Streaming V2
| | `-- false -> /responses/compact V1
Remote Compaction 当前支持:
OpenAI Provider
Azure Responses Provider
普通本地或自定义 Provider 使用 Local Summary。
90. 本地压缩的核心思路
本地压缩不是算法摘要。
它向当前模型发送一条合成 User Prompt:
You are performing a CONTEXT CHECKPOINT COMPACTION.
Create a handoff summary for another LLM...
请求仍走普通:
ModelClientSession::stream
模型输出的最后一个 Assistant Message 被当作摘要正文。
91. Local Compaction Prompt 可以覆盖
默认 Prompt 来自:
prompts/templates/compact/prompt.md
配置可以提供:
compact_prompt = """
生成一份供下一模型继续任务的结构化摘要。
"""
也存在文件型兼容配置:
experimental_compact_prompt_file
空白 Prompt 会在 Config 解析时被过滤。
92. 本地压缩请求也会带完整旧 History
流程是:
clone current history
append synthetic compact user prompt
normalize for prompt
send ordinary Responses request
模型输出的 Item 还会临时记录进原 History。
压缩成功后再从 Session History 取:
最后一个 Assistant Message
作为摘要。
93. 本地压缩遇到 ContextWindowExceeded 会逐项裁剪
如果压缩请求本身也装不进窗口:
turn_input_len > 1
则:
删除最旧 History Item
同时删除对应 Call / Output Pair
重置普通 Retry 计数
重新请求
注释写的是:
Trim from the beginning to preserve cache (prefix-based)
and keep recent messages intact.
更准确地说,它优先保留近期任务状态;每次只做一个小步裁剪。
94. 本地压缩最终保留最近 User Messages
本地替换历史由:
最近真实 User Messages
+
Compaction Summary
构成。
真实 User Message 总预算为:
20,000 Tokens
选择从最新消息向旧消息反向进行。
最老的入选消息如果只剩部分预算,会被截断。
这条本地路径通过 UserMessageItem::message() 重建纯文本 User Message,不会把
原 User Item 中的图片重新复制进 Replacement History。图片仍可在压缩请求中被
模型总结,但不会作为原始图片继续保留。
95. Summary 使用 User Role 保存
本地摘要前缀为:
Another language model started to solve this problem
and produced a summary...
最终替换历史中的摘要是:
role = user
而不是 Assistant Message。
这是明确的 Handoff 指令:
告诉下一次模型把摘要当作继续任务的输入事实
is_summary_message 通过固定前缀识别它,避免下次压缩把旧摘要当作真实用户消息重复收集。
96. 本地压缩后的替换历史
Pre-Turn 或 Manual Local Compaction:
Recent User Message 1
Recent User Message 2
Compaction Summary
此时:
reference_context_item = None
world_state_baseline = None
下一真实 Turn 再追加完整 Context。
Mid-Turn Local Compaction:
Recent User Message 1
Canonical Initial Context
Current Real User Message
Compaction Summary
此时 Baseline 被重新建立,可以立即继续当前 Turn。
97. 远程 v1 调用专用 Endpoint
远程 v1 的核心请求是:
POST /responses/compact
通过:
codex-api/src/endpoint/compact.rs
发送。
返回:
struct CompactHistoryResponse {
output: Vec<ResponseItem>,
}
它直接给出替换历史,而不是让 Core 从最后一条 Assistant Message 构造文字摘要。
98. 远程 v1 复用 Turn Sticky State
Mid-Turn 远程压缩接收:
ModelClientSession.turn_state()
/responses/compact 返回 Header:
x-codex-turn-state
时也会写入同一个 OnceLock。
所以:
Sampling
Remote Compact
Post-Compact Sampling
可以保持当前 Turn 的 Sticky Routing。
99. 远程压缩前会尝试缩小大型 Tool Output
远程 v1/v2 都先调用:
trim_function_call_history_to_fit_context_window(
&mut history,
turn_context,
&base_instructions,
)
它从 History 尾部向前检查可重写的:
FunctionCallOutput
CustomToolCallOutput
ToolSearchOutput
遇到第一个不可重写的尾部 Item 就停止,不会越过它继续扫描更早的 Output。
替换为:
Output exceeded the available model context
and was truncated
或空 Tool List。
100. 远程压缩裁剪是临时副本
远程压缩先:
clone History
再重写大型 Output。
因此这些占位符只影响:
Compact Request
Compact Trace Input
不会先改写活跃 Session History。
只有远程返回的 Replacement History 最终安装成功后,旧 History 才整体被替换。
101. 远程返回的 History 不能直接信任
process_compacted_history 会过滤远程结果。
明确删除:
所有 Developer Messages
非真实用户内容的 User Context
Reasoning
Tool Call
Tool Output
Web Search
Image Generation
CompactionTrigger
保留:
可解析为 TurnItem::UserMessage 的 User Message
Hook Prompt
Assistant Message
AgentMessage
Compaction
ContextCompaction
第一类除了真实用户输入,也可能包括压缩 Summary 或兼容性 User-Role Warning;
判断依据是 parse_turn_item,不是只检查 role == "user"。
然后再从当前 Session 重新生成 Canonical Context。
102. 为什么删除远程 Developer Message
远程结果可能包含压缩前的:
旧权限
旧 AGENTS.md
旧 Personality
旧 Skills
旧 Plugin State
如果原样安装:
陈旧 Developer Context
会与当前 Canonical Context 冲突
因此远程压缩输出只能决定对话摘要形状,不能成为配置事实源。
103. 远程 v2 使用 CompactionTrigger
远程 v2 不调用专用 /responses/compact。
它构造普通 Prompt:
History
+ ResponseItem::CompactionTrigger {}
再走:
ModelClientSession::stream
服务端应返回:
ResponseItem::Compaction
这让压缩可以复用:
HTTP/SSE
WebSocket
Retry
Sticky State
Telemetry
104. 远程 v2 要求恰好一个 Compaction Item
流处理会统计:
Output Item 总数
Compaction Item 数量
是否看到 response.completed
成功条件是:
看到 response.completed
AND
Compaction Count == 1
可以在 Compaction 前出现其他 Output Item,但:
0 个或多个 Compaction
都视为 Fatal Protocol Error。
105. 远程 v2 的 Retry Budget 更小
普通 Responses Stream Retry 可能较大。
远程 v2 额外限制:
const MAX_REMOTE_COMPACTION_V2_STREAM_RETRIES: u64 = 2;
实际值为:
min(provider.stream_max_retries, 2)
压缩请求可能运行较久,限制 Retry 可以避免长时间重复消耗。
106. 远程 v2 会保留最近 Message 前缀
远程 v2 构建替换历史时,从原 Prompt 中先筛选:
User / Developer / System Message
再经过统一 should_keep_compacted_history_item。
最终主要保留:
可解析的 User Message
Hook Prompt
文本预算为:
64,000 Tokens
选择顺序同样偏向最近消息。
最后追加服务端返回的:
Compaction Item
远程 v2 保留的是原始 User ResponseItem,因此可以继续保留其中的 Input Image。
图片不消费这 64,000 Token 的文本预算,但会单独统计 retained_image_count。
107. 远程 v1 与 v2 的本质差异
| 维度 | Remote v1 | Remote v2 |
|---|---|---|
| Endpoint | /responses/compact |
普通 Responses |
| 请求控制 | 专用 Body | CompactionTrigger |
| 返回 | Vec<ResponseItem> |
单个 Compaction Item |
| Transport | HTTP Unary | HTTP/SSE 或 WebSocket |
| Retry | Request 路径 | Stream Retry,最多 2 |
| Retention | 服务端提供完整输出 | Core 保留最近 Message + Compaction |
| 当前默认 | Feature 关闭时兼容 | Stable,默认开启 |
两者安装 Replacement History 时共享:
process_compacted_history
replace_compacted_history
108. TokenBudget 模式不是摘要压缩
Feature::TokenBudget 当前状态是:
UnderDevelopment
default_enabled = false
启用后,手动或自动压缩会:
跳过模型摘要
跳过远程压缩
直接开始新 Context Window
新 History 只安装:
当前 Canonical Initial Context
并生成一个空消息的 CompactedItem 检查点。
109. TokenBudget 模式依赖外部持久状态
新窗口会告诉模型:
Thread ID
First Context Window ID
Previous Context Window ID
Current Context Window ID
还可以从 MCP Notes 获取:
thread_hint
它的设计假设是:
Message Items 可在窗口切换时清空
Notes 和持久 History 通过外部机制跨窗口保留
这与传统“把旧会话摘要塞回 Prompt”完全不同。
110. Token Budget Reminder 每窗口最多一次
如果配置:
[features.token_budget]
enabled = true
reminder_threshold_tokens = 10000
当:
tokens_until_compaction <= threshold
时,会记录 Developer Reminder。
AutoCompactWindow 通过:
token_budget_reminder_delivered
保证同一窗口最多发送一次。
进入新窗口后重置。
111. 压缩前后都可以运行 Hook
三种压缩实现都围绕:
PreCompact Hook
Compaction
PostCompact Hook
如果 Pre Hook 返回 Stop:
不安装新 History
Compaction 状态为 Interrupted
TurnAborted
如果 Post Hook 返回 Stop:
History 已经压缩
但当前任务停止继续
这两个时机不能互换。
112. ContextCompaction 是客户端生命周期 Item
压缩开始时创建:
ContextCompactionItem::new()
发送:
item/started
安装替换历史后发送:
item/completed
旧的:
EventMsg::ContextCompacted
只保留给兼容 Raw Event 和 Rollout 消费者。
App Server v2 使用 Canonical ContextCompaction Item。
113. replace_compacted_history 是语义提交点
所有普通压缩最终进入:
Session::replace_compacted_history(
turn_context,
items,
reference_context_item,
world_state_baseline,
compacted_item,
)
它完成:
可选补 Item ID
替换 ContextManager History
设置 reference_context_item
设置 WorldState Baseline
持久化 CompactedItem
持久化 WorldState Full Snapshot
持久化 TurnContextItem
安排 SessionStart Hook Source::Compact
114. CompactedItem 是持久化检查点
协议结构为:
pub struct CompactedItem {
pub message: String,
pub replacement_history: Option<Vec<ResponseItem>>,
pub window_number: Option<u64>,
pub first_window_id: Option<String>,
pub previous_window_id: Option<String>,
pub window_id: Option<String>,
}
现代 Rollout 会保存:
replacement_history
这允许 Resume 精确恢复压缩后 History,而不是再次运行摘要。
115. 为什么同时保存 message 和 replacement_history
message 是旧兼容表示。
旧 Rollout 可能只有:
CompactedItem.message
replacement_history = None
恢复时 Core 只能:
收集旧 User Messages
用 message 重建 Local 风格 History
清空 Context Baseline
现代格式直接保存完整 Replacement History,恢复更确定。
116. 压缩会推进 Window Chain
每次成功压缩调用:
advance_auto_compact_window
生成:
window_number
first_window_id
previous_window_id
window_id
这些字段同时进入:
CompactedItem
Responses Metadata
TokenBudget Context
便于 Telemetry 和 Rollout Trace 把一次压缩前后的请求连接起来。
117. Pre-Turn 与 Mid-Turn 的 Baseline 结果不同
Pre-Turn / Manual
参数:
InitialContextInjection::DoNotInject
结果:
reference_context_item = None
world_state_baseline = None
下一真实 Turn 发送 Full Context。
Mid-Turn
参数:
BeforeLastUserMessage(world_state)
结果:
reference_context_item =
Some(current TurnContextItem)
world_state_baseline =
Some(current WorldState)
当前 Turn 可以直接继续。
118. 压缩后 Token Usage 必须重算
安装新 History 后立即:
recompute_token_usage
本地估算:
Session Base Instructions
+ Replacement History
然后更新:
last_token_usage.total_tokens
model_context_window
BodyAfterPrefix Estimated Prefill
TokenCount Event
不能继续使用压缩前的服务端 Usage。
119. Resume 从最新 Replacement History 开始
reconstruct_history_from_rollout 会从新到旧扫描。
一旦找到最新仍存活的:
CompactedItem.replacement_history
它就成为:
Base Replacement History
更旧的 Response Item 不再影响当前 History。
随后只按时间顺序重放压缩检查点之后的 Rollout Tail。
120. Resume 还必须恢复三类 Metadata
找到 Replacement History 还不够。
恢复器同时寻找:
PreviousTurnSettings
reference_context_item
WorldState Baseline
AutoCompact Window IDs
其中:
PreviousTurnSettings
-> Model / CompHash / Realtime Bridge
reference_context_item
-> Turn Context Diff Baseline
WorldState Baseline
-> Dynamic Context Diff Baseline
121. WorldState Replay 必须从 Full 开始
Resume 会按顺序处理:
WorldState(full)
WorldState(patch)
WorldState(patch)
遇到 Compaction:
旧 WorldState Baseline 清空
如果出现:
Patch without Full
Core 会记录 Warning 并忽略 Patch。
因为 RFC 7386 Patch 没有 Base 就无法确定完整状态。
122. Rollback 会清理相邻 Context Diff
drop_last_n_user_turns 不只按 User Message 切片。
它还向前清理紧邻被回滚 Turn 的:
Contextual Developer Message
Contextual User Message
否则下一次请求可能留下:
只为已回滚 Turn 生成的权限或环境更新
123. Rollback 可能清空 reference_context_item
初始 Developer Message 可能混合:
可回滚 Context Fragment
持久 Developer Instructions
如果 Rollback 删除这个混合 Bundle,Core 无法只靠稳态 Diff 精确重建它。
因此会:
reference_context_item = None
下一 Turn 重新发送 Full Context。
这是“重新注入比错误差量更安全”的又一个例子。
124. 上下文管理的四条缓存原则
原则一:稳定前缀只追加
不变的 Context 不重复发送
变化只追加 Diff
原则二:合成 ID 必须确定
相同逻辑修复
-> 相同 Prompt Item ID
原则三:重写必须显式失效
Compaction / Rollback
-> History Version 或 Prefix 比较失败
原则四:配置事实由本地 Runtime 重建
不信任 Remote Compaction 返回的旧 Developer Context
125. 自动压缩决策流程图
Turn Start
|
v
Previous Model Settings exist?
|
|-- yes --> Both Comp Hash exist and differ?
| |
| `-- yes --> Compact with Previous Model
|
|-- yes --> Switch to smaller context and old history too large?
| |
| `-- yes --> Compact with Previous Model
|
v
Current Token Status reached limit?
|
`-- yes --> Pre-Turn Compact
|
v
Inject Context + Record User Input
|
v
Sampling
|
v
Need Follow-Up?
|
|-- no --> Finish Turn
|
`-- yes
|
v
Token Limit or New Window Requested?
|
|-- no --> Continue Sampling
|
`-- yes --> Mid-Turn Compact
|
v
Reinject Context
|
v
Continue Sampling
126. 本地压缩状态机
Start ContextCompaction Item
|
v
Clone History
|
v
Append Summarization Prompt
|
v
Responses Sampling
|
|-- Retryable Error --> Backoff / Retry
|
|-- Context Exceeded
| |
| |-- More than one item --> Drop oldest pair / Retry
| `-- Cannot trim --> Fail
|
v
Read last Assistant Message
|
v
Collect recent real User Messages
|
v
Build Summary User Message
|
v
Optional Mid-Turn Context Injection
|
v
Replace History + Persist Checkpoint
|
v
Recompute Usage
|
v
Complete ContextCompaction Item
127. 远程 v2 压缩状态机
Start ContextCompaction Item
|
v
Clone History
|
v
Rewrite oversized Tool Outputs if needed
|
v
Normalize Prompt
|
v
Append CompactionTrigger
|
v
Responses Stream
|
|-- Retryable Error --> Retry, max 2
|
|-- Missing response.completed --> Fail
|
|-- Compaction Count != 1 --> Fatal
|
v
Retain recent real messages
|
v
Append Compaction Item
|
v
Filter stale context
|
v
Optional Canonical Context Injection
|
v
Replace + Persist + Trace
|
v
Complete ContextCompaction Item
128. 推荐断点顺序
Context 写入
Session::record_conversation_items
ContextManager::record_items
ContextManager::process_item
ContextManager::for_prompt
ContextManager::normalize_history
Initial Context 与 Diff
Session::capture_step_context
Session::build_world_state_for_step
Session::record_context_updates_and_set_reference_context_item
Session::build_initial_context_with_world_state
build_settings_update_items
WorldState::render_history_diff
AGENTS.md
CodexHomeUserInstructionsProvider::load_from_codex_home
load_project_instructions
read_agents_md
agents_md_paths
LoadedAgentsMd::text
AgentsMdState::render_diff
Token 与压缩
ContextManager::get_total_token_usage
context_window_token_status
run_pre_sampling_compact
maybe_run_previous_model_inline_compact
run_auto_compact
Session::replace_compacted_history
Resume
Session::reconstruct_history_from_rollout
Session::apply_rollout_reconstruction
WorldStateSnapshot::apply_merge_patch
129. 实用 Trace 字段
观察压缩决策时重点记录:
turn_id
model
active_context_tokens
auto_compact_scope_tokens
auto_compact_scope_limit
auto_compact_limit_scope
auto_compact_window_prefill_tokens
full_context_window_limit
full_context_window_limit_reached
token_limit_reached
model_needs_follow_up
has_pending_input
观察压缩结果时记录:
trigger
reason
implementation
phase
status
active_context_tokens_before
active_context_tokens_after
retained_image_count
compaction_summary_tokens
cached_input_tokens
duration_ms
不要记录:
完整 AGENTS.md
用户私有 Prompt
Tool Output Secret
Encrypted Reasoning 内容
130. 调试项目指令的最小目录
创建:
demo/
.git/
AGENTS.md
crates/
AGENTS.override.md
app/
Root:
所有 Rust 修改运行 cargo fmt。
Nested Override:
本目录只运行 app crate 的测试。
在:
demo/crates/app
启动 Thread。
预期 Sources 顺序:
demo/AGENTS.md
demo/crates/AGENTS.override.md
预期模型文本顺序:
Root
Nested Override
131. 验证同目录 Override 语义
在同一目录同时创建:
AGENTS.md
AGENTS.override.md
给出互不相同的唯一 Marker:
FROM_DEFAULT
FROM_OVERRIDE
检查请求 Input。
预期:
存在 FROM_OVERRIDE
不存在 FROM_DEFAULT
这证明 Override 是候选优先级,不是后置追加。
132. 验证总字节预算
配置:
project_doc_max_bytes = 64
让 Root 与 Nested 文档都超过 64 Bytes。
检查:
Root 先消费预算
Nested 只得到剩余 Bytes
或者完全缺失
再把 Root 文档缩短,观察 Nested 内容重新进入 Prompt。
这个练习可以直观看出:
预算是全层级共享
133. 验证创建时快照
步骤:
1. 写入 OLD_INSTRUCTION
2. 创建 Thread
3. 完成第一 Turn
4. 同路径改成 NEW_INSTRUCTION
5. 执行 /compact
6. 再发一个 Turn
默认模式预期:
Compact Request 仍包含 OLD_INSTRUCTION
后续 Turn 仍使用 OLD_INSTRUCTION
然后关闭并 Resume Thread。
Cold Resume 预期追加:
replace all previously provided...
NEW_INSTRUCTION
134. 验证 Tool Call 配对修复
构造测试 History:
User Message
FunctionCall(call_id = call-1)
不写 Output,调用:
ContextManager::for_prompt
预期得到:
FunctionCall(call-1)
FunctionCallOutput(call-1, "aborted")
连续调用两次,验证合成 Output ID 相同。
135. 验证图片能力降级
用包含:
InputImage
Tool Output InputImage
ImageGenerationCall Result
的 History 分别调用:
for_prompt([Text, Image])
for_prompt([Text])
预期:
支持图片
-> 保留图片
不支持图片
-> Message / Tool 图片变占位符
-> Image Generation Result 清空
136. 验证 Pre-Turn 压缩不包含 Incoming Input
设置很低的:
model_auto_compact_token_limit = 200
先完成一个超过阈值的 Turn,再发送:
UNIQUE_INCOMING_MESSAGE
Mock Server 应看到:
Compact Request
-> 不含 UNIQUE_INCOMING_MESSAGE
Post-Compact Sampling Request
-> 含 UNIQUE_INCOMING_MESSAGE
这是当前行为,不应把它误写为“压缩本 Turn 所有输入”。
137. 验证 Mid-Turn Context 插入位置
让模型第一次返回 Tool Call,并用 Usage 跨过压缩阈值。
远程压缩返回:
Compaction Item
检查下一次 Sampling Input:
Older Summary
Canonical Context
Current Real User Message
Latest Compaction Item
确认:
Compaction Item 保持最后
Context 位于最后真实用户消息之前
138. 验证 Comp Hash
准备三个 ModelInfo:
model-a comp_hash = hash-a
model-b comp_hash = hash-b
model-c comp_hash = None
验证:
a -> b
-> Pre-Turn Compaction
b -> c
-> 不因 Comp Hash 触发
c -> a
-> 不因 Comp Hash 触发
只有“两边都有值且不同”才触发。
139. 验证 BodyAfterPrefix
准备一个压缩后 Prefix 约 1,000 Token 的 History。
设置:
model_auto_compact_token_limit = 500
model_auto_compact_token_limit_scope = "body_after_prefix"
第一份服务端 Usage:
input_tokens = 1000
total_tokens = 1100
预期:
Prefill Baseline = 1000
Scope Growth ≈ 100
而不是 1100
后续增长到 500 才触发 Scope Limit,但完整 Effective Context Window 仍然有效。
140. 验证 Replacement History Resume
完成一次压缩后读取 Rollout JSONL。
找到:
{
"type": "compacted",
"payload": {
"replacement_history": [],
"window_number": 1,
"first_window_id": "...",
"previous_window_id": "...",
"window_id": "..."
}
}
Resume 后检查第一次 Sampling:
前缀与 replacement_history 一致
只追加 Resume 后必要 Context Diff 和新输入
141. 推荐测试入口
项目已经有针对性测试:
codex-rs/core/src/agents_md_tests.rs
codex-rs/core/src/context_manager/history_tests.rs
codex-rs/core/tests/suite/agents_md.rs
codex-rs/core/tests/suite/model_visible_layout.rs
codex-rs/core/tests/suite/compact.rs
codex-rs/core/tests/suite/compact_remote.rs
codex-rs/core/tests/suite/compact_remote_parity.rs
codex-rs/core/tests/suite/compact_resume_fork.rs
codex-rs/core/tests/suite/token_budget.rs
建议先运行最小单元测试,再运行集成 Suite。
不要一开始运行整个 Workspace。
142. 常见误区
误区一:ContextManager 保存完整 Prompt
错误。
Base Instructions 和 Tool Specs 不在普通 History 中。
误区二:AGENTS.md 通过 System Role 发送
错误。
当前实现将它渲染为 Contextual User Fragment。
误区三:每个目录的 AGENTS.md 都与 Override 合并
错误。
每个目录只选择优先级最高的一个候选。
误区四:project_doc_max_bytes 是每文件预算
错误。
它是单个 Environment 内所有项目层级文档共享的总 Bytes。
误区五:修改 AGENTS.md 会立即影响当前 Thread
错误。
默认 Session 使用创建时快照;刷新依赖环境选择和 Feature。
误区六:Token 估算是精确 Tokenizer
错误。
本地估算主要基于 Bytes,并对图片和加密内容做特殊启发式。
误区七:ContextWindowExceeded 会在当前错误分支自动恢复
错误。
普通 Turn 会结束;Usage 被标记为满,下一 Turn 再做 Pre-Turn Compaction。
误区八:达到阈值就一定立刻 Mid-Turn 压缩
错误。
只有当前 Turn 仍需 Follow-Up 时才在 Turn 中压缩。
误区九:Pre-Turn 压缩会包含当前新用户输入
错误。
当前实现先压缩旧 History,再记录 Incoming Input。
误区十:远程压缩输出可以直接替换本地 History
错误。
Core 会过滤陈旧 Context,并按当前 Session 重建 Canonical Context。
误区十一:Comp Hash 缺失表示不兼容
错误。
缺失表示无法判断,不会仅因此触发压缩。
误区十二:Remote v1 与 v2 只是 Endpoint 名不同
错误。
两者的请求控制、响应形状、Transport 和保留策略都不同。
143. 本篇小结
Codex 的上下文系统不是“把所有消息塞进数组”,而是一套带差量、预算、检查点和恢复语义的运行时。
核心结论如下:
Prompt、Conversation History、World State 和 Rollout 是四个不同层次。- Base Instructions 通常通过顶层
instructions发送,不进入普通 History。 ContextManager同时保存 Items、Token Usage、Turn Context Baseline 和 World State Baseline。record_items负责过滤和 Tool Output 截断,for_prompt负责请求级规范化。- Tool Call 与 Tool Output 必须成对,缺失 Output 会生成稳定 ID 的
abortedItem。 - 不支持图片的模型会收到文本占位符,而不是损坏的图片 Payload。
- 初始 Context 推迟到第一真实 Turn,以便合并
turn/startOverride。 - 稳态 Turn 只追加 Settings Diff 和 World State Diff,保护 Token 与 Prompt Cache。
AGENTS.override.md在每个目录优先于AGENTS.md和自定义 Fallback。- 项目指令从 Project Root 到 CWD 合并,每个 Environment 默认共享 32 KiB 总预算。
- 全局
$CODEX_HOME指令不消费项目文档预算。 - 默认 Session 保留创建时 AGENTS 快照;Cold Resume 会加载当前文件并发送 Replacement Diff。
- 服务端 Token Usage 与本地启发式估算共同决定活跃上下文大小。
- Effective Context Window 会预留输出与工具开销,Auto Compact 默认上限是原始窗口的 90%。
Total统计完整上下文,BodyAfterPrefix统计压缩窗口 Prefix 之后的增长。- Turn 前压缩处理旧 History,不包含本 Turn 尚未写入的 Context Diff 和用户输入。
- Turn 中压缩只在仍需 Follow-Up 时触发,并把 Canonical Context 插到最后真实用户消息之前。
- Model Switch 可因 Comp Hash 变化或 Context Window Downshift 触发 Pre-Turn Compaction。
- Local Compaction 用普通模型采样生成文字 Handoff Summary。
- Remote v1 调用
/responses/compact,Remote v2 使用CompactionTrigger和单个CompactionItem。 - Remote Replacement History 会过滤陈旧 Developer/User Context,再由本地 Runtime 重建。
CompactedItem.replacement_history是现代 Rollout 的压缩检查点。- Resume 从最新 Replacement History 开始,并恢复 Turn Context、World State 与 Window Chain。
- TokenBudget 实验模式不生成摘要,而是直接开启只含 Canonical Context 的新窗口。
下一篇将进入 ToolRouter,分析 Tool Spec、Registry、Handler、MCP Tool、Dynamic Tool
与并行执行策略如何组成模型可见能力。
更多推荐



所有评论(0)