English 书架

可观测性:先捕获事实,再解释运行

阅读契约: 把本章当作一次 Codex 运行的证据地图。区分 replay persistence、diagnostic trace bundle、reduced graph state、product analytics、OTEL telemetry、response debug context 和有界 local logs。读完 之后,你应该能解释为什么 transcript 只是运行的一个 projection。

可观测证据通道:分开 rollout persistence、trace bundles、reducers、analytics、OTEL 和 response debug context
Codex 先记录 runtime evidence,再让不同视图解释运行。Rollout persistence、trace bundle、reducer、analytics、OTEL 和 debug context 回答的是不同问题。

源码边界: 本章直接源码结论都固定到 OpenAI Codex commit 569ff6a1c400bd514ff79f5f1050a684dc3afde3TraceWriterTraceBundleManifestRawTraceEventRolloutTracereplay_bundleprotocol_eventparse_turn_itemAnalyticsFactAnalyticsReducerSessionTelemetryResponseDebugContextStateRuntime::insert_logs 只有在链接到锚点时才视为 verified source。 关于为什么这些 owner 要分开,是从可见 types、functions、comments 和 tests 推导出的 surrounding contract inference,不是对 OpenAI 私有服务 内部的断言。

四个本章术语会反复出现。Runtime fact 指 turn、inference、tool、 compaction、protocol event 或 log observation 在被某个受众友好化之前的事实。 Trace bundle 指本地 diagnostic artifact,由 manifest.jsontrace.jsonlpayloads/ 组成。Reduced graph 指通过 replay raw events 和 payload references 得到的 RolloutTrace 对象。Projection 指面向某个消费者的视图: transcript、client event、analytics event、OTEL span、support context 或 log query。

第 7 章停在 provider boundary:HTTP streaming、WebSocket streaming、local providers、Bedrock signing、realtime setup 和 backend tasks 都必须先变成 typed runtime events,turn loop 才能继续。本章继续追踪这些 events 进入 runtime 之后 的命运。有些证据用于 resume 或 replay thread;有些证据用于解释一次失败的 rollout;有些证据用于跨产品使用聚合;有些证据用于诊断 transport performance。 系统之所以可调试,是因为这些任务没有被塞进同一个 transcript。

问题:Agent runtime 不能只靠最终 assistant message 调试,也不能把每个内部字节都推给 product analytics。

主张:Codex 先捕获有序事实,再让 replay、trace reduction、analytics、OTEL、debug context 和 logs 只解释各自应该拥有的子集。

心智模型:transcript history 是一个 projection;trace evidence 是另一个本地 artifact;telemetry 和 analytics 是 retention/privacy 规则不同的更窄 projection。

引导问题:观察到了什么事实?哪个 owner 存储它?哪个 reducer 解释它?哪些 payload 被刻意挡在 public view 外面?

1. 不存在唯一的 Observability Plane

第一条纪律是:不要让一个 artifact 回答所有问题。同一次 tool call 可能同时表现为 model-visible response item、protocol event、raw trace payload、reduced tool object、client notification、analytics event、OTEL span 和 local log line。只要 每个 plane 有清晰 owner 和 retention model,这就不是重复。

Plane主要问题典型消费者不应该变成什么
Rollout persistencethread 能否 resume、fork 或 replay?runtime reconstruction无边界 debug dump
Rollout trace bundle哪些 raw evidence 能解释这次运行?local trace reducer 与 graph viewerproduct analytics stream
Reduced trace graph存在哪些 semantic objects?offline debugging 与 auditraw payload store
Product analytics跨 sessions 发生了什么?aggregate product analysisexact replay source of truth
OTEL telemetryruntime operations 表现如何?engineering diagnosticsdurable transcript
Response debug context哪个 upstream request 失败?support 与 failure triage被复制的 HTTP response body
Local state logs本地可检查哪些 process logs?feedback 与 local inspection无限 log archive

这套分层的收益很实际。用户问为什么 thread 用错 history resume,就看 rollout reconstruction 和 reduced trace graph。工程侧问 provider streaming 为什么慢,就看 OTEL counters、histograms 和 spans。产品想看 app/plugin 使用趋势,就看 analytics facts。支持侧要关联 upstream 401,就看 response debug context。Transcript 本身不 应该意外承担这些工作。

2. Trace Bundle 让 Capture 保持简单

TraceWriter 把 runtime facts 捕获到 manifest、trace log、payloads、sequence order 和 reducer input
TraceWriter 是 capture owner:它写 manifest、append-only event log 和 payload files。它不会在 hot path 上构造最终 graph。

2.1 本地 Layout 本身就是 Contract

Trace bundle 的布局小到可以直接审计。在 codex-rs/rollout-trace/src/bundle.rs 里,constants 定义了 manifest、raw event log、payload directory 和 reduced state cache。TraceBundleManifest 记录 trace identity、rollout identity、root thread、 start time 和标准本地路径。

pub(crate) const MANIFEST_FILE_NAME: &str = "manifest.json";
pub(crate) const RAW_EVENT_LOG_FILE_NAME: &str = "trace.jsonl";
pub(crate) const PAYLOADS_DIR_NAME: &str = "payloads";
pub const REDUCED_STATE_FILE_NAME: &str = "state.json";

pub(crate) struct TraceBundleManifest {
    pub(crate) schema_version: u32,
    pub(crate) trace_id: String,
    pub(crate) rollout_id: String,
    pub(crate) root_thread_id: AgentThreadId,
    pub(crate) started_at_unix_ms: i64,
    pub(crate) raw_event_log: String,
    pub(crate) payloads_dir: String,
}

源码注释里有一个重要判断:replay 应该失败,而不是编一个 placeholder root thread。也就是说,root thread 是必需的 identity anchor,不是 viewer 的便利字段。 每个 reduced object 都要回到这棵 thread tree。

2.2 Writer 只捕获,不解释

TraceWriter 在文件注释和结构体注释里直接声明边界:它是 hot-path trace bundle writer;它 append raw events、写 payload files;它不在内存里维护 reduced RolloutTrace。 Replay 属于 reducer。

创建路径会写 manifest、以 append mode 打开 trace.jsonl,并初始化两个单调计数器:

let payloads_dir = bundle_dir.join(PAYLOADS_DIR_NAME);
std::fs::create_dir_all(&payloads_dir)?;

let started_at_unix_ms = unix_time_ms();
let manifest =
    TraceBundleManifest::new(trace_id, rollout_id, root_thread_id, started_at_unix_ms);
write_json_file(&bundle_dir.join(MANIFEST_FILE_NAME), &manifest)?;

let event_log_path = bundle_dir.join(RAW_EVENT_LOG_FILE_NAME);
let event_log = OpenOptions::new()
    .create(true)
    .append(true)
    .open(&event_log_path)?;

next_seq: 1,
next_payload_ordinal: 1,

payload 路径更关键。大的 model request、response、tool payload 或 protocol payload 可以先写成单独 JSON file;raw event 只携带引用。writer 会先创建 payload file,再 append 引用它的 event (writer.rs#L85-L106):

let ordinal = inner.next_payload_ordinal;
inner.next_payload_ordinal += 1;
let raw_payload_id = format!("raw_payload:{ordinal}");
let relative_path = format!("{PAYLOADS_DIR_NAME}/{ordinal}.json");
let absolute_path = inner.payloads_dir.join(format!("{ordinal}.json"));

// Payload files are created before the event that references them.
write_json_file(&absolute_path, value)?;
Ok(RawPayloadRef {
    raw_payload_id,
    kind,
    path: relative_path,
})

这个顺序非常关键。如果 session 在 event append 之后被中断,replay 不应该指向一个 writer 原计划写、但实际还没写出的 payload file。同一个文件还会在写入每条 JSONL event 后 flush (writer.rs#L108-L133)。 即使 tracing code panic 导致 mutex poisoned,writer 也会恢复锁,避免在最需要诊断的 session 里丢掉后续事件 (writer.rs#L136-L140)。

这里的 invariant 很简单:hot path 捕获稳定事实和 raw payload references;运行还活着 的时候,不在这里做 graph interpretation。

3. Raw Events 先保留顺序,再谈意义

3.1 RawTraceEvent 是共享 Envelope

RawTraceEvent 是 append-only envelope。它记录 writer-local sequence、wall-clock time、rollout identity、可选 thread/turn context,以及 typed payload。

pub type RawEventSeq = u64;
pub(crate) const RAW_TRACE_EVENT_SCHEMA_VERSION: u32 = 1;

pub struct RawTraceEvent {
    pub schema_version: u32,
    pub seq: RawEventSeq,
    pub wall_time_unix_ms: i64,
    pub rollout_id: String,
    pub thread_id: Option<AgentThreadId>,
    pub codex_turn_id: Option<CodexTurnId>,
    pub payload: RawTraceEventPayload,
}

pub struct RawTraceEventContext {
    pub thread_id: Option<AgentThreadId>,
    pub codex_turn_id: Option<CodexTurnId>,
}

源码注释解释了为什么每个 event 用同一套 envelope:partial replay 和 corruption checks 可以在 reducer 理解 event-specific payload 之前先跑。这正是 diagnostic 的 正确顺序。Writer 先分配 sequence numbers 和通用 envelope;reducer 再通用地解析 log、 保留 payload references,并且只有在某个 semantic arm 实际读取 payload body 时,才暴露 缺失或格式错误的 payload file。

3.2 Payload Variants 是 Runtime Boundaries

RawTraceEventPayload 不是 transcript enum。它命名的是 runtime boundaries:rollout start/end、thread start/end、Codex turn start/end、inference lifecycle、tool lifecycle、code-cell lifecycle、compaction requests、compaction installation、agent result delivery、protocol breadcrumbs,以及用于早期 instrumentation 的 Other arm (raw_event.rs#L65-L226)。

pub enum RawTraceEventPayload {
    RolloutStarted { trace_id: String, root_thread_id: AgentThreadId },
    ThreadStarted {
        thread_id: AgentThreadId,
        agent_path: String,
        metadata_payload: Option<RawPayloadRef>,
    },
    CodexTurnStarted {
        codex_turn_id: CodexTurnId,
        thread_id: AgentThreadId,
    },
    InferenceStarted {
        inference_call_id: InferenceCallId,
        thread_id: AgentThreadId,
        codex_turn_id: CodexTurnId,
        model: String,
        provider_name: String,
        request_payload: RawPayloadRef,
    },
    ToolCallStarted {
        tool_call_id: ToolCallId,
        model_visible_call_id: Option<String>,
        code_mode_runtime_tool_id: Option<String>,
        requester: RawToolCallRequester,
        kind: ToolCallKind,
        summary: ToolCallSummary,
        invocation_payload: Option<RawPayloadRef>,
    },
    ProtocolEventObserved {
        event_type: String,
        event_payload: RawPayloadRef,
    },
    Other {
        kind: String,
        summary: String,
        payloads: Vec<RawPayloadRef>,
        metadata: Value,
    },
    // ...
}

这里有些字段刻意不是 model-facing。RawToolCallRequester 使用 runtime-local identifier 来区分 model-triggered call 与 code-cell-triggered call;只有 reducer 负责把这些 handles 映射成 CodeCellId 之类的 graph identity (raw_event.rs#L51-L63)。 这样 raw capture 能忠实于 runtime,而 reduced graph 仍然可以使用稳定的 semantic ID。

3.3 Protocol Events 可以喂给 Trace,但不是整个 Trace

trace layer 复用 session protocol events,而不是在 core 里再加第二套 hook system。 protocol_event.rs 的 module comment 说得很清楚:长的 EventMsg match 是有意的,因为大多数 protocol events 并不是 trace runtime boundaries;新 protocol variant 应该在 compile time 促使 开发者决定 trace 是否要捕获它。

borrowed payload enum 保留 exact protocol payload shape 用于端到端 debugging,而 typed trace events 仍然提供 reducer boundary (protocol_event.rs#L92-L135):

pub(crate) enum ToolRuntimePayload<'a> {
    ExecCommandBegin(&'a ExecCommandBeginEvent),
    ExecCommandEnd(&'a ExecCommandEndEvent),
    PatchApplyBegin(&'a PatchApplyBeginEvent),
    PatchApplyEnd(&'a PatchApplyEndEvent),
    McpToolCallBegin(&'a McpToolCallBeginEvent),
    McpToolCallEnd(&'a McpToolCallEndEvent),
    CollabAgentSpawnBegin(&'a codex_protocol::protocol::CollabAgentSpawnBeginEvent),
    CollabAgentSpawnEnd(&'a codex_protocol::protocol::CollabAgentSpawnEndEvent),
    CollabAgentInteractionBegin(&'a codex_protocol::protocol::CollabAgentInteractionBeginEvent),
    CollabAgentInteractionEnd(&'a codex_protocol::protocol::CollabAgentInteractionEndEvent),
    // ...
}

这是第二个关键分离:protocol breadcrumb 是 evidence,但 reduced graph 仍然应该由 typed trace semantics 构建。raw ExecCommandEndEvent 对调试有用,但 graph 还需要知道它属于 哪个 tool call、code cell、terminal operation、turn 和 thread。

4. Reducer 负责解释

Raw trace events 和 payload references 进入 strict reducer,输出带 pending queues 和 raw links 的 RolloutTrace graph
Reducer 必须严格,因为它把 append-only evidence 变成 semantic graph objects。Pending queues 解决真实的 ordering gap,而不是假装缺失 owner 已经存在。

4.1 Reduced Model 是 Graph,不是 Chat Log

reduced model 声明在 codex-rs/rollout-trace/src/model/mod.rs。 文件注释说明这些类型描述 deterministic replay output,并且有意把 model-visible conversation 与 runtime/debug objects 分开。

pub struct RolloutTrace {
    pub schema_version: u32,
    pub trace_id: String,
    pub rollout_id: String,
    pub started_at_unix_ms: i64,
    pub ended_at_unix_ms: Option<i64>,
    pub status: RolloutStatus,
    pub root_thread_id: AgentThreadId,
    pub threads: BTreeMap<AgentThreadId, AgentThread>,
    pub codex_turns: BTreeMap<CodexTurnId, CodexTurn>,
    pub conversation_items: BTreeMap<ConversationItemId, ConversationItem>,
    pub inference_calls: BTreeMap<InferenceCallId, InferenceCall>,
    pub code_cells: BTreeMap<CodeCellId, CodeCell>,
    pub tool_calls: BTreeMap<ToolCallId, ToolCall>,
    pub terminal_sessions: BTreeMap<TerminalId, TerminalSession>,
    pub terminal_operations: BTreeMap<TerminalOperationId, TerminalOperation>,
    pub compactions: BTreeMap<CompactionId, Compaction>,
    pub compaction_requests: BTreeMap<CompactionRequestId, CompactionRequest>,
    pub interaction_edges: BTreeMap<EdgeId, InteractionEdge>,
    pub raw_payloads: BTreeMap<RawPayloadId, RawPayloadRef>,
}

这个对象回答的是 transcript 很难准确回答的问题:哪个 thread spawn 了哪个 child?哪次 inference request 使用了哪个 provider 和 model?哪个 runtime-local code cell 产生了 nested tool call?哪个 terminal operation 是 command,哪个是 poll?哪个 compaction 安装了 replacement history?哪个 raw payload 能解释 reduced object?

4.2 Sequence 是 Causal Order,Wall Clock 只是展示

session model 把 ordering 规则写得很直白。ExecutionWindow 同时存 wall-clock timestamps 和 raw event sequence numbers;源码注释说 sequence numbers 是 causal ordering primitive,用于配对 observations 或打破同毫秒并列 (session.rs#L68-L80)。 CodexTurn 又提醒:一个 Codex turn 是某个 thread 的 runtime activation,不是 user/assistant message pair (session.rs#L98-L110)。

pub struct ExecutionWindow {
    pub started_at_unix_ms: i64,
    pub started_seq: RawEventSeq,
    pub ended_at_unix_ms: Option<i64>,
    pub ended_seq: Option<RawEventSeq>,
    pub status: ExecutionStatus,
}

pub struct CodexTurn {
    pub codex_turn_id: CodexTurnId,
    pub thread_id: AgentThreadId,
    pub execution: ExecutionWindow,
    pub input_item_ids: Vec<ConversationItemId>,
}

这能纠正调试 Agent 运行时的常见误解。一个用户可见 turn 可能包含多次 model requests、 tool lifecycles、compactions、pending inputs 和 child agent interactions。反过来,一个 protocol turn lifecycle event 也不等于 chat transcript 里的一行。

4.3 Replay 是 Deterministic 且 Deferred

replay_bundle 加载 manifest,初始化空的 RolloutTrace,逐行读取 trace.jsonl,解析每个 RawTraceEvent,apply 它,最后再 resolve pending spawn-edge fallbacks:

pub fn replay_bundle(bundle_dir: impl AsRef<Path>) -> Result<RolloutTrace> {
    let manifest: TraceBundleManifest =
        serde_json::from_reader(File::open(bundle_dir.join(MANIFEST_FILE_NAME))?)?;
    let mut reducer = TraceReducer {
        rollout: RolloutTrace::new(
            REDUCED_TRACE_SCHEMA_VERSION,
            manifest.trace_id,
            manifest.rollout_id,
            manifest.root_thread_id,
            manifest.started_at_unix_ms,
        ),
        pending_code_cell_starts: BTreeMap::new(),
        pending_code_cell_lifecycle_events: BTreeMap::new(),
        pending_agent_interaction_edges: Vec::new(),
        // ...
    };

    for (line_index, line) in BufReader::new(event_log).lines().enumerate() {
        let event: RawTraceEvent = serde_json::from_str(&line?)?;
        reducer.apply_event(event)?;
    }
    reducer.resolve_pending_spawn_edge_fallbacks()?;
    Ok(reducer.rollout)
}

pending queues 不是宽松处理。 TraceReducer 旁边的注释解释了真实 ordering gaps:core 可以在 stream-completion hook 记录请求该 tool 的 response payload 之前开始执行 tools;fast code cell 可以在证明其 model-visible source item 的 inference response payload reduced 之前返回;agent tool delivery 可以在 recipient thread 的 transcript materialize mailbox item 之前到达。Reducer 先排队这些事实,等 replay 暴露 precise owner 后再挂上去。

apply path 会先把 raw payload references 作为 reducer-wide evidence 保留下来,再进入 typed interpretation:

fn apply_event(&mut self, event: RawTraceEvent) -> Result<()> {
    for payload in event.payload.raw_payload_refs() {
        self.insert_raw_payload(payload);
    }

    match event.payload {
        RawTraceEventPayload::RolloutStarted { trace_id, root_thread_id } => {
            self.rollout.trace_id = trace_id;
            self.rollout.root_thread_id = root_thread_id;
        }
        RawTraceEventPayload::InferenceStarted { inference_call_id, thread_id,
            codex_turn_id, model, provider_name, request_payload } => {
            self.start_inference_call(
                event.seq,
                event.wall_time_unix_ms,
                StartedInferenceCall {
                    inference_call_id,
                    thread_id,
                    codex_turn_id,
                    model,
                    provider_name,
                    request_payload,
                },
            )?;
        }
        RawTraceEventPayload::ProtocolEventObserved { .. } => {
            // Protocol wrappers are raw debug breadcrumbs.
        }
        // ...
    }
}

因此 strict typed replay error 很有价值。它说明 semantic owner 缺失、producer emit 的 event 无法挂到已知 graph、typed reduction 读取 payload 时发现文件不可用,或者 reducer 还没理解新的 event shape。源码并没有承诺一个单独的全局 sequence-gap 或 payload-existence pass;这里的纪律是:当 reducer 需要某份 evidence 时应该失败,而不是 静默编造 owner。

5. Model-Visible Conversation 只是一个 Projection

Reduced graph 有 conversation_items,但它仍然不会把每个 runtime byte 都等同于 model-visible history。公开 event mapper 在 codex-rs/core/src/event_mapping.rs 里过滤 contextual messages、image wrappers、system messages 和 unsupported items,并把 ResponseItem 转成 client turn items。

pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
    match item {
        ResponseItem::Message { role, content, id, phase, .. } => match role.as_str() {
            "user" => parse_visible_hook_prompt_message(id.as_ref(), content)
                .map(TurnItem::HookPrompt)
                .or_else(|| parse_user_message(content).map(TurnItem::UserMessage)),
            "assistant" => Some(TurnItem::AgentMessage(parse_agent_message(
                id.as_ref(),
                content,
                phase.clone(),
            ))),
            "system" => None,
            _ => None,
        },
        ResponseItem::Reasoning { id, summary, content, .. } => {
            let summary_text = summary
                .iter()
                .map(|entry| match entry {
                    ReasoningItemReasoningSummary::SummaryText { text } => text.clone(),
                })
                .collect();
            let raw_content = content
                .clone()
                .unwrap_or_default()
                .into_iter()
                .map(|entry| match entry {
                    ReasoningItemContent::ReasoningText { text }
                    | ReasoningItemContent::Text { text } => text,
                })
                .collect();
            Some(TurnItem::Reasoning(ReasoningItem {
                id: id.clone(),
                summary_text,
                raw_content,
            }))
        }
        ResponseItem::WebSearchCall { id, action, .. } => { /* ... */ }
        ResponseItem::ImageGenerationCall { id, result, .. } => { /* ... */ }
        _ => None,
    }
}

这个函数不是 rollout trace reducer,但它体现的是同一种源码习惯:把宽泛的 runtime/API input 转成面向特定受众的 view。模型可能看到了 tool call;client 可能渲染成 card;trace 可能保留 raw protocol payload;analytics reducer 可能发出 track event。这些 projection 相关,但每个 owner 都要决定对自己的受众来说什么有意义、什么安全。

最常见的调试错误,是要求 transcript 解释它从未拥有过的 runtime state。terminal operation 失败时,transcript 里可能只有总结后的 observation。trace graph 仍然可以指回 raw payload、runtime lifecycle、terminal operation IDs 和 sequence order。产品指标缺失 时,analytics reducer 可能是因为 thread metadata 不可用而有意 drop。这是不同的 failure class。

6. Analytics 与 OTEL 从 Evidence Stream 分支

Runtime facts 分支进入 analytics reducer 与 track events/missing context,以及 OTEL 的 spans、metrics 和 logs
Analytics 与 OTEL 是 sibling projections。Analytics 把 product facts reduce 成 track events;OTEL 记录 spans、counters、histograms 和 runtime timing。

6.1 Analytics Facts 是 Product Inputs,不是 Replay Truth

analytics input vocabulary 在 codex-rs/analytics/src/facts.rs。 它包含 app-server JSON-RPC requests/responses、server notifications,以及无法自然存在于 protocol surface 上的 custom facts。

pub(crate) enum AnalyticsFact {
    Initialize { connection_id: u64, params: InitializeParams, /* ... */ },
    ClientRequest { connection_id: u64, request_id: RequestId, request: Box<ClientRequest> },
    ClientResponse { connection_id: u64, request_id: RequestId, response: Box<ClientResponsePayload> },
    ErrorResponse { connection_id: u64, request_id: RequestId, error_type: Option<AnalyticsJsonRpcError>, /* ... */ },
    ServerRequest { connection_id: u64, request: Box<ServerRequest> },
    ServerResponse { completed_at_ms: u64, response: Box<ServerResponse> },
    Notification(Box<ServerNotification>),
    Custom(CustomAnalyticsFact),
}

pub(crate) enum CustomAnalyticsFact {
    SubAgentThreadStarted(SubAgentThreadStartedInput),
    Compaction(Box<CodexCompactionEvent>),
    GuardianReview(Box<GuardianReviewEventParams>),
    TurnResolvedConfig(Box<TurnResolvedConfigFact>),
    TurnTokenUsage(Box<TurnTokenUsageFact>),
    SkillInvoked(SkillInvokedInput),
    AppMentioned(AppMentionedInput),
    AppUsed(AppUsedInput),
    HookRun(HookRunInput),
    PluginUsed(PluginUsedInput),
    PluginStateChanged(PluginStateChangedInput),
}

TurnResolvedConfigFact 说明 analytics 为什么有价值但不是 replay truth:它携带 model、 provider、permission profile、approval policy、sandbox network access、collaboration mode、personality 和其他 resolved turn settings (facts.rs#L63-L84)。 这些事实适合回答 aggregate product questions;不应该拿它们重建 exact thread history 或 raw provider payload。

6.2 Analytics Reducer 可以 Drop Events

AnalyticsReducer 保存 request、turn、connection、thread 和 tool-start state (reducer.rs#L114-L121)。 主 ingest 方法把 facts 分发给 specialized reducers (reducer.rs#L283-L330)。

drop behavior 是显式的。对 tool item analytics 来说,如果 completion 找不到匹配的 start notification,就直接忽略 (reducer.rs#L758-L791):

let key = ToolItemKey {
    thread_id: notification.thread_id.clone(),
    turn_id: notification.turn_id.clone(),
    item_id: item_id.to_string(),
};
let Some(started_at_ms) = self.tool_items_started_at_ms.remove(&key) else {
    tracing::warn!(
        thread_id = %notification.thread_id,
        turn_id = %notification.turn_id,
        item_id,
        "dropping tool item analytics event: missing item started notification"
    );
    return;
};

context helpers 对 missing thread connection、connection state 或 thread metadata 也是 同样策略 (reducer.rs#L1071-L1131)。 这对 analytics 是正确的:缺少上下文的 aggregate event 应该 warn 后 drop,而不是补造 context。但 replay reducer 在 evidence 一致性要求高的地方不能采用同一套策略。

6.3 OTEL 测量 Runtime Operations

OTEL 的 contract 又不同。在 session_telemetry.rs 里,SessionTelemetryMetadata 存 conversation ID、auth mode/env metadata、account hints、originator、session source、model、app version 和 terminal type。telemetry object 持有可选 metrics,并知道是否使用 metadata tags。

Responses event recorder 会把 event kind、function-call tool names 和 token usage 写进 span fields (session_telemetry.rs#L292-L329):

pub fn record_responses(&self, handle_responses_span: &Span, event: &ResponseEvent) {
    handle_responses_span.record("otel.name", SessionTelemetry::responses_type(event));

    match event {
        ResponseEvent::OutputItemDone(item) => {
            handle_responses_span.record("from", "output_item_done");
            if let ResponseItem::FunctionCall { name, .. } = item {
                handle_responses_span.record("tool_name", name.as_str());
            }
        }
        ResponseEvent::OutputItemAdded(item) => {
            handle_responses_span.record("from", "output_item_added");
            if let ResponseItem::FunctionCall { name, .. } = item {
                handle_responses_span.record("tool_name", name.as_str());
            }
        }
        ResponseEvent::Completed { token_usage: Some(token_usage), .. } => {
            handle_responses_span.record("gen_ai.usage.input_tokens", token_usage.input_tokens);
            handle_responses_span.record(
                "gen_ai.usage.cache_read.input_tokens",
                token_usage.cached_input(),
            );
            handle_responses_span.record("gen_ai.usage.output_tokens", token_usage.output_tokens);
            handle_responses_span.record(
                "codex.usage.reasoning_output_tokens",
                token_usage.reasoning_output_tokens,
            );
            handle_responses_span.record("codex.usage.total_tokens", token_usage.total_tokens);
        }
        _ => {}
    }
}

API request recorder 会增加 counters、记录 duration histograms,并 log/trace transport metadata,例如 status、duration、retry/auth recovery state、endpoint、request ID、 Cloudflare ray 和 auth error fields (session_telemetry.rs#L407-L468)。 SSE logger 会验证特殊 event shapes,并记录 failures,包括 idle timeout waiting for SSE (session_telemetry.rs#L689-L736)。

这个 plane 是 operational evidence。它告诉工程师一次 model call、WebSocket request、SSE event 或 tool result 表现如何。它不是 durable conversation record,也不是 product truth 的 analytics source。

7. Debug Context 与 Logs 必须有边界

Response debug context 抽取 request id、cf-ray 和 auth code,同时 local logs 执行 thread/process caps
Support evidence 刻意很窄:response debug context 只抽取 identifiers 与 sanitized status;local logs 则强制 per-thread 和 per-process caps。

7.1 Response Debug Context 抽身份,不复制 Body

response debug crate 很小,但边界清晰。在 response-debug-context/src/lib.rs 里,ResponseDebugContext 保存 request ID、Cloudflare ray、auth error 和 auth error code。它只从 TransportError::Http 提取这些字段;其他 transport errors 返回空 context。

pub struct ResponseDebugContext {
    pub request_id: Option<String>,
    pub cf_ray: Option<String>,
    pub auth_error: Option<String>,
    pub auth_error_code: Option<String>,
}

pub fn extract_response_debug_context(transport: &TransportError) -> ResponseDebugContext {
    let mut context = ResponseDebugContext::default();

    let TransportError::Http { headers, body: _, .. } = transport else {
        return context;
    };

    let extract_header = |name: &str| {
        headers
            .as_ref()
            .and_then(|headers| headers.get(name))
            .and_then(|value| value.to_str().ok())
            .map(str::to_string)
    };

    context.request_id =
        extract_header(REQUEST_ID_HEADER).or_else(|| extract_header(OAI_REQUEST_ID_HEADER));
    context.cf_ray = extract_header(CF_RAY_HEADER);
    context.auth_error = extract_header(AUTH_ERROR_HEADER);
    context.auth_error_code = extract_header(X_ERROR_JSON_HEADER).and_then(|encoded| {
        let decoded = base64::engine::general_purpose::STANDARD
            .decode(encoded)
            .ok()?;
        let parsed = serde_json::from_slice::<serde_json::Value>(&decoded).ok()?;
        parsed
            .get("error")
            .and_then(|error| error.get("code"))
            .and_then(serde_json::Value::as_str)
            .map(str::to_string)
    });

    context
}

telemetry error helpers 比 raw transport errors 更窄。HTTP transport errors 变成 "http <status>",而不是 serialized bodies (lib.rs#L63-L87)。 名为 telemetry_error_messages_omit_http_bodies 的测试构造了包含 "secret token leaked" 的 body,并断言 telemetry 仍然只报告 "http 401"

这就是 support boundary。你需要足够的 identity 去关联一次失败;你不需要把 upstream response body 复制进 telemetry。

7.2 Local Logs 可读,但有上限

local logs 属于 state runtime。在 state/src/runtime/logs.rs 里,insert_logs 将 rows 批量写入 SQLite logs table,字段包括 timestamp、level、 target、feedback body、thread ID、process UUID、module path、file、line 和 estimated byte count。源码注释说 runtime 每个 partition 保留大约 10 MiB reader-visible log content,并且 query_logs/feedback 都读取 persisted feedback_log_body

pub async fn insert_logs(&self, entries: &[LogEntry]) -> anyhow::Result<()> {
    if entries.is_empty() {
        return Ok(());
    }

    let mut tx = self.logs_pool.begin().await?;
    let mut builder = QueryBuilder::<Sqlite>::new(
        "INSERT INTO logs (ts, ts_nanos, level, target, feedback_log_body, \
         thread_id, process_uuid, module_path, file, line, estimated_bytes) ",
    );
    builder.push_values(entries, |mut row, entry| {
        let feedback_log_body = entry.feedback_log_body.as_ref().or(entry.message.as_ref());
        let estimated_bytes = feedback_log_body.map_or(0, String::len) as i64
            + entry.level.len() as i64
            + entry.target.len() as i64
            + entry.module_path.as_ref().map_or(0, String::len) as i64
            + entry.file.as_ref().map_or(0, String::len) as i64;
        // ...
    });
    builder.build().execute(&mut *tx).await?;
    self.prune_logs_after_insert(entries, &mut tx).await?;
    tx.commit().await?;
    Ok(())
}

pruning 与 insertion 在同一个 transaction 里执行 (logs.rs#L49-L73)。 thread logs 按 thread_id cap;threadless logs 按 process_uuid cap;没有 process UUID 的 rows 也形成自己的 threadless partition (logs.rs#L49-L286)。 这和 debug-context helper 是同一种设计品味:保留足够本地证据用于检查问题,但把边界写 清楚。

8. 用正确 Owner 调试

实操习惯是:把问题路由给能回答它的 owner。

问题先看哪里原因
thread resume 后 history 不对?rollout reconstruction 与 RolloutTrace graphreplay facts 和 graph state 拥有 durable history
tool 是否在没有 model-visible source item 时运行?trace reducer pending queuesruntime start 可能早于 response payload reduction
provider stream 是否卡住?OTEL SSE/WebSocket metrics 与 API request spanstransport timing 是 operational evidence
product metric 为什么消失?analytics reducer warningsanalytics 可以 drop missing-context events
upstream request 是否有 auth identity?response debug contextsupport context 抽取 request/ray/auth code
local logs 是否过大?state runtime log pruninglocal logs 受 thread/process partitions 限制
user-visible transcript 为什么缺细节?raw payload refs 与 reduced graphtranscript 是 projection,不是 raw evidence store

这张表就是架构的缩影。Codex 不需要一个万能 observability object。它需要稳定的 capture points,需要在自洽性重要的地方使用严格 reducers,也需要在 privacy、retention 或 aggregation 更重要的地方使用更窄 projections。

应用到实践

  1. 先捕获有序 runtime facts,再派生 transcripts、dashboards 或 aggregate metrics。
  2. 把 durable replay persistence 与 opt-in diagnostic trace bundles 分开,尤其当 raw payloads 可能包含 prompts、responses、paths、terminal output 或 tool data 时。
  3. 把 sequence numbers 和 payload references 当作 causal evidence,并在 typed replay 需要 owner、payload body 或 pending edge materialize 时让 trace reducer 保持严格。
  4. 让 analytics 与 OTEL 成为 sibling projections:analytics 面向 product facts,OTEL 面向 runtime operation 与 transport behavior。
  5. 给 support/debug surfaces 设边界:抽取 request identity 与 sanitized status,并按 thread 或 process cap local logs,而不是无限存储 bodies。

小结

Part II 至此建立了 runtime core:durable threads、live sessions、turn loop、provider streams、backend boundaries 和 observation planes。共同模式已经很清楚:Codex 先用明确 owner 记录事实,再从这些事实构造面向不同受众的 views。这就是为什么同一次运行可以 resumable、debuggable、measurable、supportable,并且仍然有边界。

Part III 会从 evidence 转向 side effects:Codex 如何暴露 tools、执行 commands、应用 patches、请求 approval,并把风险限制在明确 authority boundaries 内。

源码地图

概念源码锚点
Trace bundle layoutbundle.rs
Hot-path trace writerTraceWriter
Payload-before-event write rulewrite_json_payload
Raw event envelopeRawTraceEvent
Raw payload variantsRawTraceEventPayload
Protocol-to-trace mapping rationaleprotocol_event.rs
Tool runtime payload captureToolRuntimePayload
Reduced graph modelRolloutTrace
Trace session modelAgentThread, ExecutionWindow, CodexTurn
Deterministic replayreplay_bundle
Reducer pending queuesTraceReducer
Reducer event applicationapply_event
Client turn item projectionparse_turn_item
Analytics fact vocabularyAnalyticsFact
Turn resolved config factsTurnResolvedConfigFact
Analytics reducer stateAnalyticsReducer
Analytics missing-context dropsthread_context_or_warn
OTEL session metadataSessionTelemetryMetadata
Response event telemetryrecord_responses
API request telemetryrecord_api_request
SSE event telemetrylog_sse_event
Response debug contextextract_response_debug_context
HTTP body omission testtelemetry_error_messages_omit_http_bodies
Local log insertion and pruningstate/src/runtime/logs.rs
Thread and process log capsprune_logs_after_insert