Appearance
Run、Session 和 Durable State 到底各保存什么
前置:Run Lifecycle;Side-effect / Retry Semantics
核心问题:同一条“状态”到底应该属于当前 Run、长期 Session 还是 Durable Store,放错位置会在 restart/resume 时造成什么后果?
同一个聊天 Session 中,用户连续发起三个 Run;第二个 Run 因高风险 Tool 等待审批,服务随后重启。系统必须同时保留聊天连续性、当前 Run 的等待状态,以及任务跨进程继续所需的 durable facts。
Mental Model
text
用户会话时间轴
Session S-42
├── Run R-100 "帮我分析仓库"
├── Run R-101 "继续修这个 bug"
└── Run R-102 "把测试也补上"
单次执行时间轴
Run R-101
CREATED → RUNNING → WAITING_APPROVAL → RUNNING → SUCCEEDED
│ │
│ └── pending action / resume token
└── current node / tool attempt / budget / error
长任务时间轴
Task T-7
Day 1: cloned repo → located root cause
Day 2: patch applied → waiting external approval
Day 3: resumed → test passed → artifact producedRun State
Run State 是某一次 run_id 对应的当前执行事实。它应该由 Runtime 写入和推进,而不是让模型通过自然语言声明。
典型字段:
python
@dataclass
class RunState:
run_id: str
status: RunStatus
turn_index: int
active_agent: str | None
current_node_id: str | None
deadline_at: datetime | None
token_used: int
cost_used: Decimal
pending_approval_id: str | None
last_checkpoint_id: str | None
error: RunError | None
version: intSession State
Session 解决的是多次交互的连续性。在聊天式 Agent 中,它最典型的职责是让下一次 Run 能拿到之前的 conversation items。
OpenAI Agents SDK 当前的 Session 接口就是这个语义:当 Session 被提供给 Runner 时,历史项会在后续 turn/run 中被读取,新输入和输出也会持久化到该 Session。它强调的是 conversation history persistence,而不是完整 Runtime 状态。Sandbox Agent 的 Memory 又被官方明确区分为另一套能力。这个区别非常重要。
python
@dataclass
class SessionRecord:
session_id: str
user_id: str
created_at: datetime
last_active_at: datetime
metadata: dict[str, str]
@dataclass
class SessionItem:
session_id: str
seq: int
role: str
item_type: str
payload: dictDurable Task State
假设 Agent 正在执行一个持续 6 小时的代码迁移任务:
- 已经分析 80 个文件;
- 已完成 5 个修改;
- 第 6 步需要人工批准;
- 服务发布,进程被重启;
- 第二天用户继续任务。
python
@dataclass
class DurableTaskState:
task_id: str
objective: str
constraints: list[str]
plan_version: str | None
completed_nodes: set[str]
pending_nodes: set[str]
pending_approvals: list[str]
artifact_refs: list[str]
external_effect_records: list[str]
latest_checkpoint_id: str
schema_version: int🔍 源码对照:Durable state 会按恢复职责拆成多个持久文件
精确定位
- 文件:
source/host/durable-file-policy.ts - Symbol:
SAND_UPGRADE_RESUME_FILE_NAME→SAND_PENDING_WAKE_FILE_NAME→SAND_XUSER_TURN_DEDUPE_FILE_NAME - 深入索引:Code Atlas:06-durable-settlement
ts
const durableFiles = {
upgradeResume: "host-upgrade-resume.json",
pendingWake: "host-pending-wakes.json",
dedupeNonce: "host-xuser-turn-nonces.json",
ackObligations: "ack-obligations.json",
}对照理解
- 真实工程没有把“durable state”收敛成一个万能
state.json;不同恢复语义有不同 durable owner。 - 这提醒 Shadow Harness:持久化结构应围绕恢复 invariant 设计,而不是围绕内存对象结构设计。
- 文件名只是该产品实现;可迁移的是“恢复职责分离”这个原则。
源码说明
conversation state、storage、transcript mirror 的并列存在,是理解 State 分层的好案例:用户看到的会话记录、Runtime 正在推进的执行事实、需要跨进程保留的数据,并不天然属于同一个对象。把所有东西塞进一个 session.json 会让 ownership、恢复和迁移都变得模糊。
工程边界
这些目录名只提供拆分线索,不代表其每个字段都对应我们定义的 RunState/SessionState/DurableTaskState;Domain Language 仍以本知识库和 Shadow Harness 为准。
Shadow Harness 对照
- RunState 只保存一次执行;SessionState 管跨 Run 的交互容器;DurableTaskState 保存恢复任务所需事实。
- 每类 state 明确 owner、lifetime、persistence、version。
验证
关闭进程后重新打开同一 Session:新 Run 可以继承允许的 conversation/session context,但不能继承上个 Run 的 transient RUNNING node。
最小实现 / 数据结构
最小可以拆成下面这些逻辑记录:
text
runs
run_events
sessions
session_items
tasks
task_checkpoints
node_executions
approval_requests
external_effects这不表示一定要对应 9 张物理表;重点是让不同生命周期和一致性要求的数据拥有清楚的逻辑责任。
sql
CREATE TABLE runs (
run_id VARCHAR PRIMARY KEY,
task_id VARCHAR NULL,
session_id VARCHAR NULL,
status VARCHAR NOT NULL,
current_node_id VARCHAR NULL,
last_checkpoint_id VARCHAR NULL,
version BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);在 Shadow Harness 中怎么落地
不要用一个包罗万象的 MemoryManager 同时承载执行事实、会话历史和长任务状态。最小边界可以直接体现在 Store 接口上:
python
class RunStateStore(Protocol):
async def get(self, run_id: str) -> RunState: ...
async def compare_and_set(self, old: RunState, new: RunState) -> bool: ...
class SessionStore(Protocol):
async def get_items(self, session_id: str, limit: int | None = None) -> list[SessionItem]: ...
async def append_items(self, session_id: str, items: list[SessionItem]) -> None: ...
class TaskStateStore(Protocol):
async def load(self, task_id: str) -> DurableTaskState: ...
async def save(self, state: DurableTaskState) -> None: ...这样 Runtime 恢复 Run 时读取 RunStateStore,新一轮模型调用需要聊天连续性时读取 SessionStore,长任务跨进程继续时读取 TaskStateStore。三者可以落在同一个数据库,但不能因此混成同一种状态。
工程取舍
- 一个大 State 表:早期简单,但生命周期、权限和更新频率很快耦合。
- 分逻辑 Store:接口更多,但状态语义清晰,更容易测试和迁移。
- Snapshot + incremental events:通常比“只有 snapshot”或“全量 event sourcing”更平衡。
最终判断标准不是表数量,而是 crash 后能否明确回答:哪些事实已提交、谁能修改、从哪里恢复。
Failure Modes
把 conversation history 当成所有状态
症状: 系统只保存聊天记录,却没有独立 Run/Tool/Approval/External Effect 状态。crash 后只能从自然语言猜程序执行到哪里。
为什么危险: Conversation 的语义是交互历史,而外部副作用、Node completion、approval 等属于执行事实。把两者混合后,summary/trimming 会直接破坏恢复正确性。
处理: 把 conversation item、RunState、execution record、checkpoint 分开;外部动作必须有可查询的 commit/idempotency 记录。
把 Session ID 当 Run ID
症状: 同一个聊天线程中第二个请求覆盖第一个 Run 的状态,或者系统无法表达同 Session 下多个 Run。
为什么危险: Session 和 Run 生命周期、基数关系不同。把 ID 合并会把 conversation continuity 与 execution lifecycle 强绑在一起。
处理: 明确 session_id -> many run_id;Run 另有唯一标识和 terminal state,SessionStore 不拥有 Runtime status。
Durable State 保存运行时对象
症状: Checkpoint/TaskState 中出现 HTTP client、file handle、lock、coroutine、Task 等进程内对象。
为什么危险: 这些对象无法可靠跨进程、跨版本或跨 worker 恢复;强行序列化会把实现细节写进持久化协议。
处理: 只保存可版本化 domain data 和 resource identifiers;连接、client、locks、tasks 在 resume 时重新建立。
状态没有版本控制
症状: 两个 worker 同时读取 RUNNING 并各自提交更新,导致同一 Node 或副作用执行两次。
为什么危险: 跨 worker resume 时没有 optimistic version、lease 或 compare-and-set,就没有单一状态提交者。
处理: 对 Run/Task 引入 version/lease;状态提交使用 CAS,并配合 Tool idempotency key。
如何验证
| Case | 断言 |
|---|---|
| 把 conversation history 当成所有状态 | 模拟 Tool 已成功但进程在下一轮模型调用前 crash;恢复后不得因为 transcript 不完整而重复执行 Tool。 |
| 把 Session ID 当 Run ID | 在同一 session 创建两个 Run,分别暂停/完成,断言两份 RunState 完全独立且 Session history 仍按顺序追加。 |
| Durable State 保存运行时对象 | 序列化 DurableTaskState 后在新进程恢复,断言无需复用旧进程对象即可继续任务。 |
| 状态没有版本控制 | 让两个 worker 同时 claim 同一 run_version,断言只有一个 CAS 成功。 |
关联源码
Codex 源码对照:SessionState Ownership / Model Projection
要回答的问题
哪些状态属于 Session lifetime,哪些只属于当前 active Turn,哪些可以跨多次 sampling,哪些才会被投影给模型?
精确定位
Tag: rust-v0.150.1
File:
codex-rs/core/src/session/session.rscodex-rs/core/src/state/session.rscodex-rs/core/src/state/turn.rscodex-rs/core/src/context_manager/history.rscodex-rs/core/src/session/turn.rs
Symbols:
SessionSessionStateActiveTurnTurnStateContextManagerContextManager::for_prompt(...)run_turn(...)
最小调用链
text
Session
│
├─ state: SessionState
│ ├─ history: ContextManager
│ ├─ latest_rate_limits
│ ├─ previous_turn_settings
│ ├─ auto_compact_window
│ ├─ session-level granted permissions
│ └─ ...
│
└─ active_turn: ActiveTurn
├─ RunningTask / cancellation
└─ TurnState
├─ pending approvals / permission requests / user input
├─ pending input / mailbox phase
├─ turn-granted permissions
├─ tool_calls
└─ token_usage_at_turn_start
SessionState.history
→ clone_history()
→ ContextManager::for_prompt(...)
→ sampling_request_input
→ modelObservable Fact
state/session.rs 文件级注释直接标明它是 Session-wide mutable state。SessionState 中的 history、rate limit snapshot、previous turn settings、auto-compaction window、connector selection 等可跨一次 run_turn() 存活。
state/turn.rs 则把 TurnState 定义为 single-turn mutable state,包含 pending approvals / permissions / input、turn-granted permissions、tool call counter 等。run_turn() 内部多次 sampling 时,这些 active-turn state 可以继续存在。
ContextManager::for_prompt() 只生成 model-visible history projection;Session 中的 permission authority、approval state、rate-limit bookkeeping、execution controls 并不会因此自动变成 model input。
Design Inference
状态至少要按四个问题分类:
- owner 是 Session、Run 还是 single logical Turn?
- lifetime 是否跨 sampling / 跨 user turn / 跨进程?
- 它是 source-of-truth authority 还是 model-visible projection?
- resume 时是直接恢复,还是需要 revalidate?
同名结构体不是映射依据;职责和 lifecycle 才是。
Codex-specific Trade-off
Codex rust-v0.150.1 把 history 与 token bookkeeping 放进 SessionState,把 pending interaction 放进 TurnState,并另外通过 thread store/session persistence 支撑恢复。这种具体拆分是 Codex 产品结构,不能据此推出“所有 durable facts 都应放 SessionState”。
Shadow Harness Delta
职责对照而非名字对照:
| Codex | Shadow Harness | 应吸收的职责 |
|---|---|---|
SessionState.history | Session/Run history store | 跨 sampling 保存已接受事实 |
TurnState | active control state / TurnState 的一部分 | pending interaction、tool/approval control |
ContextManager::for_prompt() 输出 | ContextSnapshot | 一次 model request 的 projection |
| permission / approval config | Runtime authority | 不允许模型自行修改 |
另外为 durable resume 增加显式 revalidation:恢复后的 environment/tool schema/policy 与旧 execution fact 不应因为“Session 还在”就默认兼容。
不要过度推导
SessionState 是 session-wide mutable state,不等于“所有字段都是 durable state”;TurnState 也不等于本知识库 canonical TurnState。源码结构本身不能替代 durability contract 与 resume semantics 的单独设计。
完整映射见 OpenAI Codex Source Map。
自测
- 用户 Session 还在,但某个 Run 已结束。哪些状态属于 Run,哪些属于 Session,哪些必须持久化成跨进程 Durable State?
- 如果把临时 turn scratch 全部写进 durable store,会带来什么生命周期和恢复噪声?