上一篇跟踪了 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.mdWorldState
Token 预算和三种压缩实现。

本篇目标

阅读完成后,你应该能够:

  1. 区分 Prompt、Conversation History、World State 和 Rollout。
  2. 列出一次模型请求中所有主要上下文来源。
  3. 解释 ContextManager::record_itemsfor_prompt 的职责差异。
  4. 说明 Tool Call 和 Tool Output 为什么必须保持配对。
  5. 跟踪 AGENTS.md 从文件发现到模型可见 User Item 的完整链路。
  6. 解释 AGENTS.override.md、Fallback Filename 和总字节预算。
  7. 理解 reference_context_itemWorldStateSnapshot 两套差量基线。
  8. 区分服务端 Token Usage 和本地启发式估算。
  9. 解释 TotalBodyAfterPrefix 两种自动压缩计费范围。
  10. 区分 Turn 前、Turn 中和手动压缩。
  11. 区分本地压缩、远程 v1 和远程 v2。
  12. 说明压缩后的替换历史如何持久化并在 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 定义位于:

client_common.rs

核心结构为:

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 的模型指令

源码位于:

session/mod.rs

对应逻辑:

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 定义在:

context_manager/history.rs

核心字段为:

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

中选择一个。

项目指令由:

agents_md.rs

沿项目目录层级发现。

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_statusTotal 模式中读取:

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_limitNone,主要依赖原始窗口
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 的上下文系统不是“把所有消息塞进数组”,而是一套带差量、预算、检查点和恢复语义的运行时。

核心结论如下:

  1. Prompt、Conversation History、World State 和 Rollout 是四个不同层次。
  2. Base Instructions 通常通过顶层 instructions 发送,不进入普通 History。
  3. ContextManager 同时保存 Items、Token Usage、Turn Context Baseline 和 World State Baseline。
  4. record_items 负责过滤和 Tool Output 截断,for_prompt 负责请求级规范化。
  5. Tool Call 与 Tool Output 必须成对,缺失 Output 会生成稳定 ID 的 aborted Item。
  6. 不支持图片的模型会收到文本占位符,而不是损坏的图片 Payload。
  7. 初始 Context 推迟到第一真实 Turn,以便合并 turn/start Override。
  8. 稳态 Turn 只追加 Settings Diff 和 World State Diff,保护 Token 与 Prompt Cache。
  9. AGENTS.override.md 在每个目录优先于 AGENTS.md 和自定义 Fallback。
  10. 项目指令从 Project Root 到 CWD 合并,每个 Environment 默认共享 32 KiB 总预算。
  11. 全局 $CODEX_HOME 指令不消费项目文档预算。
  12. 默认 Session 保留创建时 AGENTS 快照;Cold Resume 会加载当前文件并发送 Replacement Diff。
  13. 服务端 Token Usage 与本地启发式估算共同决定活跃上下文大小。
  14. Effective Context Window 会预留输出与工具开销,Auto Compact 默认上限是原始窗口的 90%。
  15. Total 统计完整上下文,BodyAfterPrefix 统计压缩窗口 Prefix 之后的增长。
  16. Turn 前压缩处理旧 History,不包含本 Turn 尚未写入的 Context Diff 和用户输入。
  17. Turn 中压缩只在仍需 Follow-Up 时触发,并把 Canonical Context 插到最后真实用户消息之前。
  18. Model Switch 可因 Comp Hash 变化或 Context Window Downshift 触发 Pre-Turn Compaction。
  19. Local Compaction 用普通模型采样生成文字 Handoff Summary。
  20. Remote v1 调用 /responses/compact,Remote v2 使用 CompactionTrigger 和单个 Compaction Item。
  21. Remote Replacement History 会过滤陈旧 Developer/User Context,再由本地 Runtime 重建。
  22. CompactedItem.replacement_history 是现代 Rollout 的压缩检查点。
  23. Resume 从最新 Replacement History 开始,并恢复 Turn Context、World State 与 Window Chain。
  24. TokenBudget 实验模式不生成摘要,而是直接开启只含 Canonical Context 的新窗口。

下一篇将进入 ToolRouter,分析 Tool Spec、Registry、Handler、MCP Tool、Dynamic Tool
与并行执行策略如何组成模型可见能力。

更多推荐